15861 lines
595 KiB
Plaintext
15861 lines
595 KiB
Plaintext
================================================================================
|
||
项目代码导出文件
|
||
================================================================================
|
||
项目名称: 基于Python的线上电商系统
|
||
导出时间: 2025-07-04 03:35:45
|
||
项目路径: /Users/lishunqin/Desktop/Online_shopping_platform
|
||
文件总数: 83
|
||
================================================================================
|
||
|
||
📁 文件目录:
|
||
--------------------------------------------------
|
||
README.md (0.0 KB)
|
||
app/__init__.py (2.5 KB)
|
||
app/forms.py (5.2 KB)
|
||
app/models/__init__.py (0.8 KB)
|
||
app/models/address.py (2.8 KB)
|
||
app/models/admin.py (1.8 KB)
|
||
app/models/cart.py (4.5 KB)
|
||
app/models/operation_log.py (1.8 KB)
|
||
app/models/order.py (6.7 KB)
|
||
app/models/payment.py (2.3 KB)
|
||
app/models/product.py (9.4 KB)
|
||
app/models/review.py (2.2 KB)
|
||
app/models/user.py (1.7 KB)
|
||
app/models/verification.py (1.8 KB)
|
||
app/static/js/city_data.js (54.3 KB)
|
||
app/templates/admin/base.html (7.7 KB)
|
||
app/templates/admin/categories.html (26.3 KB)
|
||
app/templates/admin/dashboard.html (7.9 KB)
|
||
app/templates/admin/login.html (4.5 KB)
|
||
app/templates/admin/orders.html (0.0 KB)
|
||
app/templates/admin/product_form.html (32.9 KB)
|
||
app/templates/admin/products.html (17.6 KB)
|
||
app/templates/admin/profile.html (5.6 KB)
|
||
app/templates/admin/users.html (0.0 KB)
|
||
app/templates/base.html (11.4 KB)
|
||
app/templates/cart/index.html (16.2 KB)
|
||
app/templates/common/footer.html (0.0 KB)
|
||
app/templates/common/header.html (0.0 KB)
|
||
app/templates/common/pagination.html (0.0 KB)
|
||
app/templates/index.html (9.5 KB)
|
||
app/templates/order/checkout.html (14.9 KB)
|
||
app/templates/order/detail.html (14.0 KB)
|
||
app/templates/order/pay.html (10.5 KB)
|
||
app/templates/product/detail.html (24.7 KB)
|
||
app/templates/product/list.html (13.6 KB)
|
||
app/templates/test_upload.html (14.5 KB)
|
||
app/templates/user/address_form.html (14.4 KB)
|
||
app/templates/user/addresses.html (7.4 KB)
|
||
app/templates/user/login.html (2.3 KB)
|
||
app/templates/user/orders.html (13.9 KB)
|
||
app/templates/user/profile.html (26.5 KB)
|
||
app/templates/user/register.html (9.4 KB)
|
||
app/utils/__init__.py (0.0 KB)
|
||
app/utils/auth.py (0.0 KB)
|
||
app/utils/cos_client.py (7.7 KB)
|
||
app/utils/cos_upload.py (0.0 KB)
|
||
app/utils/database.py (1.0 KB)
|
||
app/utils/decorators.py (10.0 KB)
|
||
app/utils/email_service.py (2.4 KB)
|
||
app/utils/file_upload.py (12.4 KB)
|
||
app/utils/helpers.py (0.0 KB)
|
||
app/utils/sms.py (0.0 KB)
|
||
app/utils/wechat_pay.py (0.0 KB)
|
||
app/views/__init__.py (0.0 KB)
|
||
app/views/address.py (8.0 KB)
|
||
app/views/admin.py (7.7 KB)
|
||
app/views/auth.py (4.8 KB)
|
||
app/views/cart.py (7.4 KB)
|
||
app/views/main.py (6.4 KB)
|
||
app/views/order.py (10.7 KB)
|
||
app/views/payment.py (6.6 KB)
|
||
app/views/product.py (23.2 KB)
|
||
app/views/upload.py (5.3 KB)
|
||
app/views/user.py (1.0 KB)
|
||
check_avatar.py (0.6 KB)
|
||
config/__init__.py (0.0 KB)
|
||
config/config.py (1.5 KB)
|
||
config/cos_config.py (2.7 KB)
|
||
config/database.py (0.8 KB)
|
||
create_admin.py (5.2 KB)
|
||
create_sample_categories.py (4.7 KB)
|
||
create_sample_specs.py (2.0 KB)
|
||
create_test_order.py (2.8 KB)
|
||
create_test_user.py (2.0 KB)
|
||
docker/.dockerignore (0.0 KB)
|
||
docker/Dockerfile (0.0 KB)
|
||
docker/docker-compose.yml (0.0 KB)
|
||
export_code.py (9.9 KB)
|
||
requirements.txt (0.2 KB)
|
||
run.py (0.2 KB)
|
||
test_cos_connection.py (6.5 KB)
|
||
test_db_connection.py (3.3 KB)
|
||
test_email_detailed.py (8.3 KB)
|
||
|
||
================================================================================
|
||
|
||
🔸==============================================================================
|
||
📄 文件: README.md
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/__init__.py
|
||
📊 大小: 2545 bytes (2.49 KB)
|
||
🕒 修改时间: 2025-07-04 02:28:26
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
Flask应用工厂
|
||
"""
|
||
from flask import Flask
|
||
from flask_mail import Mail
|
||
from config.config import Config
|
||
from config.database import db
|
||
import re
|
||
|
||
# 初始化邮件服务
|
||
mail = Mail()
|
||
|
||
|
||
def create_app(config_name='default'):
|
||
app = Flask(__name__)
|
||
|
||
# 加载配置
|
||
app.config.from_object(Config)
|
||
|
||
# 初始化数据库
|
||
db.init_app(app)
|
||
|
||
# 初始化邮件服务
|
||
mail.init_app(app)
|
||
|
||
# 注册自定义过滤器
|
||
register_filters(app)
|
||
|
||
# 注册蓝图
|
||
register_blueprints(app)
|
||
|
||
# 创建数据库表
|
||
with app.app_context():
|
||
try:
|
||
db.create_all()
|
||
print("✅ 数据库表创建/同步成功")
|
||
except Exception as e:
|
||
print(f"❌ 数据库表创建失败: {str(e)}")
|
||
|
||
return app
|
||
|
||
|
||
def register_filters(app):
|
||
"""注册自定义过滤器"""
|
||
|
||
@app.template_filter('nl2br')
|
||
def nl2br_filter(text):
|
||
"""将换行符转换为HTML <br> 标签"""
|
||
if not text:
|
||
return ''
|
||
# 将换行符替换为 <br> 标签
|
||
return text.replace('\n', '<br>')
|
||
|
||
@app.template_filter('truncate_chars')
|
||
def truncate_chars_filter(text, length=50):
|
||
"""截断字符串"""
|
||
if not text:
|
||
return ''
|
||
if len(text) <= length:
|
||
return text
|
||
return text[:length] + '...'
|
||
|
||
|
||
def register_blueprints(app):
|
||
"""注册蓝图"""
|
||
from app.views.main import main_bp
|
||
from app.views.auth import auth_bp
|
||
from app.views.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
|
||
|
||
app.register_blueprint(main_bp)
|
||
app.register_blueprint(auth_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)
|
||
|
||
# 修复:正确注册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("✅ 购物车蓝图注册成功")
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/forms.py
|
||
📊 大小: 5318 bytes (5.19 KB)
|
||
🕒 修改时间: 2025-07-04 03:19:30
|
||
🔸==============================================================================
|
||
|
||
from flask_wtf import FlaskForm
|
||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||
from wtforms.validators import DataRequired, Length, Email, ValidationError, Regexp, EqualTo
|
||
from app.models.user import User
|
||
from wtforms import TextAreaField, SelectField, DecimalField, IntegerField, HiddenField
|
||
|
||
class LoginForm(FlaskForm):
|
||
username = StringField('用户名/手机号/邮箱', validators=[
|
||
DataRequired(message='请输入用户名、手机号或邮箱'),
|
||
Length(min=3, max=50, message='长度必须在3-50个字符之间')
|
||
])
|
||
password = PasswordField('密码', validators=[
|
||
DataRequired(message='请输入密码'),
|
||
Length(min=6, max=20, message='密码长度必须在6-20个字符之间')
|
||
])
|
||
remember_me = BooleanField('记住我')
|
||
submit = SubmitField('登录')
|
||
|
||
|
||
class RegisterForm(FlaskForm):
|
||
username = StringField('用户名', validators=[
|
||
DataRequired(message='请输入用户名'),
|
||
Length(min=3, max=20, message='用户名长度必须在3-20个字符之间'),
|
||
Regexp(r'^[a-zA-Z0-9_]+$', message='用户名只能包含字母、数字和下划线')
|
||
])
|
||
email = StringField('邮箱', validators=[
|
||
DataRequired(message='请输入邮箱'),
|
||
Email(message='请输入有效的邮箱地址')
|
||
])
|
||
email_code = StringField('邮箱验证码', validators=[
|
||
DataRequired(message='请输入邮箱验证码'),
|
||
Length(min=6, max=6, message='验证码为6位数字')
|
||
])
|
||
phone = StringField('手机号', validators=[
|
||
DataRequired(message='请输入手机号'),
|
||
Regexp(r'^1[3-9]\d{9}$', message='请输入有效的手机号')
|
||
])
|
||
password = PasswordField('密码', validators=[
|
||
DataRequired(message='请输入密码'),
|
||
Length(min=6, max=20, message='密码长度必须在6-20个字符之间'),
|
||
Regexp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{6,}$',
|
||
message='密码必须包含至少一个字母和一个数字')
|
||
])
|
||
confirm_password = PasswordField('确认密码', validators=[
|
||
DataRequired(message='请确认密码'),
|
||
EqualTo('password', message='两次输入的密码不一致')
|
||
])
|
||
submit = SubmitField('注册')
|
||
|
||
def validate_username(self, username):
|
||
user = User.query.filter_by(username=username.data).first()
|
||
if user:
|
||
raise ValidationError('用户名已存在')
|
||
|
||
def validate_email(self, email):
|
||
user = User.query.filter_by(email=email.data).first()
|
||
if user:
|
||
raise ValidationError('邮箱已被注册')
|
||
|
||
def validate_phone(self, phone):
|
||
user = User.query.filter_by(phone=phone.data).first()
|
||
if user:
|
||
raise ValidationError('手机号已被注册')
|
||
|
||
|
||
class SendEmailCodeForm(FlaskForm):
|
||
"""发送邮箱验证码表单"""
|
||
email = StringField('邮箱', validators=[
|
||
DataRequired(message='请输入邮箱'),
|
||
Email(message='请输入有效的邮箱地址')
|
||
])
|
||
submit = SubmitField('发送验证码')
|
||
|
||
|
||
class AddressForm(FlaskForm):
|
||
"""地址表单"""
|
||
receiver_name = StringField('收货人', validators=[
|
||
DataRequired(message='请输入收货人姓名'),
|
||
Length(min=2, max=20, message='收货人姓名长度必须在2-20个字符之间')
|
||
])
|
||
receiver_phone = StringField('手机号', validators=[
|
||
DataRequired(message='请输入手机号'),
|
||
Regexp(r'^1[3-9]\d{9}$', message='请输入有效的手机号')
|
||
])
|
||
province = SelectField('省份', validators=[
|
||
DataRequired(message='请选择省份')
|
||
], choices=[])
|
||
city = SelectField('城市', validators=[
|
||
DataRequired(message='请选择城市')
|
||
], choices=[])
|
||
district = SelectField('区县', validators=[
|
||
DataRequired(message='请选择区县')
|
||
], choices=[])
|
||
detail_address = StringField('详细地址', validators=[
|
||
DataRequired(message='请输入详细地址'),
|
||
Length(min=5, max=200, message='详细地址长度必须在5-200个字符之间')
|
||
])
|
||
postal_code = StringField('邮政编码', validators=[
|
||
Length(max=10, message='邮政编码长度不能超过10个字符')
|
||
])
|
||
is_default = BooleanField('设为默认地址')
|
||
submit = SubmitField('保存地址')
|
||
|
||
|
||
class CheckoutForm(FlaskForm):
|
||
"""结算表单"""
|
||
address_id = SelectField('收货地址', validators=[
|
||
DataRequired(message='请选择收货地址')
|
||
], coerce=int, choices=[])
|
||
shipping_method = SelectField('配送方式', validators=[
|
||
DataRequired(message='请选择配送方式')
|
||
], choices=[
|
||
('standard', '标准配送(免费)'),
|
||
('express', '次日达(+10元)'),
|
||
('same_day', '当日达(+20元)')
|
||
], default='standard')
|
||
payment_method = SelectField('支付方式', validators=[
|
||
DataRequired(message='请选择支付方式')
|
||
], choices=[
|
||
('wechat', '微信支付'),
|
||
('alipay', '支付宝'),
|
||
('bank', '银行卡支付')
|
||
], default='wechat')
|
||
remark = TextAreaField('订单备注', validators=[
|
||
Length(max=200, message='备注长度不能超过200个字符')
|
||
])
|
||
selected_items = HiddenField('选中商品')
|
||
submit = SubmitField('提交订单')
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/__init__.py
|
||
📊 大小: 822 bytes (0.80 KB)
|
||
🕒 修改时间: 2025-07-04 01:56:55
|
||
🔸==============================================================================
|
||
|
||
from app.models.user import User
|
||
from app.models.verification import EmailVerification
|
||
from app.models.admin import AdminUser
|
||
from app.models.operation_log import OperationLog
|
||
from app.models.product import Category, Product, ProductImage, SpecName, SpecValue, ProductInventory, InventoryLog, ProductSpecRelation
|
||
from app.models.cart import Cart
|
||
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
|
||
|
||
__all__ = [
|
||
'User', 'EmailVerification', 'AdminUser', 'OperationLog',
|
||
'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue',
|
||
'ProductInventory', 'InventoryLog', 'ProductSpecRelation',
|
||
'Cart', 'UserAddress', 'Order', 'OrderItem', 'ShippingInfo',
|
||
'Payment', 'Review'
|
||
]
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/address.py
|
||
📊 大小: 2868 bytes (2.80 KB)
|
||
🕒 修改时间: 2025-07-04 01:56:15
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
用户地址模型
|
||
"""
|
||
from datetime import datetime
|
||
from config.database import db
|
||
|
||
|
||
class UserAddress(db.Model):
|
||
"""用户地址模型"""
|
||
__tablename__ = 'user_addresses'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||
receiver_name = db.Column(db.String(50), nullable=False)
|
||
receiver_phone = db.Column(db.String(20), nullable=False)
|
||
province = db.Column(db.String(50), nullable=False)
|
||
city = db.Column(db.String(50), nullable=False)
|
||
district = db.Column(db.String(50), nullable=False)
|
||
detail_address = db.Column(db.String(200), nullable=False)
|
||
postal_code = db.Column(db.String(10))
|
||
is_default = db.Column(db.Integer, default=0)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
# 关联关系
|
||
user = db.relationship('User', backref='addresses')
|
||
|
||
def get_full_address(self):
|
||
"""获取完整地址"""
|
||
return f"{self.province} {self.city} {self.district} {self.detail_address}"
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'user_id': self.user_id,
|
||
'receiver_name': self.receiver_name,
|
||
'receiver_phone': self.receiver_phone,
|
||
'province': self.province,
|
||
'city': self.city,
|
||
'district': self.district,
|
||
'detail_address': self.detail_address,
|
||
'postal_code': self.postal_code,
|
||
'full_address': self.get_full_address(),
|
||
'is_default': self.is_default,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
@classmethod
|
||
def set_default_address(cls, user_id, address_id):
|
||
"""设置默认地址"""
|
||
try:
|
||
# 先取消所有默认地址
|
||
cls.query.filter_by(user_id=user_id).update({'is_default': 0})
|
||
# 设置新的默认地址
|
||
address = cls.query.filter_by(id=address_id, user_id=user_id).first()
|
||
if address:
|
||
address.is_default = 1
|
||
db.session.commit()
|
||
return True
|
||
return False
|
||
except Exception:
|
||
db.session.rollback()
|
||
return False
|
||
|
||
@classmethod
|
||
def get_default_address(cls, user_id):
|
||
"""获取默认地址"""
|
||
return cls.query.filter_by(user_id=user_id, is_default=1).first()
|
||
|
||
@classmethod
|
||
def get_user_addresses(cls, user_id):
|
||
"""获取用户所有地址"""
|
||
return cls.query.filter_by(user_id=user_id).order_by(
|
||
cls.is_default.desc(), cls.created_at.desc()
|
||
).all()
|
||
|
||
def __repr__(self):
|
||
return f'<UserAddress {self.receiver_name}-{self.get_full_address()}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/admin.py
|
||
📊 大小: 1814 bytes (1.77 KB)
|
||
🕒 修改时间: 2025-07-03 05:49:59
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
管理员模型
|
||
"""
|
||
from datetime import datetime
|
||
from werkzeug.security import generate_password_hash, check_password_hash
|
||
from config.database import db
|
||
|
||
|
||
class AdminUser(db.Model):
|
||
__tablename__ = 'admin_users'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
username = db.Column(db.String(50), unique=True, nullable=False)
|
||
password_hash = db.Column(db.String(255), nullable=False)
|
||
real_name = db.Column(db.String(50))
|
||
email = db.Column(db.String(100))
|
||
phone = db.Column(db.String(20))
|
||
role = db.Column(db.String(20), default='admin')
|
||
status = db.Column(db.Integer, default=1) # 0-禁用 1-正常
|
||
last_login_at = db.Column(db.DateTime)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
def set_password(self, password):
|
||
"""设置密码"""
|
||
self.password_hash = generate_password_hash(password)
|
||
|
||
def check_password(self, password):
|
||
"""验证密码"""
|
||
return check_password_hash(self.password_hash, password)
|
||
|
||
def update_last_login(self):
|
||
"""更新最后登录时间"""
|
||
self.last_login_at = datetime.utcnow()
|
||
db.session.commit()
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'username': self.username,
|
||
'real_name': self.real_name,
|
||
'email': self.email,
|
||
'phone': self.phone,
|
||
'role': self.role,
|
||
'status': self.status,
|
||
'last_login_at': self.last_login_at.isoformat() if self.last_login_at else None,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<AdminUser {self.username}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/cart.py
|
||
📊 大小: 4608 bytes (4.50 KB)
|
||
🕒 修改时间: 2025-07-03 15:24:11
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
购物车模型
|
||
"""
|
||
from datetime import datetime
|
||
from config.database import db
|
||
from app.models.product import Product, ProductInventory
|
||
|
||
|
||
class Cart(db.Model):
|
||
"""购物车模型"""
|
||
__tablename__ = 'cart'
|
||
|
||
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)
|
||
sku_code = db.Column(db.String(100))
|
||
spec_combination = db.Column(db.String(255))
|
||
quantity = db.Column(db.Integer, nullable=False, default=1)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
# 关联关系
|
||
user = db.relationship('User', backref='cart_items')
|
||
product = db.relationship('Product', backref='cart_items')
|
||
|
||
def get_sku_info(self):
|
||
"""获取SKU信息"""
|
||
if self.sku_code:
|
||
return ProductInventory.query.filter_by(sku_code=self.sku_code).first()
|
||
else:
|
||
# 如果没有SKU,返回默认库存信息
|
||
return ProductInventory.query.filter_by(
|
||
product_id=self.product_id,
|
||
is_default=1
|
||
).first()
|
||
|
||
def get_price(self):
|
||
"""获取商品价格"""
|
||
sku_info = self.get_sku_info()
|
||
if sku_info:
|
||
return sku_info.get_final_price()
|
||
return float(self.product.price) if self.product and self.product.price else 0
|
||
|
||
def get_total_price(self):
|
||
"""获取小计金额"""
|
||
return self.get_price() * self.quantity
|
||
|
||
def get_stock(self):
|
||
"""获取库存数量"""
|
||
sku_info = self.get_sku_info()
|
||
return sku_info.stock if sku_info else 0
|
||
|
||
def is_available(self):
|
||
"""检查商品是否可用"""
|
||
# 检查商品是否上架
|
||
if not self.product or self.product.status != 1:
|
||
return False
|
||
|
||
# 检查库存
|
||
if self.get_stock() < self.quantity:
|
||
return False
|
||
|
||
return True
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
sku_info = self.get_sku_info()
|
||
return {
|
||
'id': self.id,
|
||
'user_id': self.user_id,
|
||
'product_id': self.product_id,
|
||
'product_name': self.product.name if self.product else '',
|
||
'product_image': self.product.main_image if self.product else '',
|
||
'brand': self.product.brand if self.product else '',
|
||
'sku_code': self.sku_code,
|
||
'spec_combination': self.spec_combination,
|
||
'quantity': self.quantity,
|
||
'price': self.get_price(),
|
||
'total_price': self.get_total_price(),
|
||
'stock': self.get_stock(),
|
||
'is_available': self.is_available(),
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
@classmethod
|
||
def add_to_cart(cls, user_id, product_id, sku_code=None, spec_combination=None, quantity=1):
|
||
"""添加商品到购物车"""
|
||
# 检查是否已存在相同商品
|
||
existing_item = cls.query.filter_by(
|
||
user_id=user_id,
|
||
product_id=product_id,
|
||
sku_code=sku_code
|
||
).first()
|
||
|
||
if existing_item:
|
||
# 更新数量
|
||
existing_item.quantity += quantity
|
||
existing_item.updated_at = datetime.utcnow()
|
||
db.session.commit()
|
||
return existing_item
|
||
else:
|
||
# 创建新记录
|
||
cart_item = cls(
|
||
user_id=user_id,
|
||
product_id=product_id,
|
||
sku_code=sku_code,
|
||
spec_combination=spec_combination,
|
||
quantity=quantity
|
||
)
|
||
db.session.add(cart_item)
|
||
db.session.commit()
|
||
return cart_item
|
||
|
||
@classmethod
|
||
def get_user_cart(cls, user_id):
|
||
"""获取用户购物车"""
|
||
return cls.query.filter_by(user_id=user_id)\
|
||
.order_by(cls.created_at.desc()).all()
|
||
|
||
@classmethod
|
||
def get_cart_count(cls, user_id):
|
||
"""获取购物车商品数量"""
|
||
return cls.query.filter_by(user_id=user_id).count()
|
||
|
||
@classmethod
|
||
def get_cart_total(cls, user_id):
|
||
"""获取购物车总金额"""
|
||
cart_items = cls.get_user_cart(user_id)
|
||
total = 0
|
||
for item in cart_items:
|
||
if item.is_available():
|
||
total += item.get_total_price()
|
||
return total
|
||
|
||
def __repr__(self):
|
||
return f'<Cart {self.user_id}-{self.product_id}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/operation_log.py
|
||
📊 大小: 1850 bytes (1.81 KB)
|
||
🕒 修改时间: 2025-07-03 05:50:29
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
操作日志模型
|
||
"""
|
||
from datetime import datetime
|
||
from config.database import db
|
||
import json
|
||
|
||
|
||
class OperationLog(db.Model):
|
||
__tablename__ = 'operation_logs'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer)
|
||
user_type = db.Column(db.Integer) # 1-普通用户 2-管理员
|
||
action = db.Column(db.String(100), nullable=False)
|
||
resource_type = db.Column(db.String(50))
|
||
resource_id = db.Column(db.Integer)
|
||
ip_address = db.Column(db.String(45))
|
||
user_agent = db.Column(db.Text)
|
||
request_data = db.Column(db.JSON)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
@classmethod
|
||
def create_log(cls, user_id=None, user_type=1, action='', resource_type=None,
|
||
resource_id=None, ip_address=None, user_agent=None, request_data=None):
|
||
"""创建操作日志"""
|
||
log = cls(
|
||
user_id=user_id,
|
||
user_type=user_type,
|
||
action=action,
|
||
resource_type=resource_type,
|
||
resource_id=resource_id,
|
||
ip_address=ip_address,
|
||
user_agent=user_agent,
|
||
request_data=request_data
|
||
)
|
||
db.session.add(log)
|
||
db.session.commit()
|
||
return log
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'user_id': self.user_id,
|
||
'user_type': self.user_type,
|
||
'action': self.action,
|
||
'resource_type': self.resource_type,
|
||
'resource_id': self.resource_id,
|
||
'ip_address': self.ip_address,
|
||
'user_agent': self.user_agent,
|
||
'request_data': self.request_data,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<OperationLog {self.action}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/order.py
|
||
📊 大小: 6846 bytes (6.69 KB)
|
||
🕒 修改时间: 2025-07-04 02:50:35
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
订单模型
|
||
"""
|
||
from datetime import datetime, timedelta
|
||
import json
|
||
from config.database import db
|
||
from app.models.user import User
|
||
from app.models.product import Product
|
||
|
||
|
||
class Order(db.Model):
|
||
"""订单模型"""
|
||
__tablename__ = 'orders'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||
order_sn = db.Column(db.String(50), unique=True, nullable=False)
|
||
total_amount = db.Column(db.Numeric(10, 2), nullable=False)
|
||
actual_amount = db.Column(db.Numeric(10, 2), nullable=False)
|
||
shipping_fee = db.Column(db.Numeric(10, 2), default=0)
|
||
status = db.Column(db.Integer, default=1) # 1-待支付 2-待发货 3-待收货 4-待评价 5-已完成 6-已取消 7-退款中
|
||
payment_method = db.Column(db.String(20))
|
||
shipping_method = db.Column(db.String(50))
|
||
receiver_info = db.Column(db.Text) # JSON格式存储收货人信息
|
||
remark = db.Column(db.Text)
|
||
shipped_at = db.Column(db.DateTime)
|
||
received_at = db.Column(db.DateTime)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
# 关联关系
|
||
user = db.relationship('User', backref='orders')
|
||
order_items = db.relationship('OrderItem', backref='order', cascade='all, delete-orphan')
|
||
|
||
# 状态常量
|
||
STATUS_PENDING_PAYMENT = 1 # 待支付
|
||
STATUS_PENDING_SHIPMENT = 2 # 待发货
|
||
STATUS_SHIPPED = 3 # 待收货
|
||
STATUS_PENDING_REVIEW = 4 # 待评价
|
||
STATUS_COMPLETED = 5 # 已完成
|
||
STATUS_CANCELLED = 6 # 已取消
|
||
STATUS_REFUNDING = 7 # 退款中
|
||
|
||
STATUS_CHOICES = {
|
||
STATUS_PENDING_PAYMENT: '待支付',
|
||
STATUS_PENDING_SHIPMENT: '待发货',
|
||
STATUS_SHIPPED: '待收货',
|
||
STATUS_PENDING_REVIEW: '待评价',
|
||
STATUS_COMPLETED: '已完成',
|
||
STATUS_CANCELLED: '已取消',
|
||
STATUS_REFUNDING: '退款中'
|
||
}
|
||
|
||
def get_status_text(self):
|
||
"""获取状态文本"""
|
||
return self.STATUS_CHOICES.get(self.status, '未知状态')
|
||
|
||
def get_receiver_info(self):
|
||
"""获取收货人信息"""
|
||
if self.receiver_info:
|
||
try:
|
||
return json.loads(self.receiver_info)
|
||
except:
|
||
return {}
|
||
return {}
|
||
|
||
def set_receiver_info(self, info):
|
||
"""设置收货人信息"""
|
||
if isinstance(info, dict):
|
||
self.receiver_info = json.dumps(info, ensure_ascii=False)
|
||
|
||
def is_expired(self):
|
||
"""检查订单是否已过期(15分钟未支付)"""
|
||
if self.status == self.STATUS_PENDING_PAYMENT:
|
||
expire_time = self.created_at + timedelta(minutes=15)
|
||
return datetime.utcnow() > expire_time
|
||
return False
|
||
|
||
def can_cancel(self):
|
||
"""检查是否可以取消"""
|
||
return self.status in [self.STATUS_PENDING_PAYMENT, self.STATUS_PENDING_SHIPMENT]
|
||
|
||
def can_pay(self):
|
||
"""检查是否可以支付"""
|
||
return self.status == self.STATUS_PENDING_PAYMENT and not self.is_expired()
|
||
|
||
def can_confirm_receipt(self):
|
||
"""检查是否可以确认收货"""
|
||
return self.status == self.STATUS_SHIPPED
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'order_sn': self.order_sn,
|
||
'total_amount': float(self.total_amount),
|
||
'actual_amount': float(self.actual_amount),
|
||
'shipping_fee': float(self.shipping_fee),
|
||
'status': self.status,
|
||
'status_text': self.get_status_text(),
|
||
'payment_method': self.payment_method,
|
||
'shipping_method': self.shipping_method,
|
||
'receiver_info': self.get_receiver_info(),
|
||
'remark': self.remark,
|
||
'can_cancel': self.can_cancel(),
|
||
'can_pay': self.can_pay(),
|
||
'can_confirm_receipt': self.can_confirm_receipt(),
|
||
'is_expired': self.is_expired(),
|
||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||
'shipped_at': self.shipped_at.isoformat() if self.shipped_at else None,
|
||
'received_at': self.received_at.isoformat() if self.received_at else None
|
||
}
|
||
|
||
@classmethod
|
||
def generate_order_sn(cls):
|
||
"""生成订单号"""
|
||
import time
|
||
import random
|
||
timestamp = str(int(time.time()))
|
||
random_str = str(random.randint(100000, 999999))
|
||
return f"TB{timestamp}{random_str}"
|
||
|
||
def __repr__(self):
|
||
return f'<Order {self.order_sn}>'
|
||
|
||
|
||
class OrderItem(db.Model):
|
||
"""订单商品明细模型"""
|
||
__tablename__ = 'order_items'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
|
||
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
|
||
sku_code = db.Column(db.String(100))
|
||
product_name = db.Column(db.String(200), nullable=False)
|
||
product_image = db.Column(db.String(255))
|
||
spec_combination = db.Column(db.String(255))
|
||
price = db.Column(db.Numeric(10, 2), nullable=False)
|
||
quantity = db.Column(db.Integer, nullable=False)
|
||
total_price = db.Column(db.Numeric(10, 2), nullable=False)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
# 关联关系
|
||
product = db.relationship('Product', backref='order_items')
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'product_id': self.product_id,
|
||
'product_name': self.product_name,
|
||
'product_image': self.product_image,
|
||
'spec_combination': self.spec_combination,
|
||
'price': float(self.price),
|
||
'quantity': self.quantity,
|
||
'total_price': float(self.total_price)
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<OrderItem {self.product_name}>'
|
||
|
||
|
||
class ShippingInfo(db.Model):
|
||
"""物流信息模型"""
|
||
__tablename__ = 'shipping_info'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
|
||
shipping_company = db.Column(db.String(50))
|
||
tracking_number = db.Column(db.String(100))
|
||
shipping_status = db.Column(db.Integer, default=1) # 1-已发货 2-运输中 3-已送达
|
||
shipping_address = db.Column(db.Text)
|
||
estimated_delivery = db.Column(db.DateTime)
|
||
actual_delivery = db.Column(db.DateTime)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
# 关联关系
|
||
order = db.relationship('Order', backref='shipping_info')
|
||
|
||
def __repr__(self):
|
||
return f'<ShippingInfo {self.tracking_number}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/payment.py
|
||
📊 大小: 2370 bytes (2.31 KB)
|
||
🕒 修改时间: 2025-07-04 01:56:46
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
支付模型
|
||
"""
|
||
from datetime import datetime
|
||
from config.database import db
|
||
|
||
|
||
class Payment(db.Model):
|
||
"""支付记录模型"""
|
||
__tablename__ = 'payments'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
|
||
payment_sn = db.Column(db.String(64), unique=True, nullable=False)
|
||
payment_method = db.Column(db.String(20), nullable=False)
|
||
amount = db.Column(db.Numeric(10, 2), nullable=False)
|
||
status = db.Column(db.Integer, default=1) # 1-待支付 2-支付成功 3-支付失败 4-已退款
|
||
third_party_sn = db.Column(db.String(100)) # 第三方支付流水号
|
||
callback_data = db.Column(db.Text) # 支付回调数据
|
||
paid_at = db.Column(db.DateTime)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
# 关联关系
|
||
order = db.relationship('Order', backref='payments')
|
||
|
||
# 状态常量
|
||
STATUS_PENDING = 1 # 待支付
|
||
STATUS_SUCCESS = 2 # 支付成功
|
||
STATUS_FAILED = 3 # 支付失败
|
||
STATUS_REFUNDED = 4 # 已退款
|
||
|
||
STATUS_CHOICES = {
|
||
STATUS_PENDING: '待支付',
|
||
STATUS_SUCCESS: '支付成功',
|
||
STATUS_FAILED: '支付失败',
|
||
STATUS_REFUNDED: '已退款'
|
||
}
|
||
|
||
def get_status_text(self):
|
||
"""获取状态文本"""
|
||
return self.STATUS_CHOICES.get(self.status, '未知状态')
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'payment_sn': self.payment_sn,
|
||
'payment_method': self.payment_method,
|
||
'amount': float(self.amount),
|
||
'status': self.status,
|
||
'status_text': self.get_status_text(),
|
||
'third_party_sn': self.third_party_sn,
|
||
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
@classmethod
|
||
def generate_payment_sn(cls):
|
||
"""生成支付流水号"""
|
||
import time
|
||
import random
|
||
timestamp = str(int(time.time()))
|
||
random_str = str(random.randint(100000, 999999))
|
||
return f"PAY{timestamp}{random_str}"
|
||
|
||
def __repr__(self):
|
||
return f'<Payment {self.payment_sn}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/product.py
|
||
📊 大小: 9669 bytes (9.44 KB)
|
||
🕒 修改时间: 2025-07-03 07:03:27
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
商品相关模型
|
||
"""
|
||
from datetime import datetime
|
||
from config.database import db
|
||
import json
|
||
|
||
|
||
class Category(db.Model):
|
||
"""商品分类模型"""
|
||
__tablename__ = 'categories'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
name = db.Column(db.String(50), nullable=False)
|
||
parent_id = db.Column(db.Integer, default=0)
|
||
level = db.Column(db.Integer, default=1)
|
||
sort_order = db.Column(db.Integer, default=0)
|
||
icon_url = db.Column(db.String(255))
|
||
is_active = db.Column(db.Integer, default=1)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'name': self.name,
|
||
'parent_id': self.parent_id,
|
||
'level': self.level,
|
||
'sort_order': self.sort_order,
|
||
'icon_url': self.icon_url,
|
||
'is_active': self.is_active,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<Category {self.name}>'
|
||
|
||
|
||
class SpecName(db.Model):
|
||
"""规格名称模型"""
|
||
__tablename__ = 'spec_names'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
name = db.Column(db.String(50), nullable=False)
|
||
sort_order = db.Column(db.Integer, default=0)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'name': self.name,
|
||
'sort_order': self.sort_order
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<SpecName {self.name}>'
|
||
|
||
|
||
class SpecValue(db.Model):
|
||
"""规格值模型"""
|
||
__tablename__ = 'spec_values'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
spec_name_id = db.Column(db.Integer, db.ForeignKey('spec_names.id'), nullable=False)
|
||
value = db.Column(db.String(100), nullable=False)
|
||
sort_order = db.Column(db.Integer, default=0)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
spec_name = db.relationship('SpecName', backref='values')
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'spec_name_id': self.spec_name_id,
|
||
'value': self.value,
|
||
'sort_order': self.sort_order
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<SpecValue {self.value}>'
|
||
|
||
|
||
class Product(db.Model):
|
||
"""商品模型"""
|
||
__tablename__ = 'products'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
name = db.Column(db.String(200), nullable=False)
|
||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
|
||
brand = db.Column(db.String(100))
|
||
price = db.Column(db.Numeric(10, 2), nullable=False)
|
||
original_price = db.Column(db.Numeric(10, 2))
|
||
description = db.Column(db.Text)
|
||
main_image = db.Column(db.String(255))
|
||
status = db.Column(db.Integer, default=1) # 0-下架 1-上架
|
||
has_specs = db.Column(db.Integer, default=0) # 0-无规格 1-有规格
|
||
sales_count = db.Column(db.Integer, default=0)
|
||
view_count = db.Column(db.Integer, default=0)
|
||
weight = db.Column(db.Numeric(8, 2))
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
category = db.relationship('Category', backref='products')
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'name': self.name,
|
||
'category_id': self.category_id,
|
||
'category_name': self.category.name if self.category else '',
|
||
'brand': self.brand,
|
||
'price': float(self.price) if self.price else 0,
|
||
'original_price': float(self.original_price) if self.original_price else None,
|
||
'description': self.description,
|
||
'main_image': self.main_image,
|
||
'status': self.status,
|
||
'has_specs': self.has_specs,
|
||
'sales_count': self.sales_count,
|
||
'view_count': self.view_count,
|
||
'weight': float(self.weight) if self.weight else None,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<Product {self.name}>'
|
||
|
||
|
||
class ProductImage(db.Model):
|
||
"""商品图片模型"""
|
||
__tablename__ = 'product_images'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
|
||
image_url = db.Column(db.String(255), nullable=False)
|
||
sort_order = db.Column(db.Integer, default=0)
|
||
is_main = db.Column(db.Integer, default=0) # 0-否 1-是
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
product = db.relationship('Product', backref='images')
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'product_id': self.product_id,
|
||
'image_url': self.image_url,
|
||
'sort_order': self.sort_order,
|
||
'is_main': self.is_main,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<ProductImage {self.id}>'
|
||
|
||
|
||
class ProductSpecRelation(db.Model):
|
||
"""商品规格关联模型"""
|
||
__tablename__ = 'product_spec_relations'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
|
||
spec_name_id = db.Column(db.Integer, db.ForeignKey('spec_names.id'), nullable=False)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
product = db.relationship('Product', backref='spec_relations')
|
||
spec_name = db.relationship('SpecName')
|
||
|
||
def __repr__(self):
|
||
return f'<ProductSpecRelation {self.product_id}-{self.spec_name_id}>'
|
||
|
||
|
||
class ProductInventory(db.Model):
|
||
"""商品库存模型(SKU)"""
|
||
__tablename__ = 'product_inventory'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
|
||
sku_code = db.Column(db.String(100), unique=True, nullable=False)
|
||
spec_combination = db.Column(db.JSON)
|
||
price_adjustment = db.Column(db.Numeric(10, 2), default=0)
|
||
stock = db.Column(db.Integer, nullable=False, default=0)
|
||
warning_stock = db.Column(db.Integer, default=10)
|
||
is_default = db.Column(db.Integer, default=0)
|
||
status = db.Column(db.Integer, default=1)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
product = db.relationship('Product', backref='inventory')
|
||
|
||
def get_final_price(self):
|
||
"""获取最终价格"""
|
||
base_price = float(self.product.price) if self.product and self.product.price else 0
|
||
adjustment = float(self.price_adjustment) if self.price_adjustment else 0
|
||
return base_price + adjustment
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'product_id': self.product_id,
|
||
'sku_code': self.sku_code,
|
||
'spec_combination': self.spec_combination,
|
||
'price_adjustment': float(self.price_adjustment) if self.price_adjustment else 0,
|
||
'final_price': self.get_final_price(),
|
||
'stock': self.stock,
|
||
'warning_stock': self.warning_stock,
|
||
'is_default': self.is_default,
|
||
'status': self.status,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<ProductInventory {self.sku_code}>'
|
||
|
||
|
||
class InventoryLog(db.Model):
|
||
"""库存变更日志模型"""
|
||
__tablename__ = 'inventory_logs'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
|
||
sku_code = db.Column(db.String(100), nullable=False)
|
||
change_type = db.Column(db.Integer, nullable=False) # 1-入库 2-出库 3-调整
|
||
change_quantity = db.Column(db.Integer, nullable=False)
|
||
before_stock = db.Column(db.Integer, nullable=False)
|
||
after_stock = db.Column(db.Integer, nullable=False)
|
||
related_order_id = db.Column(db.Integer)
|
||
remark = db.Column(db.String(255))
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
product = db.relationship('Product')
|
||
|
||
@classmethod
|
||
def create_log(cls, product_id, sku_code, change_type, change_quantity,
|
||
before_stock, after_stock, related_order_id=None, remark=None):
|
||
"""创建库存变更日志"""
|
||
log = cls(
|
||
product_id=product_id,
|
||
sku_code=sku_code,
|
||
change_type=change_type,
|
||
change_quantity=change_quantity,
|
||
before_stock=before_stock,
|
||
after_stock=after_stock,
|
||
related_order_id=related_order_id,
|
||
remark=remark
|
||
)
|
||
db.session.add(log)
|
||
db.session.commit()
|
||
return log
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'product_id': self.product_id,
|
||
'sku_code': self.sku_code,
|
||
'change_type': self.change_type,
|
||
'change_quantity': self.change_quantity,
|
||
'before_stock': self.before_stock,
|
||
'after_stock': self.after_stock,
|
||
'related_order_id': self.related_order_id,
|
||
'remark': self.remark,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<InventoryLog {self.sku_code}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/review.py
|
||
📊 大小: 2204 bytes (2.15 KB)
|
||
🕒 修改时间: 2025-07-04 02:45:14
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
评价模型
|
||
"""
|
||
from datetime import datetime
|
||
import json
|
||
from config.database import db
|
||
|
||
|
||
class Review(db.Model):
|
||
"""商品评价模型"""
|
||
__tablename__ = 'reviews'
|
||
|
||
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)
|
||
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
|
||
rating = db.Column(db.Integer, nullable=False) # 1-5星
|
||
content = db.Column(db.Text)
|
||
images = db.Column(db.Text) # JSON格式存储图片URLs
|
||
is_anonymous = db.Column(db.Integer, default=0)
|
||
status = db.Column(db.Integer, default=1) # 0-隐藏 1-显示
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
# 关联关系
|
||
user = db.relationship('User', backref='reviews')
|
||
product = db.relationship('Product', backref='reviews')
|
||
order = db.relationship('Order', backref='reviews')
|
||
|
||
def get_images(self):
|
||
"""获取评价图片列表"""
|
||
if self.images:
|
||
try:
|
||
return json.loads(self.images)
|
||
except:
|
||
return []
|
||
return []
|
||
|
||
def set_images(self, image_list):
|
||
"""设置评价图片"""
|
||
if isinstance(image_list, list):
|
||
self.images = json.dumps(image_list)
|
||
|
||
def get_rating_stars(self):
|
||
"""获取星级显示"""
|
||
return '★' * self.rating + '☆' * (5 - self.rating)
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'user_id': self.user_id,
|
||
'username': self.user.username if not self.is_anonymous else '匿名用户',
|
||
'product_id': self.product_id,
|
||
'order_id': self.order_id,
|
||
'rating': self.rating,
|
||
'rating_stars': self.get_rating_stars(),
|
||
'content': self.content,
|
||
'images': self.get_images(),
|
||
'is_anonymous': self.is_anonymous,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<Review {self.id}-{self.rating}星>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/user.py
|
||
📊 大小: 1785 bytes (1.74 KB)
|
||
🕒 修改时间: 2025-07-03 04:43:31
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
用户模型
|
||
"""
|
||
from datetime import datetime
|
||
from werkzeug.security import generate_password_hash, check_password_hash
|
||
from config.database import db # 确保从正确位置导入
|
||
|
||
|
||
class User(db.Model):
|
||
__tablename__ = 'users'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
username = db.Column(db.String(50), unique=True, nullable=False)
|
||
phone = db.Column(db.String(20), unique=True)
|
||
email = db.Column(db.String(100), unique=True)
|
||
password_hash = db.Column(db.String(255), nullable=False)
|
||
nickname = db.Column(db.String(50))
|
||
avatar_url = db.Column(db.String(255))
|
||
gender = db.Column(db.Integer, default=0)
|
||
birthday = db.Column(db.Date)
|
||
status = db.Column(db.Integer, default=1)
|
||
wechat_openid = db.Column(db.String(100))
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
def set_password(self, password):
|
||
"""设置密码"""
|
||
self.password_hash = generate_password_hash(password)
|
||
|
||
def check_password(self, password):
|
||
"""验证密码"""
|
||
return check_password_hash(self.password_hash, password)
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
'id': self.id,
|
||
'username': self.username,
|
||
'phone': self.phone,
|
||
'email': self.email,
|
||
'nickname': self.nickname,
|
||
'avatar_url': self.avatar_url,
|
||
'gender': self.gender,
|
||
'birthday': self.birthday.isoformat() if self.birthday else None,
|
||
'status': self.status,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<User {self.username}>'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/models/verification.py
|
||
📊 大小: 1832 bytes (1.79 KB)
|
||
🕒 修改时间: 2025-07-03 03:40:15
|
||
🔸==============================================================================
|
||
|
||
from datetime import datetime, timedelta
|
||
from config.database import db
|
||
import random
|
||
import string
|
||
|
||
|
||
class EmailVerification(db.Model):
|
||
__tablename__ = 'email_verifications'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
email = db.Column(db.String(100), nullable=False, index=True)
|
||
code = db.Column(db.String(6), nullable=False)
|
||
type = db.Column(db.SmallInteger, nullable=False) # 1-注册 2-登录 3-找回密码
|
||
is_used = db.Column(db.SmallInteger, default=0) # 0-未使用 1-已使用
|
||
expired_at = db.Column(db.DateTime, nullable=False)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
@staticmethod
|
||
def generate_code():
|
||
"""生成6位数字验证码"""
|
||
return ''.join(random.choices(string.digits, k=6))
|
||
|
||
@classmethod
|
||
def create_verification(cls, email, code_type, expire_minutes=10):
|
||
"""创建验证码记录"""
|
||
code = cls.generate_code()
|
||
expired_at = datetime.utcnow() + timedelta(minutes=expire_minutes)
|
||
|
||
verification = cls(
|
||
email=email,
|
||
code=code,
|
||
type=code_type,
|
||
expired_at=expired_at
|
||
)
|
||
|
||
db.session.add(verification)
|
||
db.session.commit()
|
||
|
||
return verification
|
||
|
||
@classmethod
|
||
def verify_code(cls, email, code, code_type):
|
||
"""验证验证码"""
|
||
verification = cls.query.filter_by(
|
||
email=email,
|
||
code=code,
|
||
type=code_type,
|
||
is_used=0
|
||
).filter(
|
||
cls.expired_at > datetime.utcnow()
|
||
).first()
|
||
|
||
if verification:
|
||
verification.is_used = 1
|
||
db.session.commit()
|
||
return True
|
||
return False
|
||
|
||
def is_expired(self):
|
||
"""检查是否过期"""
|
||
return datetime.utcnow() > self.expired_at
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/static/js/city_data.js
|
||
📊 大小: 55620 bytes (54.32 KB)
|
||
🕒 修改时间: 2025-07-04 03:24:59
|
||
🔸==============================================================================
|
||
|
||
// 中国省市区数据
|
||
const cityData = {
|
||
'北京市': {
|
||
'北京市': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '门头沟区', '房山区', '通州区', '顺义区', '昌平区', '大兴区', '怀柔区', '平谷区', '密云区', '延庆区']
|
||
},
|
||
'上海市': {
|
||
'上海市': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '虹口区', '杨浦区', '闵行区', '宝山区', '嘉定区', '浦东新区', '金山区', '松江区', '青浦区', '奉贤区', '崇明区']
|
||
},
|
||
'天津市': {
|
||
'天津市': ['和平区', '河东区', '河西区', '南开区', '河北区', '红桥区', '东丽区', '西青区', '津南区', '北辰区', '武清区', '宝坻区', '滨海新区', '宁河区', '静海区', '蓟州区']
|
||
},
|
||
'重庆市': {
|
||
'重庆市': ['万州区', '涪陵区', '渝中区', '大渡口区', '江北区', '沙坪坝区', '九龙坡区', '南岸区', '北碚区', '綦江区', '大足区', '渝北区', '巴南区', '黔江区', '长寿区', '江津区', '合川区', '永川区', '南川区', '璧山区', '铜梁区', '潼南区', '荣昌区', '开州区', '梁平区', '武隆区', '城口县', '丰都县', '垫江县', '忠县', '云阳县', '奉节县', '巫山县', '巫溪县', '石柱土家族自治县', '秀山土家族苗族自治县', '酉阳土家族苗族自治县', '彭水苗族土家族自治县']
|
||
},
|
||
'河北省': {
|
||
'石家庄市': ['长安区', '桥西区', '新华区', '井陉矿区', '裕华区', '藁城区', '鹿泉区', '栾城区', '井陉县', '正定县', '行唐县', '灵寿县', '高邑县', '深泽县', '赞皇县', '无极县', '平山县', '元氏县', '赵县', '辛集市', '晋州市', '新乐市'],
|
||
'唐山市': ['路南区', '路北区', '古冶区', '开平区', '丰南区', '丰润区', '曹妃甸区', '滦州市', '滦南县', '乐亭县', '迁西县', '玉田县', '遵化市', '迁安市'],
|
||
'秦皇岛市': ['海港区', '山海关区', '北戴河区', '抚宁区', '青龙满族自治县', '昌黎县', '卢龙县'],
|
||
'邯郸市': ['邯山区', '丛台区', '复兴区', '峰峰矿区', '肥乡区', '永年区', '临漳县', '成安县', '大名县', '涉县', '磁县', '邱县', '鸡泽县', '广平县', '馆陶县', '魏县', '曲周县', '武安市'],
|
||
'邢台市': ['桥东区', '桥西区', '邢台县', '临城县', '内丘县', '柏乡县', '隆尧县', '任县', '南和县', '宁晋县', '巨鹿县', '新河县', '广宗县', '平乡县', '威县', '清河县', '临西县', '南宫市', '沙河市'],
|
||
'保定市': ['竞秀区', '莲池区', '满城区', '清苑区', '徐水区', '涞水县', '阜平县', '定兴县', '唐县', '高阳县', '容城县', '涞源县', '望都县', '安新县', '易县', '曲阳县', '蠡县', '顺平县', '博野县', '雄县', '涿州市', '定州市', '安国市', '高碑店市'],
|
||
'张家口市': ['桥东区', '桥西区', '宣化区', '下花园区', '万全区', '崇礼区', '张北县', '康保县', '沽源县', '尚义县', '蔚县', '阳原县', '怀安县', '怀来县', '涿鹿县', '赤城县'],
|
||
'承德市': ['双桥区', '双滦区', '鹰手营子矿区', '承德县', '兴隆县', '平泉市', '滦平县', '隆化县', '丰宁满族自治县', '宽城满族自治县', '围场满族蒙古族自治县'],
|
||
'沧州市': ['新华区', '运河区', '沧县', '青县', '东光县', '海兴县', '盐山县', '肃宁县', '南皮县', '吴桥县', '献县', '孟村回族自治县', '泊头市', '任丘市', '黄骅市', '河间市'],
|
||
'廊坊市': ['安次区', '广阳区', '固安县', '永清县', '香河县', '大城县', '文安县', '大厂回族自治县', '霸州市', '三河市'],
|
||
'衡水市': ['桃城区', '冀州区', '枣强县', '武邑县', '武强县', '饶阳县', '安平县', '故城县', '景县', '阜城县', '深州市']
|
||
},
|
||
'山西省': {
|
||
'太原市': ['小店区', '迎泽区', '杏花岭区', '尖草坪区', '万柏林区', '晋源区', '清徐县', '阳曲县', '娄烦县', '古交市'],
|
||
'大同市': ['平城区', '云冈区', '新荣区', '左云县', '阳高县', '天镇县', '广灵县', '灵丘县', '浑源县', '云州区'],
|
||
'阳泉市': ['城区', '矿区', '郊区', '平定县', '盂县'],
|
||
'长治市': ['潞州区', '上党区', '屯留区', '潞城区', '襄垣县', '平顺县', '黎城县', '壶关县', '长子县', '武乡县', '沁县', '沁源县'],
|
||
'晋城市': ['城区', '沁水县', '阳城县', '陵川县', '泽州县', '高平市'],
|
||
'朔州市': ['朔城区', '平鲁区', '山阴县', '应县', '右玉县', '怀仁市'],
|
||
'晋中市': ['榆次区', '榆社县', '左权县', '和顺县', '昔阳县', '寿阳县', '太谷县', '祁县', '平遥县', '灵石县', '介休市'],
|
||
'运城市': ['盐湖区', '临猗县', '万荣县', '闻喜县', '稷山县', '新绛县', '绛县', '垣曲县', '夏县', '平陆县', '芮城县', '永济市', '河津市'],
|
||
'忻州市': ['忻府区', '定襄县', '五台县', '代县', '繁峙县', '宁武县', '静乐县', '神池县', '五寨县', '岢岚县', '河曲县', '保德县', '偏关县', '原平市'],
|
||
'临汾市': ['尧都区', '曲沃县', '翼城县', '襄汾县', '洪洞县', '古县', '安泽县', '浮山县', '吉县', '乡宁县', '大宁县', '隰县', '永和县', '蒲县', '汾西县', '侯马市', '霍州市'],
|
||
'吕梁市': ['离石区', '文水县', '交城县', '兴县', '临县', '柳林县', '石楼县', '岚县', '方山县', '中阳县', '交口县', '孝义市', '汾阳市']
|
||
},
|
||
'内蒙古自治区': {
|
||
'呼和浩特市': ['新城区', '回民区', '玉泉区', '赛罕区', '土默特左旗', '托克托县', '和林格尔县', '清水河县', '武川县'],
|
||
'包头市': ['东河区', '昆都仑区', '青山区', '石拐区', '白云鄂博矿区', '九原区', '土默特右旗', '固阳县', '达尔罕茂明安联合旗'],
|
||
'乌海市': ['海勃湾区', '海南区', '乌达区'],
|
||
'赤峰市': ['红山区', '元宝山区', '松山区', '阿鲁科尔沁旗', '巴林左旗', '巴林右旗', '林西县', '克什克腾旗', '翁牛特旗', '喀喇沁旗', '宁城县', '敖汉旗'],
|
||
'通辽市': ['科尔沁区', '科尔沁左翼中旗', '科尔沁左翼后旗', '开鲁县', '库伦旗', '奈曼旗', '扎鲁特旗', '霍林郭勒市'],
|
||
'鄂尔多斯市': ['东胜区', '康巴什区', '达拉特旗', '准格尔旗', '鄂托克前旗', '鄂托克旗', '杭锦旗', '乌审旗', '伊金霍洛旗'],
|
||
'呼伦贝尔市': ['海拉尔区', '扎赉诺尔区', '阿荣旗', '莫力达瓦达斡尔族自治旗', '鄂伦春自治旗', '鄂温克族自治旗', '陈巴尔虎旗', '新巴尔虎左旗', '新巴尔虎右旗', '满洲里市', '牙克石市', '扎兰屯市', '额尔古纳市', '根河市'],
|
||
'巴彦淖尔市': ['临河区', '五原县', '磴口县', '乌拉特前旗', '乌拉特中旗', '乌拉特后旗', '杭锦后旗'],
|
||
'乌兰察布市': ['集宁区', '卓资县', '化德县', '商都县', '兴和县', '凉城县', '察哈尔右翼前旗', '察哈尔右翼中旗', '察哈尔右翼后旗', '四子王旗', '丰镇市'],
|
||
'兴安盟': ['乌兰浩特市', '阿尔山市', '科尔沁右翼前旗', '科尔沁右翼中旗', '扎赉特旗', '突泉县'],
|
||
'锡林郭勒盟': ['锡林浩特市', '阿巴嘎旗', '苏尼特左旗', '苏尼特右旗', '东乌珠穆沁旗', '西乌珠穆沁旗', '太仆寺旗', '镶黄旗', '正镶白旗', '正蓝旗', '多伦县', '二连浩特市'],
|
||
'阿拉善盟': ['阿拉善左旗', '阿拉善右旗', '额济纳旗']
|
||
},
|
||
'辽宁省': {
|
||
'沈阳市': ['和平区', '沈河区', '大东区', '皇姑区', '铁西区', '苏家屯区', '浑南区', '沈北新区', '于洪区', '辽中区', '康平县', '法库县', '新民市'],
|
||
'大连市': ['中山区', '西岗区', '沙河口区', '甘井子区', '旅顺口区', '金州区', '普兰店区', '长海县', '瓦房店市', '庄河市'],
|
||
'鞍山市': ['铁东区', '铁西区', '立山区', '千山区', '台安县', '岫岩满族自治县', '海城市'],
|
||
'抚顺市': ['新抚区', '东洲区', '望花区', '顺城区', '抚顺县', '新宾满族自治县', '清原满族自治县'],
|
||
'本溪市': ['平山区', '溪湖区', '明山区', '南芬区', '本溪满族自治县', '桓仁满族自治县'],
|
||
'丹东市': ['元宝区', '振兴区', '振安区', '宽甸满族自治县', '东港市', '凤城市'],
|
||
'锦州市': ['古塔区', '凌河区', '太和区', '黑山县', '义县', '凌海市', '北镇市'],
|
||
'营口市': ['站前区', '西市区', '鲅鱼圈区', '老边区', '盖州市', '大石桥市'],
|
||
'阜新市': ['海州区', '新邱区', '太平区', '清河门区', '细河区', '阜新蒙古族自治县', '彰武县'],
|
||
'辽阳市': ['白塔区', '文圣区', '宏伟区', '弓长岭区', '太子河区', '辽阳县', '灯塔市'],
|
||
'盘锦市': ['双台子区', '兴隆台区', '大洼区', '盘山县'],
|
||
'铁岭市': ['银州区', '清河区', '铁岭县', '西丰县', '昌图县', '调兵山市', '开原市'],
|
||
'朝阳市': ['双塔区', '龙城区', '朝阳县', '建平县', '喀喇沁左翼蒙古族自治县', '北票市', '凌源市'],
|
||
'葫芦岛市': ['连山区', '龙港区', '南票区', '绥中县', '建昌县', '兴城市']
|
||
},
|
||
'吉林省': {
|
||
'长春市': ['南关区', '宽城区', '朝阳区', '二道区', '绿园区', '双阳区', '九台区', '农安县', '榆树市', '德惠市'],
|
||
'吉林市': ['昌邑区', '龙潭区', '船营区', '丰满区', '永吉县', '蛟河市', '桦甸市', '舒兰市', '磐石市'],
|
||
'四平市': ['铁西区', '铁东区', '梨树县', '伊通满族自治县', '公主岭市', '双辽市'],
|
||
'辽源市': ['龙山区', '西安区', '东丰县', '东辽县'],
|
||
'通化市': ['东昌区', '二道江区', '通化县', '辉南县', '柳河县', '梅河口市', '集安市'],
|
||
'白山市': ['浑江区', '江源区', '抚松县', '靖宇县', '长白朝鲜族自治县', '临江市'],
|
||
'松原市': ['宁江区', '前郭尔罗斯蒙古族自治县', '长岭县', '乾安县', '扶余市'],
|
||
'白城市': ['洮北区', '镇赖县', '通榆县', '洮南市', '大安市'],
|
||
'延边朝鲜族自治州': ['延吉市', '图们市', '敦化市', '珲春市', '龙井市', '和龙市', '汪清县', '安图县']
|
||
},
|
||
'黑龙江省': {
|
||
'哈尔滨市': ['道里区', '南岗区', '道外区', '平房区', '松北区', '香坊区', '呼兰区', '阿城区', '双城区', '依兰县', '方正县', '宾县', '巴彦县', '木兰县', '通河县', '延寿县', '尚志市', '五常市'],
|
||
'齐齐哈尔市': ['龙沙区', '建华区', '铁锋区', '昂昂溪区', '富拉尔基区', '碾子山区', '梅里斯达斡尔族区', '龙江县', '依安县', '泰来县', '甘南县', '富裕县', '克山县', '克东县', '拜泉县', '讷河市'],
|
||
'鸡西市': ['鸡冠区', '恒山区', '滴道区', '梨树区', '城子河区', '麻山区', '鸡东县', '虎林市', '密山市'],
|
||
'鹤岗市': ['向阳区', '工农区', '南山区', '兴安区', '东山区', '兴山区', '萝北县', '绥滨县'],
|
||
'双鸭山市': ['尖山区', '岭东区', '四方台区', '宝山区', '集贤县', '友谊县', '宝清县', '饶河县'],
|
||
'大庆市': ['萨尔图区', '龙凤区', '让胡路区', '红岗区', '大同区', '肇州县', '肇源县', '林甸县', '杜尔伯特蒙古族自治县'],
|
||
'伊春市': ['伊春区', '南岔区', '友好区', '西林区', '翠峦区', '新青区', '美溪区', '金山屯区', '五营区', '乌马河区', '汤旺河区', '带岭区', '乌伊岭区', '红星区', '上甘岭区', '嘉荫县', '铁力市'],
|
||
'佳木斯市': ['向阳区', '前进区', '东风区', '郊区', '桦南县', '桦川县', '汤原县', '抚远市', '同江市', '富锦市'],
|
||
'七台河市': ['新兴区', '桃山区', '茄子河区', '勃利县'],
|
||
'牡丹江市': ['东安区', '阳明区', '爱民区', '西安区', '林口县', '绥芬河市', '海林市', '宁安市', '穆棱市', '东宁市'],
|
||
'黑河市': ['爱辉区', '嫩江县', '逊克县', '孙吴县', '北安市', '五大连池市'],
|
||
'绥化市': ['北林区', '望奎县', '兰西县', '青冈县', '庆安县', '明水县', '绥棱县', '安达市', '肇东市', '海伦市'],
|
||
'大兴安岭地区': ['呼玛县', '塔河县', '漠河市']
|
||
},
|
||
'江苏省': {
|
||
'南京市': ['玄武区', '秦淮区', '建邺区', '鼓楼区', '浦口区', '栖霞区', '雨花台区', '江宁区', '六合区', '溧水区', '高淳区'],
|
||
'无锡市': ['锡山区', '惠山区', '滨湖区', '梁溪区', '新吴区', '江阴市', '宜兴市'],
|
||
'徐州市': ['鼓楼区', '云龙区', '贾汪区', '泉山区', '铜山区', '丰县', '沛县', '睢宁县', '新沂市', '邳州市'],
|
||
'常州市': ['天宁区', '钟楼区', '新北区', '武进区', '金坛区', '溧阳市'],
|
||
'苏州市': ['虎丘区', '吴中区', '相城区', '姑苏区', '吴江区', '常熟市', '张家港市', '昆山市', '太仓市'],
|
||
'南通市': ['崇川区', '港闸区', '通州区', '海安市', '如东县', '启东市', '如皋市', '海门市'],
|
||
'连云港市': ['连云区', '海州区', '赣榆区', '东海县', '灌云县', '灌南县'],
|
||
'淮安市': ['淮安区', '淮阴区', '清江浦区', '洪泽区', '涟水县', '盱眙县', '金湖县'],
|
||
'盐城市': ['亭湖区', '盐都区', '大丰区', '响水县', '滨海县', '阜宁县', '射阳县', '建湖县', '东台市'],
|
||
'扬州市': ['广陵区', '邗江区', '江都区', '宝应县', '仪征市', '高邮市'],
|
||
'镇江市': ['京口区', '润州区', '丹徒区', '丹阳市', '扬中市', '句容市'],
|
||
'泰州市': ['海陵区', '高港区', '姜堰区', '兴化市', '靖江市', '泰兴市'],
|
||
'宿迁市': ['宿城区', '宿豫区', '沭阳县', '泗阳县', '泗洪县']
|
||
},
|
||
'浙江省': {
|
||
'杭州市': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区', '萧山区', '余杭区', '富阳区', '临安区', '桐庐县', '淳安县', '建德市'],
|
||
'宁波市': ['海曙区', '江北区', '北仑区', '镇海区', '鄞州区', '奉化区', '象山县', '宁海县', '余姚市', '慈溪市'],
|
||
'温州市': ['鹿城区', '龙湾区', '瓯海区', '洞头区', '永嘉县', '平阳县', '苍南县', '文成县', '泰顺县', '瑞安市', '乐清市'],
|
||
'嘉兴市': ['南湖区', '秀洲区', '嘉善县', '海盐县', '海宁市', '平湖市', '桐乡市'],
|
||
'湖州市': ['吴兴区', '南浔区', '德清县', '长兴县', '安吉县'],
|
||
'绍兴市': ['越城区', '柯桥区', '上虞区', '新昌县', '诸暨市', '嵊州市'],
|
||
'金华市': ['婺城区', '金东区', '武义县', '浦江县', '磐安县', '兰溪市', '义乌市', '东阳市', '永康市'],
|
||
'衢州市': ['柯城区', '衢江区', '常山县', '开化县', '龙游县', '江山市'],
|
||
'舟山市': ['定海区', '普陀区', '岱山县', '嵊泗县'],
|
||
'台州市': ['椒江区', '黄岩区', '路桥区', '三门县', '天台县', '仙居县', '温岭市', '临海市', '玉环市'],
|
||
'丽水市': ['莲都区', '青田县', '缙云县', '遂昌县', '松阳县', '云和县', '庆元县', '景宁畲族自治县', '龙泉市']
|
||
},
|
||
'安徽省': {
|
||
'合肥市': ['瑶海区', '庐阳区', '蜀山区', '包河区', '长丰县', '肥东县', '肥西县', '庐江县', '巢湖市'],
|
||
'芜湖市': ['镜湖区', '弋江区', '鸠江区', '三山区', '芜湖县', '繁昌县', '南陵县', '无为市'],
|
||
'蚌埠市': ['龙子湖区', '蚌山区', '禹会区', '淮上区', '怀远县', '五河县', '固镇县'],
|
||
'淮南市': ['大通区', '田家庵区', '谢家集区', '八公山区', '潘集区', '凤台县', '寿县'],
|
||
'马鞍山市': ['花山区', '雨山区', '博望区', '当涂县', '含山县', '和县'],
|
||
'淮北市': ['杜集区', '相山区', '烈山区', '濉溪县'],
|
||
'铜陵市': ['铜官区', '义安区', '郊区', '枞阳县'],
|
||
'安庆市': ['迎江区', '大观区', '宜秀区', '怀宁县', '潜山市', '太湖县', '宿松县', '望江县', '岳西县', '桐城市'],
|
||
'黄山市': ['屯溪区', '黄山区', '徽州区', '歙县', '休宁县', '黟县', '祁门县'],
|
||
'滁州市': ['琅琊区', '南谯区', '来安县', '全椒县', '定远县', '凤阳县', '天长市', '明光市'],
|
||
'阜阳市': ['颍州区', '颍东区', '颍泉区', '临泉县', '太和县', '阜南县', '颍上县', '界首市'],
|
||
'宿州市': ['埇桥区', '砀山县', '萧县', '灵璧县', '泗县'],
|
||
'六安市': ['金安区', '裕安区', '叶集区', '霍邱县', '舒城县', '金寨县', '霍山县'],
|
||
'亳州市': ['谯城区', '涡阳县', '蒙城县', '利辛县'],
|
||
'池州市': ['贵池区', '东至县', '石台县', '青阳县'],
|
||
'宣城市': ['宣州区', '郎溪县', '广德市', '泾县', '绩溪县', '旌德县', '宁国市']
|
||
},
|
||
'福建省': {
|
||
'福州市': ['鼓楼区', '台江区', '仓山区', '马尾区', '晋安区', '长乐区', '闽侯县', '连江县', '罗源县', '闽清县', '永泰县', '平潭县', '福清市'],
|
||
'厦门市': ['思明区', '海沧区', '湖里区', '集美区', '同安区', '翔安区'],
|
||
'莆田市': ['城厢区', '涵江区', '荔城区', '秀屿区', '仙游县'],
|
||
'三明市': ['梅列区', '三元区', '明溪县', '清流县', '宁化县', '大田县', '尤溪县', '沙县', '将乐县', '泰宁县', '建宁县', '永安市'],
|
||
'泉州市': ['鲤城区', '丰泽区', '洛江区', '泉港区', '惠安县', '安溪县', '永春县', '德化县', '金门县', '石狮市', '晋江市', '南安市'],
|
||
'漳州市': ['芗城区', '龙文区', '云霄县', '漳浦县', '诏安县', '长泰县', '东山县', '南靖县', '平和县', '华安县', '龙海市'],
|
||
'南平市': ['延平区', '建阳区', '顺昌县', '浦城县', '光泽县', '松溪县', '政和县', '邵武市', '武夷山市', '建瓯市'],
|
||
'龙岩市': ['新罗区', '永定区', '长汀县', '上杭县', '武平县', '连城县', '漳平市'],
|
||
'宁德市': ['蕉城区', '霞浦县', '古田县', '屏南县', '寿宁县', '周宁县', '柘荣县', '福安市', '福鼎市']
|
||
},
|
||
'江西省': {
|
||
'南昌市': ['东湖区', '西湖区', '青云谱区', '湾里区', '青山湖区', '新建区', '南昌县', '安义县', '进贤县'],
|
||
'景德镇市': ['昌江区', '珠山区', '浮梁县', '乐平市'],
|
||
'萍乡市': ['安源区', '湘东区', '莲花县', '上栗县', '芦溪县'],
|
||
'九江市': ['濂溪区', '浔阳区', '柴桑区', '武宁县', '修水县', '永修县', '德安县', '都昌县', '湖口县', '彭泽县', '瑞昌市', '共青城市', '庐山市'],
|
||
'新余市': ['渝水区', '分宜县'],
|
||
'鹰潭市': ['月湖区', '余江区', '贵溪市'],
|
||
'赣州市': ['章贡区', '南康区', '赣县区', '信丰县', '大余县', '上犹县', '崇义县', '安远县', '龙南县', '定南县', '全南县', '宁都县', '于都县', '兴国县', '会昌县', '寻乌县', '石城县', '瑞金市'],
|
||
'吉安市': ['吉州区', '青原区', '吉安县', '吉水县', '峡江县', '新干县', '永丰县', '泰和县', '遂川县', '万安县', '安福县', '永新县', '井冈山市'],
|
||
'宜春市': ['袁州区', '奉新县', '万载县', '上高县', '宜丰县', '靖安县', '铜鼓县', '丰城市', '樟树市', '高安市'],
|
||
'抚州市': ['临川区', '东乡区', '南城县', '黎川县', '南丰县', '崇仁县', '乐安县', '宜黄县', '金溪县', '资溪县', '广昌县'],
|
||
'上饶市': ['信州区', '广丰区', '广信区', '玉山县', '铅山县', '横峰县', '弋阳县', '余干县', '鄱阳县', '万年县', '婺源县', '德兴市']
|
||
},
|
||
'山东省': {
|
||
'济南市': ['历下区', '市中区', '槐荫区', '天桥区', '历城区', '长清区', '章丘区', '济阳区', '莱芜区', '钢城区', '平阴县', '商河县'],
|
||
'青岛市': ['市南区', '市北区', '黄岛区', '崂山区', '李沧区', '城阳区', '即墨区', '胶州市', '平度市', '莱西市'],
|
||
'淄博市': ['淄川区', '张店区', '博山区', '临淄区', '周村区', '桓台县', '高青县', '沂源县'],
|
||
'枣庄市': ['市中区', '薛城区', '峄城区', '台儿庄区', '山亭区', '滕州市'],
|
||
'东营市': ['东营区', '河口区', '垦利区', '利津县', '广饶县'],
|
||
'烟台市': ['芝罘区', '福山区', '牟平区', '莱山区', '长岛县', '龙口市', '莱阳市', '莱州市', '蓬莱市', '招远市', '栖霞市', '海阳市'],
|
||
'潍坊市': ['潍城区', '寒亭区', '坊子区', '奎文区', '临朐县', '昌乐县', '青州市', '诸城市', '寿光市', '安丘市', '高密市', '昌邑市'],
|
||
'济宁市': ['任城区', '兖州区', '微山县', '鱼台县', '金乡县', '嘉祥县', '汶上县', '泗水县', '梁山县', '曲阜市', '邹城市'],
|
||
'泰安市': ['泰山区', '岱岳区', '宁阳县', '东平县', '新泰市', '肥城市'],
|
||
'威海市': ['环翠区', '文登区', '荣成市', '乳山市'],
|
||
'日照市': ['东港区', '岚山区', '五莲县', '莒县'],
|
||
'临沂市': ['兰山区', '罗庄区', '河东区', '沂南县', '郯城县', '沂水县', '兰陵县', '费县', '平邑县', '莒南县', '蒙阴县', '临沭县'],
|
||
'德州市': ['德城区', '陵城区', '宁津县', '庆云县', '临邑县', '齐河县', '平原县', '夏津县', '武城县', '乐陵市', '禹城市'],
|
||
'聊城市': ['东昌府区', '茌平区', '阳谷县', '莘县', '茌平县', '东阿县', '冠县', '高唐县', '临清市'],
|
||
'滨州市': ['滨城区', '沾化区', '惠民县', '阳信县', '无棣县', '博兴县', '邹平市'],
|
||
'菏泽市': ['牡丹区', '定陶区', '曹县', '单县', '成武县', '巨野县', '郓城县', '鄄城县', '东明县']
|
||
},
|
||
'河南省': {
|
||
'郑州市': ['中原区', '二七区', '管城回族区', '金水区', '上街区', '惠济区', '中牟县', '巩义市', '荥阳市', '新密市', '新郑市', '登封市'],
|
||
'开封市': ['龙亭区', '顺河回族区', '鼓楼区', '禹王台区', '祥符区', '杞县', '通许县', '尉氏县', '兰考县'],
|
||
'洛阳市': ['老城区', '西工区', '瀍河回族区', '涧西区', '吉利区', '洛龙区', '孟津县', '新安县', '栾川县', '嵩县', '汝阳县', '宜阳县', '洛宁县', '伊川县', '偃师市'],
|
||
'平顶山市': ['新华区', '卫东区', '石龙区', '湛河区', '宝丰县', '叶县', '鲁山县', '郏县', '舞钢市', '汝州市'],
|
||
'安阳市': ['文峰区', '北关区', '殷都区', '龙安区', '安阳县', '汤阴县', '滑县', '内黄县', '林州市'],
|
||
'鹤壁市': ['鹤山区', '山城区', '淇滨区', '浚县', '淇县'],
|
||
'新乡市': ['红旗区', '卫滨区', '凤泉区', '牧野区', '新乡县', '获嘉县', '原阳县', '延津县', '封丘县', '长垣市', '卫辉市', '辉县市'],
|
||
'焦作市': ['解放区', '中站区', '马村区', '山阳区', '修武县', '博爱县', '武陟县', '温县', '沁阳市', '孟州市'],
|
||
'濮阳市': ['华龙区', '清丰县', '南乐县', '范县', '台前县', '濮阳县'],
|
||
'许昌市': ['魏都区', '建安区', '鄢陵县', '襄城县', '禹州市', '长葛市'],
|
||
'漯河市': ['源汇区', '郾城区', '召陵区', '舞阳县', '临颍县'],
|
||
'三门峡市': ['湖滨区', '陕州区', '渑池县', '卢氏县', '义马市', '灵宝市'],
|
||
'南阳市': ['宛城区', '卧龙区', '南召县', '方城县', '西峡县', '镇平县', '内乡县', '淅川县', '社旗县', '唐河县', '新野县', '桐柏县', '邓州市'],
|
||
'商丘市': ['梁园区', '睢阳区', '民权县', '睢县', '宁陵县', '柘城县', '虞城县', '夏邑县', '永城市'],
|
||
'信阳市': ['浉河区', '平桥区', '罗山县', '光山县', '新县', '商城县', '固始县', '潢川县', '淮滨县', '息县'],
|
||
'周口市': ['川汇区', '扶沟县', '西华县', '商水县', '沈丘县', '郸城县', '淮阳区', '太康县', '鹿邑县', '项城市'],
|
||
'驻马店市': ['驿城区', '西平县', '上蔡县', '平舆县', '正阳县', '确山县', '泌阳县', '汝南县', '遂平县', '新蔡县'],
|
||
'济源市': ['济源市']
|
||
},
|
||
'湖北省': {
|
||
'武汉市': ['江岸区', '江汉区', '硚口区', '汉阳区', '武昌区', '青山区', '洪山区', '东西湖区', '汉南区', '蔡甸区', '江夏区', '黄陂区', '新洲区'],
|
||
'黄石市': ['黄石港区', '西塞山区', '下陆区', '铁山区', '阳新县', '大冶市'],
|
||
'十堰市': ['茅箭区', '张湾区', '郧阳区', '郧西县', '竹山县', '竹溪县', '房县', '丹江口市'],
|
||
'宜昌市': ['西陵区', '伍家岗区', '点军区', '猇亭区', '夷陵区', '远安县', '兴山县', '秭归县', '长阳土家族自治县', '五峰土家族自治县', '宜都市', '当阳市', '枝江市'],
|
||
'襄阳市': ['襄城区', '樊城区', '襄州区', '南漳县', '谷城县', '保康县', '老河口市', '枣阳市', '宜城市'],
|
||
'鄂州市': ['梁子湖区', '华容区', '鄂城区'],
|
||
'荆门市': ['东宝区', '掇刀区', '京山市', '沙洋县', '钟祥市'],
|
||
'孝感市': ['孝南区', '孝昌县', '大悟县', '云梦县', '应城市', '安陆市', '汉川市'],
|
||
'荆州市': ['沙市区', '荆州区', '公安县', '监利县', '江陵县', '石首市', '洪湖市', '松滋市'],
|
||
'黄冈市': ['黄州区', '团风县', '红安县', '罗田县', '英山县', '浠水县', '蕲春县', '黄梅县', '麻城市', '武穴市'],
|
||
'咸宁市': ['咸安区', '嘉鱼县', '通城县', '崇阳县', '通山县', '赤壁市'],
|
||
'随州市': ['曾都区', '随县', '广水市'],
|
||
'恩施土家族苗族自治州': ['恩施市', '利川市', '建始县', '巴东县', '宣恩县', '咸丰县', '来凤县', '鹤峰县'],
|
||
'仙桃市': ['仙桃市'],
|
||
'潜江市': ['潜江市'],
|
||
'天门市': ['天门市'],
|
||
'神农架林区': ['神农架林区']
|
||
},
|
||
'湖南省': {
|
||
'长沙市': ['芙蓉区', '天心区', '岳麓区', '开福区', '雨花区', '望城区', '长沙县', '宁乡市', '浏阳市'],
|
||
'株洲市': ['荷塘区', '芦淞区', '石峰区', '天元区', '渌口区', '攸县', '茶陵县', '炎陵县', '醴陵市'],
|
||
'湘潭市': ['雨湖区', '岳塘区', '湘潭县', '湘乡市', '韶山市'],
|
||
'衡阳市': ['珠晖区', '雁峰区', '石鼓区', '蒸湘区', '南岳区', '衡阳县', '衡南县', '衡山县', '衡东县', '祁东县', '耒阳市', '常宁市'],
|
||
'邵阳市': ['双清区', '大祥区', '北塔区', '邵东市', '新邵县', '邵阳县', '隆回县', '洞口县', '绥宁县', '新宁县', '城步苗族自治县', '武冈市'],
|
||
'岳阳市': ['岳阳楼区', '云溪区', '君山区', '岳阳县', '华容县', '湘阴县', '平江县', '汨罗市', '临湘市'],
|
||
'常德市': ['武陵区', '鼎城区', '安乡县', '汉寿县', '澧县', '临澧县', '桃源县', '石门县', '津市市'],
|
||
'张家界市': ['永定区', '武陵源区', '慈利县', '桑植县'],
|
||
'益阳市': ['资阳区', '赫山区', '南县', '桃江县', '安化县', '沅江市'],
|
||
'郴州市': ['北湖区', '苏仙区', '桂阳县', '宜章县', '永兴县', '嘉禾县', '临武县', '汝城县', '桂东县', '安仁县', '资兴市'],
|
||
'永州市': ['零陵区', '冷水滩区', '祁阳县', '东安县', '双牌县', '道县', '江永县', '宁远县', '蓝山县', '新田县', '江华瑶族自治县'],
|
||
'怀化市': ['鹤城区', '中方县', '沅陵县', '辰溪县', '溆浦县', '会同县', '麻阳苗族自治县', '新晃侗族自治县', '芷江侗族自治县', '靖州苗族侗族自治县', '通道侗族自治县', '洪江市'],
|
||
'娄底市': ['娄星区', '双峰县', '新化县', '冷水江市', '涟源市'],
|
||
'湘西土家族苗族自治州': ['吉首市', '泸溪县', '凤凰县', '花垣县', '保靖县', '古丈县', '永顺县', '龙山县']
|
||
},
|
||
'广东省': {
|
||
'广州市': ['荔湾区', '越秀区', '海珠区', '天河区', '白云区', '黄埔区', '番禺区', '花都区', '南沙区', '从化区', '增城区'],
|
||
'深圳市': ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区', '龙华区', '坪山区', '光明区', '大鹏新区'],
|
||
'珠海市': ['香洲区', '斗门区', '金湾区'],
|
||
'汕头市': ['龙湖区', '金平区', '濠江区', '潮阳区', '潮南区', '澄海区', '南澳县'],
|
||
'佛山市': ['禅城区', '南海区', '顺德区', '三水区', '高明区'],
|
||
'韶关市': ['武江区', '浈江区', '曲江区', '始兴县', '仁化县', '翁源县', '乳源瑶族自治县', '新丰县', '乐昌市', '南雄市'],
|
||
'湛江市': ['赤坎区', '霞山区', '坡头区', '麻章区', '遂溪县', '徐闻县', '廉江市', '雷州市', '吴川市'],
|
||
'肇庆市': ['端州区', '鼎湖区', '高要区', '广宁县', '怀集县', '封开县', '德庆县', '四会市'],
|
||
'江门市': ['蓬江区', '江海区', '新会区', '台山市', '开平市', '鹤山市', '恩平市'],
|
||
'茂名市': ['茂南区', '电白区', '高州市', '化州市', '信宜市'],
|
||
'惠州市': ['惠城区', '惠阳区', '博罗县', '惠东县', '龙门县'],
|
||
'梅州市': ['梅江区', '梅县区', '大埔县', '丰顺县', '五华县', '平远县', '蕉岭县', '兴宁市'],
|
||
'汕尾市': ['城区', '海丰县', '陆河县', '陆丰市'],
|
||
'河源市': ['源城区', '紫金县', '龙川县', '连平县', '和平县', '东源县'],
|
||
'阳江市': ['江城区', '阳东区', '阳西县', '阳春市'],
|
||
'清远市': ['清城区', '清新区', '佛冈县', '阳山县', '连山壮族瑶族自治县', '连南瑶族自治县', '英德市', '连州市'],
|
||
'东莞市': ['东莞市'],
|
||
'中山市': ['中山市'],
|
||
'潮州市': ['湘桥区', '潮安区', '饶平县'],
|
||
'揭阳市': ['榕城区', '揭东区', '揭西县', '惠来县', '普宁市'],
|
||
'云浮市': ['云城区', '云安区', '新兴县', '郁南县', '罗定市']
|
||
},
|
||
'广西壮族自治区': {
|
||
'南宁市': ['兴宁区', '青秀区', '江南区', '西乡塘区', '良庆区', '邕宁区', '武鸣区', '隆安县', '马山县', '上林县', '宾阳县', '横县'],
|
||
'柳州市': ['城中区', '鱼峰区', '柳南区', '柳北区', '柳江区', '柳城县', '鹿寨县', '融安县', '融水苗族自治县', '三江侗族自治县'],
|
||
'桂林市': ['秀峰区', '叠彩区', '象山区', '七星区', '雁山区', '临桂区', '阳朔县', '灵川县', '全州县', '兴安县', '永福县', '灌阳县', '龙胜各族自治县', '资源县', '平乐县', '荔浦市', '恭城瑶族自治县'],
|
||
'梧州市': ['万秀区', '长洲区', '龙圩区', '苍梧县', '藤县', '蒙山县', '岑溪市'],
|
||
'北海市': ['海城区', '银海区', '铁山港区', '合浦县'],
|
||
'防城港市': ['港口区', '防城区', '上思县', '东兴市'],
|
||
'钦州市': ['钦南区', '钦北区', '灵山县', '浦北县'],
|
||
'贵港市': ['港北区', '港南区', '覃塘区', '平南县', '桂平市'],
|
||
'玉林市': ['玉州区', '福绵区', '容县', '陆川县', '博白县', '兴业县', '北流市'],
|
||
'百色市': ['右江区', '田阳区', '田东县', '平果市', '德保县', '那坡县', '凌云县', '乐业县', '田林县', '西林县', '隆林各族自治县', '靖西市'],
|
||
'贺州市': ['八步区', '平桂区', '昭平县', '钟山县', '富川瑶族自治县'],
|
||
'河池市': ['金城江区', '宜州区', '南丹县', '天峨县', '凤山县', '东兰县', '罗城仫佬族自治县', '环江毛南族自治县', '巴马瑶族自治县', '都安瑶族自治县', '大化瑶族自治县'],
|
||
'来宾市': ['兴宾区', '忻城县', '象州县', '武宣县', '金秀瑶族自治县', '合山市'],
|
||
'崇左市': ['江州区', '扶绥县', '宁明县', '龙州县', '大新县', '天等县', '凭祥市']
|
||
},
|
||
'海南省': {
|
||
'海口市': ['秀英区', '龙华区', '琼山区', '美兰区'],
|
||
'三亚市': ['海棠区', '吉阳区', '天涯区', '崖州区'],
|
||
'三沙市': ['西沙群岛', '南沙群岛', '中沙群岛'],
|
||
'儋州市': ['儋州市'],
|
||
'五指山市': ['五指山市'],
|
||
'琼海市': ['琼海市'],
|
||
'文昌市': ['文昌市'],
|
||
'万宁市': ['万宁市'],
|
||
'东方市': ['东方市'],
|
||
'定安县': ['定安县'],
|
||
'屯昌县': ['屯昌县'],
|
||
'澄迈县': ['澄迈县'],
|
||
'临高县': ['临高县'],
|
||
'白沙黎族自治县': ['白沙黎族自治县'],
|
||
'昌江黎族自治县': ['昌江黎族自治县'],
|
||
'乐东黎族自治县': ['乐东黎族自治县'],
|
||
'陵水黎族自治县': ['陵水黎族自治县'],
|
||
'保亭黎族苗族自治县': ['保亭黎族苗族自治县'],
|
||
'琼中黎族苗族自治县': ['琼中黎族苗族自治县']
|
||
},
|
||
'四川省': {
|
||
'成都市': ['锦江区', '青羊区', '金牛区', '武侯区', '成华区', '龙泉驿区', '青白江区', '新都区', '温江区', '双流区', '郫都区', '新津区', '金堂县', '大邑县', '蒲江县', '都江堰市', '彭州市', '邛崃市', '崇州市', '简阳市'],
|
||
'自贡市': ['自流井区', '贡井区', '大安区', '沿滩区', '荣县', '富顺县'],
|
||
'攀枝花市': ['东区', '西区', '仁和区', '米易县', '盐边县'],
|
||
'泸州市': ['江阳区', '纳溪区', '龙马潭区', '泸县', '合江县', '叙永县', '古蔺县'],
|
||
'德阳市': ['旌阳区', '罗江区', '中江县', '广汉市', '什邡市', '绵竹市'],
|
||
'绵阳市': ['涪城区', '游仙区', '安州区', '三台县', '盐亭县', '梓潼县', '北川羌族自治县', '平武县', '江油市'],
|
||
'广元市': ['利州区', '昭化区', '朝天区', '旺苍县', '青川县', '剑阁县', '苍溪县'],
|
||
'遂宁市': ['船山区', '安居区', '蓬溪县', '射洪市', '大英县'],
|
||
'内江市': ['市中区', '东兴区', '威远县', '资中县', '隆昌市'],
|
||
'乐山市': ['市中区', '沙湾区', '五通桥区', '金口河区', '犍为县', '井研县', '夹江县', '沐川县', '峨边彝族自治县', '马边彝族自治县', '峨眉山市'],
|
||
'南充市': ['顺庆区', '高坪区', '嘉陵区', '南部县', '营山县', '蓬安县', '仪陇县', '西充县', '阆中市'],
|
||
'眉山市': ['东坡区', '彭山区', '仁寿县', '洪雅县', '丹棱县', '青神县'],
|
||
'宜宾市': ['翠屏区', '南溪区', '叙州区', '江安县', '长宁县', '高县', '珙县', '筠连县', '兴文县', '屏山县'],
|
||
'广安市': ['广安区', '前锋区', '岳池县', '武胜县', '邻水县', '华蓥市'],
|
||
'达州市': ['通川区', '达川区', '宣汉县', '开江县', '大竹县', '渠县', '万源市'],
|
||
'雅安市': ['雨城区', '名山区', '荥经县', '汉源县', '石棉县', '天全县', '芦山县', '宝兴县'],
|
||
'巴中市': ['巴州区', '恩阳区', '通江县', '南江县', '平昌县'],
|
||
'资阳市': ['雁江区', '安岳县', '乐至县'],
|
||
'阿坝藏族羌族自治州': ['马尔康市', '汶川县', '理县', '茂县', '松潘县', '九寨沟县', '金川县', '小金县', '黑水县', '壤塘县', '阿坝县', '若尔盖县', '红原县'],
|
||
'甘孜藏族自治州': ['康定市', '泸定县', '丹巴县', '九龙县', '雅江县', '道孚县', '炉霍县', '甘孜县', '新龙县', '德格县', '白玉县', '石渠县', '色达县', '理塘县', '巴塘县', '乡城县', '稻城县', '得荣县'],
|
||
'凉山彝族自治州': ['西昌市', '木里藏族自治县', '盐源县', '德昌县', '会理市', '会东县', '宁南县', '普格县', '布拖县', '金阳县', '昭觉县', '喜德县', '冕宁县', '越西县', '甘洛县', '美姑县', '雷波县']
|
||
},
|
||
'贵州省': {
|
||
'贵阳市': ['南明区', '云岩区', '花溪区', '乌当区', '白云区', '观山湖区', '开阳县', '息烽县', '修文县', '清镇市'],
|
||
'六盘水市': ['钟山区', '六枝特区', '水城区', '盘州市'],
|
||
'遵义市': ['红花岗区', '汇川区', '播州区', '桐梓县', '绥阳县', '正安县', '道真仡佬族苗族自治县', '务川仡佬族苗族自治县', '凤冈县', '湄潭县', '余庆县', '习水县', '赤水市', '仁怀市'],
|
||
'安顺市': ['西秀区', '平坝区', '普定县', '镇宁布依族苗族自治县', '关岭布依族苗族自治县', '紫云苗族布依族自治县'],
|
||
'毕节市': ['七星关区', '大方县', '黔西市', '金沙县', '织金县', '纳雍县', '威宁彝族回族苗族自治县', '赫章县'],
|
||
'铜仁市': ['碧江区', '万山区', '江口县', '玉屏侗族自治县', '石阡县', '思南县', '印江土家族苗族自治县', '德江县', '沿河土家族自治县', '松桃苗族自治县'],
|
||
'黔西南布依族苗族自治州': ['兴义市', '兴仁市', '普安县', '晴隆县', '贞丰县', '望谟县', '册亨县', '安龙县'],
|
||
'黔东南苗族侗族自治州': ['凯里市', '黄平县', '施秉县', '三穗县', '镇远县', '岑巩县', '天柱县', '锦屏县', '剑河县', '台江县', '黎平县', '榕江县', '从江县', '雷山县', '麻江县', '丹寨县'],
|
||
'黔南布依族苗族自治州': ['都匀市', '福泉市', '荔波县', '贵定县', '瓮安县', '独山县', '平塘县', '罗甸县', '长顺县', '龙里县', '惠水县', '三都水族自治县']
|
||
},
|
||
'云南省': {
|
||
'昆明市': ['五华区', '盘龙区', '官渡区', '西山区', '东川区', '呈贡区', '晋宁区', '富民县', '宜良县', '石林彝族自治县', '嵩明县', '禄劝彝族苗族自治县', '寻甸回族彝族自治县', '安宁市'],
|
||
'曲靖市': ['麒麟区', '沾益区', '马龙区', '陆良县', '师宗县', '罗平县', '富源县', '会泽县', '宣威市'],
|
||
'玉溪市': ['红塔区', '江川区', '澄江市', '通海县', '华宁县', '易门县', '峨山彝族自治县', '新平彝族傣族自治县', '元江哈尼族彝族傣族自治县'],
|
||
'保山市': ['隆阳区', '施甸县', '龙陵县', '昌宁县', '腾冲市'],
|
||
'昭通市': ['昭阳区', '鲁甸县', '巧家县', '盐津县', '大关县', '永善县', '绥江县', '镇雄县', '彝良县', '威信县', '水富市'],
|
||
'丽江市': ['古城区', '玉龙纳西族自治县', '永胜县', '华坪县', '宁蒗彝族自治县'],
|
||
'普洱市': ['思茅区', '宁洱哈尼族彝族自治县', '墨江哈尼族自治县', '景东彝族自治县', '景谷傣族彝族自治县', '镇沅彝族哈尼族拉祜族自治县', '江城哈尼族彝族自治县', '孟连傣族拉祜族佤族自治县', '澜沧拉祜族自治县', '西盟佤族自治县'],
|
||
'临沧市': ['临翔区', '凤庆县', '云县', '永德县', '镇康县', '双江拉祜族佤族布朗族傣族自治县', '耿马傣族佤族自治县', '沧源佤族自治县'],
|
||
'楚雄彝族自治州': ['楚雄市', '双柏县', '牟定县', '南华县', '姚安县', '大姚县', '永仁县', '元谋县', '武定县', '禄丰市'],
|
||
'红河哈尼族彝族自治州': ['个旧市', '开远市', '蒙自市', '弥勒市', '屏边苗族自治县', '建水县', '石屏县', '泸西县', '元阳县', '红河县', '金平苗族瑶族傣族自治县', '绿春县', '河口瑶族自治县'],
|
||
'文山壮族苗族自治州': ['文山市', '砚山县', '西畴县', '麻栗坡县', '马关县', '丘北县', '广南县', '富宁县'],
|
||
'西双版纳傣族自治州': ['景洪市', '勐海县', '勐腊县'],
|
||
'大理白族自治州': ['大理市', '漾濞彝族自治县', '祥云县', '宾川县', '弥渡县', '南涧彝族自治县', '巍山彝族回族自治县', '永平县', '云龙县', '洱源县', '剑川县', '鹤庆县'],
|
||
'德宏傣族景颇族自治州': ['瑞丽市', '芒市', '梁河县', '盈江县', '陇川县'],
|
||
'怒江傈僳族自治州': ['泸水市', '福贡县', '贡山独龙族怒族自治县', '兰坪白族普米族自治县'],
|
||
'迪庆藏族自治州': ['香格里拉市', '德钦县', '维西傈僳族自治县']
|
||
},
|
||
'西藏自治区': {
|
||
'拉萨市': ['城关区', '堆龙德庆区', '达孜区', '林周县', '当雄县', '尼木县', '曲水县', '墨竹工卡县'],
|
||
'日喀则市': ['桑珠孜区', '南木林县', '江孜县', '定日县', '萨迦县', '拉孜县', '昂仁县', '谢通门县', '白朗县', '仁布县', '康马县', '定结县', '仲巴县', '亚东县', '吉隆县', '聂拉木县', '萨嘎县', '岗巴县'],
|
||
'昌都市': ['卡若区', '江达县', '贡觉县', '类乌齐县', '丁青县', '察雅县', '八宿县', '左贡县', '芒康县', '洛隆县', '边坝县'],
|
||
'林芝市': ['巴宜区', '工布江达县', '米林县', '墨脱县', '波密县', '察隅县', '朗县'],
|
||
'山南市': ['乃东区', '扎囊县', '贡嘎县', '桑日县', '琼结县', '曲松县', '措美县', '洛扎县', '加查县', '隆子县', '错那县', '浪卡子县'],
|
||
'那曲市': ['色尼区', '嘉黎县', '比如县', '聂荣县', '安多县', '申扎县', '索县', '班戈县', '巴青县', '尼玛县', '双湖县'],
|
||
'阿里地区': ['普兰县', '札达县', '噶尔县', '日土县', '革吉县', '改则县', '措勤县']
|
||
},
|
||
'陕西省': {
|
||
'西安市': ['新城区', '碑林区', '莲湖区', '灞桥区', '未央区', '雁塔区', '阎良区', '临潼区', '长安区', '高陵区', '鄠邑区', '蓝田县', '周至县'],
|
||
'铜川市': ['王益区', '印台区', '耀州区', '宜君县'],
|
||
'宝鸡市': ['渭滨区', '金台区', '陈仓区', '凤翔区', '岐山县', '扶风县', '眉县', '陇县', '千阳县', '麟游县', '凤县', '太白县'],
|
||
'咸阳市': ['秦都区', '杨陵区', '渭城区', '三原县', '泾阳县', '乾县', '礼泉县', '永寿县', '长武县', '旬邑县', '淳化县', '武功县', '兴平市', '彬州市'],
|
||
'渭南市': ['临渭区', '华州区', '潼关县', '大荔县', '合阳县', '澄城县', '蒲城县', '白水县', '富平县', '韩城市', '华阴市'],
|
||
'延安市': ['宝塔区', '安塞区', '延长县', '延川县', '志丹县', '吴起县', '甘泉县', '富县', '洛川县', '宜川县', '黄龙县', '黄陵县', '子长市'],
|
||
'汉中市': ['汉台区', '南郑区', '城固县', '洋县', '西乡县', '勉县', '宁强县', '略阳县', '镇巴县', '留坝县', '佛坪县'],
|
||
'榆林市': ['榆阳区', '横山区', '府谷县', '靖边县', '定边县', '绥德县', '米脂县', '佳县', '吴堡县', '清涧县', '子洲县', '神木市'],
|
||
'安康市': ['汉滨区', '汉阴县', '石泉县', '宁陕县', '紫阳县', '岚皋县', '平利县', '镇坪县', '旬阳县', '白河县'],
|
||
'商洛市': ['商州区', '洛南县', '丹凤县', '商南县', '山阳县', '镇安县', '柞水县']
|
||
},
|
||
'甘肃省': {
|
||
'兰州市': ['城关区', '七里河区', '西固区', '安宁区', '红古区', '永登县', '皋兰县', '榆中县'],
|
||
'嘉峪关市': ['嘉峪关市'],
|
||
'金昌市': ['金川区', '永昌县'],
|
||
'白银市': ['白银区', '平川区', '靖远县', '会宁县', '景泰县'],
|
||
'天水市': ['秦州区', '麦积区', '清水县', '秦安县', '甘谷县', '武山县', '张家川回族自治县'],
|
||
'武威市': ['凉州区', '民勤县', '古浪县', '天祝藏族自治县'],
|
||
'张掖市': ['甘州区', '肃南裕固族自治县', '民乐县', '临泽县', '高台县', '山丹县'],
|
||
'平凉市': ['崆峒区', '泾川县', '灵台县', '崇信县', '华亭市', '庄浪县', '静宁县'],
|
||
'酒泉市': ['肃州区', '金塔县', '瓜州县', '肃北蒙古族自治县', '阿克塞哈萨克族自治县', '玉门市', '敦煌市'],
|
||
'庆阳市': ['西峰区', '庆城县', '环县', '华池县', '合水县', '正宁县', '宁县', '镇原县'],
|
||
'定西市': ['安定区', '通渭县', '陇西县', '渭源县', '临洮县', '漳县', '岷县'],
|
||
'陇南市': ['武都区', '成县', '文县', '宕昌县', '康县', '西和县', '礼县', '徽县', '两当县'],
|
||
'临夏回族自治州': ['临夏市', '临夏县', '康乐县', '永靖县', '广河县', '和政县', '东乡族自治县', '积石山保安族东乡族撒拉族自治县'],
|
||
'甘南藏族自治州': ['合作市', '临潭县', '卓尼县', '舟曲县', '迭部县', '玛曲县', '碌曲县', '夏河县']
|
||
},
|
||
'青海省': {
|
||
'西宁市': ['城东区', '城中区', '城西区', '城北区', '大通回族土族自治县', '湟中区', '湟源县'],
|
||
'海东市': ['乐都区', '平安区', '民和回族土族自治县', '互助土族自治县', '化隆回族自治县', '循化撒拉族自治县'],
|
||
'海北藏族自治州': ['门源回族自治县', '祁连县', '海晏县', '刚察县'],
|
||
'黄南藏族自治州': ['同仁市', '尖扎县', '泽库县', '河南蒙古族自治县'],
|
||
'海南藏族自治州': ['共和县', '同德县', '贵德县', '兴海县', '贵南县'],
|
||
'果洛藏族自治州': ['玛沁县', '班玛县', '甘德县', '达日县', '久治县', '玛多县'],
|
||
'玉树藏族自治州': ['玉树市', '杂多县', '称多县', '治多县', '囊谦县', '曲麻莱县'],
|
||
'海西蒙古族藏族自治州': ['德令哈市', '格尔木市', '茫崖市', '乌兰县', '都兰县', '天峻县', '大柴旦行委']
|
||
},
|
||
'宁夏回族自治区': {
|
||
'银川市': ['兴庆区', '西夏区', '金凤区', '永宁县', '贺兰县', '灵武市'],
|
||
'石嘴山市': ['大武口区', '惠农区', '平罗县'],
|
||
'吴忠市': ['利通区', '红寺堡区', '盐池县', '同心县', '青铜峡市'],
|
||
'固原市': ['原州区', '西吉县', '隆德县', '泾源县', '彭阳县'],
|
||
'中卫市': ['沙坡头区', '中宁县', '海原县']
|
||
},
|
||
'新疆维吾尔自治区': {
|
||
'乌鲁木齐市': ['天山区', '沙依巴克区', '新市区', '水磨沟区', '头屯河区', '达坂城区', '米东区', '乌鲁木齐县'],
|
||
'克拉玛依市': ['独山子区', '克拉玛依区', '白碱滩区', '乌尔禾区'],
|
||
'吐鲁番市': ['高昌区', '鄯善县', '托克逊县'],
|
||
'哈密市': ['伊州区', '巴里坤哈萨克自治县', '伊吾县'],
|
||
'昌吉回族自治州': ['昌吉市', '阜康市', '呼图壁县', '玛纳斯县', '奇台县', '吉木萨尔县', '木垒哈萨克自治县'],
|
||
'博尔塔拉蒙古自治州': ['博乐市', '阿拉山口市', '精河县', '温泉县'],
|
||
'巴音郭楞蒙古自治州': ['库尔勒市', '轮台县', '尉犁县', '若羌县', '且末县', '焉耆回族自治县', '和静县', '和硕县', '博湖县'],
|
||
'阿克苏地区': ['阿克苏市', '温宿县', '库车市', '沙雅县', '新和县', '拜城县', '乌什县', '阿瓦提县', '柯坪县'],
|
||
'克孜勒苏柯尔克孜自治州': ['阿图什市', '阿克陶县', '阿合奇县', '乌恰县'],
|
||
'喀什地区': ['喀什市', '疏附县', '疏勒县', '英吉沙县', '泽普县', '莎车县', '叶城县', '麦盖提县', '岳普湖县', '伽师县', '巴楚县', '塔什库尔干塔吉克自治县'],
|
||
'和田地区': ['和田市', '和田县', '墨玉县', '皮山县', '洛浦县', '策勒县', '于田县', '民丰县'],
|
||
'伊犁哈萨克自治州': ['伊宁市', '奎屯市', '霍尔果斯市', '伊宁县', '察布查尔锡伯自治县', '霍城县', '巩留县', '新源县', '昭苏县', '特克斯县', '尼勒克县'],
|
||
'塔城地区': ['塔城市', '乌苏市', '额敏县', '沙湾市', '托里县', '裕民县', '和布克赛尔蒙古自治县'],
|
||
'阿勒泰地区': ['阿勒泰市', '布尔津县', '富蕴县', '福海县', '哈巴河县', '青河县', '吉木乃县'],
|
||
'石河子市': ['石河子市'],
|
||
'阿拉尔市': ['阿拉尔市'],
|
||
'图木舒克市': ['图木舒克市'],
|
||
'五家渠市': ['五家渠市'],
|
||
'北屯市': ['北屯市'],
|
||
'铁门关市': ['铁门关市'],
|
||
'双河市': ['双河市'],
|
||
'可克达拉市': ['可克达拉市'],
|
||
'昆玉市': ['昆玉市'],
|
||
'胡杨河市': ['胡杨河市']
|
||
},
|
||
'香港特别行政区': {
|
||
'香港岛': ['中西区', '湾仔区', '东区', '南区'],
|
||
'九龙': ['油尖旺区', '深水埗区', '九龙城区', '黄大仙区', '观塘区'],
|
||
'新界': ['北区', '大埔区', '沙田区', '西贡区', '荃湾区', '屯门区', '元朗区', '葵青区', '离岛区']
|
||
},
|
||
'澳门特别行政区': {
|
||
'澳门半岛': ['花地玛堂区', '圣安多尼堂区', '大堂区', '望德堂区', '风顺堂区'],
|
||
'氹仔': ['氹仔'],
|
||
'路环': ['路环']
|
||
},
|
||
'台湾省': {
|
||
'台北市': ['中正区', '大同区', '中山区', '松山区', '大安区', '万华区', '信义区', '士林区', '北投区', '内湖区', '南港区', '文山区'],
|
||
'新北市': ['万里区', '金山区', '板桥区', '汐止区', '深坑区', '石碇区', '瑞芳区', '平溪区', '双溪区', '贡寮区', '新店区', '坪林区', '乌来区', '永和区', '中和区', '土城区', '三峡区', '树林区', '莺歌区', '三重区', '新庄区', '泰山区', '林口区', '芦洲区', '五股区', '八里区', '淡水区', '三芝区', '石门区'],
|
||
'桃园市': ['中坜区', '平镇区', '龙潭区', '杨梅区', '新屋区', '观音区', '桃园区', '龟山区', '八德区', '大溪区', '复兴区', '大园区', '芦竹区'],
|
||
'台中市': ['中区', '东区', '南区', '西区', '北区', '北屯区', '西屯区', '南屯区', '太平区', '大里区', '雾峰区', '乌日区', '丰原区', '后里区', '石冈区', '东势区', '和平区', '新社区', '潭子区', '大雅区', '神冈区', '大肚区', '沙鹿区', '龙井区', '梧栖区', '清水区', '大甲区', '外埔区', '大安区'],
|
||
'台南市': ['中西区', '东区', '南区', '北区', '安平区', '安南区', '永康区', '归仁区', '新化区', '左镇区', '玉井区', '楠西区', '南化区', '仁德区', '关庙区', '龙崎区', '官田区', '麻豆区', '佳里区', '西港区', '七股区', '将军区', '学甲区', '北门区', '新营区', '后壁区', '白河区', '东山区', '六甲区', '下营区', '柳营区', '盐水区', '善化区', '大内区', '山上区', '新市区', '安定区'],
|
||
'高雄市': ['新兴区', '前金区', '苓雅区', '盐埕区', '鼓山区', '旗津区', '前镇区', '三民区', '楠梓区', '小港区', '左营区', '仁武区', '大社区', '冈山区', '路竹区', '阿莲区', '田寮区', '燕巢区', '桥头区', '梓官区', '弥陀区', '永安区', '湖内区', '凤山区', '大寮区', '林园区', '鸟松区', '大树区', '旗山区', '美浓区', '六龟区', '内门区', '杉林区', '甲仙区', '桃源区', '那玛夏区', '茂林区', '茄萣区'],
|
||
'基隆市': ['仁爱区', '信义区', '中正区', '中山区', '安乐区', '暖暖区', '七堵区'],
|
||
'新竹市': ['东区', '北区', '香山区'],
|
||
'嘉义市': ['东区', '西区'],
|
||
'新竹县': ['竹北市', '湖口乡', '新丰乡', '新埔镇', '关西镇', '芎林乡', '宝山乡', '竹东镇', '五峰乡', '横山乡', '尖石乡', '北埔乡', '峨眉乡'],
|
||
'苗栗县': ['竹南镇', '头份市', '三湾乡', '南庄乡', '狮潭乡', '后龙镇', '通霄镇', '苑里镇', '苗栗市', '造桥乡', '头屋乡', '公馆乡', '大湖乡', '泰安乡', '铜锣乡', '三义乡', '西湖乡', '卓兰镇'],
|
||
'彰化县': ['彰化市', '芬园乡', '花坛乡', '秀水乡', '鹿港镇', '福兴乡', '线西乡', '和美镇', '伸港乡', '员林市', '社头乡', '永靖乡', '埔心乡', '溪湖镇', '大村乡', '埔盐乡', '田中镇', '北斗镇', '田尾乡', '埤头乡', '溪州乡', '竹塘乡', '二林镇', '大城乡', '芳苑乡', '二水乡'],
|
||
'南投县': ['南投市', '中寮乡', '草屯镇', '国姓乡', '埔里镇', '仁爱乡', '名间乡', '集集镇', '水里乡', '鱼池乡', '信义乡', '竹山镇', '鹿谷乡'],
|
||
'云林县': ['斗南镇', '大埤乡', '虎尾镇', '土库镇', '褒忠乡', '东势乡', '台西乡', '仑背乡', '麦寮乡', '斗六市', '林内乡', '古坑乡', '莿桐乡', '西螺镇', '二仑乡', '北港镇', '水林乡', '口湖乡', '四湖乡', '元长乡'],
|
||
'嘉义县': ['番路乡', '梅山乡', '竹崎乡', '阿里山乡', '中埔乡', '大埔乡', '水上乡', '鹿草乡', '太保市', '朴子市', '东石乡', '六脚乡', '新港乡', '民雄乡', '大林镇', '溪口乡', '义竹乡', '布袋镇'],
|
||
'屏东县': ['屏东市', '三地门乡', '雾台乡', '玛家乡', '九如乡', '里港乡', '高树乡', '盐埔乡', '长治乡', '麟洛乡', '竹田乡', '内埔乡', '万丹乡', '潮州镇', '泰武乡', '来义乡', '万峦乡', '崁顶乡', '新埤乡', '南州乡', '林边乡', '东港镇', '琉球乡', '佳冬乡', '新园乡', '枋寮乡', '枋山乡', '春日乡', '狮子乡', '车城乡', '牡丹乡', '恒春镇', '满州乡'],
|
||
'宜兰县': ['宜兰市', '头城镇', '礁溪乡', '壮围乡', '员山乡', '罗东镇', '三星乡', '大同乡', '五结乡', '冬山乡', '苏澳镇', '南澳乡'],
|
||
'花莲县': ['花莲市', '新城乡', '秀林乡', '吉安乡', '寿丰乡', '凤林镇', '光复乡', '丰滨乡', '瑞穗乡', '万荣乡', '玉里镇', '卓溪乡', '富里乡'],
|
||
'台东县': ['台东市', '绿岛乡', '兰屿乡', '延平乡', '卑南乡', '鹿野乡', '关山镇', '海端乡', '池上乡', '东河乡', '成功镇', '长滨乡', '太麻里乡', '金峰乡', '大武乡', '达仁乡'],
|
||
'澎湖县': ['马公市', '西屿乡', '望安乡', '七美乡', '白沙乡', '湖西乡'],
|
||
'金门县': ['金沙镇', '金湖镇', '金宁乡', '金城镇', '烈屿乡', '乌坵乡'],
|
||
'连江县': ['南竿乡', '北竿乡', '莒光乡', '东引乡']
|
||
}
|
||
};
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/base.html
|
||
📊 大小: 7904 bytes (7.72 KB)
|
||
🕒 修改时间: 2025-07-03 07:06:17
|
||
🔸==============================================================================
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<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">
|
||
<!-- Bootstrap Icons -->
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
|
||
|
||
<style>
|
||
:root {
|
||
--admin-primary: #0d6efd;
|
||
--admin-sidebar: #212529;
|
||
--admin-sidebar-hover: #495057;
|
||
--admin-bg: #f8f9fa;
|
||
}
|
||
|
||
body {
|
||
background-color: var(--admin-bg);
|
||
}
|
||
|
||
.admin-sidebar {
|
||
min-height: 100vh;
|
||
background-color: var(--admin-sidebar);
|
||
width: 250px;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 1000;
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.admin-sidebar .nav-link {
|
||
color: #fff;
|
||
padding: 12px 20px;
|
||
border-radius: 0;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.admin-sidebar .nav-link:hover,
|
||
.admin-sidebar .nav-link.active {
|
||
background-color: var(--admin-sidebar-hover);
|
||
color: #fff;
|
||
}
|
||
|
||
.admin-sidebar .nav-link i {
|
||
margin-right: 10px;
|
||
width: 20px;
|
||
}
|
||
|
||
.admin-main {
|
||
margin-left: 250px;
|
||
padding: 0;
|
||
}
|
||
|
||
.admin-header {
|
||
background-color: #fff;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
padding: 15px 30px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.admin-content {
|
||
padding: 0 30px 30px;
|
||
}
|
||
|
||
.stats-card {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
margin-bottom: 20px;
|
||
border: none;
|
||
}
|
||
|
||
.stats-card.success {
|
||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||
}
|
||
|
||
.stats-card.warning {
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
}
|
||
|
||
.stats-card.info {
|
||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||
}
|
||
|
||
.admin-table {
|
||
background: white;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.admin-table th {
|
||
background-color: #f8f9fa;
|
||
border: none;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
}
|
||
|
||
.admin-table td {
|
||
border: none;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.admin-table tbody tr {
|
||
border-bottom: 1px solid #f8f9fa;
|
||
}
|
||
|
||
.admin-table tbody tr:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.sidebar-brand {
|
||
color: #fff;
|
||
font-size: 1.2rem;
|
||
font-weight: bold;
|
||
padding: 0 20px 30px;
|
||
border-bottom: 1px solid #495057;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.sidebar-brand i {
|
||
margin-right: 10px;
|
||
color: var(--admin-primary);
|
||
}
|
||
</style>
|
||
|
||
{% block extra_css %}{% endblock %}
|
||
</head>
|
||
<body>
|
||
<!-- 侧边栏 -->
|
||
<div class="admin-sidebar">
|
||
<div class="sidebar-brand">
|
||
<i class="bi bi-shop"></i>
|
||
太白购物商城
|
||
</div>
|
||
|
||
<ul class="nav flex-column">
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
|
||
href="{{ url_for('admin.dashboard') }}">
|
||
<i class="bi bi-speedometer2"></i>
|
||
仪表板
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if request.endpoint == 'admin.users' %}active{% endif %}"
|
||
href="{{ url_for('admin.users') }}">
|
||
<i class="bi bi-people"></i>
|
||
用户管理
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if request.endpoint.startswith('product.') %}active{% endif %}"
|
||
href="{{ url_for('product.index') }}">
|
||
<i class="bi bi-box"></i>
|
||
商品管理
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if request.endpoint == 'admin.orders' %}active{% endif %}"
|
||
href="#">
|
||
<i class="bi bi-receipt"></i>
|
||
订单管理
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if request.endpoint == 'admin.logs' %}active{% endif %}"
|
||
href="{{ url_for('admin.logs') }}">
|
||
<i class="bi bi-journal-text"></i>
|
||
操作日志
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if request.endpoint == 'admin.profile' %}active{% endif %}"
|
||
href="{{ url_for('admin.profile') }}">
|
||
<i class="bi bi-person-gear"></i>
|
||
个人资料
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="admin-main">
|
||
<!-- 顶部导航 -->
|
||
<div class="admin-header d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h4 class="mb-0">{% block page_title %}管理后台{% endblock %}</h4>
|
||
<small class="text-muted">{% block page_description %}{% endblock %}</small>
|
||
</div>
|
||
|
||
<div class="d-flex align-items-center">
|
||
<div class="dropdown">
|
||
<a class="btn btn-link text-decoration-none dropdown-toggle" href="#" role="button"
|
||
data-bs-toggle="dropdown" aria-expanded="false">
|
||
<i class="bi bi-person-circle"></i>
|
||
{{ session.admin_username }}
|
||
</a>
|
||
<ul class="dropdown-menu">
|
||
<li><a class="dropdown-item" href="{{ url_for('admin.profile') }}">
|
||
<i class="bi bi-person-gear"></i> 个人资料
|
||
</a></li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('admin.logout') }}">
|
||
<i class="bi bi-box-arrow-right"></i> 退出登录
|
||
</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区域 -->
|
||
<div class="admin-content">
|
||
<!-- 消息提示 -->
|
||
{% 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" aria-label="Close"></button>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
{% block content %}{% endblock %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bootstrap JS -->
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
||
{% block extra_js %}{% endblock %}
|
||
</body>
|
||
</html>
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/categories.html
|
||
📊 大小: 26972 bytes (26.34 KB)
|
||
🕒 修改时间: 2025-07-03 07:19:17
|
||
🔸==============================================================================
|
||
|
||
{% extends "admin/base.html" %}
|
||
|
||
{% block title %}分类管理 - 太白购物商城管理后台{% endblock %}
|
||
|
||
{% block page_title %}分类管理{% endblock %}
|
||
{% block page_description %}商品分类层级管理{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.category-tree {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.category-item {
|
||
border: 1px solid #e9ecef;
|
||
border-radius: 8px;
|
||
margin-bottom: 10px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.category-item:hover {
|
||
border-color: #0d6efd;
|
||
box-shadow: 0 2px 8px rgba(13, 110, 253, 0.15);
|
||
}
|
||
|
||
.category-header {
|
||
padding: 15px 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px 8px 0 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.category-level-1 .category-header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.category-level-2 .category-header {
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
color: white;
|
||
}
|
||
|
||
.category-level-3 .category-header {
|
||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||
color: white;
|
||
}
|
||
|
||
.category-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.category-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
object-fit: cover;
|
||
border: 2px solid rgba(255,255,255,0.3);
|
||
}
|
||
|
||
.default-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
background: rgba(255,255,255,0.2);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.category-details h6 {
|
||
margin: 0;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.category-meta {
|
||
font-size: 12px;
|
||
opacity: 0.8;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.category-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.children-categories {
|
||
padding: 0 20px 20px;
|
||
margin-left: 40px;
|
||
border-left: 2px dashed #dee2e6;
|
||
}
|
||
|
||
.btn-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.add-category-form {
|
||
background: #f8f9fa;
|
||
border-radius: 10px;
|
||
padding: 25px;
|
||
margin-bottom: 30px;
|
||
border: 2px dashed #dee2e6;
|
||
}
|
||
|
||
.icon-upload-area {
|
||
width: 80px;
|
||
height: 80px;
|
||
border: 2px dashed #dee2e6;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
background: white;
|
||
}
|
||
|
||
.icon-upload-area:hover {
|
||
border-color: #0d6efd;
|
||
background: #e3f2fd;
|
||
}
|
||
|
||
.icon-upload-area img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.sort-handle {
|
||
cursor: move;
|
||
color: #6c757d;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.sort-handle:hover {
|
||
color: #0d6efd;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.empty-state i {
|
||
font-size: 4rem;
|
||
margin-bottom: 20px;
|
||
opacity: 0.5;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- 添加分类表单 -->
|
||
<div class="add-category-form">
|
||
<h5 class="mb-3">
|
||
<i class="bi bi-plus-circle"></i> 添加新分类
|
||
</h5>
|
||
|
||
<form id="addCategoryForm" method="POST" action="{{ url_for('product.save_category') }}" enctype="multipart/form-data">
|
||
<div class="row">
|
||
<div class="col-md-2">
|
||
<label class="form-label">分类图标</label>
|
||
<div class="icon-upload-area" id="iconUploadArea">
|
||
<i class="bi bi-image fs-3 text-muted"></i>
|
||
<img id="iconPreview" style="display: none;">
|
||
</div>
|
||
<input type="file" id="iconInput" name="icon" accept="image/*" style="display: none;">
|
||
<small class="text-muted">点击上传图标</small>
|
||
</div>
|
||
|
||
<div class="col-md-3">
|
||
<label for="name" class="form-label">分类名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="name" name="name" required>
|
||
</div>
|
||
|
||
<div class="col-md-3">
|
||
<label for="parent_id" class="form-label">父分类</label>
|
||
<select name="parent_id" id="parent_id" class="form-select">
|
||
<option value="0">顶级分类</option>
|
||
{% for category in categories %}
|
||
{% if category.level <= 2 %}
|
||
<option value="{{ category.id }}">
|
||
{{ '└─' * (category.level - 1) }}{{ category.name }}
|
||
</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="col-md-2">
|
||
<label for="sort_order" class="form-label">排序</label>
|
||
<input type="number" class="form-control" id="sort_order" name="sort_order" value="0">
|
||
</div>
|
||
|
||
<div class="col-md-2 d-flex align-items-end">
|
||
<button type="submit" class="btn btn-primary w-100">
|
||
<i class="bi bi-plus-lg"></i> 添加
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 分类列表 -->
|
||
<div class="category-tree">
|
||
{% if categories %}
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-diagram-3"></i> 分类结构
|
||
<span class="badge bg-secondary">{{ categories|length }}</span>
|
||
</h5>
|
||
|
||
<div class="btn-group btn-group-sm">
|
||
<button class="btn btn-outline-primary" onclick="expandAll()">
|
||
<i class="bi bi-arrows-expand"></i> 展开全部
|
||
</button>
|
||
<button class="btn btn-outline-secondary" onclick="collapseAll()">
|
||
<i class="bi bi-arrows-collapse"></i> 收起全部
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="categoryList">
|
||
<!-- 渲染顶级分类 -->
|
||
{% set top_categories = categories | selectattr('parent_id', 'equalto', 0) | sort(attribute='sort_order') %}
|
||
{% for category in top_categories %}
|
||
<div class="category-item category-level-{{ category.level }}" data-id="{{ category.id }}">
|
||
<div class="category-header">
|
||
<div class="category-info">
|
||
<i class="bi bi-grip-vertical sort-handle"></i>
|
||
|
||
{% if category.icon_url %}
|
||
<img src="{{ category.icon_url }}" alt="{{ category.name }}" class="category-icon">
|
||
{% else %}
|
||
<div class="default-icon">
|
||
<i class="bi bi-folder"></i>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="category-details">
|
||
<h6>{{ category.name }}</h6>
|
||
<div class="category-meta">
|
||
ID: {{ category.id }} |
|
||
层级: {{ category.level }} |
|
||
排序: {{ category.sort_order }} |
|
||
{% if category.is_active %}
|
||
<span class="text-success">启用</span>
|
||
{% else %}
|
||
<span class="text-danger">禁用</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="category-actions">
|
||
<button class="btn btn-light btn-icon" onclick="editCategory({{ category.id }})" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
|
||
<button class="btn btn-light btn-icon" onclick="addSubCategory({{ category.id }})" title="添加子分类">
|
||
<i class="bi bi-plus"></i>
|
||
</button>
|
||
|
||
{% if category.level < 3 %}
|
||
<button class="btn btn-light btn-icon" onclick="toggleCategory({{ category.id }})" title="展开/收起">
|
||
<i class="bi bi-chevron-down"></i>
|
||
</button>
|
||
{% endif %}
|
||
|
||
<button class="btn btn-light btn-icon text-danger" onclick="deleteCategory({{ category.id }})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 二级分类 -->
|
||
{% set level2_categories = categories | selectattr('parent_id', 'equalto', category.id) | sort(attribute='sort_order') %}
|
||
{% if level2_categories %}
|
||
<div class="children-categories">
|
||
{% for child in level2_categories %}
|
||
<div class="category-item category-level-{{ child.level }}" data-id="{{ child.id }}">
|
||
<div class="category-header">
|
||
<div class="category-info">
|
||
<i class="bi bi-grip-vertical sort-handle"></i>
|
||
|
||
{% if child.icon_url %}
|
||
<img src="{{ child.icon_url }}" alt="{{ child.name }}" class="category-icon">
|
||
{% else %}
|
||
<div class="default-icon">
|
||
<i class="bi bi-folder"></i>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="category-details">
|
||
<h6>{{ child.name }}</h6>
|
||
<div class="category-meta">
|
||
ID: {{ child.id }} |
|
||
层级: {{ child.level }} |
|
||
排序: {{ child.sort_order }} |
|
||
{% if child.is_active %}
|
||
<span class="text-success">启用</span>
|
||
{% else %}
|
||
<span class="text-danger">禁用</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="category-actions">
|
||
<button class="btn btn-light btn-icon" onclick="editCategory({{ child.id }})" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
|
||
{% if child.level < 3 %}
|
||
<button class="btn btn-light btn-icon" onclick="addSubCategory({{ child.id }})" title="添加子分类">
|
||
<i class="bi bi-plus"></i>
|
||
</button>
|
||
{% endif %}
|
||
|
||
<button class="btn btn-light btn-icon text-danger" onclick="deleteCategory({{ child.id }})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 三级分类 -->
|
||
{% set level3_categories = categories | selectattr('parent_id', 'equalto', child.id) | sort(attribute='sort_order') %}
|
||
{% if level3_categories %}
|
||
<div class="children-categories">
|
||
{% for grandchild in level3_categories %}
|
||
<div class="category-item category-level-{{ grandchild.level }}" data-id="{{ grandchild.id }}">
|
||
<div class="category-header">
|
||
<div class="category-info">
|
||
<i class="bi bi-grip-vertical sort-handle"></i>
|
||
|
||
{% if grandchild.icon_url %}
|
||
<img src="{{ grandchild.icon_url }}" alt="{{ grandchild.name }}" class="category-icon">
|
||
{% else %}
|
||
<div class="default-icon">
|
||
<i class="bi bi-folder"></i>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="category-details">
|
||
<h6>{{ grandchild.name }}</h6>
|
||
<div class="category-meta">
|
||
ID: {{ grandchild.id }} |
|
||
层级: {{ grandchild.level }} |
|
||
排序: {{ grandchild.sort_order }} |
|
||
{% if grandchild.is_active %}
|
||
<span class="text-success">启用</span>
|
||
{% else %}
|
||
<span class="text-danger">禁用</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="category-actions">
|
||
<button class="btn btn-light btn-icon" onclick="editCategory({{ grandchild.id }})" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
|
||
<button class="btn btn-light btn-icon text-danger" onclick="deleteCategory({{ grandchild.id }})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="empty-state">
|
||
<i class="bi bi-folder"></i>
|
||
<h5>还没有创建任何分类</h5>
|
||
<p class="text-muted">点击上方的"添加新分类"来创建第一个商品分类</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 编辑分类模态框 -->
|
||
<div class="modal fade" id="editCategoryModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-pencil"></i> 编辑分类
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
|
||
<form id="editCategoryForm" method="POST" action="{{ url_for('product.save_category') }}" enctype="multipart/form-data">
|
||
<div class="modal-body">
|
||
<input type="hidden" id="edit_category_id" name="category_id">
|
||
|
||
<div class="row">
|
||
<div class="col-md-4">
|
||
<label class="form-label">分类图标</label>
|
||
<div class="icon-upload-area" id="editIconUploadArea">
|
||
<i class="bi bi-image fs-3 text-muted"></i>
|
||
<img id="editIconPreview" style="display: none;">
|
||
</div>
|
||
<input type="file" id="editIconInput" name="icon" accept="image/*" style="display: none;">
|
||
<small class="text-muted">点击更换图标</small>
|
||
</div>
|
||
|
||
<div class="col-md-8">
|
||
<div class="mb-3">
|
||
<label for="edit_name" class="form-label">分类名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="edit_name" name="name" required>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="edit_parent_id" class="form-label">父分类</label>
|
||
<select name="parent_id" id="edit_parent_id" class="form-select">
|
||
<option value="0">顶级分类</option>
|
||
{% for category in categories %}
|
||
{% if category.level <= 2 %}
|
||
<option value="{{ category.id }}">
|
||
{{ '└─' * (category.level - 1) }}{{ category.name }}
|
||
</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<label for="edit_sort_order" class="form-label">排序</label>
|
||
<input type="number" class="form-control" id="edit_sort_order" name="sort_order">
|
||
</div>
|
||
<div class="col-6">
|
||
<label for="edit_is_active" class="form-label">状态</label>
|
||
<select name="is_active" id="edit_is_active" class="form-select">
|
||
<option value="1">启用</option>
|
||
<option value="0">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-check-lg"></i> 保存修改
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initIconUpload();
|
||
initEditIconUpload();
|
||
});
|
||
|
||
// 初始化图标上传
|
||
function initIconUpload() {
|
||
const uploadArea = document.getElementById('iconUploadArea');
|
||
const iconInput = document.getElementById('iconInput');
|
||
const iconPreview = document.getElementById('iconPreview');
|
||
|
||
uploadArea.addEventListener('click', () => iconInput.click());
|
||
|
||
iconInput.addEventListener('change', function(e) {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
iconPreview.src = e.target.result;
|
||
iconPreview.style.display = 'block';
|
||
uploadArea.querySelector('i').style.display = 'none';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化编辑图标上传
|
||
function initEditIconUpload() {
|
||
const uploadArea = document.getElementById('editIconUploadArea');
|
||
const iconInput = document.getElementById('editIconInput');
|
||
const iconPreview = document.getElementById('editIconPreview');
|
||
|
||
uploadArea.addEventListener('click', () => iconInput.click());
|
||
|
||
iconInput.addEventListener('change', function(e) {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
iconPreview.src = e.target.result;
|
||
iconPreview.style.display = 'block';
|
||
uploadArea.querySelector('i').style.display = 'none';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 编辑分类
|
||
function editCategory(categoryId) {
|
||
fetch(`/admin/products/categories/${categoryId}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const category = data.category;
|
||
document.getElementById('edit_category_id').value = category.id;
|
||
document.getElementById('edit_name').value = category.name;
|
||
document.getElementById('edit_parent_id').value = category.parent_id;
|
||
document.getElementById('edit_sort_order').value = category.sort_order;
|
||
document.getElementById('edit_is_active').value = category.is_active;
|
||
|
||
// 设置图标预览
|
||
const iconPreview = document.getElementById('editIconPreview');
|
||
const uploadIcon = document.getElementById('editIconUploadArea').querySelector('i');
|
||
|
||
if (category.icon_url) {
|
||
iconPreview.src = category.icon_url;
|
||
iconPreview.style.display = 'block';
|
||
uploadIcon.style.display = 'none';
|
||
} else {
|
||
iconPreview.style.display = 'none';
|
||
uploadIcon.style.display = 'block';
|
||
}
|
||
|
||
// 禁用当前分类及其子分类作为父分类选项
|
||
const parentSelect = document.getElementById('edit_parent_id');
|
||
Array.from(parentSelect.options).forEach(option => {
|
||
option.disabled = false;
|
||
if (option.value == categoryId) {
|
||
option.disabled = true;
|
||
}
|
||
});
|
||
|
||
new bootstrap.Modal(document.getElementById('editCategoryModal')).show();
|
||
} else {
|
||
alert('获取分类信息失败');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('获取分类信息失败: ' + error);
|
||
});
|
||
}
|
||
|
||
// 添加子分类
|
||
function addSubCategory(parentId) {
|
||
document.getElementById('parent_id').value = parentId;
|
||
document.getElementById('name').focus();
|
||
|
||
// 滚动到添加表单
|
||
document.querySelector('.add-category-form').scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: 'start'
|
||
});
|
||
}
|
||
|
||
// 删除分类
|
||
function deleteCategory(categoryId) {
|
||
if (confirm('确定要删除这个分类吗?删除后无法恢复!')) {
|
||
fetch(`/admin/products/categories/${categoryId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('分类删除成功');
|
||
location.reload();
|
||
} else {
|
||
alert('删除失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('删除失败: ' + error);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 切换分类展开/收起
|
||
function toggleCategory(categoryId) {
|
||
const categoryItem = document.querySelector(`[data-id="${categoryId}"]`);
|
||
const childrenDiv = categoryItem.querySelector('.children-categories');
|
||
const toggleBtn = categoryItem.querySelector('.category-header .bi-chevron-down, .category-header .bi-chevron-up');
|
||
|
||
if (childrenDiv) {
|
||
if (childrenDiv.style.display === 'none') {
|
||
childrenDiv.style.display = 'block';
|
||
toggleBtn.className = 'bi bi-chevron-up';
|
||
} else {
|
||
childrenDiv.style.display = 'none';
|
||
toggleBtn.className = 'bi bi-chevron-down';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 展开全部
|
||
function expandAll() {
|
||
document.querySelectorAll('.children-categories').forEach(div => {
|
||
div.style.display = 'block';
|
||
});
|
||
document.querySelectorAll('.bi-chevron-down').forEach(icon => {
|
||
icon.className = 'bi bi-chevron-up';
|
||
});
|
||
}
|
||
|
||
// 收起全部
|
||
function collapseAll() {
|
||
document.querySelectorAll('.children-categories').forEach(div => {
|
||
div.style.display = 'none';
|
||
});
|
||
document.querySelectorAll('.bi-chevron-up').forEach(icon => {
|
||
icon.className = 'bi bi-chevron-down';
|
||
});
|
||
}
|
||
|
||
// 表单提交成功后重置
|
||
document.getElementById('addCategoryForm').addEventListener('submit', function(e) {
|
||
setTimeout(() => {
|
||
if (!document.querySelector('.alert-danger')) {
|
||
// 重置表单
|
||
this.reset();
|
||
document.getElementById('iconPreview').style.display = 'none';
|
||
document.getElementById('iconUploadArea').querySelector('i').style.display = 'block';
|
||
}
|
||
}, 100);
|
||
});
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/dashboard.html
|
||
📊 大小: 8105 bytes (7.92 KB)
|
||
🕒 修改时间: 2025-07-03 05:58:50
|
||
🔸==============================================================================
|
||
|
||
{% extends "admin/base.html" %}
|
||
|
||
{% block title %}仪表板 - 太白购物商城管理后台{% endblock %}
|
||
|
||
{% block page_title %}仪表板{% endblock %}
|
||
{% block page_description %}系统概览和数据统计{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row">
|
||
<!-- 统计卡片 -->
|
||
<div class="col-md-3 mb-4">
|
||
<div class="card stats-card">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1">{{ stats.total_users or 0 }}</h3>
|
||
<p class="mb-0">总用户数</p>
|
||
</div>
|
||
<div class="fs-1">
|
||
<i class="bi bi-people"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 mb-4">
|
||
<div class="card stats-card success">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1">{{ stats.active_users or 0 }}</h3>
|
||
<p class="mb-0">活跃用户</p>
|
||
</div>
|
||
<div class="fs-1">
|
||
<i class="bi bi-person-check"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 mb-4">
|
||
<div class="card stats-card warning">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1">{{ stats.total_admins or 0 }}</h3>
|
||
<p class="mb-0">管理员数</p>
|
||
</div>
|
||
<div class="fs-1">
|
||
<i class="bi bi-shield-check"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 mb-4">
|
||
<div class="card stats-card info">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1">{{ stats.recent_logs_count or 0 }}</h3>
|
||
<p class="mb-0">7天操作数</p>
|
||
</div>
|
||
<div class="fs-1">
|
||
<i class="bi bi-activity"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<!-- 用户注册趋势 -->
|
||
<div class="col-md-8 mb-4">
|
||
<div class="card">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-graph-up"></i>
|
||
用户注册趋势(最近7天)
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<canvas id="userTrendChart" width="400" height="200"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统状态 -->
|
||
<div class="col-md-4 mb-4">
|
||
<div class="card">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-info-circle"></i>
|
||
系统状态
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<span>数据库连接</span>
|
||
<span class="badge bg-success">正常</span>
|
||
</div>
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<span>文件存储</span>
|
||
<span class="badge bg-success">正常</span>
|
||
</div>
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<span>邮件服务</span>
|
||
<span class="badge bg-success">正常</span>
|
||
</div>
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<span>系统版本</span>
|
||
<span class="badge bg-info">v1.0.0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 最近操作日志 -->
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card admin-table">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-journal-text"></i>
|
||
最近操作日志
|
||
</h5>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>操作者</th>
|
||
<th>操作类型</th>
|
||
<th>操作内容</th>
|
||
<th>IP地址</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% if recent_logs %}
|
||
{% for log in recent_logs %}
|
||
<tr>
|
||
<td>{{ log.created_at.strftime('%m-%d %H:%M') if log.created_at else '' }}</td>
|
||
<td>
|
||
{% if log.user_type == 2 %}
|
||
<span class="badge bg-warning">管理员</span>
|
||
{% else %}
|
||
<span class="badge bg-info">用户</span>
|
||
{% endif %}
|
||
{{ log.user_id }}
|
||
</td>
|
||
<td>{{ log.action }}</td>
|
||
<td>
|
||
{% if log.resource_type %}
|
||
{{ log.resource_type }}
|
||
{% if log.resource_id %}#{{ log.resource_id }}{% endif %}
|
||
{% else %}
|
||
-
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ log.ip_address or '-' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% else %}
|
||
<tr>
|
||
<td colspan="5" class="text-center py-4">
|
||
<i class="bi bi-inbox"></i> 暂无操作日志
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if recent_logs %}
|
||
<div class="card-footer bg-white text-center">
|
||
<a href="{{ url_for('admin.logs') }}" class="btn btn-outline-primary btn-sm">
|
||
查看全部日志 <i class="bi bi-arrow-right"></i>
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<script>
|
||
// 用户注册趋势图表
|
||
const ctx = document.getElementById('userTrendChart').getContext('2d');
|
||
const userTrendChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [
|
||
{% for item in user_trend %}
|
||
'{{ item.date }}'{% if not loop.last %},{% endif %}
|
||
{% endfor %}
|
||
],
|
||
datasets: [{
|
||
label: '注册用户数',
|
||
data: [
|
||
{% for item in user_trend %}
|
||
{{ item.count }}{% if not loop.last %},{% endif %}
|
||
{% endfor %}
|
||
],
|
||
borderColor: 'rgb(102, 126, 234)',
|
||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||
tension: 0.4,
|
||
fill: true
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
stepSize: 1
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/login.html
|
||
📊 大小: 4628 bytes (4.52 KB)
|
||
🕒 修改时间: 2025-07-03 05:58:36
|
||
🔸==============================================================================
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>管理员登录 - 太白购物商城</title>
|
||
|
||
<!-- Bootstrap CSS -->
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<!-- Bootstrap Icons -->
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
|
||
|
||
<style>
|
||
body {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.login-card {
|
||
background: white;
|
||
border-radius: 15px;
|
||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||
padding: 40px;
|
||
min-width: 400px;
|
||
}
|
||
|
||
.login-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.login-header h2 {
|
||
color: #333;
|
||
font-weight: 600;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.login-header p {
|
||
color: #666;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.form-control {
|
||
padding: 12px 15px;
|
||
border-radius: 8px;
|
||
border: 1px solid #ddd;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.form-control:focus {
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||
}
|
||
|
||
.btn-login {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border: none;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-login:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.input-group-text {
|
||
background-color: #f8f9fa;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px 0 0 8px;
|
||
}
|
||
|
||
.form-control {
|
||
border-radius: 0 8px 8px 0;
|
||
}
|
||
|
||
.form-control:first-child {
|
||
border-radius: 8px 0 0 8px;
|
||
}
|
||
|
||
.back-link {
|
||
text-decoration: none;
|
||
color: #667eea;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.back-link:hover {
|
||
color: #764ba2;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="login-card">
|
||
<div class="login-header">
|
||
<h2><i class="bi bi-shield-lock"></i> 管理员登录</h2>
|
||
<p>太白购物商城管理后台</p>
|
||
</div>
|
||
|
||
<!-- 消息提示 -->
|
||
{% 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" aria-label="Close"></button>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
<form method="POST">
|
||
<div class="mb-3">
|
||
<div class="input-group">
|
||
<span class="input-group-text">
|
||
<i class="bi bi-person"></i>
|
||
</span>
|
||
<input type="text" class="form-control" name="username" placeholder="管理员用户名" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<div class="input-group">
|
||
<span class="input-group-text">
|
||
<i class="bi bi-lock"></i>
|
||
</span>
|
||
<input type="password" class="form-control" name="password" placeholder="登录密码" required>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary btn-login w-100">
|
||
<i class="bi bi-box-arrow-in-right"></i>
|
||
登录管理后台
|
||
</button>
|
||
</form>
|
||
|
||
<div class="text-center mt-3">
|
||
<a href="{{ url_for('main.index') }}" class="back-link">
|
||
<i class="bi bi-arrow-left"></i> 返回前台
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bootstrap JS -->
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
</body>
|
||
</html>
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/orders.html
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/product_form.html
|
||
📊 大小: 33693 bytes (32.90 KB)
|
||
🕒 修改时间: 2025-07-03 15:08:21
|
||
🔸==============================================================================
|
||
|
||
{% extends "admin/base.html" %}
|
||
|
||
{% block title %}
|
||
{% if product %}编辑商品{% else %}添加商品{% endif %} - 太白购物商城管理后台
|
||
{% endblock %}
|
||
|
||
{% block page_title %}
|
||
{% if product %}编辑商品{% else %}添加商品{% endif %}
|
||
{% endblock %}
|
||
|
||
{% block page_description %}
|
||
商品信息管理
|
||
{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.image-upload-area {
|
||
border: 2px dashed #dee2e6;
|
||
border-radius: 10px;
|
||
padding: 30px;
|
||
text-align: center;
|
||
background-color: #f8f9fa;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.image-upload-area:hover {
|
||
border-color: #0d6efd;
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.image-upload-area.dragover {
|
||
border-color: #0d6efd;
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.image-gallery {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 15px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.image-item {
|
||
position: relative;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
border: 2px solid #dee2e6;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.image-item:hover {
|
||
border-color: #0d6efd;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.image-item.main-image {
|
||
border-color: #198754;
|
||
}
|
||
|
||
.image-item img {
|
||
width: 100%;
|
||
height: 120px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.image-controls {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.image-controls .btn {
|
||
padding: 2px 6px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.main-badge {
|
||
position: absolute;
|
||
bottom: 5px;
|
||
left: 5px;
|
||
background-color: rgba(25, 135, 84, 0.9);
|
||
color: white;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.upload-progress {
|
||
display: none;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.inventory-table {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.inventory-table th {
|
||
background-color: #f8f9fa;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.spec-selector {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
padding: 10px;
|
||
}
|
||
|
||
.spec-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.spec-option {
|
||
margin: 2px;
|
||
}
|
||
|
||
.inventory-row {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.inventory-row:nth-child(even) {
|
||
background-color: white;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<form method="POST" action="{{ url_for('product.save') }}" id="productForm">
|
||
{% if product %}
|
||
<input type="hidden" name="product_id" value="{{ product.id }}">
|
||
{% endif %}
|
||
|
||
<div class="row">
|
||
<!-- 基本信息 -->
|
||
<div class="col-md-8">
|
||
<div class="card mb-4">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-info-circle"></i> 基本信息
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<div class="mb-3">
|
||
<label for="name" class="form-label">商品名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="name" name="name"
|
||
value="{{ product.name if product else '' }}" required>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="mb-3">
|
||
<label for="category_id" class="form-label">商品分类 <span class="text-danger">*</span></label>
|
||
<select name="category_id" id="category_id" class="form-select" required>
|
||
<option value="">选择分类</option>
|
||
{% for category in categories %}
|
||
<option value="{{ category.id }}"
|
||
{% if product and product.category_id == category.id %}selected{% endif %}>
|
||
{{ category.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-4">
|
||
<div class="mb-3">
|
||
<label for="price" class="form-label">销售价格 <span class="text-danger">*</span></label>
|
||
<div class="input-group">
|
||
<span class="input-group-text">¥</span>
|
||
<input type="number" class="form-control" id="price" name="price"
|
||
step="0.01" min="0" value="{{ product.price if product else '' }}" required>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="mb-3">
|
||
<label for="original_price" class="form-label">原价</label>
|
||
<div class="input-group">
|
||
<span class="input-group-text">¥</span>
|
||
<input type="number" class="form-control" id="original_price" name="original_price"
|
||
step="0.01" min="0" value="{{ product.original_price if product else '' }}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="mb-3">
|
||
<label for="brand" class="form-label">品牌</label>
|
||
<input type="text" class="form-control" id="brand" name="brand"
|
||
value="{{ product.brand if product else '' }}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="weight" class="form-label">重量 (kg)</label>
|
||
<input type="number" class="form-control" id="weight" name="weight"
|
||
step="0.01" min="0" value="{{ product.weight if product else '' }}">
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="status" class="form-label">状态</label>
|
||
<select name="status" id="status" class="form-select">
|
||
<option value="1" {% if not product or product.status == 1 %}selected{% endif %}>上架</option>
|
||
<option value="0" {% if product and product.status == 0 %}selected{% endif %}>下架</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="description" class="form-label">商品描述</label>
|
||
<textarea class="form-control" id="description" name="description" rows="5">{{ product.description if product else '' }}</textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 库存管理 -->
|
||
<div class="card mb-4">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-boxes"></i> 库存管理
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="inventory_type"
|
||
id="single_sku" value="single" checked onchange="toggleInventoryType()">
|
||
<label class="form-check-label" for="single_sku">
|
||
单一规格(无变体)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="inventory_type"
|
||
id="multi_sku" value="multi" onchange="toggleInventoryType()">
|
||
<label class="form-check-label" for="multi_sku">
|
||
多规格(有变体)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 单一规格库存 -->
|
||
<div id="singleInventorySection">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="single_stock" class="form-label">库存数量</label>
|
||
<input type="number" class="form-control" id="single_stock"
|
||
name="single_stock" min="0" value="0">
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="warning_stock" class="form-label">预警库存</label>
|
||
<input type="number" class="form-control" id="warning_stock"
|
||
name="warning_stock" min="0" value="10">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 多规格管理 -->
|
||
<div id="multiInventorySection" style="display: none;">
|
||
<!-- 规格选择 -->
|
||
<div class="mb-3">
|
||
<label class="form-label">选择规格类型:</label>
|
||
<div class="spec-selector">
|
||
{% for spec_name in spec_names %}
|
||
<div class="spec-group">
|
||
<div class="form-check">
|
||
<input class="form-check-input spec-checkbox" type="checkbox"
|
||
id="spec_{{ spec_name.id }}" value="{{ spec_name.id }}"
|
||
data-spec-name="{{ spec_name.name }}" onchange="updateSpecValues()">
|
||
<label class="form-check-label fw-bold" for="spec_{{ spec_name.id }}">
|
||
{{ spec_name.name }}
|
||
</label>
|
||
</div>
|
||
<div class="spec-values ms-3" id="values_{{ spec_name.id }}" style="display: none;">
|
||
{% for value in spec_name.values %}
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input spec-value-checkbox" type="checkbox"
|
||
id="value_{{ value.id }}" value="{{ value.value }}"
|
||
data-spec-id="{{ spec_name.id }}" data-spec-name="{{ spec_name.name }}">
|
||
<label class="form-check-label" for="value_{{ value.id }}">
|
||
{{ value.value }}
|
||
</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<button type="button" class="btn btn-success" onclick="generateSkuTable()">
|
||
<i class="bi bi-gear"></i> 生成SKU表格
|
||
</button>
|
||
</div>
|
||
|
||
<!-- SKU表格 -->
|
||
<div id="skuTableContainer" style="display: none;">
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered inventory-table" id="skuTable">
|
||
<thead>
|
||
<tr>
|
||
<th>规格组合</th>
|
||
<th>SKU编码</th>
|
||
<th>价格调整</th>
|
||
<th>库存数量</th>
|
||
<th>预警库存</th>
|
||
<th>默认规格</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="skuTableBody">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 现有库存信息展示(编辑时) -->
|
||
{% if product and product.inventory %}
|
||
<div class="mt-4">
|
||
<h6>当前库存信息:</h6>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm">
|
||
<thead>
|
||
<tr>
|
||
<th>SKU编码</th>
|
||
<th>规格组合</th>
|
||
<th>库存</th>
|
||
<th>价格</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for inventory in product.inventory %}
|
||
<tr>
|
||
<td>{{ inventory.sku_code }}</td>
|
||
<td>
|
||
{% if inventory.spec_combination %}
|
||
{% for key, value in inventory.spec_combination.items() %}
|
||
{{ key }}:{{ value }}{% if not loop.last %}, {% endif %}
|
||
{% endfor %}
|
||
{% else %}
|
||
默认规格
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ inventory.stock }}</td>
|
||
<td>¥{{ "%.2f"|format(inventory.get_final_price()) }}</td>
|
||
<td>
|
||
{% if inventory.status %}
|
||
<span class="badge bg-success">启用</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary">禁用</span>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品图片 -->
|
||
{% if product %}
|
||
<div class="card">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-images"></i> 商品图片
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- 图片上传区域 -->
|
||
<div class="image-upload-area" id="imageUploadArea">
|
||
<i class="bi bi-cloud-upload fs-1 text-muted"></i>
|
||
<h5 class="mt-2">拖拽图片到这里或点击选择</h5>
|
||
<p class="text-muted mb-0">支持 JPG、PNG、GIF 格式,单张图片不超过 5MB</p>
|
||
<input type="file" id="imageInput" multiple accept="image/*" style="display: none;">
|
||
</div>
|
||
|
||
<!-- 上传进度 -->
|
||
<div class="upload-progress">
|
||
<div class="progress">
|
||
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
||
</div>
|
||
<small class="text-muted">上传中...</small>
|
||
</div>
|
||
|
||
<!-- 图片画廊 -->
|
||
<div class="image-gallery" id="imageGallery">
|
||
{% if product.images %}
|
||
{% for image in product.images|sort(attribute='sort_order') %}
|
||
<div class="image-item {% if image.is_main %}main-image{% endif %}" data-id="{{ image.id }}">
|
||
<img src="{{ image.image_url }}" alt="商品图片">
|
||
<div class="image-controls">
|
||
{% if not image.is_main %}
|
||
<button type="button" class="btn btn-success btn-sm"
|
||
onclick="setMainImage({{ image.id }})" title="设为主图">
|
||
<i class="bi bi-star"></i>
|
||
</button>
|
||
{% endif %}
|
||
<button type="button" class="btn btn-danger btn-sm"
|
||
onclick="deleteImage({{ image.id }})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
{% if image.is_main %}
|
||
<div class="main-badge">主图</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 侧边栏 -->
|
||
<div class="col-md-4">
|
||
<div class="card mb-4">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-gear"></i> 操作
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<button type="submit" class="btn btn-primary w-100 mb-2">
|
||
<i class="bi bi-check-lg"></i> 保存商品
|
||
</button>
|
||
|
||
{% if product %}
|
||
<a href="{{ url_for('product.index') }}" class="btn btn-outline-secondary w-100 mb-2">
|
||
<i class="bi bi-arrow-left"></i> 返回列表
|
||
</a>
|
||
|
||
<button type="button" class="btn btn-outline-danger w-100"
|
||
onclick="deleteProduct({{ product.id }})">
|
||
<i class="bi bi-trash"></i> 删除商品
|
||
</button>
|
||
{% else %}
|
||
<a href="{{ url_for('product.index') }}" class="btn btn-outline-secondary w-100">
|
||
<i class="bi bi-arrow-left"></i> 返回列表
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if product %}
|
||
<div class="card">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-info-circle"></i> 商品信息
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-2">
|
||
<strong>商品ID:</strong>{{ product.id }}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>销量:</strong>{{ product.sales_count }}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>浏览量:</strong>{{ product.view_count }}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>创建时间:</strong><br>
|
||
{{ product.created_at.strftime('%Y-%m-%d %H:%M:%S') if product.created_at else '' }}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>更新时间:</strong><br>
|
||
{{ product.updated_at.strftime('%Y-%m-%d %H:%M:%S') if product.updated_at else '' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</form>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
let skuCounter = 0;
|
||
|
||
// 切换库存类型
|
||
function toggleInventoryType() {
|
||
const singleSection = document.getElementById('singleInventorySection');
|
||
const multiSection = document.getElementById('multiInventorySection');
|
||
const singleRadio = document.getElementById('single_sku');
|
||
|
||
if (singleRadio.checked) {
|
||
singleSection.style.display = 'block';
|
||
multiSection.style.display = 'none';
|
||
} else {
|
||
singleSection.style.display = 'none';
|
||
multiSection.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// 更新规格值显示
|
||
function updateSpecValues() {
|
||
document.querySelectorAll('.spec-checkbox').forEach(checkbox => {
|
||
const valuesDiv = document.getElementById('values_' + checkbox.value);
|
||
if (checkbox.checked) {
|
||
valuesDiv.style.display = 'block';
|
||
} else {
|
||
valuesDiv.style.display = 'none';
|
||
// 取消选中该规格下的所有值
|
||
valuesDiv.querySelectorAll('.spec-value-checkbox').forEach(valueCheckbox => {
|
||
valueCheckbox.checked = false;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// 生成SKU表格
|
||
function generateSkuTable() {
|
||
const selectedSpecs = {};
|
||
|
||
// 收集选中的规格和值
|
||
document.querySelectorAll('.spec-value-checkbox:checked').forEach(checkbox => {
|
||
const specId = checkbox.getAttribute('data-spec-id');
|
||
const specName = checkbox.getAttribute('data-spec-name');
|
||
const value = checkbox.value;
|
||
|
||
if (!selectedSpecs[specName]) {
|
||
selectedSpecs[specName] = [];
|
||
}
|
||
selectedSpecs[specName].push(value);
|
||
});
|
||
|
||
if (Object.keys(selectedSpecs).length === 0) {
|
||
alert('请先选择规格');
|
||
return;
|
||
}
|
||
|
||
// 生成SKU组合
|
||
const combinations = generateCombinations(selectedSpecs);
|
||
|
||
// 创建表格
|
||
const tableBody = document.getElementById('skuTableBody');
|
||
tableBody.innerHTML = '';
|
||
|
||
combinations.forEach((combination, index) => {
|
||
const row = document.createElement('tr');
|
||
row.className = 'inventory-row';
|
||
|
||
// 规格组合显示
|
||
const specText = Object.entries(combination).map(([key, value]) => `${key}:${value}`).join(', ');
|
||
|
||
// 生成SKU编码
|
||
const skuCode = generateSkuCode(combination);
|
||
|
||
row.innerHTML = `
|
||
<td>${specText}</td>
|
||
<td>
|
||
<input type="text" class="form-control form-control-sm"
|
||
name="sku_codes[]" value="${skuCode}" required>
|
||
<input type="hidden" name="spec_combinations[]" value='${JSON.stringify(combination)}'>
|
||
</td>
|
||
<td>
|
||
<div class="input-group input-group-sm">
|
||
<span class="input-group-text">¥</span>
|
||
<input type="number" class="form-control" name="price_adjustments[]"
|
||
value="0" step="0.01">
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<input type="number" class="form-control form-control-sm"
|
||
name="stocks[]" value="0" min="0" required>
|
||
</td>
|
||
<td>
|
||
<input type="number" class="form-control form-control-sm"
|
||
name="warning_stocks[]" value="10" min="0">
|
||
</td>
|
||
<td>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="default_sku"
|
||
value="${index}" ${index === 0 ? 'checked' : ''}>
|
||
</div>
|
||
</td>
|
||
`;
|
||
|
||
tableBody.appendChild(row);
|
||
});
|
||
|
||
document.getElementById('skuTableContainer').style.display = 'block';
|
||
}
|
||
|
||
// 生成组合
|
||
function generateCombinations(specs) {
|
||
const keys = Object.keys(specs);
|
||
const values = Object.values(specs);
|
||
|
||
function cartesian(arrays) {
|
||
return arrays.reduce((a, b) =>
|
||
a.flatMap(x => b.map(y => [...x, y])), [[]]
|
||
);
|
||
}
|
||
|
||
const combinations = cartesian(values);
|
||
|
||
return combinations.map(combination => {
|
||
const result = {};
|
||
keys.forEach((key, index) => {
|
||
result[key] = combination[index];
|
||
});
|
||
return result;
|
||
});
|
||
}
|
||
|
||
// 生成SKU编码
|
||
function generateSkuCode(combination) {
|
||
const productName = document.getElementById('name').value || 'PRODUCT';
|
||
const shortName = productName.substring(0, 3).toUpperCase();
|
||
const specCode = Object.values(combination).map(v => v.substring(0, 2)).join('');
|
||
const timestamp = Date.now().toString().slice(-4);
|
||
|
||
return `${shortName}-${specCode}-${timestamp}`;
|
||
}
|
||
|
||
// 表单提交前的验证
|
||
document.getElementById('productForm').addEventListener('submit', function(e) {
|
||
const inventoryType = document.querySelector('input[name="inventory_type"]:checked').value;
|
||
|
||
if (inventoryType === 'single') {
|
||
// 单规格验证
|
||
const stock = document.getElementById('single_stock').value;
|
||
if (!stock || stock < 0) {
|
||
e.preventDefault();
|
||
alert('请输入正确的库存数量');
|
||
return;
|
||
}
|
||
} else {
|
||
// 多规格验证
|
||
const skuTable = document.getElementById('skuTableContainer');
|
||
if (skuTable.style.display === 'none') {
|
||
e.preventDefault();
|
||
alert('请生成SKU表格并设置库存信息');
|
||
return;
|
||
}
|
||
|
||
const stockInputs = document.querySelectorAll('input[name="stocks[]"]');
|
||
if (stockInputs.length === 0) {
|
||
e.preventDefault();
|
||
alert('请添加至少一个SKU');
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
|
||
{% if product %}
|
||
// 图片上传功能
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const uploadArea = document.getElementById('imageUploadArea');
|
||
const imageInput = document.getElementById('imageInput');
|
||
const imageGallery = document.getElementById('imageGallery');
|
||
const progressDiv = document.querySelector('.upload-progress');
|
||
const progressBar = document.querySelector('.progress-bar');
|
||
|
||
// 点击上传区域
|
||
uploadArea.addEventListener('click', function() {
|
||
imageInput.click();
|
||
});
|
||
|
||
// 文件选择
|
||
imageInput.addEventListener('change', function(e) {
|
||
uploadImages(e.target.files);
|
||
});
|
||
|
||
// 拖拽上传
|
||
uploadArea.addEventListener('dragover', function(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', function(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', function(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
uploadImages(e.dataTransfer.files);
|
||
});
|
||
|
||
// 上传图片
|
||
function uploadImages(files) {
|
||
if (files.length === 0) return;
|
||
|
||
const formData = new FormData();
|
||
for (let file of files) {
|
||
formData.append('files', file);
|
||
}
|
||
|
||
// 显示进度条
|
||
progressDiv.style.display = 'block';
|
||
progressBar.style.width = '0%';
|
||
|
||
fetch(`/admin/products/upload-images/{{ product.id }}`, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
progressDiv.style.display = 'none';
|
||
|
||
if (data.success) {
|
||
// 添加新图片到画廊
|
||
data.images.forEach(image => {
|
||
addImageToGallery(image);
|
||
});
|
||
alert(`成功上传 ${data.images.length} 张图片`);
|
||
} else {
|
||
alert('上传失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
progressDiv.style.display = 'none';
|
||
alert('上传失败: ' + error);
|
||
});
|
||
}
|
||
|
||
// 添加图片到画廊
|
||
function addImageToGallery(image) {
|
||
const imageItem = document.createElement('div');
|
||
imageItem.className = 'image-item';
|
||
imageItem.setAttribute('data-id', image.id);
|
||
|
||
imageItem.innerHTML = `
|
||
<img src="${image.url}" alt="商品图片">
|
||
<div class="image-controls">
|
||
<button type="button" class="btn btn-success btn-sm"
|
||
onclick="setMainImage(${image.id})" title="设为主图">
|
||
<i class="bi bi-star"></i>
|
||
</button>
|
||
<button type="button" class="btn btn-danger btn-sm"
|
||
onclick="deleteImage(${image.id})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
imageGallery.appendChild(imageItem);
|
||
}
|
||
});
|
||
|
||
// 设置主图
|
||
function setMainImage(imageId) {
|
||
fetch(`/admin/products/set-main-image/${imageId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 更新UI
|
||
document.querySelectorAll('.image-item').forEach(item => {
|
||
item.classList.remove('main-image');
|
||
const badge = item.querySelector('.main-badge');
|
||
if (badge) badge.remove();
|
||
|
||
const starBtn = item.querySelector('.btn-success');
|
||
if (starBtn) starBtn.style.display = 'block';
|
||
});
|
||
|
||
const currentItem = document.querySelector(`[data-id="${imageId}"]`);
|
||
currentItem.classList.add('main-image');
|
||
currentItem.querySelector('.btn-success').style.display = 'none';
|
||
|
||
const badge = document.createElement('div');
|
||
badge.className = 'main-badge';
|
||
badge.textContent = '主图';
|
||
currentItem.appendChild(badge);
|
||
|
||
alert('主图设置成功');
|
||
} else {
|
||
alert('设置失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('设置失败: ' + error);
|
||
});
|
||
}
|
||
|
||
// 删除图片
|
||
function deleteImage(imageId) {
|
||
if (confirm('确定要删除这张图片吗?')) {
|
||
fetch(`/admin/products/delete-image/${imageId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
document.querySelector(`[data-id="${imageId}"]`).remove();
|
||
alert('图片删除成功');
|
||
} else {
|
||
alert('删除失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('删除失败: ' + error);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 删除商品
|
||
function deleteProduct(productId) {
|
||
if (confirm('确定要删除这个商品吗?此操作不可恢复!')) {
|
||
fetch(`/admin/products/delete/${productId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('商品删除成功');
|
||
window.location.href = '{{ url_for("product.index") }}';
|
||
} else {
|
||
alert('删除失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('删除失败: ' + error);
|
||
});
|
||
}
|
||
}
|
||
{% endif %}
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/products.html
|
||
📊 大小: 17989 bytes (17.57 KB)
|
||
🕒 修改时间: 2025-07-03 15:15:27
|
||
🔸==============================================================================
|
||
|
||
{% extends "admin/base.html" %}
|
||
|
||
{% block title %}商品管理 - 太白购物商城管理后台{% endblock %}
|
||
|
||
{% block page_title %}商品管理{% endblock %}
|
||
{% block page_description %}商品信息管理{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row mb-3">
|
||
<div class="col-md-8">
|
||
<!-- 搜索和筛选 -->
|
||
<form method="GET" class="d-flex">
|
||
<input type="text" class="form-control me-2" name="search"
|
||
placeholder="搜索商品名称..." value="{{ search }}">
|
||
|
||
<select name="category_id" class="form-select me-2">
|
||
<option value="">全部分类</option>
|
||
{% for category in categories %}
|
||
<option value="{{ category.id }}"
|
||
{% if category_id == category.id|string %}selected{% endif %}>
|
||
{{ category.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
|
||
<select name="status" class="form-select me-2">
|
||
<option value="">全部状态</option>
|
||
<option value="1" {% if status == '1' %}selected{% endif %}>上架</option>
|
||
<option value="0" {% if status == '0' %}selected{% endif %}>下架</option>
|
||
</select>
|
||
|
||
<button type="submit" class="btn btn-outline-primary">
|
||
<i class="bi bi-search"></i> 搜索
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="col-md-4 text-end">
|
||
<a href="{{ url_for('product.add') }}" class="btn btn-primary">
|
||
<i class="bi bi-plus-lg"></i> 添加商品
|
||
</a>
|
||
<a href="{{ url_for('product.categories') }}" class="btn btn-outline-secondary">
|
||
<i class="bi bi-tags"></i> 分类管理
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品列表 -->
|
||
<div class="card admin-table">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th width="80">ID</th>
|
||
<th width="100">商品图片</th>
|
||
<th>商品名称</th>
|
||
<th width="120">分类</th>
|
||
<th width="100">价格</th>
|
||
<th width="100">库存</th>
|
||
<th width="80">状态</th>
|
||
<th width="100">销量</th>
|
||
<th width="120">创建时间</th>
|
||
<th width="150">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% if products.items %}
|
||
{% for product in products.items %}
|
||
<tr>
|
||
<td>{{ product.id }}</td>
|
||
<td>
|
||
{% if product.main_image %}
|
||
<img src="{{ product.main_image }}" class="img-thumbnail"
|
||
style="width: 60px; height: 60px; object-fit: cover;">
|
||
{% else %}
|
||
<div class="bg-light text-center d-flex align-items-center justify-content-center"
|
||
style="width: 60px; height: 60px;">
|
||
<i class="bi bi-image text-muted"></i>
|
||
</div>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<div>
|
||
<strong>{{ product.name[:40] }}{% if product.name|length > 40 %}...{% endif %}</strong>
|
||
{% if product.brand %}
|
||
<br><small class="text-muted"><i class="bi bi-award"></i> {{ product.brand }}</small>
|
||
{% endif %}
|
||
{% if product.has_specs %}
|
||
<br><span class="badge bg-info badge-sm">多规格</span>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="badge bg-secondary">{{ product.category.name if product.category else '未分类' }}</span>
|
||
</td>
|
||
<td>
|
||
<strong class="text-danger">¥{{ "%.2f"|format(product.price) }}</strong>
|
||
{% if product.original_price and product.original_price > product.price %}
|
||
<br><small class="text-muted text-decoration-line-through">
|
||
¥{{ "%.2f"|format(product.original_price) }}
|
||
</small>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% set total_stock = product.inventory|sum(attribute='stock') if product.inventory else 0 %}
|
||
{% set sku_count = product.inventory|length if product.inventory else 0 %}
|
||
|
||
<div class="text-center">
|
||
<span class="fw-bold {% if total_stock <= 0 %}text-danger{% elif total_stock <= 10 %}text-warning{% else %}text-success{% endif %}">
|
||
{{ total_stock }}
|
||
</span>
|
||
{% if sku_count > 1 %}
|
||
<br><small class="text-muted">{{ sku_count }}个SKU</small>
|
||
{% endif %}
|
||
{% if total_stock <= 0 %}
|
||
<br><small class="text-danger">缺货</small>
|
||
{% elif total_stock <= 10 %}
|
||
<br><small class="text-warning">库存不足</small>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
<td>
|
||
{% if product.status == 1 %}
|
||
<span class="badge bg-success">上架</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary">下架</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="text-center">
|
||
<div>{{ product.sales_count }}</div>
|
||
<small class="text-muted">浏览:{{ product.view_count }}</small>
|
||
</td>
|
||
<td>
|
||
<div class="text-center">
|
||
{{ product.created_at.strftime('%m-%d') if product.created_at else '' }}
|
||
<br><small class="text-muted">{{ product.created_at.strftime('%H:%M') if product.created_at else '' }}</small>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="btn-group-vertical btn-group-sm" role="group">
|
||
<a href="{{ url_for('product.edit', product_id=product.id) }}"
|
||
class="btn btn-outline-primary btn-sm" title="编辑">
|
||
<i class="bi bi-pencil"></i> 编辑
|
||
</a>
|
||
{% if product.inventory %}
|
||
<button class="btn btn-outline-info btn-sm"
|
||
onclick="showInventory({{ product.id }})" title="库存详情">
|
||
<i class="bi bi-boxes"></i> 库存
|
||
</button>
|
||
{% endif %}
|
||
<button class="btn btn-outline-danger btn-sm"
|
||
onclick="deleteProduct({{ product.id }})" title="删除">
|
||
<i class="bi bi-trash"></i> 删除
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% else %}
|
||
<tr>
|
||
<td colspan="10" class="text-center py-4">
|
||
<div class="text-muted">
|
||
<i class="bi bi-inbox display-4"></i>
|
||
<p class="mt-2">暂无商品数据</p>
|
||
{% if search or category_id or status %}
|
||
<a href="{{ url_for('product.index') }}" class="btn btn-outline-primary btn-sm">
|
||
<i class="bi bi-arrow-clockwise"></i> 清除筛选
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
{% if products.pages > 1 %}
|
||
<div class="card-footer bg-white">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<small class="text-muted">
|
||
显示第 {{ (products.page - 1) * products.per_page + 1 }} -
|
||
{{ products.page * products.per_page if products.page * products.per_page < products.total else products.total }}
|
||
条,共 {{ products.total }} 条
|
||
</small>
|
||
|
||
<nav aria-label="商品分页">
|
||
<ul class="pagination pagination-sm mb-0">
|
||
{% if products.has_prev %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('product.index', page=products.prev_num, search=search, category_id=category_id, status=status) }}">
|
||
<i class="bi bi-chevron-left"></i> 上一页
|
||
</a>
|
||
</li>
|
||
{% endif %}
|
||
|
||
{% for page_num in products.iter_pages() %}
|
||
{% if page_num %}
|
||
{% if page_num != products.page %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('product.index', page=page_num, search=search, category_id=category_id, 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 products.has_next %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('product.index', page=products.next_num, search=search, category_id=category_id, status=status) }}">
|
||
下一页 <i class="bi bi-chevron-right"></i>
|
||
</a>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 库存详情模态框 -->
|
||
<div class="modal fade" id="inventoryModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-boxes"></i> 库存详情
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body" id="inventoryModalBody">
|
||
<div class="text-center">
|
||
<div class="spinner-border" role="status">
|
||
<span class="visually-hidden">加载中...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
function deleteProduct(productId) {
|
||
if (confirm('确定要删除这个商品吗?此操作不可恢复!\n删除商品将同时删除所有相关的库存信息。')) {
|
||
fetch(`/admin/products/delete/${productId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showSuccessMessage('商品删除成功');
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1000);
|
||
} else {
|
||
alert('删除失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('删除失败: ' + error);
|
||
});
|
||
}
|
||
}
|
||
|
||
function showInventory(productId) {
|
||
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
|
||
const modalBody = document.getElementById('inventoryModalBody');
|
||
|
||
// 显示加载状态
|
||
modalBody.innerHTML = `
|
||
<div class="text-center">
|
||
<div class="spinner-border" role="status">
|
||
<span class="visually-hidden">加载中...</span>
|
||
</div>
|
||
<p class="mt-2">正在加载库存信息...</p>
|
||
</div>
|
||
`;
|
||
|
||
modal.show();
|
||
|
||
// 获取库存信息
|
||
fetch(`/admin/products/inventory/${productId}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
displayInventoryInfo(data.inventory);
|
||
} else {
|
||
modalBody.innerHTML = `<div class="alert alert-danger">加载失败: ${data.message}</div>`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
modalBody.innerHTML = `<div class="alert alert-danger">加载失败: ${error}</div>`;
|
||
});
|
||
}
|
||
|
||
function displayInventoryInfo(inventoryList) {
|
||
const modalBody = document.getElementById('inventoryModalBody');
|
||
|
||
if (!inventoryList || inventoryList.length === 0) {
|
||
modalBody.innerHTML = '<div class="alert alert-info">暂无库存信息</div>';
|
||
return;
|
||
}
|
||
|
||
let html = `
|
||
<div class="table-responsive">
|
||
<table class="table table-sm">
|
||
<thead>
|
||
<tr>
|
||
<th>SKU编码</th>
|
||
<th>规格组合</th>
|
||
<th>库存数量</th>
|
||
<th>预警库存</th>
|
||
<th>价格调整</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
inventoryList.forEach(item => {
|
||
const specText = item.spec_combination ?
|
||
Object.entries(item.spec_combination).map(([k, v]) => `${k}:${v}`).join(', ') :
|
||
'默认规格';
|
||
|
||
const stockClass = item.stock <= 0 ? 'text-danger' :
|
||
item.stock <= item.warning_stock ? 'text-warning' : 'text-success';
|
||
|
||
html += `
|
||
<tr>
|
||
<td>
|
||
<code>${item.sku_code}</code>
|
||
${item.is_default ? '<span class="badge bg-primary badge-sm ms-1">默认</span>' : ''}
|
||
</td>
|
||
<td>${specText}</td>
|
||
<td class="${stockClass} fw-bold">${item.stock}</td>
|
||
<td>${item.warning_stock}</td>
|
||
<td>
|
||
${item.price_adjustment > 0 ? '+' : ''}¥${item.price_adjustment.toFixed(2)}
|
||
</td>
|
||
<td>
|
||
${item.status ? '<span class="badge bg-success">启用</span>' : '<span class="badge bg-secondary">禁用</span>'}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
|
||
modalBody.innerHTML = html;
|
||
}
|
||
|
||
function showSuccessMessage(message) {
|
||
const alertDiv = document.createElement('div');
|
||
alertDiv.className = 'alert alert-success 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.parentNode.removeChild(alertDiv);
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
// 页面加载完成后的初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 添加表格行悬停效果
|
||
const tableRows = document.querySelectorAll('.table tbody tr');
|
||
tableRows.forEach(row => {
|
||
row.addEventListener('mouseenter', function() {
|
||
this.style.backgroundColor = '#f8f9fa';
|
||
});
|
||
row.addEventListener('mouseleave', function() {
|
||
this.style.backgroundColor = '';
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.badge-sm {
|
||
font-size: 0.7em;
|
||
}
|
||
|
||
.btn-group-vertical .btn {
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.table td {
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.img-thumbnail {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.admin-table .table thead th {
|
||
background-color: #f8f9fa;
|
||
border-bottom: 2px solid #dee2e6;
|
||
font-weight: 600;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.btn-group-vertical {
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-group-vertical .btn {
|
||
font-size: 0.8rem;
|
||
padding: 0.25rem 0.5rem;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/profile.html
|
||
📊 大小: 5778 bytes (5.64 KB)
|
||
🕒 修改时间: 2025-07-03 05:59:09
|
||
🔸==============================================================================
|
||
|
||
{% extends "admin/base.html" %}
|
||
|
||
{% block title %}个人资料 - 太白购物商城管理后台{% endblock %}
|
||
|
||
{% block page_title %}个人资料{% endblock %}
|
||
{% block page_description %}管理员个人信息设置{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<div class="card">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-person-gear"></i>
|
||
基本信息
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="POST" action="{{ url_for('admin.edit_profile') }}">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="username" class="form-label">用户名</label>
|
||
<input type="text" class="form-control" id="username"
|
||
value="{{ admin.username }}" readonly>
|
||
<div class="form-text">用户名不可修改</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="real_name" class="form-label">真实姓名</label>
|
||
<input type="text" class="form-control" id="real_name" name="real_name"
|
||
value="{{ admin.real_name or '' }}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="email" class="form-label">邮箱地址</label>
|
||
<input type="email" class="form-control" id="email" name="email"
|
||
value="{{ admin.email or '' }}">
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label for="phone" class="form-label">手机号</label>
|
||
<input type="tel" class="form-control" id="phone" name="phone"
|
||
value="{{ admin.phone or '' }}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-check-lg"></i> 保存修改
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4">
|
||
<div class="card">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-info-circle"></i>
|
||
账号信息
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<strong>角色:</strong>
|
||
<span class="badge bg-success">{{ admin.role }}</span>
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>状态:</strong>
|
||
{% if admin.status == 1 %}
|
||
<span class="badge bg-success">正常</span>
|
||
{% else %}
|
||
<span class="badge bg-danger">禁用</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>创建时间:</strong><br>
|
||
{{ admin.created_at.strftime('%Y-%m-%d %H:%M:%S') if admin.created_at else '' }}
|
||
</div>
|
||
<div class="mb-3">
|
||
<strong>最后登录:</strong><br>
|
||
{{ admin.last_login_at.strftime('%Y-%m-%d %H:%M:%S') if admin.last_login_at else '从未登录' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 修改密码 -->
|
||
<div class="card mt-4">
|
||
<div class="card-header bg-white">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-key"></i>
|
||
修改密码
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="POST" action="{{ url_for('admin.change_password') }}">
|
||
<div class="mb-3">
|
||
<label for="current_password" class="form-label">当前密码</label>
|
||
<input type="password" class="form-control" id="current_password"
|
||
name="current_password" required>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="new_password" class="form-label">新密码</label>
|
||
<input type="password" class="form-control" id="new_password"
|
||
name="new_password" required>
|
||
<div class="form-text">密码长度至少6位,建议包含字母和数字</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="confirm_password" class="form-label">确认新密码</label>
|
||
<input type="password" class="form-control" id="confirm_password"
|
||
name="confirm_password" required>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-warning">
|
||
<i class="bi bi-key"></i> 修改密码
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/admin/users.html
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/base.html
|
||
📊 大小: 11658 bytes (11.38 KB)
|
||
🕒 修改时间: 2025-07-03 15:26:16
|
||
🔸==============================================================================
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{% block title %}太白购物商城{% endblock %}</title>
|
||
<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">
|
||
<style>
|
||
.navbar-brand {
|
||
font-weight: bold;
|
||
color: #007bff !important;
|
||
}
|
||
.footer {
|
||
background-color: #f8f9fa;
|
||
padding: 2rem 0;
|
||
margin-top: 3rem;
|
||
}
|
||
.alert {
|
||
margin-bottom: 0;
|
||
}
|
||
.search-form {
|
||
max-width: 300px;
|
||
}
|
||
@media (max-width: 768px) {
|
||
.search-form {
|
||
max-width: 100%;
|
||
margin: 10px 0;
|
||
}
|
||
}
|
||
.cart-badge {
|
||
position: relative;
|
||
top: -2px;
|
||
font-size: 0.7rem;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 导航栏 -->
|
||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||
<div class="container">
|
||
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
||
<i class="bi bi-shop"></i> 太白购物商城
|
||
</a>
|
||
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
|
||
<div class="collapse navbar-collapse" id="navbarNav">
|
||
<ul class="navbar-nav me-auto">
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="{{ url_for('main.index') }}">首页</a>
|
||
</li>
|
||
<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;">
|
||
<button class="btn btn-outline-primary" type="submit">
|
||
<i class="bi bi-search"></i>
|
||
</button>
|
||
</form>
|
||
|
||
<ul class="navbar-nav">
|
||
{% if session.user_id %}
|
||
<li class="nav-item">
|
||
<a class="nav-link position-relative" href="{{ url_for('cart.index') }}" title="购物车">
|
||
<i class="bi bi-cart"></i> 购物车
|
||
<span class="badge bg-danger cart-badge" id="cartBadge" style="display: none;">0</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item dropdown">
|
||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||
<i class="bi bi-person-circle"></i> {{ session.nickname or session.username }}
|
||
</a>
|
||
<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><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">退出登录</a></li>
|
||
</ul>
|
||
</li>
|
||
{% else %}
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="{{ url_for('auth.login') }}">登录</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="{{ url_for('auth.register') }}">注册</a>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- 消息提示 -->
|
||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||
{% if messages %}
|
||
<div class="container mt-3">
|
||
{% for category, message in messages %}
|
||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||
{{ message }}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
<!-- 主要内容 -->
|
||
<main class="container mt-4">
|
||
{% block content %}{% endblock %}
|
||
</main>
|
||
|
||
<!-- 页脚 -->
|
||
<footer class="footer mt-auto">
|
||
<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>
|
||
</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>
|
||
</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>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- 返回顶部按钮 -->
|
||
<button type="button" class="btn btn-primary position-fixed bottom-0 end-0 m-3" id="backToTop"
|
||
style="display: none; z-index: 1000;" onclick="scrollToTop()">
|
||
<i class="bi bi-arrow-up"></i>
|
||
</button>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
||
<!-- 全局JavaScript -->
|
||
<script>
|
||
// 返回顶部功能
|
||
window.addEventListener('scroll', function() {
|
||
const backToTop = document.getElementById('backToTop');
|
||
if (window.pageYOffset > 300) {
|
||
backToTop.style.display = 'block';
|
||
} else {
|
||
backToTop.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
function scrollToTop() {
|
||
window.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
|
||
// 购物车数量更新(后续购物车功能开发时使用)
|
||
function updateCartBadge(count) {
|
||
const badge = document.getElementById('cartBadge');
|
||
if (count > 0) {
|
||
badge.textContent = count;
|
||
badge.style.display = 'inline-block';
|
||
} else {
|
||
badge.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后的初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 当前页面高亮(可选)
|
||
const currentPath = window.location.pathname;
|
||
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
|
||
|
||
navLinks.forEach(link => {
|
||
if (link.getAttribute('href') === currentPath) {
|
||
link.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// 初始化购物车数量(从localStorage或服务器获取)
|
||
// TODO: 实现购物车数量获取
|
||
});
|
||
|
||
// 通用AJAX错误处理
|
||
function handleAjaxError(xhr) {
|
||
if (xhr.status === 401) {
|
||
alert('请先登录');
|
||
window.location.href = '/auth/login';
|
||
} else if (xhr.status === 403) {
|
||
alert('没有权限执行此操作');
|
||
} else {
|
||
alert('操作失败,请稍后再试');
|
||
}
|
||
}
|
||
|
||
// 通用成功提示
|
||
function showSuccessMessage(message) {
|
||
// 创建临时提示框
|
||
const alertDiv = document.createElement('div');
|
||
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
||
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);
|
||
|
||
// 3秒后自动消失
|
||
setTimeout(() => {
|
||
if (alertDiv.parentNode) {
|
||
alertDiv.parentNode.removeChild(alertDiv);
|
||
}
|
||
}, 3000);
|
||
}
|
||
</script>
|
||
|
||
{% block scripts %}{% endblock %}
|
||
</body>
|
||
</html>
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/cart/index.html
|
||
📊 大小: 16640 bytes (16.25 KB)
|
||
🕒 修改时间: 2025-07-03 15:25:13
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
|
||
{% block title %}购物车 - 太白购物商城{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container">
|
||
<!-- 面包屑导航 -->
|
||
<nav aria-label="breadcrumb" class="mb-3">
|
||
<ol class="breadcrumb">
|
||
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
|
||
<li class="breadcrumb-item active">购物车</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<h3><i class="bi bi-cart"></i> 我的购物车</h3>
|
||
<hr>
|
||
</div>
|
||
</div>
|
||
|
||
{% if cart_items %}
|
||
<div class="row">
|
||
<!-- 购物车商品列表 -->
|
||
<div class="col-md-8">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="row align-items-center">
|
||
<div class="col-1">
|
||
<input type="checkbox" class="form-check-input" id="selectAll">
|
||
</div>
|
||
<div class="col-5">商品信息</div>
|
||
<div class="col-2 text-center">单价</div>
|
||
<div class="col-2 text-center">数量</div>
|
||
<div class="col-2 text-center">操作</div>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
{% for item in cart_items %}
|
||
<div class="cart-item border-bottom p-3" data-cart-id="{{ item.id }}">
|
||
<div class="row align-items-center">
|
||
<!-- 选择框 -->
|
||
<div class="col-1">
|
||
<input type="checkbox" class="form-check-input item-checkbox"
|
||
value="{{ item.id }}" {% if item.is_available() %}{% else %}disabled{% endif %}>
|
||
</div>
|
||
|
||
<!-- 商品信息 -->
|
||
<div class="col-5">
|
||
<div class="d-flex">
|
||
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}"
|
||
class="text-decoration-none">
|
||
{% if item.product.main_image %}
|
||
<img src="{{ item.product.main_image }}" alt="{{ item.product.name }}"
|
||
class="me-3" style="width: 80px; height: 80px; object-fit: cover;">
|
||
{% else %}
|
||
<div class="bg-light me-3 d-flex align-items-center justify-content-center"
|
||
style="width: 80px; height: 80px;">
|
||
<i class="bi bi-image text-muted"></i>
|
||
</div>
|
||
{% endif %}
|
||
</a>
|
||
<div class="flex-grow-1">
|
||
<h6 class="mb-1">
|
||
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}"
|
||
class="text-decoration-none text-dark">
|
||
{{ item.product.name }}
|
||
</a>
|
||
</h6>
|
||
{% if item.product.brand %}
|
||
<small class="text-muted">品牌:{{ item.product.brand }}</small><br>
|
||
{% endif %}
|
||
{% if item.spec_combination %}
|
||
<small class="text-muted">规格:{{ item.spec_combination }}</small><br>
|
||
{% endif %}
|
||
<small class="text-muted">库存:{{ item.get_stock() }}件</small>
|
||
|
||
{% if not item.is_available() %}
|
||
<div class="mt-1">
|
||
{% if item.product.status != 1 %}
|
||
<span class="badge bg-danger">商品已下架</span>
|
||
{% elif item.get_stock() < item.quantity %}
|
||
<span class="badge bg-warning">库存不足</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 单价 -->
|
||
<div class="col-2 text-center">
|
||
<span class="fw-bold text-danger">¥{{ "%.2f"|format(item.get_price()) }}</span>
|
||
</div>
|
||
|
||
<!-- 数量 -->
|
||
<div class="col-2 text-center">
|
||
<div class="input-group" style="width: 100px; margin: 0 auto;">
|
||
<button class="btn btn-outline-secondary btn-sm" type="button"
|
||
onclick="changeQuantity({{ item.id }}, -1)"
|
||
{% if not item.is_available() or item.quantity <= 1 %}disabled{% endif %}>-</button>
|
||
<input type="number" class="form-control form-control-sm text-center quantity-input"
|
||
value="{{ item.quantity }}" min="1" max="{{ item.get_stock() }}"
|
||
data-cart-id="{{ item.id }}" onchange="updateQuantity({{ item.id }}, this.value)"
|
||
{% if not item.is_available() %}disabled{% endif %}>
|
||
<button class="btn btn-outline-secondary btn-sm" type="button"
|
||
onclick="changeQuantity({{ item.id }}, 1)"
|
||
{% if not item.is_available() or item.quantity >= item.get_stock() %}disabled{% endif %}>+</button>
|
||
</div>
|
||
<div class="mt-1">
|
||
<small class="text-muted">小计:¥<span class="item-total">{{ "%.2f"|format(item.get_total_price()) }}</span></small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作 -->
|
||
<div class="col-2 text-center">
|
||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||
onclick="removeItem({{ item.id }})">
|
||
<i class="bi bi-trash"></i> 删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 购物车操作栏 -->
|
||
<div class="col-md-4">
|
||
<div class="card position-sticky" style="top: 20px;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-calculator"></i> 结算信息</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between mb-2">
|
||
<span>已选商品:</span>
|
||
<span id="selectedCount">0</span>件
|
||
</div>
|
||
<div class="d-flex justify-content-between mb-3">
|
||
<span>商品总价:</span>
|
||
<span class="text-danger fw-bold h5">¥<span id="selectedTotal">0.00</span></span>
|
||
</div>
|
||
<hr>
|
||
<div class="d-flex justify-content-between mb-3">
|
||
<span class="fw-bold">应付总额:</span>
|
||
<span class="text-danger fw-bold h4">¥<span id="finalTotal">0.00</span></span>
|
||
</div>
|
||
|
||
<div class="d-grid gap-2">
|
||
<button type="button" class="btn btn-danger btn-lg" id="checkoutBtn"
|
||
onclick="checkout()" disabled>
|
||
<i class="bi bi-credit-card"></i> 去结算
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary" onclick="clearCart()">
|
||
<i class="bi bi-trash"></i> 清空购物车
|
||
</button>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
<small class="text-muted">
|
||
<i class="bi bi-shield-check"></i> 7天无理由退换<br>
|
||
<i class="bi bi-truck"></i> 全国包邮<br>
|
||
<i class="bi bi-award"></i> 正品保证
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% else %}
|
||
<!-- 空购物车 -->
|
||
<div class="text-center py-5">
|
||
<i class="bi bi-cart-x text-muted" style="font-size: 5rem;"></i>
|
||
<h4 class="text-muted mt-3">购物车是空的</h4>
|
||
<p class="text-muted">快去选购您喜欢的商品吧!</p>
|
||
<a href="{{ url_for('main.product_list') }}" class="btn btn-primary btn-lg">
|
||
<i class="bi bi-shop"></i> 去购物
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
let selectedItems = new Set();
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
updateSelectAllState();
|
||
updateTotalPrice();
|
||
});
|
||
|
||
// 全选/取消全选
|
||
document.getElementById('selectAll').addEventListener('change', function() {
|
||
const checkboxes = document.querySelectorAll('.item-checkbox:not(:disabled)');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.checked = this.checked;
|
||
if (this.checked) {
|
||
selectedItems.add(parseInt(checkbox.value));
|
||
} else {
|
||
selectedItems.delete(parseInt(checkbox.value));
|
||
}
|
||
});
|
||
updateTotalPrice();
|
||
});
|
||
|
||
// 单个商品选择
|
||
document.querySelectorAll('.item-checkbox').forEach(checkbox => {
|
||
checkbox.addEventListener('change', function() {
|
||
const cartId = parseInt(this.value);
|
||
if (this.checked) {
|
||
selectedItems.add(cartId);
|
||
} else {
|
||
selectedItems.delete(cartId);
|
||
}
|
||
updateSelectAllState();
|
||
updateTotalPrice();
|
||
});
|
||
});
|
||
|
||
// 更新全选状态
|
||
function updateSelectAllState() {
|
||
const selectAllCheckbox = document.getElementById('selectAll');
|
||
const availableCheckboxes = document.querySelectorAll('.item-checkbox:not(:disabled)');
|
||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:not(:disabled):checked');
|
||
|
||
if (availableCheckboxes.length === 0) {
|
||
selectAllCheckbox.disabled = true;
|
||
selectAllCheckbox.checked = false;
|
||
} else {
|
||
selectAllCheckbox.disabled = false;
|
||
selectAllCheckbox.checked = availableCheckboxes.length === checkedCheckboxes.length;
|
||
}
|
||
}
|
||
|
||
// 更新总价
|
||
function updateTotalPrice() {
|
||
let totalPrice = 0;
|
||
let selectedCount = 0;
|
||
|
||
selectedItems.forEach(cartId => {
|
||
const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`);
|
||
if (cartItem) {
|
||
const itemTotal = parseFloat(cartItem.querySelector('.item-total').textContent);
|
||
const quantity = parseInt(cartItem.querySelector('.quantity-input').value);
|
||
totalPrice += itemTotal;
|
||
selectedCount += quantity;
|
||
}
|
||
});
|
||
|
||
document.getElementById('selectedCount').textContent = selectedCount;
|
||
document.getElementById('selectedTotal').textContent = totalPrice.toFixed(2);
|
||
document.getElementById('finalTotal').textContent = totalPrice.toFixed(2);
|
||
|
||
// 更新结算按钮状态
|
||
const checkoutBtn = document.getElementById('checkoutBtn');
|
||
checkoutBtn.disabled = selectedItems.size === 0;
|
||
}
|
||
|
||
// 修改数量
|
||
function changeQuantity(cartId, delta) {
|
||
const input = document.querySelector(`[data-cart-id="${cartId}"]`);
|
||
const currentValue = parseInt(input.value);
|
||
const newValue = currentValue + delta;
|
||
|
||
if (newValue >= 1 && newValue <= parseInt(input.max)) {
|
||
updateQuantity(cartId, newValue);
|
||
}
|
||
}
|
||
|
||
// 更新数量
|
||
function updateQuantity(cartId, quantity) {
|
||
if (quantity < 1) return;
|
||
|
||
fetch('/cart/update', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
cart_id: cartId,
|
||
quantity: parseInt(quantity)
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 更新页面显示
|
||
const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`);
|
||
cartItem.querySelector('.quantity-input').value = quantity;
|
||
cartItem.querySelector('.item-total').textContent = data.item_total.toFixed(2);
|
||
|
||
// 更新总价
|
||
updateTotalPrice();
|
||
|
||
// 更新全局购物车数量
|
||
updateCartBadge(data.cart_count);
|
||
|
||
showSuccessMessage('数量更新成功');
|
||
} else {
|
||
alert(data.message);
|
||
// 恢复原始值
|
||
location.reload();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('更新失败');
|
||
location.reload();
|
||
});
|
||
}
|
||
|
||
// 删除商品
|
||
function removeItem(cartId) {
|
||
if (!confirm('确定要删除这件商品吗?')) {
|
||
return;
|
||
}
|
||
|
||
fetch('/cart/remove', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
cart_id: cartId
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 从页面中移除商品
|
||
const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`);
|
||
cartItem.remove();
|
||
|
||
// 从选中列表中移除
|
||
selectedItems.delete(cartId);
|
||
|
||
// 更新显示
|
||
updateSelectAllState();
|
||
updateTotalPrice();
|
||
updateCartBadge(data.cart_count);
|
||
|
||
showSuccessMessage('商品已删除');
|
||
|
||
// 如果购物车为空,刷新页面
|
||
if (data.cart_count === 0) {
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1000);
|
||
}
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('删除失败');
|
||
});
|
||
}
|
||
|
||
// 清空购物车
|
||
function clearCart() {
|
||
if (!confirm('确定要清空购物车吗?')) {
|
||
return;
|
||
}
|
||
|
||
fetch('/cart/clear', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showSuccessMessage('购物车已清空');
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1000);
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('清空失败');
|
||
});
|
||
}
|
||
|
||
// 去结算
|
||
function checkout() {
|
||
if (selectedItems.size === 0) {
|
||
alert('请选择要购买的商品');
|
||
return;
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
selectedItems.forEach(cartId => {
|
||
params.append('items', cartId);
|
||
});
|
||
|
||
window.location.href = `/cart/checkout?${params.toString()}`;
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.cart-item {
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.cart-item:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.quantity-input {
|
||
width: 60px;
|
||
}
|
||
|
||
.item-checkbox {
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
.position-sticky {
|
||
top: 20px !important;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.col-md-4 .position-sticky {
|
||
position: relative !important;
|
||
top: auto !important;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/common/footer.html
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/common/header.html
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/common/pagination.html
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/index.html
|
||
📊 大小: 9706 bytes (9.48 KB)
|
||
🕒 修改时间: 2025-07-03 14:49:14
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
|
||
{% block title %}首页 - 太白购物商城{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- 欢迎横幅 -->
|
||
<div class="jumbotron bg-primary text-white rounded p-5 mb-4">
|
||
<div class="container-fluid py-5">
|
||
<h1 class="display-5 fw-bold">欢迎来到太白购物商城</h1>
|
||
{% if user %}
|
||
<p class="col-md-8 fs-4">你好,{{ user.nickname or user.username }}!开始您的购物之旅吧!</p>
|
||
{% else %}
|
||
<p class="col-md-8 fs-4">发现优质商品,享受便捷购物体验</p>
|
||
<a class="btn btn-light btn-lg" href="{{ url_for('auth.register') }}" role="button">立即注册</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品分类导航 -->
|
||
{% if top_categories %}
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<h4><i class="bi bi-grid"></i> 商品分类</h4>
|
||
<hr>
|
||
</div>
|
||
{% for category in top_categories %}
|
||
<div class="col-md-2 col-6 mb-3">
|
||
<a href="{{ url_for('main.product_list', category_id=category.id) }}" class="text-decoration-none">
|
||
<div class="card text-center h-100 category-card">
|
||
<div class="card-body">
|
||
{% if category.icon_url %}
|
||
<img src="{{ category.icon_url }}" alt="{{ category.name }}" class="mb-2" style="width: 48px; height: 48px; object-fit: cover;">
|
||
{% else %}
|
||
<i class="bi bi-tag display-4 text-primary mb-2"></i>
|
||
{% endif %}
|
||
<h6 class="card-title">{{ category.name }}</h6>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 热门商品 -->
|
||
{% if hot_products %}
|
||
<div class="row mb-4">
|
||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||
<h4><i class="bi bi-fire"></i> 热门商品</h4>
|
||
<a href="{{ url_for('main.product_list', sort='sales') }}" class="btn btn-outline-primary btn-sm">
|
||
查看更多 <i class="bi bi-arrow-right"></i>
|
||
</a>
|
||
</div>
|
||
<hr>
|
||
{% for product in hot_products %}
|
||
<div class="col-lg-3 col-md-4 col-sm-6 mb-4">
|
||
<div class="card h-100 product-card">
|
||
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none">
|
||
{% if product.main_image %}
|
||
<img src="{{ product.main_image }}" class="card-img-top" alt="{{ product.name }}" style="height: 200px; object-fit: cover;">
|
||
{% else %}
|
||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 200px;">
|
||
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
|
||
</div>
|
||
{% endif %}
|
||
</a>
|
||
<div class="card-body">
|
||
<h6 class="card-title">
|
||
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none text-dark">
|
||
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
|
||
</a>
|
||
</h6>
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<span class="text-danger fw-bold">¥{{ "%.2f"|format(product.price) }}</span>
|
||
{% if product.original_price and product.original_price > product.price %}
|
||
<small class="text-muted text-decoration-line-through">¥{{ "%.2f"|format(product.original_price) }}</small>
|
||
{% endif %}
|
||
</div>
|
||
<small class="text-muted">销量{{ product.sales_count }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 最新商品 -->
|
||
{% if new_products %}
|
||
<div class="row mb-4">
|
||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||
<h4><i class="bi bi-stars"></i> 最新商品</h4>
|
||
<a href="{{ url_for('main.product_list', sort='newest') }}" class="btn btn-outline-primary btn-sm">
|
||
查看更多 <i class="bi bi-arrow-right"></i>
|
||
</a>
|
||
</div>
|
||
<hr>
|
||
{% for product in new_products %}
|
||
<div class="col-lg-3 col-md-4 col-sm-6 mb-4">
|
||
<div class="card h-100 product-card">
|
||
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none">
|
||
{% if product.main_image %}
|
||
<img src="{{ product.main_image }}" class="card-img-top" alt="{{ product.name }}" style="height: 200px; object-fit: cover;">
|
||
{% else %}
|
||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 200px;">
|
||
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
|
||
</div>
|
||
{% endif %}
|
||
</a>
|
||
<div class="card-body">
|
||
<h6 class="card-title">
|
||
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none text-dark">
|
||
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
|
||
</a>
|
||
</h6>
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<span class="text-danger fw-bold">¥{{ "%.2f"|format(product.price) }}</span>
|
||
{% if product.original_price and product.original_price > product.price %}
|
||
<small class="text-muted text-decoration-line-through">¥{{ "%.2f"|format(product.original_price) }}</small>
|
||
{% endif %}
|
||
</div>
|
||
<small class="text-muted">销量{{ product.sales_count }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if user %}
|
||
<!-- 用户专区 -->
|
||
<div class="row mt-5">
|
||
<div class="col-12">
|
||
<h4><i class="bi bi-person-circle"></i> 我的专区</h4>
|
||
<hr>
|
||
</div>
|
||
|
||
<div class="col-md-3 mb-3">
|
||
<div class="card text-center">
|
||
<div class="card-body">
|
||
<i class="bi bi-person display-4 text-info mb-2"></i>
|
||
<h6 class="card-title">个人中心</h6>
|
||
<a href="{{ url_for('user.profile') }}" class="btn btn-sm btn-outline-info">进入</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 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>
|
||
<a href="{{ url_for('user.orders') }}" class="btn btn-sm btn-outline-primary">查看</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 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>
|
||
<a href="#" class="btn btn-sm btn-outline-success">查看</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 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>
|
||
<a href="#" class="btn btn-sm btn-outline-danger">查看</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 功能特色 -->
|
||
<div class="row mt-5">
|
||
<div class="col-12">
|
||
<h4><i class="bi bi-star"></i> 服务特色</h4>
|
||
<hr>
|
||
</div>
|
||
|
||
<div class="col-md-4 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-body text-center">
|
||
<i class="bi bi-tags display-4 text-primary mb-3"></i>
|
||
<h5 class="card-title">精选商品</h5>
|
||
<p class="card-text">汇聚全球优质商品,品质保证,价格实惠</p>
|
||
<a href="{{ url_for('main.product_list') }}" class="btn btn-outline-primary">浏览商品</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-body text-center">
|
||
<i class="bi bi-truck display-4 text-success mb-3"></i>
|
||
<h5 class="card-title">快速配送</h5>
|
||
<p class="card-text">全国包邮,快速配送,让您尽快收到心仪商品</p>
|
||
<a href="#" class="btn btn-outline-success">了解更多</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-body text-center">
|
||
<i class="bi bi-shield-check display-4 text-warning mb-3"></i>
|
||
<h5 class="card-title">安全保障</h5>
|
||
<p class="card-text">正品保证,售后无忧,让您购物更放心</p>
|
||
<a href="#" class="btn btn-outline-warning">服务保障</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<style>
|
||
.product-card {
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.product-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.category-card {
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.category-card:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/order/checkout.html
|
||
📊 大小: 15245 bytes (14.89 KB)
|
||
🕒 修改时间: 2025-07-04 02:41:53
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
{% block title %}订单结算 - 太白购物商城{% endblock %}
|
||
|
||
{% block head %}
|
||
<style>
|
||
.checkout-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.address-card {
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.address-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.address-card.selected {
|
||
border-color: #007bff;
|
||
background-color: #f8f9ff;
|
||
}
|
||
|
||
.product-item {
|
||
border-bottom: 1px solid #eee;
|
||
padding: 15px 0;
|
||
}
|
||
|
||
.product-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.order-summary {
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.price-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.total-price {
|
||
font-size: 1.2em;
|
||
font-weight: bold;
|
||
color: #e74c3c;
|
||
}
|
||
</style>
|
||
{% 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('cart.index') }}">购物车</a></li>
|
||
<li class="breadcrumb-item active">订单结算</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<div class="row">
|
||
<div class="col-lg-8">
|
||
<!-- 收货地址 -->
|
||
<div class="card checkout-section">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5><i class="bi bi-geo-alt"></i> 收货地址</h5>
|
||
<a href="{{ url_for('address.add') }}" class="btn btn-outline-primary btn-sm">
|
||
<i class="bi bi-plus"></i> 新增地址
|
||
</a>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row" id="addressList">
|
||
{% for address in addresses %}
|
||
<div class="col-md-6 mb-3">
|
||
<div class="card address-card {% if address.is_default %}selected{% endif %}"
|
||
data-address-id="{{ address.id }}" onclick="selectAddress({{ address.id }})">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div>
|
||
<h6 class="mb-1">{{ address.receiver_name }}</h6>
|
||
<p class="text-muted mb-1">{{ address.receiver_phone }}</p>
|
||
<p class="mb-0">{{ address.get_full_address() }}</p>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="address_id"
|
||
value="{{ address.id }}" {% if address.is_default %}checked{% endif %}>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品信息 -->
|
||
<div class="card checkout-section">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-box"></i> 商品信息</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% for item in cart_items %}
|
||
<div class="product-item">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-2">
|
||
<img src="{{ item.product.main_image or '/static/images/default-product.jpg' }}"
|
||
class="img-fluid rounded" alt="{{ item.product.name }}">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<h6>{{ item.product.name }}</h6>
|
||
{% if item.spec_combination %}
|
||
<p class="text-muted mb-0">{{ item.spec_combination }}</p>
|
||
{% endif %}
|
||
{% if item.product.brand %}
|
||
<small class="text-muted">{{ item.product.brand }}</small>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-md-2 text-center">
|
||
<span class="text-muted">× {{ item.quantity }}</span>
|
||
</div>
|
||
<div class="col-md-2 text-end">
|
||
<span class="fw-bold">¥{{ "%.2f"|format(item.get_total_price()) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 配送方式 -->
|
||
<div class="card checkout-section">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-truck"></i> 配送方式</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-md-4">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="shipping_method"
|
||
value="standard" id="shipping_standard" checked onchange="updateShippingFee()">
|
||
<label class="form-check-label" for="shipping_standard">
|
||
<strong>标准配送</strong><br>
|
||
<small class="text-muted">免费 • 3-5个工作日</small>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="shipping_method"
|
||
value="express" id="shipping_express" onchange="updateShippingFee()">
|
||
<label class="form-check-label" for="shipping_express">
|
||
<strong>次日达</strong><br>
|
||
<small class="text-muted">+10元 • 次日送达</small>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="shipping_method"
|
||
value="same_day" id="shipping_same_day" onchange="updateShippingFee()">
|
||
<label class="form-check-label" for="shipping_same_day">
|
||
<strong>当日达</strong><br>
|
||
<small class="text-muted">+20元 • 当日送达</small>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支付方式 -->
|
||
<div class="card checkout-section">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-credit-card"></i> 支付方式</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-md-4">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="payment_method"
|
||
value="wechat" id="payment_wechat" checked>
|
||
<label class="form-check-label" for="payment_wechat">
|
||
<i class="bi bi-wechat text-success me-2"></i>
|
||
<strong>微信支付</strong>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="payment_method"
|
||
value="alipay" id="payment_alipay">
|
||
<label class="form-check-label" for="payment_alipay">
|
||
<i class="bi bi-alipay text-primary me-2"></i>
|
||
<strong>支付宝</strong>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="payment_method"
|
||
value="bank" id="payment_bank">
|
||
<label class="form-check-label" for="payment_bank">
|
||
<i class="bi bi-credit-card text-info me-2"></i>
|
||
<strong>银行卡</strong>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 备注 -->
|
||
<div class="card checkout-section">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-chat-text"></i> 订单备注</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<textarea class="form-control" id="orderRemark" rows="3"
|
||
placeholder="如有特殊需求请在此说明(选填)"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 订单摘要 -->
|
||
<div class="col-lg-4">
|
||
<div class="card position-sticky" style="top: 20px;">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-receipt"></i> 订单摘要</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="order-summary">
|
||
<div class="price-row">
|
||
<span>商品总价:</span>
|
||
<span id="subtotal">¥{{ "%.2f"|format(total_amount) }}</span>
|
||
</div>
|
||
<div class="price-row">
|
||
<span>运费:</span>
|
||
<span id="shippingFee">¥{{ "%.2f"|format(shipping_fee) }}</span>
|
||
</div>
|
||
<hr>
|
||
<div class="price-row total-price">
|
||
<span>应付总额:</span>
|
||
<span id="totalAmount">¥{{ "%.2f"|format(final_amount) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-danger w-100 mt-3 btn-lg" onclick="submitOrder()">
|
||
<i class="bi bi-check-circle"></i> 提交订单
|
||
</button>
|
||
|
||
<div class="mt-3 text-center">
|
||
<small class="text-muted">
|
||
点击"提交订单"表示您同意
|
||
<a href="#" class="text-decoration-none">《用户协议》</a>
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
let selectedAddressId = {{ (addresses[0].id if addresses else 0) }};
|
||
let subtotal = {{ total_amount }};
|
||
|
||
// 选择地址
|
||
function selectAddress(addressId) {
|
||
selectedAddressId = addressId;
|
||
|
||
// 更新UI
|
||
document.querySelectorAll('.address-card').forEach(card => {
|
||
card.classList.remove('selected');
|
||
});
|
||
|
||
document.querySelector(`[data-address-id="${addressId}"]`).classList.add('selected');
|
||
|
||
// 更新单选按钮
|
||
document.querySelector(`input[value="${addressId}"]`).checked = true;
|
||
}
|
||
|
||
// 更新运费
|
||
function updateShippingFee() {
|
||
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value;
|
||
let fee = 0;
|
||
|
||
switch(shippingMethod) {
|
||
case 'express':
|
||
fee = 10;
|
||
break;
|
||
case 'same_day':
|
||
fee = 20;
|
||
break;
|
||
default:
|
||
fee = 0;
|
||
}
|
||
|
||
document.getElementById('shippingFee').textContent = `¥${fee.toFixed(2)}`;
|
||
document.getElementById('totalAmount').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;
|
||
|
||
// 获取选中的购物车商品ID
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const selectedItems = urlParams.getAll('items');
|
||
|
||
if (selectedItems.length === 0) {
|
||
showAlert('没有选中的商品', 'error');
|
||
return;
|
||
}
|
||
|
||
const orderData = {
|
||
selected_items: selectedItems,
|
||
address_id: selectedAddressId,
|
||
shipping_method: shippingMethod,
|
||
payment_method: paymentMethod,
|
||
remark: remark
|
||
};
|
||
|
||
// 显示加载状态
|
||
const submitBtn = document.querySelector('.btn-danger');
|
||
const originalText = submitBtn.innerHTML;
|
||
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...';
|
||
submitBtn.disabled = true;
|
||
|
||
fetch('/order/create', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(orderData)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert('订单创建成功!正在跳转到支付页面...', 'success');
|
||
setTimeout(() => {
|
||
window.location.href = `/order/pay/${data.payment_sn}`;
|
||
}, 1500);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
submitBtn.innerHTML = originalText;
|
||
submitBtn.disabled = false;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('提交订单失败,请重试', 'error');
|
||
submitBtn.innerHTML = originalText;
|
||
submitBtn.disabled = false;
|
||
});
|
||
}
|
||
|
||
// 初始化选中的地址
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const defaultAddress = document.querySelector('input[name="address_id"]:checked');
|
||
if (defaultAddress) {
|
||
selectedAddressId = parseInt(defaultAddress.value);
|
||
}
|
||
});
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/order/detail.html
|
||
📊 大小: 14300 bytes (13.96 KB)
|
||
🕒 修改时间: 2025-07-04 02:44:35
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
{% block title %}订单详情 - 太白购物商城{% endblock %}
|
||
|
||
{% block head %}
|
||
<style>
|
||
.order-status-timeline {
|
||
position: relative;
|
||
padding-left: 30px;
|
||
}
|
||
|
||
.timeline-item {
|
||
position: relative;
|
||
padding-bottom: 20px;
|
||
}
|
||
|
||
.timeline-item::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: -30px;
|
||
top: 0;
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
background-color: #dee2e6;
|
||
}
|
||
|
||
.timeline-item.completed::before {
|
||
background-color: #28a745;
|
||
}
|
||
|
||
.timeline-item.current::before {
|
||
background-color: #007bff;
|
||
box-shadow: 0 0 0 4px rgba(0,123,255,0.2);
|
||
}
|
||
|
||
.timeline-item::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: -24px;
|
||
top: 12px;
|
||
width: 2px;
|
||
height: calc(100% - 12px);
|
||
background-color: #dee2e6;
|
||
}
|
||
|
||
.timeline-item:last-child::after {
|
||
display: none;
|
||
}
|
||
|
||
.timeline-item.completed::after {
|
||
background-color: #28a745;
|
||
}
|
||
|
||
.order-detail-card {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.product-item {
|
||
border-bottom: 1px solid #f0f0f0;
|
||
padding: 15px 0;
|
||
}
|
||
|
||
.product-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.product-image {
|
||
width: 80px;
|
||
height: 80px;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.total-amount {
|
||
color: #e74c3c;
|
||
font-weight: bold;
|
||
font-size: 1.2em;
|
||
}
|
||
</style>
|
||
{% 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 active">订单详情</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<div class="row">
|
||
<div class="col-lg-8">
|
||
<!-- 订单状态 -->
|
||
<div class="card order-detail-card">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-clock-history"></i> 订单状态</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="order-status-timeline">
|
||
<div class="timeline-item completed">
|
||
<h6>订单已提交</h6>
|
||
<p class="text-muted mb-0">{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||
</div>
|
||
|
||
<div class="timeline-item {% if order.status >= 2 %}completed{% elif order.status == 1 %}current{% endif %}">
|
||
<h6>等待买家付款</h6>
|
||
{% if order.status >= 2 %}
|
||
<p class="text-muted mb-0">已完成</p>
|
||
{% else %}
|
||
<p class="text-muted mb-0">请在15分钟内完成支付</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="timeline-item {% if order.status >= 3 %}completed{% elif order.status == 2 %}current{% endif %}">
|
||
<h6>卖家发货</h6>
|
||
{% if order.status >= 3 %}
|
||
<p class="text-muted mb-0">{{ order.shipped_at.strftime('%Y-%m-%d %H:%M:%S') if order.shipped_at else '已发货' }}</p>
|
||
{% else %}
|
||
<p class="text-muted mb-0">等待卖家发货</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="timeline-item {% if order.status >= 4 %}completed{% elif order.status == 3 %}current{% endif %}">
|
||
<h6>确认收货</h6>
|
||
{% if order.status >= 4 %}
|
||
<p class="text-muted mb-0">{{ order.received_at.strftime('%Y-%m-%d %H:%M:%S') if order.received_at else '已确认收货' }}</p>
|
||
{% else %}
|
||
<p class="text-muted mb-0">等待买家确认收货</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="timeline-item {% if order.status == 5 %}completed{% endif %}">
|
||
<h6>交易完成</h6>
|
||
{% if order.status == 5 %}
|
||
<p class="text-muted mb-0">交易成功</p>
|
||
{% else %}
|
||
<p class="text-muted mb-0">等待交易完成</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品信息 -->
|
||
<div class="card order-detail-card">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-box"></i> 商品信息</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% for item in order.order_items %}
|
||
<div class="product-item">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-2">
|
||
<img src="{{ item.product_image or '/static/images/default-product.jpg' }}"
|
||
class="product-image" alt="{{ item.product_name }}">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<h6 class="mb-1">{{ item.product_name }}</h6>
|
||
{% if item.spec_combination %}
|
||
<p class="text-muted mb-1">{{ item.spec_combination }}</p>
|
||
{% endif %}
|
||
<small class="text-muted">单价:¥{{ "%.2f"|format(item.price) }}</small>
|
||
</div>
|
||
<div class="col-md-2 text-center">
|
||
<span class="text-muted">× {{ item.quantity }}</span>
|
||
</div>
|
||
<div class="col-md-2 text-end">
|
||
<span class="fw-bold">¥{{ "%.2f"|format(item.total_price) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 物流信息 -->
|
||
{% if order.shipping_info %}
|
||
<div class="card order-detail-card">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-truck"></i> 物流信息</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% for shipping in order.shipping_info %}
|
||
<div class="info-row">
|
||
<span>物流公司:</span>
|
||
<span>{{ shipping.shipping_company or '待发货' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>快递单号:</span>
|
||
<span>{{ shipping.tracking_number or '待发货' }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<!-- 订单信息 -->
|
||
<div class="card order-detail-card">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-receipt"></i> 订单信息</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="info-row">
|
||
<span>订单号:</span>
|
||
<span>{{ order.order_sn }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>下单时间:</span>
|
||
<span>{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>订单状态:</span>
|
||
<span>
|
||
{% if order.status == 1 %}
|
||
<span class="badge bg-warning">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 2 %}
|
||
<span class="badge bg-info">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 3 %}
|
||
<span class="badge bg-primary">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 5 %}
|
||
<span class="badge bg-success">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 6 %}
|
||
<span class="badge bg-secondary">{{ order.get_status_text() }}</span>
|
||
{% else %}
|
||
<span class="badge bg-dark">{{ order.get_status_text() }}</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>支付方式:</span>
|
||
<span>{{ order.payment_method or '未选择' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>配送方式:</span>
|
||
<span>{{ order.shipping_method or '标准配送' }}</span>
|
||
</div>
|
||
{% if order.remark %}
|
||
<div class="info-row">
|
||
<span>备注:</span>
|
||
<span>{{ order.remark }}</span>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 收货信息 -->
|
||
<div class="card order-detail-card">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-geo-alt"></i> 收货信息</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% set receiver = order.get_receiver_info() %}
|
||
<div class="info-row">
|
||
<span>收货人:</span>
|
||
<span>{{ receiver.receiver_name or '未知' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>联系电话:</span>
|
||
<span>{{ receiver.receiver_phone or '未知' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>收货地址:</span>
|
||
<span>{{ receiver.full_address or '未知' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 费用明细 -->
|
||
<div class="card order-detail-card">
|
||
<div class="card-header">
|
||
<h5><i class="bi bi-calculator"></i> 费用明细</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="info-row">
|
||
<span>商品总价:</span>
|
||
<span>¥{{ "%.2f"|format(order.total_amount) }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>运费:</span>
|
||
<span>¥{{ "%.2f"|format(order.shipping_fee) }}</span>
|
||
</div>
|
||
<hr>
|
||
<div class="info-row">
|
||
<span><strong>应付总额:</strong></span>
|
||
<span class="total-amount">¥{{ "%.2f"|format(order.actual_amount) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="d-grid gap-2">
|
||
{% if order.can_pay() %}
|
||
<a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}"
|
||
class="btn btn-danger">立即支付</a>
|
||
{% endif %}
|
||
|
||
{% if order.can_cancel() %}
|
||
<button class="btn btn-outline-secondary" onclick="cancelOrder({{ order.id }})">
|
||
取消订单
|
||
</button>
|
||
{% endif %}
|
||
|
||
{% if order.can_confirm_receipt() %}
|
||
<button class="btn btn-success" onclick="confirmReceipt({{ order.id }})">
|
||
确认收货
|
||
</button>
|
||
{% endif %}
|
||
|
||
{% if order.status == 4 %}
|
||
<a href="#" class="btn btn-outline-warning">评价商品</a>
|
||
{% endif %}
|
||
|
||
<a href="{{ url_for('order.list') }}" class="btn btn-outline-primary">
|
||
返回订单列表
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function cancelOrder(orderId) {
|
||
if (confirm('确定要取消这个订单吗?取消后无法恢复。')) {
|
||
fetch(`/order/cancel/${orderId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert(data.message, 'success');
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('操作失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
function confirmReceipt(orderId) {
|
||
if (confirm('确定已收到商品吗?确认后订单将完成。')) {
|
||
fetch(`/order/confirm_receipt/${orderId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert(data.message, 'success');
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('操作失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/order/pay.html
|
||
📊 大小: 10772 bytes (10.52 KB)
|
||
🕒 修改时间: 2025-07-04 02:42:37
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
{% block title %}订单支付 - 太白购物商城{% endblock %}
|
||
|
||
{% block head %}
|
||
<style>
|
||
.pay-container {
|
||
max-width: 600px;
|
||
margin: 50px auto;
|
||
}
|
||
|
||
.order-info {
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.payment-method {
|
||
border: 2px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin-bottom: 15px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.payment-method:hover {
|
||
border-color: #007bff;
|
||
background-color: #f8f9ff;
|
||
}
|
||
|
||
.payment-method.selected {
|
||
border-color: #007bff;
|
||
background-color: #f8f9ff;
|
||
}
|
||
|
||
.qr-code {
|
||
text-align: center;
|
||
padding: 30px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.countdown {
|
||
font-size: 1.2em;
|
||
color: #e74c3c;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.payment-status {
|
||
text-align: center;
|
||
padding: 20px;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="pay-container">
|
||
<div class="card">
|
||
<div class="card-header text-center">
|
||
<h4><i class="bi bi-credit-card"></i> 订单支付</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- 订单信息 -->
|
||
<div class="order-info">
|
||
<h6 class="mb-3">订单信息</h6>
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<strong>订单号:</strong>{{ order.order_sn }}
|
||
</div>
|
||
<div class="col-6 text-end">
|
||
<strong class="text-danger">¥{{ "%.2f"|format(order.actual_amount) }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="row mt-2">
|
||
<div class="col-6">
|
||
<strong>支付方式:</strong>{{ order.payment_method }}
|
||
</div>
|
||
<div class="col-6 text-end">
|
||
<span class="countdown" id="countdown">14:59</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支付区域 -->
|
||
<div id="paymentArea">
|
||
{% if order.payment_method == 'wechat' %}
|
||
<div class="payment-method selected">
|
||
<div class="d-flex align-items-center">
|
||
<i class="bi bi-wechat text-success fs-1 me-3"></i>
|
||
<div>
|
||
<h6>微信支付</h6>
|
||
<p class="text-muted mb-0">请使用微信扫描二维码完成支付</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="qr-code" id="qrCodeArea" style="display: none;">
|
||
<div id="qrCodeImage">
|
||
<i class="bi bi-qr-code display-1 text-muted"></i>
|
||
<p class="mt-2">正在生成支付二维码...</p>
|
||
</div>
|
||
<p class="mt-3 text-muted">请使用微信扫描上方二维码完成支付</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if order.payment_method == 'alipay' %}
|
||
<div class="payment-method selected">
|
||
<div class="d-flex align-items-center">
|
||
<i class="bi bi-alipay text-primary fs-1 me-3"></i>
|
||
<div>
|
||
<h6>支付宝</h6>
|
||
<p class="text-muted mb-0">正在跳转到支付宝...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if order.payment_method == 'bank' %}
|
||
<div class="payment-method selected">
|
||
<div class="d-flex align-items-center">
|
||
<i class="bi bi-credit-card text-info fs-1 me-3"></i>
|
||
<div>
|
||
<h6>银行卡支付</h6>
|
||
<p class="text-muted mb-0">正在跳转到网银...</p>
|
||
</div>
|
||
</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>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="d-flex gap-2 mt-4">
|
||
<button class="btn btn-primary flex-fill" onclick="startPayment()">
|
||
<i class="bi bi-credit-card"></i> 立即支付
|
||
</button>
|
||
<button class="btn btn-outline-secondary" onclick="checkPaymentStatus()">
|
||
<i class="bi bi-arrow-clockwise"></i> 刷新状态
|
||
</button>
|
||
<button class="btn btn-outline-danger" onclick="cancelOrder()">
|
||
<i class="bi bi-x-circle"></i> 取消订单
|
||
</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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
let countdownTimer;
|
||
let statusCheckTimer;
|
||
let timeLeft = 15 * 60; // 15分钟
|
||
|
||
// 开始倒计时
|
||
function startCountdown() {
|
||
countdownTimer = setInterval(() => {
|
||
timeLeft--;
|
||
|
||
const minutes = Math.floor(timeLeft / 60);
|
||
const seconds = timeLeft % 60;
|
||
|
||
document.getElementById('countdown').textContent =
|
||
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
|
||
if (timeLeft <= 0) {
|
||
clearInterval(countdownTimer);
|
||
showAlert('订单已过期,请重新下单', 'warning');
|
||
setTimeout(() => {
|
||
window.location.href = '/order/list';
|
||
}, 2000);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// 开始支付
|
||
function startPayment() {
|
||
const paymentSn = '{{ payment.payment_sn }}';
|
||
const paymentMethod = '{{ order.payment_method }}';
|
||
|
||
fetch('/payment/process', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
payment_sn: paymentSn,
|
||
payment_method: paymentMethod
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
if (data.payment_type === 'qrcode') {
|
||
showQRCode(data.qr_code_url);
|
||
startStatusCheck();
|
||
} else if (data.payment_type === 'redirect') {
|
||
window.open(data.pay_url, '_blank');
|
||
startStatusCheck();
|
||
}
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('支付启动失败,请重试', 'error');
|
||
});
|
||
}
|
||
|
||
// 显示二维码
|
||
function showQRCode(qrUrl) {
|
||
const qrArea = document.getElementById('qrCodeArea');
|
||
const qrImage = document.getElementById('qrCodeImage');
|
||
|
||
// 这里应该使用真实的二维码生成库,现在用文本模拟
|
||
qrImage.innerHTML = `
|
||
<div style="width: 200px; height: 200px; margin: 0 auto; background: #f0f0f0;
|
||
display: flex; align-items: center; justify-content: center; border: 1px solid #ddd;">
|
||
<div style="text-align: center;">
|
||
<i class="bi bi-qr-code display-4"></i><br>
|
||
<small>微信支付二维码</small>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
qrArea.style.display = 'block';
|
||
}
|
||
|
||
// 开始检查支付状态
|
||
function startStatusCheck() {
|
||
statusCheckTimer = setInterval(() => {
|
||
checkPaymentStatus();
|
||
}, 3000); // 每3秒检查一次
|
||
}
|
||
|
||
// 检查支付状态
|
||
function checkPaymentStatus() {
|
||
const paymentSn = '{{ payment.payment_sn }}';
|
||
|
||
fetch(`/payment/check_status/${paymentSn}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
if (data.status === 2) { // 支付成功
|
||
clearInterval(statusCheckTimer);
|
||
clearInterval(countdownTimer);
|
||
showPaymentSuccess();
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('状态检查失败:', error);
|
||
});
|
||
}
|
||
|
||
// 显示支付成功
|
||
function showPaymentSuccess() {
|
||
document.getElementById('paymentArea').style.display = 'none';
|
||
document.getElementById('paymentStatus').style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
window.location.href = '/order/detail/{{ order.id }}';
|
||
}, 2000);
|
||
}
|
||
|
||
// 取消订单
|
||
function cancelOrder() {
|
||
if (confirm('确定要取消这个订单吗?')) {
|
||
fetch('/order/cancel/{{ order.id }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert('订单已取消', 'success');
|
||
setTimeout(() => {
|
||
window.location.href = '/order/list';
|
||
}, 1500);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('取消失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
// 模拟支付成功(开发测试用)
|
||
function simulatePayment() {
|
||
if (confirm('这是测试功能,确定要模拟支付成功吗?')) {
|
||
fetch('/payment/simulate_success/{{ payment.payment_sn }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert('模拟支付成功', 'success');
|
||
setTimeout(() => {
|
||
showPaymentSuccess();
|
||
}, 1000);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('模拟支付失败', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
// 页面加载时开始倒计时
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
startCountdown();
|
||
});
|
||
|
||
// 页面卸载时清理定时器
|
||
window.addEventListener('beforeunload', function() {
|
||
if (countdownTimer) clearInterval(countdownTimer);
|
||
if (statusCheckTimer) clearInterval(statusCheckTimer);
|
||
});
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/product/detail.html
|
||
📊 大小: 25312 bytes (24.72 KB)
|
||
🕒 修改时间: 2025-07-03 15:31:39
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ product.name }} - 太白购物商城{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container">
|
||
<!-- 面包屑导航 -->
|
||
<nav aria-label="breadcrumb" class="mb-3">
|
||
<ol class="breadcrumb">
|
||
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
|
||
<li class="breadcrumb-item">
|
||
<a href="{{ url_for('main.product_list', category_id=product.category_id) }}">
|
||
{{ product.category.name }}
|
||
</a>
|
||
</li>
|
||
<li class="breadcrumb-item active">{{ product.name }}</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<div class="row">
|
||
<!-- 左侧:商品图片 -->
|
||
<div class="col-md-6">
|
||
{% if images %}
|
||
<!-- 主图显示区域 -->
|
||
<div id="productImageCarousel" class="carousel slide mb-3" data-bs-ride="carousel">
|
||
<div class="carousel-inner">
|
||
{% for image in images %}
|
||
<div class="carousel-item {% if loop.first or image.is_main %}active{% endif %}">
|
||
<img src="{{ image.image_url }}" class="d-block w-100" alt="{{ product.name }}"
|
||
style="height: 400px; object-fit: cover; border-radius: 8px;">
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if images|length > 1 %}
|
||
<button class="carousel-control-prev" type="button" data-bs-target="#productImageCarousel" data-bs-slide="prev">
|
||
<span class="carousel-control-prev-icon"></span>
|
||
</button>
|
||
<button class="carousel-control-next" type="button" data-bs-target="#productImageCarousel" data-bs-slide="next">
|
||
<span class="carousel-control-next-icon"></span>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 缩略图 -->
|
||
{% if images|length > 1 %}
|
||
<div class="row">
|
||
{% for image in images %}
|
||
<div class="col-3 mb-2">
|
||
<img src="{{ image.image_url }}" class="img-thumbnail thumbnail-image"
|
||
alt="{{ product.name }}" style="height: 80px; object-fit: cover; cursor: pointer;"
|
||
onclick="goToSlide({{ loop.index0 }})">
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<!-- 无图片占位 -->
|
||
<div class="bg-light d-flex align-items-center justify-content-center"
|
||
style="height: 400px; border-radius: 8px;">
|
||
<i class="bi bi-image text-muted" style="font-size: 5rem;"></i>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 右侧:商品信息 -->
|
||
<div class="col-md-6">
|
||
<h2 class="mb-3">{{ product.name }}</h2>
|
||
|
||
<!-- 品牌 -->
|
||
{% if product.brand %}
|
||
<p class="text-muted mb-2">
|
||
<strong>品牌:</strong>{{ product.brand }}
|
||
</p>
|
||
{% endif %}
|
||
|
||
<!-- 价格 -->
|
||
<div class="price-section mb-4">
|
||
<div class="d-flex align-items-baseline">
|
||
<span class="text-danger fw-bold" style="font-size: 2rem;">
|
||
¥<span id="currentPrice">{{ "%.2f"|format(product.price) }}</span>
|
||
</span>
|
||
{% if product.original_price and product.original_price > product.price %}
|
||
<span class="text-muted text-decoration-line-through ms-3" style="font-size: 1.2rem;">
|
||
¥{{ "%.2f"|format(product.original_price) }}
|
||
</span>
|
||
<span class="badge bg-danger ms-2">
|
||
省{{ "%.0f"|format(((product.original_price - product.price) / product.original_price * 100)) }}%
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="mt-2">
|
||
<small class="text-muted">销量:{{ product.sales_count }} | 浏览:{{ product.view_count }}</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品规格选择 -->
|
||
{% if inventory_list and inventory_list|length > 1 %}
|
||
<div class="specs-section mb-4">
|
||
<h6>选择规格:</h6>
|
||
<div id="specsContainer">
|
||
{% set spec_groups = {} %}
|
||
{% for sku in inventory_list %}
|
||
{% if sku.spec_combination %}
|
||
{% for spec_name, spec_value in sku.spec_combination.items() %}
|
||
{% if spec_name not in spec_groups %}
|
||
{% set _ = spec_groups.update({spec_name: []}) %}
|
||
{% endif %}
|
||
{% if spec_value not in spec_groups[spec_name] %}
|
||
{% set _ = spec_groups[spec_name].append(spec_value) %}
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endfor %}
|
||
|
||
{% for spec_name, spec_values in spec_groups.items() %}
|
||
<div class="spec-group mb-3">
|
||
<label class="form-label">{{ spec_name }}:</label>
|
||
<div class="spec-options">
|
||
{% for spec_value in spec_values %}
|
||
<button type="button" class="btn btn-outline-secondary spec-option me-2 mb-2"
|
||
data-spec-name="{{ spec_name }}" data-spec-value="{{ spec_value }}">
|
||
{{ spec_value }}
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 库存信息 -->
|
||
<div class="stock-section mb-4">
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<strong>库存:</strong>
|
||
<span id="stockCount" class="text-success">
|
||
{% if inventory_list %}
|
||
{{ inventory_list[0].stock if inventory_list|length == 1 else '请选择规格' }}
|
||
{% else %}
|
||
暂无库存
|
||
{% endif %}
|
||
</span>
|
||
<span id="stockUnit">件</span>
|
||
</div>
|
||
{% if product.weight %}
|
||
<div class="col-6">
|
||
<strong>重量:</strong>{{ product.weight }}kg
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 购买数量 -->
|
||
<div class="quantity-section mb-4">
|
||
<label class="form-label"><strong>数量:</strong></label>
|
||
<div class="input-group" style="width: 150px;">
|
||
<button class="btn btn-outline-secondary" type="button" onclick="changeQuantity(-1)">-</button>
|
||
<input type="number" class="form-control text-center" id="quantity" value="1" min="1" max="999">
|
||
<button class="btn btn-outline-secondary" type="button" onclick="changeQuantity(1)">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="action-buttons mb-4">
|
||
<div class="d-grid gap-2 d-md-flex">
|
||
<button type="button" class="btn btn-warning btn-lg flex-fill" id="addToCartBtn"
|
||
onclick="addToCart()" {% if not inventory_list %}disabled{% endif %}>
|
||
<i class="bi bi-cart-plus"></i> 加入购物车
|
||
</button>
|
||
<button type="button" class="btn btn-danger btn-lg flex-fill" id="buyNowBtn"
|
||
onclick="buyNow()" {% if not inventory_list %}disabled{% endif %}>
|
||
<i class="bi bi-lightning-fill"></i> 立即购买
|
||
</button>
|
||
</div>
|
||
<div class="mt-2">
|
||
<button type="button" class="btn btn-outline-secondary" onclick="addToFavorites()">
|
||
<i class="bi bi-heart"></i> 收藏商品
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 服务承诺 -->
|
||
<div class="service-promises">
|
||
<h6>服务承诺:</h6>
|
||
<ul class="list-unstyled">
|
||
<li><i class="bi bi-check-circle text-success"></i> 正品保证</li>
|
||
<li><i class="bi bi-check-circle text-success"></i> 7天无理由退换</li>
|
||
<li><i class="bi bi-check-circle text-success"></i> 全国包邮</li>
|
||
<li><i class="bi bi-check-circle text-success"></i> 售后服务</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品详情标签页 -->
|
||
<div class="row mt-5">
|
||
<div class="col-12">
|
||
<ul class="nav nav-tabs" id="productDetailTabs" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" id="description-tab" data-bs-toggle="tab"
|
||
data-bs-target="#description" type="button" role="tab">商品详情</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="specs-tab" data-bs-toggle="tab"
|
||
data-bs-target="#specs" type="button" role="tab">规格参数</button>
|
||
</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>
|
||
</li>
|
||
</ul>
|
||
|
||
<div class="tab-content" id="productDetailTabContent">
|
||
<!-- 商品详情 -->
|
||
<div class="tab-pane fade show active" id="description" role="tabpanel">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
{% if product.description %}
|
||
<div class="product-description">
|
||
{{ product.description|replace('\n', '<br>')|safe }}
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">暂无详细描述</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 规格参数 -->
|
||
<div class="tab-pane fade" id="specs" role="tabpanel">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<table class="table table-striped">
|
||
<tbody>
|
||
<tr>
|
||
<td width="150"><strong>商品名称</strong></td>
|
||
<td>{{ product.name }}</td>
|
||
</tr>
|
||
{% if product.brand %}
|
||
<tr>
|
||
<td><strong>商品品牌</strong></td>
|
||
<td>{{ product.brand }}</td>
|
||
</tr>
|
||
{% endif %}
|
||
<tr>
|
||
<td><strong>商品分类</strong></td>
|
||
<td>{{ product.category.name }}</td>
|
||
</tr>
|
||
{% if product.weight %}
|
||
<tr>
|
||
<td><strong>商品重量</strong></td>
|
||
<td>{{ product.weight }}kg</td>
|
||
</tr>
|
||
{% endif %}
|
||
<tr>
|
||
<td><strong>上架时间</strong></td>
|
||
<td>{{ product.created_at.strftime('%Y-%m-%d') }}</td>
|
||
</tr>
|
||
{% if inventory_list %}
|
||
<tr>
|
||
<td><strong>库存信息</strong></td>
|
||
<td>
|
||
{% if inventory_list|length == 1 %}
|
||
{{ inventory_list[0].stock }}件
|
||
{% else %}
|
||
多规格商品,请选择具体规格查看库存
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品评价 -->
|
||
<div class="tab-pane fade" id="reviews" role="tabpanel">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<p class="text-muted">评价功能开发中...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 推荐商品 -->
|
||
{% if recommended_products %}
|
||
<div class="row mt-5">
|
||
<div class="col-12">
|
||
<h4><i class="bi bi-heart-fill text-danger"></i> 相关推荐</h4>
|
||
<hr>
|
||
</div>
|
||
{% for rec_product in recommended_products %}
|
||
<div class="col-lg-3 col-md-6 mb-4">
|
||
<div class="card h-100 product-card">
|
||
<a href="{{ url_for('main.product_detail', product_id=rec_product.id) }}" class="text-decoration-none">
|
||
{% if rec_product.main_image %}
|
||
<img src="{{ rec_product.main_image }}" class="card-img-top" alt="{{ rec_product.name }}"
|
||
style="height: 200px; object-fit: cover;">
|
||
{% else %}
|
||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
|
||
style="height: 200px;">
|
||
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
|
||
</div>
|
||
{% endif %}
|
||
</a>
|
||
<div class="card-body">
|
||
<h6 class="card-title">
|
||
<a href="{{ url_for('main.product_detail', product_id=rec_product.id) }}"
|
||
class="text-decoration-none text-dark">
|
||
{{ rec_product.name[:40] }}{% if rec_product.name|length > 40 %}...{% endif %}
|
||
</a>
|
||
</h6>
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<span class="text-danger fw-bold">¥{{ "%.2f"|format(rec_product.price) }}</span>
|
||
<small class="text-muted">销量{{ rec_product.sales_count }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 库存数据(用于JavaScript) -->
|
||
<script type="application/json" id="inventoryData">
|
||
{% if inventory_data %}
|
||
{{ inventory_data|tojson }}
|
||
{% else %}
|
||
[]
|
||
{% endif %}
|
||
</script>
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
// 获取库存数据
|
||
const inventoryData = JSON.parse(document.getElementById('inventoryData').textContent);
|
||
let selectedSpecs = {};
|
||
let currentSku = null;
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 如果只有一个SKU,自动选择
|
||
if (inventoryData.length === 1) {
|
||
currentSku = inventoryData[0];
|
||
updateStockInfo();
|
||
}
|
||
|
||
// 绑定规格选择事件
|
||
document.querySelectorAll('.spec-option').forEach(button => {
|
||
button.addEventListener('click', function() {
|
||
selectSpec(this);
|
||
});
|
||
});
|
||
|
||
// 初始化购物车数量显示
|
||
{% if session.user_id %}
|
||
loadCartCount();
|
||
{% endif %}
|
||
});
|
||
|
||
// 规格选择
|
||
function selectSpec(button) {
|
||
const specName = button.getAttribute('data-spec-name');
|
||
const specValue = button.getAttribute('data-spec-value');
|
||
|
||
// 清除同组其他选择
|
||
document.querySelectorAll(`[data-spec-name="${specName}"]`).forEach(btn => {
|
||
btn.classList.remove('btn-primary');
|
||
btn.classList.add('btn-outline-secondary');
|
||
});
|
||
|
||
// 选中当前项
|
||
button.classList.remove('btn-outline-secondary');
|
||
button.classList.add('btn-primary');
|
||
|
||
// 更新选择状态
|
||
selectedSpecs[specName] = specValue;
|
||
|
||
// 查找匹配的SKU
|
||
findMatchingSku();
|
||
}
|
||
|
||
// 查找匹配的SKU
|
||
function findMatchingSku() {
|
||
for (let sku of inventoryData) {
|
||
if (sku.spec_combination) {
|
||
let isMatch = true;
|
||
for (let [specName, specValue] of Object.entries(selectedSpecs)) {
|
||
if (sku.spec_combination[specName] !== specValue) {
|
||
isMatch = false;
|
||
break;
|
||
}
|
||
}
|
||
if (isMatch && Object.keys(selectedSpecs).length === Object.keys(sku.spec_combination).length) {
|
||
currentSku = sku;
|
||
updateStockInfo();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 未找到完全匹配的SKU
|
||
currentSku = null;
|
||
updateStockInfo();
|
||
}
|
||
|
||
// 更新库存信息
|
||
function updateStockInfo() {
|
||
const stockElement = document.getElementById('stockCount');
|
||
const priceElement = document.getElementById('currentPrice');
|
||
const addToCartBtn = document.getElementById('addToCartBtn');
|
||
const buyNowBtn = document.getElementById('buyNowBtn');
|
||
const quantityInput = document.getElementById('quantity');
|
||
|
||
if (currentSku) {
|
||
stockElement.textContent = currentSku.stock;
|
||
stockElement.className = currentSku.stock > 0 ? 'text-success' : 'text-danger';
|
||
priceElement.textContent = currentSku.final_price.toFixed(2);
|
||
|
||
if (currentSku.stock > 0) {
|
||
addToCartBtn.disabled = false;
|
||
buyNowBtn.disabled = false;
|
||
quantityInput.max = currentSku.stock;
|
||
} else {
|
||
addToCartBtn.disabled = true;
|
||
buyNowBtn.disabled = true;
|
||
quantityInput.max = 0;
|
||
}
|
||
} else if (inventoryData.length > 1) {
|
||
stockElement.textContent = '请选择规格';
|
||
stockElement.className = 'text-warning';
|
||
addToCartBtn.disabled = true;
|
||
buyNowBtn.disabled = true;
|
||
}
|
||
}
|
||
|
||
// 数量变更
|
||
function changeQuantity(delta) {
|
||
const quantityInput = document.getElementById('quantity');
|
||
let quantity = parseInt(quantityInput.value) + delta;
|
||
|
||
const min = parseInt(quantityInput.min) || 1;
|
||
const max = parseInt(quantityInput.max) || 999;
|
||
|
||
quantity = Math.max(min, Math.min(max, quantity));
|
||
quantityInput.value = quantity;
|
||
}
|
||
|
||
// 轮播图跳转
|
||
function goToSlide(index) {
|
||
const carousel = new bootstrap.Carousel(document.getElementById('productImageCarousel'));
|
||
carousel.to(index);
|
||
}
|
||
|
||
// 加载购物车数量
|
||
function loadCartCount() {
|
||
fetch('/cart/count')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
updateCartBadge(data.cart_count);
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading cart count:', error);
|
||
});
|
||
}
|
||
|
||
// 加入购物车
|
||
function addToCart() {
|
||
if (!currentSku) {
|
||
alert('请选择商品规格');
|
||
return;
|
||
}
|
||
|
||
const quantity = parseInt(document.getElementById('quantity').value);
|
||
if (quantity <= 0 || quantity > currentSku.stock) {
|
||
alert('请选择正确的购买数量');
|
||
return;
|
||
}
|
||
|
||
// 检查登录状态
|
||
{% if not session.user_id %}
|
||
if (confirm('请先登录后再加入购物车,是否前往登录?')) {
|
||
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
|
||
}
|
||
return;
|
||
{% endif %}
|
||
|
||
// 禁用按钮,防止重复点击
|
||
const addToCartBtn = document.getElementById('addToCartBtn');
|
||
addToCartBtn.disabled = true;
|
||
addToCartBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 添加中...';
|
||
|
||
// 提交到购物车
|
||
fetch('/cart/add', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
product_id: {{ product.id }},
|
||
sku_code: currentSku.sku_code,
|
||
spec_combination: Object.keys(selectedSpecs).length > 0 ? JSON.stringify(selectedSpecs) : '',
|
||
quantity: quantity
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showSuccessMessage(data.message);
|
||
updateCartBadge(data.cart_count);
|
||
|
||
// 询问是否查看购物车
|
||
setTimeout(() => {
|
||
if (confirm('商品已添加到购物车,是否查看购物车?')) {
|
||
window.location.href = '/cart/';
|
||
}
|
||
}, 500);
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('加入购物车失败,请稍后再试');
|
||
})
|
||
.finally(() => {
|
||
// 恢复按钮状态
|
||
addToCartBtn.disabled = false;
|
||
addToCartBtn.innerHTML = '<i class="bi bi-cart-plus"></i> 加入购物车';
|
||
});
|
||
}
|
||
|
||
// 立即购买
|
||
function buyNow() {
|
||
if (!currentSku) {
|
||
alert('请选择商品规格');
|
||
return;
|
||
}
|
||
|
||
const quantity = parseInt(document.getElementById('quantity').value);
|
||
if (quantity <= 0 || quantity > currentSku.stock) {
|
||
alert('请选择正确的购买数量');
|
||
return;
|
||
}
|
||
|
||
// 检查登录状态
|
||
{% if not session.user_id %}
|
||
if (confirm('请先登录后再购买,是否前往登录?')) {
|
||
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
|
||
}
|
||
return;
|
||
{% endif %}
|
||
|
||
// 先添加到购物车,然后跳转到结算页面
|
||
fetch('/cart/add', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
product_id: {{ product.id }},
|
||
sku_code: currentSku.sku_code,
|
||
spec_combination: Object.keys(selectedSpecs).length > 0 ? JSON.stringify(selectedSpecs) : '',
|
||
quantity: quantity
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 立即跳转到购物车结算
|
||
window.location.href = '/cart/';
|
||
} else {
|
||
alert(data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('购买失败,请稍后再试');
|
||
});
|
||
}
|
||
|
||
// 收藏商品
|
||
function addToFavorites() {
|
||
{% if not session.user_id %}
|
||
if (confirm('请先登录后再收藏,是否前往登录?')) {
|
||
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
|
||
}
|
||
return;
|
||
{% endif %}
|
||
|
||
// TODO: 实现收藏功能
|
||
alert('收藏功能开发中...');
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.product-card {
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.product-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.spec-option {
|
||
border-radius: 4px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.spec-option:hover {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.thumbnail-image {
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.thumbnail-image:hover {
|
||
transform: scale(1.05);
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.price-section {
|
||
background: linear-gradient(135deg, #fff5f5 0%, #ffeee8 100%);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
border: 1px solid #ffe6e6;
|
||
}
|
||
|
||
.product-description {
|
||
line-height: 1.8;
|
||
white-space: pre-line;
|
||
}
|
||
|
||
.service-promises li {
|
||
padding: 5px 0;
|
||
}
|
||
|
||
/* 规格选择动效 */
|
||
.spec-option.btn-primary {
|
||
background-color: #007bff;
|
||
border-color: #007bff;
|
||
color: white;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
/* 按钮禁用状态样式 */
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 响应式优化 */
|
||
@media (max-width: 768px) {
|
||
.price-section {
|
||
text-align: center;
|
||
}
|
||
|
||
.action-buttons .d-md-flex {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.action-buttons .btn {
|
||
margin-bottom: 10px;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/product/list.html
|
||
📊 大小: 13955 bytes (13.63 KB)
|
||
🕒 修改时间: 2025-07-03 14:45:55
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
|
||
{% block title %}
|
||
{% if current_category %}{{ current_category.name }} - {% endif %}
|
||
{% if search %}搜索"{{ search }}" - {% endif %}
|
||
商品列表 - 太白购物商城
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row">
|
||
<!-- 侧边栏 -->
|
||
<div class="col-md-3">
|
||
<!-- 搜索框 -->
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-search"></i> 商品搜索</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="GET" action="{{ url_for('main.product_list') }}">
|
||
<div class="input-group">
|
||
<input type="text" class="form-control" name="search"
|
||
placeholder="搜索商品..." value="{{ search or '' }}">
|
||
<button class="btn btn-primary" type="submit">
|
||
<i class="bi bi-search"></i>
|
||
</button>
|
||
</div>
|
||
<!-- 保持其他筛选条件 -->
|
||
{% if category_id %}<input type="hidden" name="category_id" value="{{ category_id }}">{% endif %}
|
||
{% if sort %}<input type="hidden" name="sort" value="{{ sort }}">{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分类筛选 -->
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-grid"></i> 商品分类</h6>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div class="list-group list-group-flush">
|
||
<a href="{{ url_for('main.product_list') }}"
|
||
class="list-group-item list-group-item-action {% if not category_id %}active{% endif %}">
|
||
全部商品
|
||
</a>
|
||
{% for category in categories %}
|
||
<a href="{{ url_for('main.product_list', category_id=category.id) }}"
|
||
class="list-group-item list-group-item-action {% if category_id == category.id %}active{% endif %}">
|
||
{% if category.icon_url %}
|
||
<img src="{{ category.icon_url }}" alt="{{ category.name }}"
|
||
style="width: 20px; height: 20px; margin-right: 8px;">
|
||
{% endif %}
|
||
{{ category.name }}
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 价格筛选 -->
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-currency-dollar"></i> 价格筛选</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="GET" action="{{ url_for('main.product_list') }}" id="priceForm">
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<input type="number" class="form-control form-control-sm"
|
||
name="min_price" placeholder="最低价"
|
||
value="{{ min_price or '' }}" step="0.01">
|
||
</div>
|
||
<div class="col-6">
|
||
<input type="number" class="form-control form-control-sm"
|
||
name="max_price" placeholder="最高价"
|
||
value="{{ max_price or '' }}" step="0.01">
|
||
</div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<button type="submit" class="btn btn-primary btn-sm me-2">筛选</button>
|
||
<a href="{{ url_for('main.product_list') }}" class="btn btn-outline-secondary btn-sm">重置</a>
|
||
</div>
|
||
<!-- 保持其他条件 -->
|
||
{% if search %}<input type="hidden" name="search" value="{{ search }}">{% endif %}
|
||
{% if category_id %}<input type="hidden" name="category_id" value="{{ category_id }}">{% endif %}
|
||
{% if sort %}<input type="hidden" name="sort" value="{{ sort }}">{% endif %}
|
||
</form>
|
||
|
||
<!-- 快速价格选择 -->
|
||
<div class="mt-3">
|
||
<div class="d-grid gap-1">
|
||
<a href="{{ url_for('main.product_list', max_price=100, **request.args) }}"
|
||
class="btn btn-outline-secondary btn-sm">100元以下</a>
|
||
<a href="{{ url_for('main.product_list', min_price=100, max_price=500, **request.args) }}"
|
||
class="btn btn-outline-secondary btn-sm">100-500元</a>
|
||
<a href="{{ url_for('main.product_list', min_price=500, max_price=1000, **request.args) }}"
|
||
class="btn btn-outline-secondary btn-sm">500-1000元</a>
|
||
<a href="{{ url_for('main.product_list', min_price=1000, **request.args) }}"
|
||
class="btn btn-outline-secondary btn-sm">1000元以上</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主内容区 -->
|
||
<div class="col-md-9">
|
||
<!-- 面包屑导航 -->
|
||
<nav aria-label="breadcrumb" class="mb-3">
|
||
<ol class="breadcrumb">
|
||
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
|
||
{% if current_category %}
|
||
<li class="breadcrumb-item active">{{ current_category.name }}</li>
|
||
{% elif search %}
|
||
<li class="breadcrumb-item active">搜索结果</li>
|
||
{% else %}
|
||
<li class="breadcrumb-item active">全部商品</li>
|
||
{% endif %}
|
||
</ol>
|
||
</nav>
|
||
|
||
<!-- 筛选和排序栏 -->
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<h5>
|
||
{% if current_category %}
|
||
{{ current_category.name }}
|
||
{% elif search %}
|
||
搜索"{{ search }}"
|
||
{% else %}
|
||
全部商品
|
||
{% endif %}
|
||
<small class="text-muted">(共{{ products.total }}个商品)</small>
|
||
</h5>
|
||
</div>
|
||
<div class="col-md-6 text-end">
|
||
<div class="btn-group" role="group">
|
||
<input type="radio" class="btn-check" name="sort" id="sort_default"
|
||
{% if not sort or sort == 'default' %}checked{% endif %}>
|
||
<label class="btn btn-outline-secondary btn-sm" for="sort_default"
|
||
onclick="changeSort('default')">综合</label>
|
||
|
||
<input type="radio" class="btn-check" name="sort" id="sort_newest"
|
||
{% if sort == 'newest' %}checked{% endif %}>
|
||
<label class="btn btn-outline-secondary btn-sm" for="sort_newest"
|
||
onclick="changeSort('newest')">最新</label>
|
||
|
||
<input type="radio" class="btn-check" name="sort" id="sort_sales"
|
||
{% if sort == 'sales' %}checked{% endif %}>
|
||
<label class="btn btn-outline-secondary btn-sm" for="sort_sales"
|
||
onclick="changeSort('sales')">销量</label>
|
||
|
||
<input type="radio" class="btn-check" name="sort" id="sort_price_asc"
|
||
{% if sort == 'price_asc' %}checked{% endif %}>
|
||
<label class="btn btn-outline-secondary btn-sm" for="sort_price_asc"
|
||
onclick="changeSort('price_asc')">价格↑</label>
|
||
|
||
<input type="radio" class="btn-check" name="sort" id="sort_price_desc"
|
||
{% if sort == 'price_desc' %}checked{% endif %}>
|
||
<label class="btn btn-outline-secondary btn-sm" for="sort_price_desc"
|
||
onclick="changeSort('price_desc')">价格↓</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品列表 -->
|
||
{% if products.items %}
|
||
<div class="row">
|
||
{% for product in products.items %}
|
||
<div class="col-lg-4 col-md-6 mb-4">
|
||
<div class="card h-100 product-card">
|
||
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none">
|
||
{% if product.main_image %}
|
||
<img src="{{ product.main_image }}" class="card-img-top" alt="{{ product.name }}"
|
||
style="height: 200px; object-fit: cover;">
|
||
{% else %}
|
||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
|
||
style="height: 200px;">
|
||
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
|
||
</div>
|
||
{% endif %}
|
||
</a>
|
||
<div class="card-body">
|
||
<h6 class="card-title">
|
||
<a href="{{ url_for('main.product_detail', product_id=product.id) }}"
|
||
class="text-decoration-none text-dark">
|
||
{{ product.name[:60] }}{% if product.name|length > 60 %}...{% endif %}
|
||
</a>
|
||
</h6>
|
||
{% if product.brand %}
|
||
<p class="card-text text-muted small">{{ product.brand }}</p>
|
||
{% endif %}
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<span class="text-danger fw-bold h6">¥{{ "%.2f"|format(product.price) }}</span>
|
||
{% if product.original_price and product.original_price > product.price %}
|
||
<small class="text-muted text-decoration-line-through d-block">
|
||
¥{{ "%.2f"|format(product.original_price) }}
|
||
</small>
|
||
{% endif %}
|
||
</div>
|
||
<div class="text-end">
|
||
<small class="text-muted d-block">销量{{ product.sales_count }}</small>
|
||
<small class="text-muted">浏览{{ product.view_count }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
{% if products.pages > 1 %}
|
||
<nav aria-label="商品分页">
|
||
<ul class="pagination justify-content-center">
|
||
{% if products.has_prev %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('main.product_list', page=products.prev_num, **request.args) }}">
|
||
<i class="bi bi-chevron-left"></i> 上一页
|
||
</a>
|
||
</li>
|
||
{% endif %}
|
||
|
||
{% for page_num in products.iter_pages() %}
|
||
{% if page_num %}
|
||
{% if page_num != products.page %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('main.product_list', page=page_num, **request.args) }}">
|
||
{{ 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 products.has_next %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('main.product_list', page=products.next_num, **request.args) }}">
|
||
下一页 <i class="bi bi-chevron-right"></i>
|
||
</a>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
</nav>
|
||
{% endif %}
|
||
|
||
{% else %}
|
||
<!-- 无商品提示 -->
|
||
<div class="text-center py-5">
|
||
<i class="bi bi-search text-muted" style="font-size: 5rem;"></i>
|
||
<h4 class="text-muted mt-3">暂无找到相关商品</h4>
|
||
<p class="text-muted">请尝试调整搜索条件或浏览其他分类</p>
|
||
<a href="{{ url_for('main.product_list') }}" class="btn btn-primary">
|
||
<i class="bi bi-arrow-left"></i> 返回全部商品
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function changeSort(sortType) {
|
||
const url = new URL(window.location);
|
||
url.searchParams.set('sort', sortType);
|
||
url.searchParams.set('page', '1'); // 重置到第一页
|
||
window.location.href = url.toString();
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.product-card {
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.product-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.btn-check:checked + .btn {
|
||
background-color: #007bff;
|
||
border-color: #007bff;
|
||
color: white;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/test_upload.html
|
||
📊 大小: 14836 bytes (14.49 KB)
|
||
🕒 修改时间: 2025-07-03 04:21:18
|
||
🔸==============================================================================
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>COS上传测试</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
</head>
|
||
<body>
|
||
<div class="container mt-5">
|
||
<div class="row justify-content-center">
|
||
<div class="col-md-8">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h4>腾讯云COS上传测试</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- 头像上传测试 -->
|
||
<div class="mb-4">
|
||
<h5>头像上传测试</h5>
|
||
<form id="avatarForm" enctype="multipart/form-data">
|
||
<div class="mb-3">
|
||
<input type="file" class="form-control" id="avatarFile" name="avatar" accept="image/*">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">上传头像</button>
|
||
</form>
|
||
<div id="avatarResult" class="mt-3"></div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<!-- 通用图片上传测试 -->
|
||
<div class="mb-4">
|
||
<h5>通用图片上传测试</h5>
|
||
<form id="imageForm" enctype="multipart/form-data">
|
||
<div class="mb-3">
|
||
<input type="file" class="form-control" id="imageFile" name="image" accept="image/*">
|
||
</div>
|
||
<div class="mb-3">
|
||
<select class="form-select" name="folder_type">
|
||
<option value="temp">临时文件</option>
|
||
<option value="product">商品图片</option>
|
||
<option value="review">评价图片</option>
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-success">上传图片</button>
|
||
</form>
|
||
<div id="imageResult" class="mt-3"></div>
|
||
</div>
|
||
|
||
<!-- 上传历史 -->
|
||
<div class="mb-4">
|
||
<h5>上传历史</h5>
|
||
<div id="uploadHistory"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
// 上传历史记录
|
||
let uploadHistory = [];
|
||
|
||
// 头像上传
|
||
document.getElementById('avatarForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData();
|
||
const fileInput = document.getElementById('avatarFile');
|
||
|
||
if (!fileInput.files[0]) {
|
||
showResult('avatarResult', false, '请选择文件');
|
||
return;
|
||
}
|
||
|
||
formData.append('avatar', fileInput.files[0]);
|
||
|
||
uploadFile('/upload/avatar', formData, 'avatarResult', '头像');
|
||
});
|
||
|
||
// 通用图片上传
|
||
document.getElementById('imageForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData(this);
|
||
const fileInput = document.getElementById('imageFile');
|
||
|
||
if (!fileInput.files[0]) {
|
||
showResult('imageResult', false, '请选择文件');
|
||
return;
|
||
}
|
||
|
||
uploadFile('/upload/image', formData, 'imageResult', '图片');
|
||
});
|
||
|
||
// 上传文件函数
|
||
function uploadFile(url, formData, resultId, fileType) {
|
||
const resultDiv = document.getElementById(resultId);
|
||
resultDiv.innerHTML = '<div class="alert alert-info">上传中...</div>';
|
||
|
||
fetch(url, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showResult(resultId, true, `${fileType}上传成功!`, data.url);
|
||
addToHistory(fileType, data.url, data.file_key);
|
||
} else {
|
||
showResult(resultId, false, data.error || `${fileType}上传失败`);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showResult(resultId, false, `上传失败: ${error.message}`);
|
||
});
|
||
}
|
||
|
||
// 显示结果
|
||
function showResult(resultId, success, message, imageUrl = null) {
|
||
const resultDiv = document.getElementById(resultId);
|
||
const alertClass = success ? 'alert-success' : 'alert-danger';
|
||
|
||
let html = `<div class="alert ${alertClass}">${message}</div>`;
|
||
|
||
if (success && imageUrl) {
|
||
html += `
|
||
<div class="mt-2">
|
||
<img src="${imageUrl}" class="img-thumbnail" style="max-width: 200px;">
|
||
<p class="mt-2"><small>访问地址: <a href="${imageUrl}" target="_blank">${imageUrl}</a></small></p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
resultDiv.innerHTML = html;
|
||
}
|
||
|
||
// 添加到历史记录
|
||
function addToHistory(type, url, fileKey) {
|
||
uploadHistory.unshift({
|
||
type: type,
|
||
url: url,
|
||
fileKey: fileKey,
|
||
time: new Date().toLocaleString()
|
||
});
|
||
|
||
updateHistoryDisplay();
|
||
}
|
||
|
||
// 更新历史记录显示
|
||
function updateHistoryDisplay() {
|
||
const historyDiv = document.getElementById('uploadHistory');
|
||
|
||
if (uploadHistory.length === 0) {
|
||
historyDiv.innerHTML = '<p class="text-muted">暂无上传记录</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="list-group">';
|
||
|
||
uploadHistory.slice(0, 5).forEach(item => {
|
||
html += `
|
||
<div class="list-group-item">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div>
|
||
<h6 class="mb-1">${item.type}</h6>
|
||
<p class="mb-1"><small>${item.fileKey}</small></p>
|
||
<small class="text-muted">${item.time}</small>
|
||
</div>
|
||
<div>
|
||
<a href="${item.url}" target="_blank" class="btn btn-sm btn-outline-primary">查看</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += '</div>';
|
||
historyDiv.innerHTML = html;
|
||
}
|
||
|
||
// 初始化历史记录显示
|
||
updateHistoryDisplay();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>COS上传测试</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
</head>
|
||
<body>
|
||
<div class="container mt-5">
|
||
<div class="row justify-content-center">
|
||
<div class="col-md-8">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h4>腾讯云COS上传测试</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- 头像上传测试 -->
|
||
<div class="mb-4">
|
||
<h5>头像上传测试</h5>
|
||
<form id="avatarForm" enctype="multipart/form-data">
|
||
<div class="mb-3">
|
||
<input type="file" class="form-control" id="avatarFile" name="avatar" accept="image/*">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">上传头像</button>
|
||
</form>
|
||
<div id="avatarResult" class="mt-3"></div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<!-- 通用图片上传测试 -->
|
||
<div class="mb-4">
|
||
<h5>通用图片上传测试</h5>
|
||
<form id="imageForm" enctype="multipart/form-data">
|
||
<div class="mb-3">
|
||
<input type="file" class="form-control" id="imageFile" name="image" accept="image/*">
|
||
</div>
|
||
<div class="mb-3">
|
||
<select class="form-select" name="folder_type">
|
||
<option value="temp">临时文件</option>
|
||
<option value="product">商品图片</option>
|
||
<option value="review">评价图片</option>
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-success">上传图片</button>
|
||
</form>
|
||
<div id="imageResult" class="mt-3"></div>
|
||
</div>
|
||
|
||
<!-- 上传历史 -->
|
||
<div class="mb-4">
|
||
<h5>上传历史</h5>
|
||
<div id="uploadHistory"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
// 上传历史记录
|
||
let uploadHistory = [];
|
||
|
||
// 头像上传
|
||
document.getElementById('avatarForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData();
|
||
const fileInput = document.getElementById('avatarFile');
|
||
|
||
if (!fileInput.files[0]) {
|
||
showResult('avatarResult', false, '请选择文件');
|
||
return;
|
||
}
|
||
|
||
formData.append('avatar', fileInput.files[0]);
|
||
|
||
uploadFile('/upload/avatar', formData, 'avatarResult', '头像');
|
||
});
|
||
|
||
// 通用图片上传
|
||
document.getElementById('imageForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData(this);
|
||
const fileInput = document.getElementById('imageFile');
|
||
|
||
if (!fileInput.files[0]) {
|
||
showResult('imageResult', false, '请选择文件');
|
||
return;
|
||
}
|
||
|
||
uploadFile('/upload/image', formData, 'imageResult', '图片');
|
||
});
|
||
|
||
// 上传文件函数
|
||
function uploadFile(url, formData, resultId, fileType) {
|
||
const resultDiv = document.getElementById(resultId);
|
||
resultDiv.innerHTML = '<div class="alert alert-info">上传中...</div>';
|
||
|
||
fetch(url, {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showResult(resultId, true, `${fileType}上传成功!`, data.url);
|
||
addToHistory(fileType, data.url, data.file_key);
|
||
} else {
|
||
showResult(resultId, false, data.error || `${fileType}上传失败`);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showResult(resultId, false, `上传失败: ${error.message}`);
|
||
});
|
||
}
|
||
|
||
// 显示结果
|
||
function showResult(resultId, success, message, imageUrl = null) {
|
||
const resultDiv = document.getElementById(resultId);
|
||
const alertClass = success ? 'alert-success' : 'alert-danger';
|
||
|
||
let html = `<div class="alert ${alertClass}">${message}</div>`;
|
||
|
||
if (success && imageUrl) {
|
||
html += `
|
||
<div class="mt-2">
|
||
<img src="${imageUrl}" class="img-thumbnail" style="max-width: 200px;">
|
||
<p class="mt-2"><small>访问地址: <a href="${imageUrl}" target="_blank">${imageUrl}</a></small></p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
resultDiv.innerHTML = html;
|
||
}
|
||
|
||
// 添加到历史记录
|
||
function addToHistory(type, url, fileKey) {
|
||
uploadHistory.unshift({
|
||
type: type,
|
||
url: url,
|
||
fileKey: fileKey,
|
||
time: new Date().toLocaleString()
|
||
});
|
||
|
||
updateHistoryDisplay();
|
||
}
|
||
|
||
// 更新历史记录显示
|
||
function updateHistoryDisplay() {
|
||
const historyDiv = document.getElementById('uploadHistory');
|
||
|
||
if (uploadHistory.length === 0) {
|
||
historyDiv.innerHTML = '<p class="text-muted">暂无上传记录</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="list-group">';
|
||
|
||
uploadHistory.slice(0, 5).forEach(item => {
|
||
html += `
|
||
<div class="list-group-item">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div>
|
||
<h6 class="mb-1">${item.type}</h6>
|
||
<p class="mb-1"><small>${item.fileKey}</small></p>
|
||
<small class="text-muted">${item.time}</small>
|
||
</div>
|
||
<div>
|
||
<a href="${item.url}" target="_blank" class="btn btn-sm btn-outline-primary">查看</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += '</div>';
|
||
historyDiv.innerHTML = html;
|
||
}
|
||
|
||
// 初始化历史记录显示
|
||
updateHistoryDisplay();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/user/address_form.html
|
||
📊 大小: 14730 bytes (14.38 KB)
|
||
🕒 修改时间: 2025-07-04 03:22:24
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
{% block title %}{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %} - 太白购物商城{% 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 active">
|
||
<i class="bi bi-geo-alt"></i> 收货地址
|
||
</a>
|
||
<a href="#" 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">
|
||
<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-geo-alt"></i>
|
||
{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %}
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- 调试信息 -->
|
||
<div class="alert alert-info" id="debugAlert" style="display: none;">
|
||
<strong>调试信息:</strong>
|
||
<div id="debugInfo">加载中...</div>
|
||
</div>
|
||
|
||
<form method="POST" id="addressForm">
|
||
{{ form.hidden_tag() }}
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">{{ form.receiver_name.label.text }} <span class="text-danger">*</span></label>
|
||
{{ form.receiver_name(class="form-control") }}
|
||
{% if form.receiver_name.errors %}
|
||
<div class="text-danger">{{ form.receiver_name.errors[0] }}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">{{ form.receiver_phone.label.text }} <span class="text-danger">*</span></label>
|
||
{{ form.receiver_phone(class="form-control") }}
|
||
{% if form.receiver_phone.errors %}
|
||
<div class="text-danger">{{ form.receiver_phone.errors[0] }}</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label">省份 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="province" name="province" required>
|
||
<option value="">加载中...</option>
|
||
</select>
|
||
<input type="hidden" id="provinceValue" value="{% if address %}{{ address.province }}{% endif %}">
|
||
{% if form.province.errors %}
|
||
<div class="text-danger">{{ form.province.errors[0] }}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label">城市 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="city" name="city" required>
|
||
<option value="">请选择城市</option>
|
||
</select>
|
||
<input type="hidden" id="cityValue" value="{% if address %}{{ address.city }}{% endif %}">
|
||
{% if form.city.errors %}
|
||
<div class="text-danger">{{ form.city.errors[0] }}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label">区县 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="district" name="district" required>
|
||
<option value="">请选择区县</option>
|
||
</select>
|
||
<input type="hidden" id="districtValue" value="{% if address %}{{ address.district }}{% endif %}">
|
||
{% if form.district.errors %}
|
||
<div class="text-danger">{{ form.district.errors[0] }}</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-8 mb-3">
|
||
<label class="form-label">{{ form.detail_address.label.text }} <span class="text-danger">*</span></label>
|
||
{{ form.detail_address(class="form-control", placeholder="街道、门牌号等详细信息") }}
|
||
{% if form.detail_address.errors %}
|
||
<div class="text-danger">{{ form.detail_address.errors[0] }}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="col-md-4 mb-3">
|
||
<label class="form-label">{{ form.postal_code.label.text }}</label>
|
||
{{ form.postal_code(class="form-control", placeholder="选填") }}
|
||
{% if form.postal_code.errors %}
|
||
<div class="text-danger">{{ form.postal_code.errors[0] }}</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
{{ form.is_default(class="form-check-input") }}
|
||
<label class="form-check-label" for="{{ form.is_default.id }}">
|
||
{{ form.is_default.label.text }}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex gap-2">
|
||
{{ form.submit(class="btn btn-primary") }}
|
||
<a href="{{ url_for('address.index') }}" class="btn btn-outline-secondary">取消</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<!-- 在scripts块中引入城市数据,确保在base.html的脚本之后加载 -->
|
||
<script src="{{ url_for('static', filename='js/city_data.js') }}"></script>
|
||
|
||
<script>
|
||
// 全局变量,避免重复初始化
|
||
let addressFormInitialized = false;
|
||
|
||
// 页面完全加载后初始化
|
||
window.addEventListener('load', function() {
|
||
console.log('=== 地址表单初始化开始 ===');
|
||
|
||
// 显示调试信息
|
||
document.getElementById('debugAlert').style.display = 'block';
|
||
|
||
// 检查数据是否加载
|
||
if (typeof cityData === 'undefined') {
|
||
console.error('❌ cityData 未加载');
|
||
document.getElementById('debugInfo').innerHTML = '<span class="text-danger">❌ 地址数据加载失败</span>';
|
||
showAlert('地址数据加载失败,请刷新页面重试', 'error');
|
||
return;
|
||
}
|
||
|
||
console.log('✅ cityData 已加载,省份数量:', Object.keys(cityData).length);
|
||
document.getElementById('debugInfo').innerHTML = '<span class="text-success">✅ 地址数据已加载,省份数量: ' + Object.keys(cityData).length + '</span>';
|
||
|
||
// 避免重复初始化
|
||
if (addressFormInitialized) {
|
||
console.log('地址表单已初始化,跳过');
|
||
return;
|
||
}
|
||
|
||
addressFormInitialized = true;
|
||
|
||
// 初始化省份列表
|
||
initializeProvinces();
|
||
|
||
// 设置事件监听器
|
||
setupEventListeners();
|
||
|
||
// 如果是编辑模式,设置初始值
|
||
const savedProvince = document.getElementById('provinceValue').value;
|
||
const savedCity = document.getElementById('cityValue').value;
|
||
const savedDistrict = document.getElementById('districtValue').value;
|
||
|
||
console.log('初始值:', {savedProvince, savedCity, savedDistrict});
|
||
|
||
if (savedProvince) {
|
||
setTimeout(() => {
|
||
setInitialValues(savedProvince, savedCity, savedDistrict);
|
||
}, 500); // 增加延迟确保DOM完全准备好
|
||
}
|
||
|
||
console.log('=== 地址表单初始化完成 ===');
|
||
});
|
||
|
||
// 初始化省份列表
|
||
function initializeProvinces() {
|
||
const provinceSelect = document.getElementById('province');
|
||
|
||
if (!provinceSelect) {
|
||
console.error('省份选择框未找到');
|
||
return;
|
||
}
|
||
|
||
console.log('开始初始化省份列表...');
|
||
|
||
// 清空并添加默认选项
|
||
provinceSelect.innerHTML = '<option value="">请选择省份</option>';
|
||
|
||
// 添加所有省份
|
||
const provinces = Object.keys(cityData);
|
||
console.log('可用省份:', provinces);
|
||
|
||
provinces.forEach(province => {
|
||
const option = document.createElement('option');
|
||
option.value = province;
|
||
option.textContent = province;
|
||
provinceSelect.appendChild(option);
|
||
console.log('添加省份:', province);
|
||
});
|
||
|
||
console.log('省份列表初始化完成,总计:', provinces.length, '个');
|
||
}
|
||
|
||
// 设置事件监听器
|
||
function setupEventListeners() {
|
||
console.log('设置事件监听器...');
|
||
|
||
// 省份变化事件
|
||
const provinceSelect = document.getElementById('province');
|
||
if (provinceSelect) {
|
||
provinceSelect.addEventListener('change', function() {
|
||
const province = this.value;
|
||
console.log('选择省份:', province);
|
||
updateCities(province);
|
||
clearDistricts();
|
||
});
|
||
}
|
||
|
||
// 城市变化事件
|
||
const citySelect = document.getElementById('city');
|
||
if (citySelect) {
|
||
citySelect.addEventListener('change', function() {
|
||
const province = document.getElementById('province').value;
|
||
const city = this.value;
|
||
console.log('选择城市:', city, '省份:', province);
|
||
updateDistricts(province, city);
|
||
});
|
||
}
|
||
|
||
// 表单提交验证
|
||
document.getElementById('addressForm').addEventListener('submit', function(e) {
|
||
const province = document.getElementById('province').value;
|
||
const city = document.getElementById('city').value;
|
||
const district = document.getElementById('district').value;
|
||
|
||
console.log('表单验证:', {province, city, district});
|
||
|
||
if (!province) {
|
||
e.preventDefault();
|
||
showAlert('请选择省份', 'warning');
|
||
return false;
|
||
}
|
||
|
||
if (!city) {
|
||
e.preventDefault();
|
||
showAlert('请选择城市', 'warning');
|
||
return false;
|
||
}
|
||
|
||
if (!district) {
|
||
e.preventDefault();
|
||
showAlert('请选择区县', 'warning');
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
console.log('事件监听器设置完成');
|
||
}
|
||
|
||
// 更新城市列表
|
||
function updateCities(province) {
|
||
const citySelect = document.getElementById('city');
|
||
citySelect.innerHTML = '<option value="">请选择城市</option>';
|
||
|
||
console.log('更新城市列表,省份:', province);
|
||
|
||
if (!province || !cityData[province]) {
|
||
console.log('省份为空或数据不存在');
|
||
return;
|
||
}
|
||
|
||
const cities = Object.keys(cityData[province]);
|
||
console.log('可用城市:', cities);
|
||
|
||
cities.forEach(city => {
|
||
const option = document.createElement('option');
|
||
option.value = city;
|
||
option.textContent = city;
|
||
citySelect.appendChild(option);
|
||
});
|
||
|
||
console.log('城市列表更新完成,总计:', cities.length, '个');
|
||
}
|
||
|
||
// 更新区县列表
|
||
function updateDistricts(province, city) {
|
||
const districtSelect = document.getElementById('district');
|
||
districtSelect.innerHTML = '<option value="">请选择区县</option>';
|
||
|
||
console.log('更新区县列表,省份:', province, '城市:', city);
|
||
|
||
if (!province || !city || !cityData[province] || !cityData[province][city]) {
|
||
console.log('省份或城市为空或数据不存在');
|
||
return;
|
||
}
|
||
|
||
const districts = cityData[province][city];
|
||
console.log('可用区县:', districts);
|
||
|
||
districts.forEach(district => {
|
||
const option = document.createElement('option');
|
||
option.value = district;
|
||
option.textContent = district;
|
||
districtSelect.appendChild(option);
|
||
});
|
||
|
||
console.log('区县列表更新完成,总计:', districts.length, '个');
|
||
}
|
||
|
||
// 清空区县列表
|
||
function clearDistricts() {
|
||
const districtSelect = document.getElementById('district');
|
||
districtSelect.innerHTML = '<option value="">请选择区县</option>';
|
||
console.log('区县列表已清空');
|
||
}
|
||
|
||
// 设置初始值(编辑模式)
|
||
function setInitialValues(province, city, district) {
|
||
console.log('设置初始值:', {province, city, district});
|
||
|
||
const provinceSelect = document.getElementById('province');
|
||
const citySelect = document.getElementById('city');
|
||
const districtSelect = document.getElementById('district');
|
||
|
||
// 设置省份
|
||
if (province && provinceSelect) {
|
||
provinceSelect.value = province;
|
||
console.log('省份设置为:', province);
|
||
updateCities(province);
|
||
|
||
// 延迟设置城市
|
||
setTimeout(() => {
|
||
if (city && citySelect) {
|
||
citySelect.value = city;
|
||
console.log('城市设置为:', city);
|
||
updateDistricts(province, city);
|
||
|
||
// 延迟设置区县
|
||
setTimeout(() => {
|
||
if (district && districtSelect) {
|
||
districtSelect.value = district;
|
||
console.log('区县设置为:', district);
|
||
}
|
||
}, 200);
|
||
}
|
||
}, 200);
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/user/addresses.html
|
||
📊 大小: 7551 bytes (7.37 KB)
|
||
🕒 修改时间: 2025-07-04 02:41:21
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
{% block title %}收货地址 - 太白购物商城{% 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 active">
|
||
<i class="bi bi-geo-alt"></i> 收货地址
|
||
</a>
|
||
<a href="#" 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">
|
||
<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-geo-alt"></i> 收货地址</h5>
|
||
<a href="{{ url_for('address.add') }}" class="btn btn-primary">
|
||
<i class="bi bi-plus"></i> 添加地址
|
||
</a>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if addresses %}
|
||
<div class="row">
|
||
{% for address in addresses %}
|
||
<div class="col-md-6 mb-3">
|
||
<div class="card address-card {% if address.is_default %}border-primary{% endif %}">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||
<div>
|
||
<h6 class="card-title mb-1">
|
||
{{ address.receiver_name }}
|
||
{% if address.is_default %}
|
||
<span class="badge bg-primary ms-2">默认</span>
|
||
{% endif %}
|
||
</h6>
|
||
<p class="text-muted mb-0">{{ address.receiver_phone }}</p>
|
||
</div>
|
||
<div class="dropdown">
|
||
<button class="btn btn-link btn-sm" type="button" data-bs-toggle="dropdown">
|
||
<i class="bi bi-three-dots-vertical"></i>
|
||
</button>
|
||
<ul class="dropdown-menu">
|
||
<li>
|
||
<a class="dropdown-item" href="{{ url_for('address.edit', address_id=address.id) }}">
|
||
<i class="bi bi-pencil"></i> 编辑
|
||
</a>
|
||
</li>
|
||
{% if not address.is_default %}
|
||
<li>
|
||
<a class="dropdown-item" href="#" onclick="setDefaultAddress({{ address.id }})">
|
||
<i class="bi bi-star"></i> 设为默认
|
||
</a>
|
||
</li>
|
||
{% endif %}
|
||
<li>
|
||
<a class="dropdown-item text-danger" href="#" onclick="deleteAddress({{ address.id }})">
|
||
<i class="bi bi-trash"></i> 删除
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<p class="card-text">
|
||
<i class="bi bi-geo-alt text-muted"></i>
|
||
{{ address.get_full_address() }}
|
||
</p>
|
||
{% if address.postal_code %}
|
||
<p class="text-muted mb-0">
|
||
<small>邮编:{{ address.postal_code }}</small>
|
||
</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center py-5">
|
||
<i class="bi bi-geo-alt-fill display-1 text-muted"></i>
|
||
<h5 class="mt-3 text-muted">暂无收货地址</h5>
|
||
<p class="text-muted">请添加您的收货地址,方便下单购物</p>
|
||
<a href="{{ url_for('address.add') }}" class="btn btn-primary">
|
||
<i class="bi bi-plus"></i> 添加地址
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function setDefaultAddress(addressId) {
|
||
if (confirm('确定要设置为默认地址吗?')) {
|
||
fetch(`/address/set_default/${addressId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert(data.message, 'success');
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('操作失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
function deleteAddress(addressId) {
|
||
if (confirm('确定要删除这个地址吗?删除后无法恢复。')) {
|
||
fetch(`/address/delete/${addressId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert(data.message, 'success');
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('删除失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/user/login.html
|
||
📊 大小: 2349 bytes (2.29 KB)
|
||
🕒 修改时间: 2025-07-03 03:01:24
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
|
||
{% block title %}用户登录 - 太白购物商城{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row justify-content-center">
|
||
<div class="col-md-6 col-lg-4">
|
||
<div class="card shadow">
|
||
<div class="card-header text-center">
|
||
<h4><i class="bi bi-person-circle"></i> 用户登录</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="POST">
|
||
{{ form.hidden_tag() }}
|
||
|
||
<div class="mb-3">
|
||
{{ form.username.label(class="form-label") }}
|
||
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
|
||
{% if form.username.errors %}
|
||
<div class="invalid-feedback">
|
||
{% for error in form.username.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
{{ form.password.label(class="form-label") }}
|
||
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
|
||
{% if form.password.errors %}
|
||
<div class="invalid-feedback">
|
||
{% for error in form.password.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="mb-3 form-check">
|
||
{{ form.remember_me(class="form-check-input") }}
|
||
{{ form.remember_me.label(class="form-check-label") }}
|
||
</div>
|
||
|
||
<div class="d-grid">
|
||
{{ form.submit(class="btn btn-primary") }}
|
||
</div>
|
||
</form>
|
||
|
||
<hr>
|
||
<div class="text-center">
|
||
<p class="mb-0">还没有账户? <a href="{{ url_for('auth.register') }}">立即注册</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/user/orders.html
|
||
📊 大小: 14280 bytes (13.95 KB)
|
||
🕒 修改时间: 2025-07-04 02:43:57
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
{% block title %}我的订单 - 太白购物商城{% endblock %}
|
||
|
||
{% block head %}
|
||
<style>
|
||
.order-card {
|
||
margin-bottom: 20px;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.order-header {
|
||
background-color: #f8f9fa;
|
||
padding: 15px;
|
||
border-bottom: 1px solid #dee2e6;
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
|
||
.order-item {
|
||
padding: 15px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.order-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.order-footer {
|
||
background-color: #f8f9fa;
|
||
padding: 15px;
|
||
border-top: 1px solid #dee2e6;
|
||
border-radius: 0 0 8px 8px;
|
||
}
|
||
|
||
.status-badge {
|
||
font-size: 0.85em;
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
.order-amount {
|
||
color: #e74c3c;
|
||
font-weight: bold;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.product-image {
|
||
width: 80px;
|
||
height: 80px;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|
||
{% 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 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="#" 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">
|
||
<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-bag"></i> 我的订单</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- 订单状态筛选 -->
|
||
<ul class="nav nav-pills mb-4">
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if not current_status %}active{% endif %}"
|
||
href="{{ url_for('order.list') }}">全部订单</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if current_status == 1 %}active{% endif %}"
|
||
href="{{ url_for('order.list', status=1) }}">待支付</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if current_status == 2 %}active{% endif %}"
|
||
href="{{ url_for('order.list', status=2) }}">待发货</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if current_status == 3 %}active{% endif %}"
|
||
href="{{ url_for('order.list', status=3) }}">待收货</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link {% if current_status == 5 %}active{% endif %}"
|
||
href="{{ url_for('order.list', status=5) }}">已完成</a>
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- 订单列表 -->
|
||
{% if orders.items %}
|
||
{% for order in orders.items %}
|
||
<div class="order-card">
|
||
<!-- 订单头部 -->
|
||
<div class="order-header">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-3">
|
||
<strong>订单号:</strong>{{ order.order_sn }}
|
||
</div>
|
||
<div class="col-md-3">
|
||
<strong>下单时间:</strong>{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||
</div>
|
||
<div class="col-md-3">
|
||
{% if order.status == 1 %}
|
||
<span class="badge bg-warning status-badge">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 2 %}
|
||
<span class="badge bg-info status-badge">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 3 %}
|
||
<span class="badge bg-primary status-badge">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 5 %}
|
||
<span class="badge bg-success status-badge">{{ order.get_status_text() }}</span>
|
||
{% elif order.status == 6 %}
|
||
<span class="badge bg-secondary status-badge">{{ order.get_status_text() }}</span>
|
||
{% else %}
|
||
<span class="badge bg-dark status-badge">{{ order.get_status_text() }}</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-md-3 text-end">
|
||
<a href="{{ url_for('order.detail', order_id=order.id) }}"
|
||
class="btn btn-outline-primary btn-sm">查看详情</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 订单商品 -->
|
||
{% for item in order.order_items[:3] %}
|
||
<div class="order-item">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-2">
|
||
<img src="{{ item.product_image or '/static/images/default-product.jpg' }}"
|
||
class="product-image" alt="{{ item.product_name }}">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<h6 class="mb-1">{{ item.product_name }}</h6>
|
||
{% if item.spec_combination %}
|
||
<p class="text-muted mb-0">{{ item.spec_combination }}</p>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-md-2 text-center">
|
||
<span class="text-muted">× {{ item.quantity }}</span>
|
||
</div>
|
||
<div class="col-md-2 text-end">
|
||
<span class="fw-bold">¥{{ "%.2f"|format(item.total_price) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
{% if order.order_items|length > 3 %}
|
||
<div class="order-item text-center text-muted">
|
||
<small>还有 {{ order.order_items|length - 3 }} 件商品...</small>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 订单底部 -->
|
||
<div class="order-footer">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-6">
|
||
<div class="d-flex gap-2">
|
||
{% if order.can_pay() %}
|
||
<a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}"
|
||
class="btn btn-danger btn-sm">立即支付</a>
|
||
{% endif %}
|
||
|
||
{% if order.can_cancel() %}
|
||
<button class="btn btn-outline-secondary btn-sm"
|
||
onclick="cancelOrder({{ order.id }})">取消订单</button>
|
||
{% endif %}
|
||
|
||
{% if order.can_confirm_receipt() %}
|
||
<button class="btn btn-success btn-sm"
|
||
onclick="confirmReceipt({{ order.id }})">确认收货</button>
|
||
{% endif %}
|
||
|
||
{% if order.status == 4 %}
|
||
<a href="#" class="btn btn-outline-warning btn-sm">评价商品</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6 text-end">
|
||
<div>
|
||
<span class="text-muted">共 {{ order.order_items|length }} 件商品,</span>
|
||
<span class="text-muted">应付:</span>
|
||
<span class="order-amount">¥{{ "%.2f"|format(order.actual_amount) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<!-- 分页 -->
|
||
{% 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('order.list', page=orders.prev_num, status=current_status) }}">上一页</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('order.list', page=page_num, status=current_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 orders.has_next %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('order.list', page=orders.next_num, status=current_status) }}">下一页</a>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
</nav>
|
||
{% endif %}
|
||
|
||
{% else %}
|
||
<div class="text-center py-5">
|
||
<i class="bi bi-bag-x display-1 text-muted"></i>
|
||
<h5 class="mt-3 text-muted">
|
||
{% if current_status %}
|
||
暂无{{ orders.items[0].get_status_text() if orders.items else '该状态' }}订单
|
||
{% else %}
|
||
暂无订单
|
||
{% endif %}
|
||
</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>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function cancelOrder(orderId) {
|
||
if (confirm('确定要取消这个订单吗?取消后无法恢复。')) {
|
||
fetch(`/order/cancel/${orderId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert(data.message, 'success');
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('操作失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
function confirmReceipt(orderId) {
|
||
if (confirm('确定已收到商品吗?确认后订单将完成。')) {
|
||
fetch(`/order/confirm_receipt/${orderId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showAlert(data.message, 'success');
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showAlert(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAlert('操作失败,请重试', 'error');
|
||
});
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/user/profile.html
|
||
📊 大小: 27113 bytes (26.48 KB)
|
||
🕒 修改时间: 2025-07-04 02:45:53
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
{% block title %}个人中心 - 太白购物商城{% endblock %}
|
||
|
||
{% block head %}
|
||
<!-- 头像上传相关CSS -->
|
||
<style>
|
||
/* 头像上传相关CSS */
|
||
.avatar-upload {
|
||
position: relative !important;
|
||
display: inline-block !important;
|
||
width: 120px !important;
|
||
height: 120px !important;
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
/* 强制限制头像尺寸 - 多重选择器确保优先级 */
|
||
.avatar-preview,
|
||
#avatarPreview,
|
||
.avatar-upload .avatar-preview,
|
||
.avatar-upload #avatarPreview {
|
||
width: 120px !important;
|
||
height: 120px !important;
|
||
border-radius: 50% !important;
|
||
border: 3px solid #ddd !important;
|
||
object-fit: cover !important;
|
||
cursor: pointer !important;
|
||
transition: all 0.3s ease !important;
|
||
display: block !important;
|
||
max-width: 120px !important;
|
||
max-height: 120px !important;
|
||
min-width: 120px !important;
|
||
min-height: 120px !important;
|
||
}
|
||
|
||
.avatar-preview:hover,
|
||
#avatarPreview:hover {
|
||
border-color: #007bff !important;
|
||
transform: scale(1.05) !important;
|
||
}
|
||
|
||
.avatar-placeholder {
|
||
width: 120px !important;
|
||
height: 120px !important;
|
||
border-radius: 50% !important;
|
||
border: 3px dashed #ddd !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
cursor: pointer !important;
|
||
transition: all 0.3s ease !important;
|
||
background-color: #f8f9fa !important;
|
||
}
|
||
|
||
.avatar-placeholder:hover {
|
||
border-color: #007bff !important;
|
||
background-color: #e3f2fd !important;
|
||
}
|
||
|
||
.upload-overlay {
|
||
position: absolute !important;
|
||
top: 0 !important;
|
||
left: 0 !important;
|
||
width: 120px !important;
|
||
height: 120px !important;
|
||
border-radius: 50% !important;
|
||
background: rgba(0, 0, 0, 0.5) !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
opacity: 0 !important;
|
||
transition: opacity 0.3s ease !important;
|
||
cursor: pointer !important;
|
||
}
|
||
|
||
.avatar-upload:hover .upload-overlay {
|
||
opacity: 1 !important;
|
||
}
|
||
|
||
.upload-progress {
|
||
display: none !important;
|
||
margin-top: 10px !important;
|
||
}
|
||
|
||
.upload-progress.show {
|
||
display: block !important;
|
||
}
|
||
|
||
/* 图片预览模态框样式 */
|
||
.image-preview-modal .modal-dialog {
|
||
max-width: 500px !important;
|
||
margin: 1.75rem auto !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
min-height: calc(100% - 3.5rem) !important;
|
||
}
|
||
|
||
.image-preview-modal .modal-content {
|
||
max-height: 90vh !important;
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
}
|
||
|
||
.image-preview-modal .modal-body {
|
||
overflow-y: auto !important;
|
||
}
|
||
|
||
.preview-container {
|
||
background: #f8f9fa !important;
|
||
border-radius: 12px !important;
|
||
padding: 30px !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
.preview-image-wrapper {
|
||
position: relative !important;
|
||
display: block !important;
|
||
margin-bottom: 20px !important;
|
||
max-width: 100% !important;
|
||
}
|
||
|
||
/* 强制限制预览图片大小 */
|
||
.preview-image,
|
||
#previewImage,
|
||
.preview-image-wrapper .preview-image,
|
||
.preview-image-wrapper #previewImage {
|
||
max-width: 280px !important;
|
||
max-height: 280px !important;
|
||
width: auto !important;
|
||
height: auto !important;
|
||
object-fit: contain !important;
|
||
border-radius: 12px !important;
|
||
box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important;
|
||
border: 3px solid #fff !important;
|
||
}
|
||
|
||
.preview-info {
|
||
background: #fff !important;
|
||
border-radius: 8px !important;
|
||
padding: 15px !important;
|
||
margin-top: 15px !important;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important;
|
||
}
|
||
|
||
.preview-stats {
|
||
display: flex !important;
|
||
justify-content: space-around !important;
|
||
margin-top: 10px !important;
|
||
}
|
||
|
||
.stat-item {
|
||
text-align: center !important;
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: bold !important;
|
||
color: #007bff !important;
|
||
font-size: 1.1em !important;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.85em !important;
|
||
color: #6c757d !important;
|
||
margin-top: 2px !important;
|
||
}
|
||
|
||
/* 进度条样式 */
|
||
.upload-progress .progress {
|
||
height: 8px !important;
|
||
margin-bottom: 5px !important;
|
||
border-radius: 4px !important;
|
||
}
|
||
|
||
.upload-progress .progress-bar {
|
||
transition: width 0.3s ease !important;
|
||
border-radius: 4px !important;
|
||
}
|
||
|
||
/* 模态框动画 */
|
||
.modal.fade .modal-dialog {
|
||
transition: transform 0.3s ease-out !important;
|
||
transform: translate(0, -50px) !important;
|
||
}
|
||
|
||
.modal.show .modal-dialog {
|
||
transform: none !important;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.image-preview-modal .modal-dialog {
|
||
max-width: 95% !important;
|
||
margin: 10px auto !important;
|
||
}
|
||
|
||
.preview-image,
|
||
#previewImage {
|
||
max-width: 250px !important;
|
||
max-height: 250px !important;
|
||
}
|
||
|
||
.preview-container {
|
||
padding: 20px !important;
|
||
}
|
||
}
|
||
|
||
/* 大屏幕优化 */
|
||
@media (min-width: 1200px) {
|
||
.preview-image,
|
||
#previewImage {
|
||
max-width: 300px !important;
|
||
max-height: 300px !important;
|
||
}
|
||
}
|
||
|
||
/* 终极覆盖规则 - 确保所有情况下样式都生效 */
|
||
img.avatar-preview,
|
||
img#avatarPreview {
|
||
width: 120px !important;
|
||
height: 120px !important;
|
||
border-radius: 50% !important;
|
||
object-fit: cover !important;
|
||
max-width: 120px !important;
|
||
max-height: 120px !important;
|
||
min-width: 120px !important;
|
||
min-height: 120px !important;
|
||
}
|
||
|
||
/* 防止任何外部样式影响 */
|
||
.col-md-4 .avatar-upload img,
|
||
.text-center .avatar-upload img {
|
||
width: 120px !important;
|
||
height: 120px !important;
|
||
border-radius: 50% !important;
|
||
object-fit: cover !important;
|
||
}
|
||
|
||
</style>
|
||
|
||
|
||
{% 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 active">
|
||
<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="#" 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">
|
||
<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-person"></i> 基本信息</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<table class="table table-borderless">
|
||
<tr>
|
||
<td width="120"><strong>用户名:</strong></td>
|
||
<td>{{ user.username }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>昵称:</strong></td>
|
||
<td>{{ user.nickname or '未设置' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>手机号:</strong></td>
|
||
<td>{{ user.phone or '未绑定' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>邮箱:</strong></td>
|
||
<td>{{ user.email or '未绑定' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>性别:</strong></td>
|
||
<td>
|
||
{% if user.gender == 1 %}男
|
||
{% elif user.gender == 2 %}女
|
||
{% else %}未设置
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>注册时间:</strong></td>
|
||
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M:%S') if user.created_at else '未知' }}</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="mt-3">
|
||
<button class="btn btn-primary me-2">
|
||
<i class="bi bi-pencil"></i> 编辑资料
|
||
</button>
|
||
<button class="btn btn-outline-secondary">
|
||
<i class="bi bi-key"></i> 修改密码
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4 text-center">
|
||
<div class="mb-3">
|
||
<div class="avatar-upload">
|
||
{% if user.avatar_url %}
|
||
<img src="{{ user.avatar_url }}" alt="头像" class="avatar-preview" id="avatarPreview">
|
||
{% else %}
|
||
<div class="avatar-placeholder" id="avatarPlaceholder">
|
||
<i class="bi bi-person display-4 text-muted"></i>
|
||
</div>
|
||
{% endif %}
|
||
<div class="upload-overlay" onclick="triggerFileInput()">
|
||
<i class="bi bi-camera text-white fs-3"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 隐藏的文件输入 -->
|
||
<input type="file" id="avatarInput" accept="image/*" style="display: none;" onchange="handleFileSelect(event)">
|
||
|
||
<!-- 上传进度 -->
|
||
<div class="upload-progress" id="uploadProgress">
|
||
<div class="progress">
|
||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
|
||
</div>
|
||
<small class="text-muted">上传中...</small>
|
||
</div>
|
||
|
||
<button class="btn btn-outline-primary btn-sm" onclick="triggerFileInput()">
|
||
<i class="bi bi-camera"></i> 更换头像
|
||
</button>
|
||
<div class="mt-2">
|
||
<small class="text-muted">支持 JPG、PNG 格式,大小不超过 2MB</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 快捷操作 -->
|
||
<div class="row mt-4">
|
||
<div class="col-md-3 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-3 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重新设计的图片预览模态框 -->
|
||
<div class="modal fade image-preview-modal" id="imagePreviewModal" tabindex="-1">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header border-0 pb-0">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-image text-primary"></i> 头像预览
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="preview-container">
|
||
<div class="preview-image-wrapper">
|
||
<img src="" alt="预览图片" class="preview-image" id="previewImage">
|
||
</div>
|
||
|
||
<div class="preview-info">
|
||
<div class="mb-2">
|
||
<i class="bi bi-info-circle text-primary"></i>
|
||
<strong>图片信息</strong>
|
||
</div>
|
||
<div class="preview-stats">
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="imageWidth">-</div>
|
||
<div class="stat-label">宽度(px)</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="imageHeight">-</div>
|
||
<div class="stat-label">高度(px)</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="imageSize">-</div>
|
||
<div class="stat-label">大小</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="imageType">-</div>
|
||
<div class="stat-label">格式</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
<small class="text-muted">
|
||
<i class="bi bi-check-circle text-success"></i>
|
||
图片将被自动调整为合适的头像尺寸
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer border-0 pt-0">
|
||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">
|
||
<i class="bi bi-x-circle"></i> 取消
|
||
</button>
|
||
<button type="button" class="btn btn-primary" onclick="confirmUpload()">
|
||
<i class="bi bi-upload"></i> 确认上传
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
let selectedFile = null;
|
||
|
||
// 强制设置头像样式的函数
|
||
function forceAvatarStyle(imgElement) {
|
||
if (!imgElement) return;
|
||
|
||
// 强制设置所有样式属性
|
||
imgElement.style.width = '120px';
|
||
imgElement.style.height = '120px';
|
||
imgElement.style.borderRadius = '50%';
|
||
imgElement.style.border = '3px solid #ddd';
|
||
imgElement.style.objectFit = 'cover';
|
||
imgElement.style.cursor = 'pointer';
|
||
imgElement.style.transition = 'all 0.3s ease';
|
||
imgElement.style.display = 'block';
|
||
imgElement.style.maxWidth = '120px';
|
||
imgElement.style.maxHeight = '120px';
|
||
imgElement.style.minWidth = '120px';
|
||
imgElement.style.minHeight = '120px';
|
||
|
||
// 设置属性避免被覆盖
|
||
imgElement.setAttribute('width', '120');
|
||
imgElement.setAttribute('height', '120');
|
||
}
|
||
|
||
// 页面加载完成后的处理
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 隐藏进度条
|
||
const progressContainer = document.getElementById('uploadProgress');
|
||
if (progressContainer) {
|
||
progressContainer.classList.remove('show');
|
||
progressContainer.style.display = 'none';
|
||
}
|
||
|
||
// *** 关键:强制设置已存在的头像样式 ***
|
||
const existingAvatar = document.getElementById('avatarPreview');
|
||
if (existingAvatar) {
|
||
forceAvatarStyle(existingAvatar);
|
||
|
||
// 图片加载完成后再次强制设置
|
||
if (existingAvatar.complete) {
|
||
forceAvatarStyle(existingAvatar);
|
||
} else {
|
||
existingAvatar.onload = function() {
|
||
forceAvatarStyle(existingAvatar);
|
||
};
|
||
}
|
||
}
|
||
|
||
// 添加拖拽上传支持
|
||
initDragAndDrop();
|
||
});
|
||
|
||
// 触发文件选择
|
||
function triggerFileInput() {
|
||
document.getElementById('avatarInput').click();
|
||
}
|
||
|
||
// 处理文件选择
|
||
function handleFileSelect(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
// 验证文件类型
|
||
if (!file.type.match('image.*')) {
|
||
showAlert('请选择图片文件!', 'error');
|
||
return;
|
||
}
|
||
|
||
// 验证文件大小 (2MB)
|
||
if (file.size > 2 * 1024 * 1024) {
|
||
showAlert('图片大小不能超过 2MB!', 'error');
|
||
return;
|
||
}
|
||
|
||
selectedFile = file;
|
||
|
||
// 预览图片
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const previewImage = document.getElementById('previewImage');
|
||
previewImage.src = e.target.result;
|
||
|
||
// 更新文件信息
|
||
updateFileInfo(file);
|
||
|
||
// 确保图片加载完成后再显示模态框
|
||
previewImage.onload = function() {
|
||
// *** 强制设置预览图片样式 ***
|
||
previewImage.style.maxWidth = '280px';
|
||
previewImage.style.maxHeight = '280px';
|
||
previewImage.style.width = 'auto';
|
||
previewImage.style.height = 'auto';
|
||
previewImage.style.objectFit = 'contain';
|
||
previewImage.style.borderRadius = '12px';
|
||
previewImage.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)';
|
||
previewImage.style.border = '3px solid #fff';
|
||
|
||
// 更新图片尺寸信息
|
||
document.getElementById('imageWidth').textContent = previewImage.naturalWidth;
|
||
document.getElementById('imageHeight').textContent = previewImage.naturalHeight;
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('imagePreviewModal'));
|
||
modal.show();
|
||
};
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
// 更新文件信息
|
||
function updateFileInfo(file) {
|
||
// 文件大小
|
||
const sizeInMB = (file.size / 1024 / 1024).toFixed(2);
|
||
document.getElementById('imageSize').textContent = sizeInMB + ' MB';
|
||
|
||
// 文件类型
|
||
const fileType = file.type.split('/')[1].toUpperCase();
|
||
document.getElementById('imageType').textContent = fileType;
|
||
}
|
||
|
||
// 确认上传
|
||
function confirmUpload() {
|
||
if (!selectedFile) return;
|
||
|
||
// 关闭模态框
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('imagePreviewModal'));
|
||
modal.hide();
|
||
|
||
// 开始上传
|
||
uploadAvatar(selectedFile);
|
||
}
|
||
|
||
// 上传头像
|
||
function uploadAvatar(file) {
|
||
const formData = new FormData();
|
||
formData.append('avatar', file);
|
||
|
||
// 显示上传进度
|
||
const progressContainer = document.getElementById('uploadProgress');
|
||
const progressBar = progressContainer.querySelector('.progress-bar');
|
||
|
||
progressContainer.style.display = 'block';
|
||
progressContainer.classList.add('show');
|
||
progressBar.style.width = '0%';
|
||
progressBar.textContent = '0%';
|
||
|
||
// 创建XMLHttpRequest以支持进度显示
|
||
const xhr = new XMLHttpRequest();
|
||
|
||
// 上传进度
|
||
xhr.upload.addEventListener('progress', function(e) {
|
||
if (e.lengthComputable) {
|
||
const percentComplete = (e.loaded / e.total) * 100;
|
||
progressBar.style.width = percentComplete + '%';
|
||
progressBar.textContent = Math.round(percentComplete) + '%';
|
||
}
|
||
});
|
||
|
||
// 上传完成
|
||
xhr.addEventListener('load', function() {
|
||
// 隐藏进度条
|
||
progressContainer.style.display = 'none';
|
||
progressContainer.classList.remove('show');
|
||
|
||
if (xhr.status === 200) {
|
||
try {
|
||
const response = JSON.parse(xhr.responseText);
|
||
if (response.success) {
|
||
// 更新头像显示
|
||
updateAvatarDisplay(response.avatar_url);
|
||
showAlert('头像上传成功!', 'success');
|
||
} else {
|
||
showAlert(response.message || '上传失败', 'error');
|
||
}
|
||
} catch (e) {
|
||
showAlert('服务器响应错误', 'error');
|
||
}
|
||
} else {
|
||
showAlert('上传失败,请重试', 'error');
|
||
}
|
||
|
||
// 清理文件输入
|
||
document.getElementById('avatarInput').value = '';
|
||
selectedFile = null;
|
||
});
|
||
|
||
// 上传错误
|
||
xhr.addEventListener('error', function() {
|
||
progressContainer.style.display = 'none';
|
||
progressContainer.classList.remove('show');
|
||
showAlert('网络错误,请重试', 'error');
|
||
|
||
// 清理文件输入
|
||
document.getElementById('avatarInput').value = '';
|
||
selectedFile = null;
|
||
});
|
||
|
||
// 发送请求
|
||
xhr.open('POST', '/upload/avatar');
|
||
xhr.send(formData);
|
||
}
|
||
|
||
// *** 关键:更新头像显示函数 ***
|
||
function updateAvatarDisplay(avatarUrl) {
|
||
const avatarPreview = document.getElementById('avatarPreview');
|
||
const avatarPlaceholder = document.getElementById('avatarPlaceholder');
|
||
|
||
if (avatarPreview) {
|
||
// 更新现有头像
|
||
avatarPreview.src = avatarUrl + '?t=' + new Date().getTime();
|
||
|
||
// *** 强制设置头像样式 ***
|
||
avatarPreview.onload = function() {
|
||
forceAvatarStyle(avatarPreview);
|
||
|
||
// 延迟再次确保样式生效
|
||
setTimeout(function() {
|
||
forceAvatarStyle(avatarPreview);
|
||
}, 100);
|
||
};
|
||
|
||
} else if (avatarPlaceholder) {
|
||
// 替换占位符为头像
|
||
const avatarUpload = avatarPlaceholder.parentElement;
|
||
avatarPlaceholder.remove();
|
||
|
||
const img = document.createElement('img');
|
||
img.src = avatarUrl + '?t=' + new Date().getTime();
|
||
img.alt = '头像';
|
||
img.className = 'avatar-preview';
|
||
img.id = 'avatarPreview';
|
||
|
||
// *** 创建新头像时强制设置样式 ***
|
||
img.onload = function() {
|
||
forceAvatarStyle(img);
|
||
|
||
// 延迟再次确保样式生效
|
||
setTimeout(function() {
|
||
forceAvatarStyle(img);
|
||
}, 100);
|
||
};
|
||
|
||
avatarUpload.insertBefore(img, avatarUpload.firstChild);
|
||
}
|
||
}
|
||
|
||
// 显示提示信息
|
||
function showAlert(message, type = 'info') {
|
||
// 移除现有的提示框
|
||
const existingAlerts = document.querySelectorAll('.alert.position-fixed');
|
||
existingAlerts.forEach(alert => alert.remove());
|
||
|
||
// 创建提示框
|
||
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; max-width: 400px;';
|
||
|
||
const icon = type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-triangle' : 'info-circle';
|
||
|
||
alertDiv.innerHTML = `
|
||
<i class="bi bi-${icon} me-2"></i>
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
`;
|
||
|
||
document.body.appendChild(alertDiv);
|
||
|
||
// 3秒后自动消失
|
||
setTimeout(() => {
|
||
if (alertDiv.parentElement) {
|
||
alertDiv.classList.remove('show');
|
||
setTimeout(() => {
|
||
if (alertDiv.parentElement) {
|
||
alertDiv.remove();
|
||
}
|
||
}, 150);
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
// 初始化拖拽上传功能
|
||
function initDragAndDrop() {
|
||
const avatarUpload = document.querySelector('.avatar-upload');
|
||
|
||
if (avatarUpload) {
|
||
// 防止默认拖拽行为
|
||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||
avatarUpload.addEventListener(eventName, preventDefaults, false);
|
||
});
|
||
|
||
function preventDefaults(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
|
||
// 拖拽进入和离开的视觉反馈
|
||
['dragenter', 'dragover'].forEach(eventName => {
|
||
avatarUpload.addEventListener(eventName, highlight, false);
|
||
});
|
||
|
||
['dragleave', 'drop'].forEach(eventName => {
|
||
avatarUpload.addEventListener(eventName, unhighlight, false);
|
||
});
|
||
|
||
function highlight(e) {
|
||
avatarUpload.style.transform = 'scale(1.05)';
|
||
avatarUpload.style.boxShadow = '0 0 20px rgba(0,123,255,0.5)';
|
||
}
|
||
|
||
function unhighlight(e) {
|
||
avatarUpload.style.transform = '';
|
||
avatarUpload.style.boxShadow = '';
|
||
}
|
||
|
||
// 处理文件拖拽
|
||
avatarUpload.addEventListener('drop', handleDrop, false);
|
||
|
||
function handleDrop(e) {
|
||
const dt = e.dataTransfer;
|
||
const files = dt.files;
|
||
|
||
if (files.length > 0) {
|
||
const file = files[0];
|
||
// 模拟文件输入事件
|
||
const event = { target: { files: [file] } };
|
||
handleFileSelect(event);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 额外的保险措施:定期检查并修正头像样式
|
||
setInterval(function() {
|
||
const avatar = document.getElementById('avatarPreview');
|
||
if (avatar) {
|
||
// 检查头像是否超出预期尺寸
|
||
const rect = avatar.getBoundingClientRect();
|
||
if (rect.width > 125 || rect.height > 125) {
|
||
forceAvatarStyle(avatar);
|
||
}
|
||
}
|
||
}, 1000); // 每秒检查一次
|
||
|
||
</script>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/templates/user/register.html
|
||
📊 大小: 9627 bytes (9.40 KB)
|
||
🕒 修改时间: 2025-07-03 04:04:29
|
||
🔸==============================================================================
|
||
|
||
{% extends "base.html" %}
|
||
|
||
{% block title %}用户注册 - 太白购物商城{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="row justify-content-center">
|
||
<div class="col-md-6 col-lg-5">
|
||
<div class="card shadow">
|
||
<div class="card-header text-center">
|
||
<h4><i class="bi bi-person-plus"></i> 用户注册</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="POST" id="registerForm">
|
||
{{ form.hidden_tag() }}
|
||
|
||
<div class="mb-3">
|
||
{{ form.username.label(class="form-label") }}
|
||
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
|
||
{% if form.username.errors %}
|
||
<div class="invalid-feedback">
|
||
{% for error in form.username.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
<div class="form-text">用户名只能包含字母、数字和下划线,3-20个字符</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
{{ form.email.label(class="form-label") }}
|
||
<div class="input-group">
|
||
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else ""), id="emailInput") }}
|
||
<button type="button" class="btn btn-outline-primary" id="sendEmailCodeBtn">
|
||
<span id="btnText">发送验证码</span>
|
||
</button>
|
||
</div>
|
||
{% if form.email.errors %}
|
||
<div class="invalid-feedback d-block">
|
||
{% for error in form.email.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
{{ form.email_code.label(class="form-label") }}
|
||
{{ form.email_code(class="form-control" + (" is-invalid" if form.email_code.errors else ""), placeholder="请输入6位数字验证码") }}
|
||
{% if form.email_code.errors %}
|
||
<div class="invalid-feedback">
|
||
{% for error in form.email_code.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
{{ form.phone.label(class="form-label") }}
|
||
{{ form.phone(class="form-control" + (" is-invalid" if form.phone.errors else "")) }}
|
||
{% if form.phone.errors %}
|
||
<div class="invalid-feedback">
|
||
{% for error in form.phone.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
{{ form.password.label(class="form-label") }}
|
||
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else ""), id="passwordInput") }}
|
||
{% if form.password.errors %}
|
||
<div class="invalid-feedback">
|
||
{% for error in form.password.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
<div class="form-text">密码必须包含至少一个字母和一个数字,6-20个字符</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
{{ form.confirm_password.label(class="form-label") }}
|
||
{{ form.confirm_password(class="form-control" + (" is-invalid" if form.confirm_password.errors else ""), id="confirmPasswordInput") }}
|
||
{% if form.confirm_password.errors %}
|
||
<div class="invalid-feedback">
|
||
{% for error in form.confirm_password.errors %}
|
||
{{ error }}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
<div id="passwordMatchMessage" class="form-text"></div>
|
||
</div>
|
||
|
||
<div class="d-grid">
|
||
{{ form.submit(class="btn btn-success") }}
|
||
</div>
|
||
</form>
|
||
|
||
<hr>
|
||
<div class="text-center">
|
||
<p class="mb-0">已有账户? <a href="{{ url_for('auth.login') }}">立即登录</a></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 邮箱验证码发送和密码确认的JavaScript -->
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const sendBtn = document.getElementById('sendEmailCodeBtn');
|
||
const emailInput = document.getElementById('emailInput');
|
||
const btnText = document.getElementById('btnText');
|
||
const passwordInput = document.getElementById('passwordInput');
|
||
const confirmPasswordInput = document.getElementById('confirmPasswordInput');
|
||
const passwordMatchMessage = document.getElementById('passwordMatchMessage');
|
||
let countdown = 0;
|
||
let timer = null;
|
||
|
||
// 密码确认实时验证
|
||
function checkPasswordMatch() {
|
||
const password = passwordInput.value;
|
||
const confirmPassword = confirmPasswordInput.value;
|
||
|
||
if (confirmPassword === '') {
|
||
passwordMatchMessage.textContent = '';
|
||
passwordMatchMessage.className = 'form-text';
|
||
return;
|
||
}
|
||
|
||
if (password === confirmPassword) {
|
||
passwordMatchMessage.textContent = '✓ 密码匹配';
|
||
passwordMatchMessage.className = 'form-text text-success';
|
||
confirmPasswordInput.classList.remove('is-invalid');
|
||
confirmPasswordInput.classList.add('is-valid');
|
||
} else {
|
||
passwordMatchMessage.textContent = '✗ 密码不匹配';
|
||
passwordMatchMessage.className = 'form-text text-danger';
|
||
confirmPasswordInput.classList.remove('is-valid');
|
||
confirmPasswordInput.classList.add('is-invalid');
|
||
}
|
||
}
|
||
|
||
// 监听密码输入
|
||
passwordInput.addEventListener('input', checkPasswordMatch);
|
||
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
|
||
|
||
// 发送验证码
|
||
sendBtn.addEventListener('click', function() {
|
||
const email = emailInput.value.trim();
|
||
|
||
// 简单的邮箱格式验证
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!email) {
|
||
alert('请输入邮箱地址');
|
||
emailInput.focus();
|
||
return;
|
||
}
|
||
|
||
if (!emailRegex.test(email)) {
|
||
alert('请输入有效的邮箱地址');
|
||
emailInput.focus();
|
||
return;
|
||
}
|
||
|
||
// 发送AJAX请求
|
||
fetch('/auth/send_email_code', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value
|
||
},
|
||
body: JSON.stringify({
|
||
email: email,
|
||
type: 1 // 1表示注册
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('验证码已发送到您的邮箱,请查收!');
|
||
startCountdown();
|
||
} else {
|
||
alert(data.message || '发送失败,请重试');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('发送失败,请检查网络连接');
|
||
});
|
||
});
|
||
|
||
// 倒计时功能
|
||
function startCountdown() {
|
||
countdown = 60;
|
||
sendBtn.disabled = true;
|
||
sendBtn.classList.add('disabled');
|
||
|
||
timer = setInterval(function() {
|
||
btnText.textContent = `${countdown}秒后重发`;
|
||
countdown--;
|
||
|
||
if (countdown < 0) {
|
||
clearInterval(timer);
|
||
sendBtn.disabled = false;
|
||
sendBtn.classList.remove('disabled');
|
||
btnText.textContent = '发送验证码';
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// 表单提交前验证
|
||
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
||
const password = passwordInput.value;
|
||
const confirmPassword = confirmPasswordInput.value;
|
||
|
||
if (password !== confirmPassword) {
|
||
e.preventDefault();
|
||
alert('两次输入的密码不一致,请重新输入');
|
||
confirmPasswordInput.focus();
|
||
return false;
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.is-valid {
|
||
border-color: #28a745 !important;
|
||
}
|
||
|
||
.is-invalid {
|
||
border-color: #dc3545 !important;
|
||
}
|
||
|
||
.text-success {
|
||
color: #28a745 !important;
|
||
}
|
||
|
||
.text-danger {
|
||
color: #dc3545 !important;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/__init__.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/auth.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/cos_client.py
|
||
📊 大小: 7860 bytes (7.68 KB)
|
||
🕒 修改时间: 2025-07-03 04:22:19
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
腾讯云COS客户端工具
|
||
"""
|
||
import sys
|
||
import os
|
||
import uuid
|
||
import logging
|
||
from datetime import datetime
|
||
from qcloud_cos import CosConfig, CosS3Client
|
||
from qcloud_cos.cos_exception import CosClientError, CosServiceError
|
||
from config.cos_config import COSConfig
|
||
|
||
# 配置日志
|
||
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class COSClient:
|
||
"""腾讯云COS客户端"""
|
||
|
||
def __init__(self):
|
||
"""初始化COS客户端"""
|
||
try:
|
||
# 配置COS
|
||
config = CosConfig(
|
||
Region=COSConfig.REGION,
|
||
SecretId=COSConfig.SECRET_ID,
|
||
SecretKey=COSConfig.SECRET_KEY,
|
||
Token=None, # 临时密钥需要传入Token,永久密钥不需要
|
||
Scheme='https' # 指定使用 http/https 协议来访问COS,默认为https
|
||
)
|
||
|
||
# 创建客户端
|
||
self.client = CosS3Client(config)
|
||
self.bucket = COSConfig.BUCKET_NAME
|
||
|
||
logger.info("COS客户端初始化成功")
|
||
|
||
except Exception as e:
|
||
logger.error(f"COS客户端初始化失败: {str(e)}")
|
||
raise
|
||
|
||
def generate_file_key(self, folder_type, original_filename):
|
||
"""
|
||
生成文件存储路径
|
||
|
||
Args:
|
||
folder_type: 文件夹类型 (avatar, product, review, temp)
|
||
original_filename: 原始文件名
|
||
|
||
Returns:
|
||
str: 生成的文件路径
|
||
"""
|
||
# 获取文件扩展名
|
||
file_ext = original_filename.rsplit('.', 1)[1].lower() if '.' in original_filename else ''
|
||
|
||
# 生成唯一文件名
|
||
unique_filename = f"{uuid.uuid4().hex}.{file_ext}" if file_ext else uuid.uuid4().hex
|
||
|
||
# 按日期分组
|
||
date_folder = datetime.now().strftime('%Y/%m/%d')
|
||
|
||
# 获取存储路径前缀
|
||
folder_prefix = COSConfig.UPLOAD_FOLDERS.get(folder_type, COSConfig.UPLOAD_FOLDERS['temp'])
|
||
|
||
# 组合完整路径
|
||
file_key = f"{folder_prefix}{date_folder}/{unique_filename}"
|
||
|
||
return file_key
|
||
|
||
def upload_file(self, file_obj, folder_type='temp', original_filename=None):
|
||
"""
|
||
上传文件到COS
|
||
|
||
Args:
|
||
file_obj: 文件对象或文件路径
|
||
folder_type: 文件夹类型
|
||
original_filename: 原始文件名
|
||
|
||
Returns:
|
||
dict: 上传结果 {'success': bool, 'file_key': str, 'url': str, 'error': str}
|
||
"""
|
||
try:
|
||
# 生成文件路径
|
||
if original_filename is None:
|
||
if hasattr(file_obj, 'filename'):
|
||
original_filename = file_obj.filename
|
||
else:
|
||
original_filename = 'unknown'
|
||
|
||
file_key = self.generate_file_key(folder_type, original_filename)
|
||
|
||
# 上传文件
|
||
if hasattr(file_obj, 'read'):
|
||
# 文件对象
|
||
response = self.client.put_object(
|
||
Bucket=self.bucket,
|
||
Body=file_obj,
|
||
Key=file_key,
|
||
StorageClass='STANDARD',
|
||
EnableMD5=False
|
||
)
|
||
else:
|
||
# 文件路径
|
||
response = self.client.put_object_from_local_file(
|
||
Bucket=self.bucket,
|
||
LocalFilePath=file_obj,
|
||
Key=file_key,
|
||
EnableMD5=False
|
||
)
|
||
|
||
# 生成访问URL
|
||
file_url = COSConfig.get_full_url(file_key)
|
||
|
||
logger.info(f"文件上传成功: {file_key}")
|
||
|
||
return {
|
||
'success': True,
|
||
'file_key': file_key,
|
||
'url': file_url,
|
||
'etag': response['ETag'],
|
||
'error': None
|
||
}
|
||
|
||
except CosClientError as e:
|
||
error_msg = f"COS客户端错误: {str(e)}"
|
||
logger.error(error_msg)
|
||
return {
|
||
'success': False,
|
||
'file_key': None,
|
||
'url': None,
|
||
'error': error_msg
|
||
}
|
||
|
||
except CosServiceError as e:
|
||
error_msg = f"COS服务错误: {e.get_error_code()} - {e.get_error_msg()}"
|
||
logger.error(error_msg)
|
||
return {
|
||
'success': False,
|
||
'file_key': None,
|
||
'url': None,
|
||
'error': error_msg
|
||
}
|
||
|
||
except Exception as e:
|
||
error_msg = f"上传失败: {str(e)}"
|
||
logger.error(error_msg)
|
||
return {
|
||
'success': False,
|
||
'file_key': None,
|
||
'url': None,
|
||
'error': error_msg
|
||
}
|
||
|
||
def delete_file(self, file_key):
|
||
"""
|
||
删除COS中的文件
|
||
|
||
Args:
|
||
file_key: 文件路径
|
||
|
||
Returns:
|
||
dict: 删除结果
|
||
"""
|
||
try:
|
||
response = self.client.delete_object(
|
||
Bucket=self.bucket,
|
||
Key=file_key
|
||
)
|
||
|
||
logger.info(f"文件删除成功: {file_key}")
|
||
|
||
return {
|
||
'success': True,
|
||
'error': None
|
||
}
|
||
|
||
except Exception as e:
|
||
error_msg = f"删除文件失败: {str(e)}"
|
||
logger.error(error_msg)
|
||
return {
|
||
'success': False,
|
||
'error': error_msg
|
||
}
|
||
|
||
def get_file_url(self, file_key, expires=3600):
|
||
"""
|
||
获取文件访问URL(用于私有文件)
|
||
|
||
Args:
|
||
file_key: 文件路径
|
||
expires: 过期时间(秒)
|
||
|
||
Returns:
|
||
str: 预签名URL
|
||
"""
|
||
try:
|
||
response = self.client.get_presigned_download_url(
|
||
Bucket=self.bucket,
|
||
Key=file_key,
|
||
Expired=expires
|
||
)
|
||
return response
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成预签名URL失败: {str(e)}")
|
||
return None
|
||
|
||
def list_files(self, prefix='', max_keys=100):
|
||
"""
|
||
列出存储桶中的文件
|
||
|
||
Args:
|
||
prefix: 文件路径前缀
|
||
max_keys: 最大返回数量
|
||
|
||
Returns:
|
||
list: 文件列表
|
||
"""
|
||
try:
|
||
response = self.client.list_objects(
|
||
Bucket=self.bucket,
|
||
Prefix=prefix,
|
||
MaxKeys=max_keys
|
||
)
|
||
|
||
files = []
|
||
if 'Contents' in response:
|
||
for obj in response['Contents']:
|
||
files.append({
|
||
'key': obj['Key'],
|
||
'size': obj['Size'],
|
||
'last_modified': obj['LastModified'],
|
||
'url': COSConfig.get_full_url(obj['Key'])
|
||
})
|
||
|
||
return files
|
||
|
||
except Exception as e:
|
||
logger.error(f"列出文件失败: {str(e)}")
|
||
return []
|
||
|
||
def test_connection(self):
|
||
"""
|
||
测试COS连接
|
||
|
||
Returns:
|
||
dict: 测试结果
|
||
"""
|
||
try:
|
||
# 尝试列出存储桶
|
||
response = self.client.list_objects(
|
||
Bucket=self.bucket,
|
||
MaxKeys=1
|
||
)
|
||
|
||
return {
|
||
'success': True,
|
||
'message': 'COS连接测试成功',
|
||
'bucket': self.bucket,
|
||
'region': COSConfig.REGION
|
||
}
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'message': f'COS连接测试失败: {str(e)}',
|
||
'bucket': self.bucket,
|
||
'region': COSConfig.REGION
|
||
}
|
||
|
||
|
||
# 创建全局COS客户端实例
|
||
cos_client = COSClient()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/cos_upload.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/database.py
|
||
📊 大小: 1074 bytes (1.05 KB)
|
||
🕒 修改时间: 2025-07-03 04:26:13
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
数据库工具模块
|
||
"""
|
||
from flask_sqlalchemy import SQLAlchemy
|
||
import sys
|
||
|
||
# 创建数据库实例
|
||
db = SQLAlchemy()
|
||
|
||
def init_db(app):
|
||
"""初始化数据库"""
|
||
db.init_app(app)
|
||
|
||
try:
|
||
with app.app_context():
|
||
# 测试数据库连接
|
||
result = db.session.execute(db.text('SELECT 1'))
|
||
print("✅ 数据库连接成功")
|
||
|
||
# 由于表已存在,我们只需要确保模型与数据库同步
|
||
# 不需要重新创建表
|
||
print("✅ 数据库初始化完成")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 数据库初始化失败: {e}")
|
||
print("请检查数据库配置和网络连接")
|
||
# 在开发环境中不退出,允许继续运行
|
||
print("⚠️ 继续运行,但可能会有数据库相关问题")
|
||
|
||
def test_connection():
|
||
"""测试数据库连接"""
|
||
try:
|
||
result = db.session.execute(db.text('SELECT 1'))
|
||
return True, "数据库连接正常"
|
||
except Exception as e:
|
||
return False, f"数据库连接失败: {str(e)}"
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/decorators.py
|
||
📊 大小: 10277 bytes (10.04 KB)
|
||
🕒 修改时间: 2025-07-03 05:56:41
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
装饰器工具模块
|
||
提供登录验证、权限控制等装饰器功能
|
||
"""
|
||
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
|
||
|
||
|
||
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')
|
||
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')
|
||
return redirect(url_for('admin.login'))
|
||
|
||
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请求返回错误响应
|
||
"""
|
||
|
||
@wraps(f)
|
||
def decorated_function(*args, **kwargs):
|
||
if not request.is_json:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求必须是JSON格式',
|
||
'code': 'JSON_REQUIRED'
|
||
}), 400
|
||
|
||
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 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 # 默认普通用户
|
||
|
||
# 检查是否是管理员操作
|
||
if 'admin_id' in session:
|
||
user_id = session['admin_id']
|
||
user_type = 2
|
||
elif 'user_id' in session:
|
||
user_id = session['user_id']
|
||
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
|
||
)
|
||
|
||
except Exception as e:
|
||
# 日志记录失败不影响主要功能
|
||
print(f"记录操作日志失败: {str(e)}")
|
||
|
||
return result
|
||
|
||
return decorated_function
|
||
|
||
return decorator
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/email_service.py
|
||
📊 大小: 2485 bytes (2.43 KB)
|
||
🕒 修改时间: 2025-07-03 03:35:36
|
||
🔸==============================================================================
|
||
|
||
from flask import current_app
|
||
from flask_mail import Mail, Message
|
||
from threading import Thread
|
||
|
||
mail = Mail()
|
||
|
||
|
||
def send_async_email(app, msg):
|
||
"""异步发送邮件"""
|
||
with app.app_context():
|
||
try:
|
||
mail.send(msg)
|
||
except Exception as e:
|
||
print(f"邮件发送失败: {e}")
|
||
|
||
|
||
def send_email(to, subject, template, **kwargs):
|
||
"""发送邮件"""
|
||
app = current_app._get_current_object()
|
||
msg = Message(
|
||
subject=subject,
|
||
recipients=[to],
|
||
html=template,
|
||
sender=current_app.config['MAIL_DEFAULT_SENDER']
|
||
)
|
||
|
||
# 异步发送
|
||
thr = Thread(target=send_async_email, args=[app, msg])
|
||
thr.start()
|
||
return thr
|
||
|
||
|
||
def send_verification_email(email, code, code_type):
|
||
"""发送验证码邮件"""
|
||
type_map = {
|
||
1: '注册',
|
||
2: '登录',
|
||
3: '找回密码'
|
||
}
|
||
|
||
subject = f'【太白购物】{type_map.get(code_type, "验证")}验证码'
|
||
|
||
html_template = f"""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>验证码邮件</title>
|
||
</head>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<div style="text-align: center; margin-bottom: 30px;">
|
||
<h2 style="color: #007bff;">太白购物平台</h2>
|
||
</div>
|
||
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
|
||
<h3>您好!</h3>
|
||
<p>您正在进行<strong>{type_map.get(code_type, "验证")}</strong>操作,验证码为:</p>
|
||
<div style="text-align: center; margin: 20px 0;">
|
||
<span style="font-size: 24px; font-weight: bold; color: #007bff; background: #e9ecef; padding: 10px 20px; border-radius: 5px; letter-spacing: 2px;">{code}</span>
|
||
</div>
|
||
<p style="color: #666;">验证码有效期为10分钟,请及时使用。</p>
|
||
<p style="color: #666;">如果这不是您的操作,请忽略此邮件。</p>
|
||
</div>
|
||
|
||
<div style="text-align: center; color: #666; font-size: 12px;">
|
||
<p>此邮件由系统自动发送,请勿回复。</p>
|
||
<p>© 2024 太白购物平台 版权所有</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
return send_email(email, subject, html_template)
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/file_upload.py
|
||
📊 大小: 12684 bytes (12.39 KB)
|
||
🕒 修改时间: 2025-07-03 04:20:25
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
文件上传处理工具
|
||
"""
|
||
import os
|
||
import magic
|
||
from PIL import Image
|
||
from io import BytesIO
|
||
from werkzeug.utils import secure_filename
|
||
from config.cos_config import COSConfig
|
||
from .cos_client import cos_client
|
||
|
||
|
||
class FileUploadHandler:
|
||
"""文件上传处理器"""
|
||
|
||
@staticmethod
|
||
def validate_file(file_obj, file_type='image'):
|
||
"""
|
||
验证文件
|
||
|
||
Args:
|
||
file_obj: 文件对象
|
||
file_type: 文件类型 (image, file)
|
||
|
||
Returns:
|
||
dict: 验证结果
|
||
"""
|
||
if not file_obj or not file_obj.filename:
|
||
return {'valid': False, 'error': '请选择文件'}
|
||
|
||
# 检查文件扩展名
|
||
filename = secure_filename(file_obj.filename)
|
||
if '.' not in filename:
|
||
return {'valid': False, 'error': '文件格式不正确'}
|
||
|
||
file_ext = filename.rsplit('.', 1)[1].lower()
|
||
|
||
if file_type == 'image':
|
||
allowed_extensions = COSConfig.ALLOWED_IMAGE_EXTENSIONS
|
||
max_size = COSConfig.MAX_IMAGE_SIZE
|
||
else:
|
||
allowed_extensions = COSConfig.ALLOWED_FILE_EXTENSIONS
|
||
max_size = COSConfig.MAX_FILE_SIZE
|
||
|
||
if file_ext not in allowed_extensions:
|
||
return {
|
||
'valid': False,
|
||
'error': f'不支持的文件格式,支持格式: {", ".join(allowed_extensions)}'
|
||
}
|
||
|
||
# 检查文件大小
|
||
file_obj.seek(0, 2) # 移动到文件末尾
|
||
file_size = file_obj.tell()
|
||
file_obj.seek(0) # 重置文件指针
|
||
|
||
if file_size > max_size:
|
||
max_size_mb = max_size / (1024 * 1024)
|
||
return {'valid': False, 'error': f'文件大小不能超过 {max_size_mb:.1f}MB'}
|
||
|
||
# 验证文件内容类型(防止恶意文件)
|
||
try:
|
||
file_content = file_obj.read(1024) # 读取前1KB用于检测
|
||
file_obj.seek(0) # 重置文件指针
|
||
|
||
mime_type = magic.from_buffer(file_content, mime=True)
|
||
|
||
if file_type == 'image' and not mime_type.startswith('image/'):
|
||
return {'valid': False, 'error': '文件内容不是有效的图片格式'}
|
||
|
||
except Exception:
|
||
# 如果magic检测失败,继续处理(某些环境可能没有libmagic)
|
||
pass
|
||
|
||
return {'valid': True, 'filename': filename, 'size': file_size}
|
||
|
||
@staticmethod
|
||
def process_image(file_obj, max_width=1200, max_height=1200, quality=None):
|
||
"""
|
||
处理图片(压缩、调整尺寸)
|
||
|
||
Args:
|
||
file_obj: 图片文件对象
|
||
max_width: 最大宽度
|
||
max_height: 最大高度
|
||
quality: 压缩质量
|
||
|
||
Returns:
|
||
BytesIO: 处理后的图片数据
|
||
"""
|
||
try:
|
||
# 打开图片
|
||
image = Image.open(file_obj)
|
||
|
||
# 转换RGBA到RGB(处理PNG透明背景)
|
||
if image.mode in ('RGBA', 'LA', 'P'):
|
||
background = Image.new('RGB', image.size, (255, 255, 255))
|
||
if image.mode == 'P':
|
||
image = image.convert('RGBA')
|
||
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
|
||
image = background
|
||
|
||
# 调整图片尺寸
|
||
if image.width > max_width or image.height > max_height:
|
||
image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||
|
||
# 保存处理后的图片
|
||
output = BytesIO()
|
||
image.save(
|
||
output,
|
||
format='JPEG',
|
||
quality=quality or COSConfig.IMAGE_QUALITY,
|
||
optimize=True
|
||
)
|
||
output.seek(0)
|
||
|
||
return output
|
||
|
||
except Exception as e:
|
||
raise Exception(f"图片处理失败: {str(e)}")
|
||
|
||
@staticmethod
|
||
def upload_image(file_obj, folder_type='temp', process_image=True):
|
||
"""
|
||
上传图片到COS
|
||
|
||
Args:
|
||
file_obj: 图片文件对象
|
||
folder_type: 存储文件夹类型
|
||
process_image: 是否处理图片
|
||
|
||
Returns:
|
||
dict: 上传结果
|
||
"""
|
||
# 验证文件
|
||
validation = FileUploadHandler.validate_file(file_obj, 'image')
|
||
if not validation['valid']:
|
||
return {
|
||
'success': False,
|
||
'error': validation['error'],
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
try:
|
||
# 处理图片
|
||
if process_image:
|
||
processed_file = FileUploadHandler.process_image(file_obj)
|
||
upload_file = processed_file
|
||
else:
|
||
file_obj.seek(0)
|
||
upload_file = file_obj
|
||
|
||
# 上传到COS
|
||
result = cos_client.upload_file(
|
||
upload_file,
|
||
folder_type,
|
||
validation['filename']
|
||
)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': f"上传失败: {str(e)}",
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
@staticmethod
|
||
def upload_file(file_obj, folder_type='temp'):
|
||
"""
|
||
上传普通文件到COS
|
||
|
||
Args:
|
||
file_obj: 文件对象
|
||
folder_type: 存储文件夹类型
|
||
|
||
Returns:
|
||
dict: 上传结果
|
||
"""
|
||
# 验证文件
|
||
validation = FileUploadHandler.validate_file(file_obj, 'file')
|
||
if not validation['valid']:
|
||
return {
|
||
'success': False,
|
||
'error': validation['error'],
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
try:
|
||
file_obj.seek(0)
|
||
|
||
# 上传到COS
|
||
result = cos_client.upload_file(
|
||
file_obj,
|
||
folder_type,
|
||
validation['filename']
|
||
)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': f"上传失败: {str(e)}",
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
|
||
# 创建全局文件上传处理器实例
|
||
file_upload_handler = FileUploadHandler()
|
||
"""
|
||
文件上传处理工具
|
||
"""
|
||
import os
|
||
import magic
|
||
from PIL import Image
|
||
from io import BytesIO
|
||
from werkzeug.utils import secure_filename
|
||
from config.cos_config import COSConfig
|
||
from .cos_client import cos_client
|
||
|
||
|
||
class FileUploadHandler:
|
||
"""文件上传处理器"""
|
||
|
||
@staticmethod
|
||
def validate_file(file_obj, file_type='image'):
|
||
"""
|
||
验证文件
|
||
|
||
Args:
|
||
file_obj: 文件对象
|
||
file_type: 文件类型 (image, file)
|
||
|
||
Returns:
|
||
dict: 验证结果
|
||
"""
|
||
if not file_obj or not file_obj.filename:
|
||
return {'valid': False, 'error': '请选择文件'}
|
||
|
||
# 检查文件扩展名
|
||
filename = secure_filename(file_obj.filename)
|
||
if '.' not in filename:
|
||
return {'valid': False, 'error': '文件格式不正确'}
|
||
|
||
file_ext = filename.rsplit('.', 1)[1].lower()
|
||
|
||
if file_type == 'image':
|
||
allowed_extensions = COSConfig.ALLOWED_IMAGE_EXTENSIONS
|
||
max_size = COSConfig.MAX_IMAGE_SIZE
|
||
else:
|
||
allowed_extensions = COSConfig.ALLOWED_FILE_EXTENSIONS
|
||
max_size = COSConfig.MAX_FILE_SIZE
|
||
|
||
if file_ext not in allowed_extensions:
|
||
return {
|
||
'valid': False,
|
||
'error': f'不支持的文件格式,支持格式: {", ".join(allowed_extensions)}'
|
||
}
|
||
|
||
# 检查文件大小
|
||
file_obj.seek(0, 2) # 移动到文件末尾
|
||
file_size = file_obj.tell()
|
||
file_obj.seek(0) # 重置文件指针
|
||
|
||
if file_size > max_size:
|
||
max_size_mb = max_size / (1024 * 1024)
|
||
return {'valid': False, 'error': f'文件大小不能超过 {max_size_mb:.1f}MB'}
|
||
|
||
# 验证文件内容类型(防止恶意文件)
|
||
try:
|
||
file_content = file_obj.read(1024) # 读取前1KB用于检测
|
||
file_obj.seek(0) # 重置文件指针
|
||
|
||
mime_type = magic.from_buffer(file_content, mime=True)
|
||
|
||
if file_type == 'image' and not mime_type.startswith('image/'):
|
||
return {'valid': False, 'error': '文件内容不是有效的图片格式'}
|
||
|
||
except Exception:
|
||
# 如果magic检测失败,继续处理(某些环境可能没有libmagic)
|
||
pass
|
||
|
||
return {'valid': True, 'filename': filename, 'size': file_size}
|
||
|
||
@staticmethod
|
||
def process_image(file_obj, max_width=1200, max_height=1200, quality=None):
|
||
"""
|
||
处理图片(压缩、调整尺寸)
|
||
|
||
Args:
|
||
file_obj: 图片文件对象
|
||
max_width: 最大宽度
|
||
max_height: 最大高度
|
||
quality: 压缩质量
|
||
|
||
Returns:
|
||
BytesIO: 处理后的图片数据
|
||
"""
|
||
try:
|
||
# 打开图片
|
||
image = Image.open(file_obj)
|
||
|
||
# 转换RGBA到RGB(处理PNG透明背景)
|
||
if image.mode in ('RGBA', 'LA', 'P'):
|
||
background = Image.new('RGB', image.size, (255, 255, 255))
|
||
if image.mode == 'P':
|
||
image = image.convert('RGBA')
|
||
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
|
||
image = background
|
||
|
||
# 调整图片尺寸
|
||
if image.width > max_width or image.height > max_height:
|
||
image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||
|
||
# 保存处理后的图片
|
||
output = BytesIO()
|
||
image.save(
|
||
output,
|
||
format='JPEG',
|
||
quality=quality or COSConfig.IMAGE_QUALITY,
|
||
optimize=True
|
||
)
|
||
output.seek(0)
|
||
|
||
return output
|
||
|
||
except Exception as e:
|
||
raise Exception(f"图片处理失败: {str(e)}")
|
||
|
||
@staticmethod
|
||
def upload_image(file_obj, folder_type='temp', process_image=True):
|
||
"""
|
||
上传图片到COS
|
||
|
||
Args:
|
||
file_obj: 图片文件对象
|
||
folder_type: 存储文件夹类型
|
||
process_image: 是否处理图片
|
||
|
||
Returns:
|
||
dict: 上传结果
|
||
"""
|
||
# 验证文件
|
||
validation = FileUploadHandler.validate_file(file_obj, 'image')
|
||
if not validation['valid']:
|
||
return {
|
||
'success': False,
|
||
'error': validation['error'],
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
try:
|
||
# 处理图片
|
||
if process_image:
|
||
processed_file = FileUploadHandler.process_image(file_obj)
|
||
upload_file = processed_file
|
||
else:
|
||
file_obj.seek(0)
|
||
upload_file = file_obj
|
||
|
||
# 上传到COS
|
||
result = cos_client.upload_file(
|
||
upload_file,
|
||
folder_type,
|
||
validation['filename']
|
||
)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': f"上传失败: {str(e)}",
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
@staticmethod
|
||
def upload_file(file_obj, folder_type='temp'):
|
||
"""
|
||
上传普通文件到COS
|
||
|
||
Args:
|
||
file_obj: 文件对象
|
||
folder_type: 存储文件夹类型
|
||
|
||
Returns:
|
||
dict: 上传结果
|
||
"""
|
||
# 验证文件
|
||
validation = FileUploadHandler.validate_file(file_obj, 'file')
|
||
if not validation['valid']:
|
||
return {
|
||
'success': False,
|
||
'error': validation['error'],
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
try:
|
||
file_obj.seek(0)
|
||
|
||
# 上传到COS
|
||
result = cos_client.upload_file(
|
||
file_obj,
|
||
folder_type,
|
||
validation['filename']
|
||
)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': f"上传失败: {str(e)}",
|
||
'url': None,
|
||
'file_key': None
|
||
}
|
||
|
||
|
||
# 创建全局文件上传处理器实例
|
||
file_upload_handler = FileUploadHandler()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/helpers.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/sms.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/utils/wechat_pay.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/__init__.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/address.py
|
||
📊 大小: 8172 bytes (7.98 KB)
|
||
🕒 修改时间: 2025-07-04 03:10:28
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
地址管理视图
|
||
"""
|
||
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash
|
||
from app.models.address import UserAddress
|
||
from app.models.user import User
|
||
from app.forms import AddressForm
|
||
from app.utils.decorators import login_required
|
||
from config.database import db
|
||
|
||
address_bp = Blueprint('address', __name__, url_prefix='/address')
|
||
|
||
|
||
@address_bp.route('/')
|
||
@login_required
|
||
def index():
|
||
"""地址管理页面"""
|
||
user_id = session['user_id']
|
||
addresses = UserAddress.get_user_addresses(user_id)
|
||
return render_template('user/addresses.html', addresses=addresses)
|
||
|
||
|
||
@address_bp.route('/add', methods=['GET', 'POST'])
|
||
@login_required
|
||
def add():
|
||
"""添加地址"""
|
||
form = AddressForm()
|
||
|
||
if request.method == 'POST':
|
||
# 手动验证必填字段
|
||
if not all([
|
||
form.receiver_name.data,
|
||
form.receiver_phone.data,
|
||
form.province.data,
|
||
form.city.data,
|
||
form.district.data,
|
||
form.detail_address.data
|
||
]):
|
||
flash('请填写所有必填信息', 'error')
|
||
return render_template('user/address_form.html', form=form, action='add')
|
||
|
||
# 验证手机号格式
|
||
import re
|
||
if not re.match(r'^1[3-9]\d{9}$', form.receiver_phone.data):
|
||
flash('请输入有效的手机号', 'error')
|
||
return render_template('user/address_form.html', form=form, action='add')
|
||
|
||
try:
|
||
user_id = session['user_id']
|
||
|
||
# 如果是第一个地址或设为默认,处理默认地址
|
||
if form.is_default.data or not UserAddress.query.filter_by(user_id=user_id).first():
|
||
UserAddress.query.filter_by(user_id=user_id).update({'is_default': 0})
|
||
is_default = 1
|
||
else:
|
||
is_default = 0
|
||
|
||
address = UserAddress(
|
||
user_id=user_id,
|
||
receiver_name=form.receiver_name.data.strip(),
|
||
receiver_phone=form.receiver_phone.data.strip(),
|
||
province=form.province.data.strip(),
|
||
city=form.city.data.strip(),
|
||
district=form.district.data.strip(),
|
||
detail_address=form.detail_address.data.strip(),
|
||
postal_code=form.postal_code.data.strip() if form.postal_code.data else None,
|
||
is_default=is_default
|
||
)
|
||
|
||
db.session.add(address)
|
||
db.session.commit()
|
||
|
||
flash('地址添加成功', 'success')
|
||
return redirect(url_for('address.index'))
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(f'添加失败: {str(e)}', 'error')
|
||
|
||
return render_template('user/address_form.html', form=form, action='add')
|
||
|
||
|
||
@address_bp.route('/edit/<int:address_id>', methods=['GET', 'POST'])
|
||
@login_required
|
||
def edit(address_id):
|
||
"""编辑地址"""
|
||
user_id = session['user_id']
|
||
address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first_or_404()
|
||
|
||
form = AddressForm()
|
||
|
||
if request.method == 'GET':
|
||
# 预填充表单数据
|
||
form.receiver_name.data = address.receiver_name
|
||
form.receiver_phone.data = address.receiver_phone
|
||
form.province.data = address.province
|
||
form.city.data = address.city
|
||
form.district.data = address.district
|
||
form.detail_address.data = address.detail_address
|
||
form.postal_code.data = address.postal_code
|
||
form.is_default.data = bool(address.is_default)
|
||
|
||
elif request.method == 'POST':
|
||
# 手动验证必填字段
|
||
if not all([
|
||
form.receiver_name.data,
|
||
form.receiver_phone.data,
|
||
form.province.data,
|
||
form.city.data,
|
||
form.district.data,
|
||
form.detail_address.data
|
||
]):
|
||
flash('请填写所有必填信息', 'error')
|
||
return render_template('user/address_form.html', form=form, action='edit', address=address)
|
||
|
||
# 验证手机号格式
|
||
import re
|
||
if not re.match(r'^1[3-9]\d{9}$', form.receiver_phone.data):
|
||
flash('请输入有效的手机号', 'error')
|
||
return render_template('user/address_form.html', form=form, action='edit', address=address)
|
||
|
||
try:
|
||
# 如果设为默认地址,先取消其他默认地址
|
||
if form.is_default.data and not address.is_default:
|
||
UserAddress.query.filter_by(user_id=user_id).update({'is_default': 0})
|
||
address.is_default = 1
|
||
elif not form.is_default.data and address.is_default:
|
||
# 如果取消当前默认地址,需要检查是否还有其他地址
|
||
other_addresses = UserAddress.query.filter(
|
||
UserAddress.user_id == user_id,
|
||
UserAddress.id != address_id
|
||
).first()
|
||
if other_addresses:
|
||
address.is_default = 0
|
||
else:
|
||
flash('至少需要保留一个默认地址', 'warning')
|
||
return render_template('user/address_form.html', form=form, action='edit', address=address)
|
||
|
||
# 更新地址信息
|
||
address.receiver_name = form.receiver_name.data.strip()
|
||
address.receiver_phone = form.receiver_phone.data.strip()
|
||
address.province = form.province.data.strip()
|
||
address.city = form.city.data.strip()
|
||
address.district = form.district.data.strip()
|
||
address.detail_address = form.detail_address.data.strip()
|
||
address.postal_code = form.postal_code.data.strip() if form.postal_code.data else None
|
||
|
||
db.session.commit()
|
||
flash('地址更新成功', 'success')
|
||
return redirect(url_for('address.index'))
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(f'更新失败: {str(e)}', 'error')
|
||
|
||
return render_template('user/address_form.html', form=form, action='edit', address=address)
|
||
|
||
|
||
@address_bp.route('/delete/<int:address_id>', methods=['POST'])
|
||
@login_required
|
||
def delete(address_id):
|
||
"""删除地址"""
|
||
try:
|
||
user_id = session['user_id']
|
||
address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first()
|
||
|
||
if not address:
|
||
return jsonify({'success': False, 'message': '地址不存在'})
|
||
|
||
# 检查是否是唯一地址
|
||
address_count = UserAddress.query.filter_by(user_id=user_id).count()
|
||
if address_count <= 1:
|
||
return jsonify({'success': False, 'message': '至少需要保留一个地址'})
|
||
|
||
# 如果删除的是默认地址,需要设置新的默认地址
|
||
if address.is_default:
|
||
other_address = UserAddress.query.filter(
|
||
UserAddress.user_id == user_id,
|
||
UserAddress.id != address_id
|
||
).first()
|
||
if other_address:
|
||
other_address.is_default = 1
|
||
|
||
db.session.delete(address)
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '地址删除成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
|
||
|
||
|
||
@address_bp.route('/set_default/<int:address_id>', methods=['POST'])
|
||
@login_required
|
||
def set_default(address_id):
|
||
"""设置默认地址"""
|
||
try:
|
||
user_id = session['user_id']
|
||
success = UserAddress.set_default_address(user_id, address_id)
|
||
|
||
if success:
|
||
return jsonify({'success': True, 'message': '默认地址设置成功'})
|
||
else:
|
||
return jsonify({'success': False, 'message': '地址不存在'})
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': f'设置失败: {str(e)}'})
|
||
|
||
|
||
@address_bp.route('/api/list')
|
||
@login_required
|
||
def api_list():
|
||
"""获取用户地址列表API"""
|
||
user_id = session['user_id']
|
||
addresses = UserAddress.get_user_addresses(user_id)
|
||
return jsonify({
|
||
'success': True,
|
||
'addresses': [addr.to_dict() for addr in addresses]
|
||
})
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/admin.py
|
||
📊 大小: 7865 bytes (7.68 KB)
|
||
🕒 修改时间: 2025-07-03 05:56:57
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
管理员视图
|
||
"""
|
||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify, g
|
||
from werkzeug.security import generate_password_hash
|
||
from app.models.admin import AdminUser
|
||
from app.models.user import User
|
||
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
|
||
|
||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||
|
||
|
||
@admin_bp.route('/login', methods=['GET', 'POST'])
|
||
def login():
|
||
"""管理员登录"""
|
||
if request.method == 'POST':
|
||
username = request.form.get('username', '').strip()
|
||
password = request.form.get('password', '').strip()
|
||
|
||
if not username or not password:
|
||
flash('请输入用户名和密码', 'error')
|
||
return render_template('admin/login.html')
|
||
|
||
# 查找管理员
|
||
admin = AdminUser.query.filter_by(username=username).first()
|
||
|
||
if not admin or not admin.check_password(password):
|
||
flash('用户名或密码错误', 'error')
|
||
return render_template('admin/login.html')
|
||
|
||
if admin.status != 1:
|
||
flash('账号已被禁用,请联系系统管理员', 'error')
|
||
return render_template('admin/login.html')
|
||
|
||
# 登录成功
|
||
session['admin_id'] = admin.id
|
||
session['admin_username'] = admin.username
|
||
|
||
# 更新最后登录时间
|
||
admin.update_last_login()
|
||
|
||
# 记录登录日志
|
||
try:
|
||
OperationLog.create_log(
|
||
user_id=admin.id,
|
||
user_type=2,
|
||
action='管理员登录',
|
||
ip_address=request.remote_addr,
|
||
user_agent=request.headers.get('User-Agent')
|
||
)
|
||
except Exception as e:
|
||
print(f"记录登录日志失败: {str(e)}")
|
||
|
||
flash('登录成功', 'success')
|
||
return redirect(url_for('admin.dashboard'))
|
||
|
||
return render_template('admin/login.html')
|
||
|
||
|
||
@admin_bp.route('/logout')
|
||
@admin_required
|
||
@log_operation('管理员登出')
|
||
def logout():
|
||
"""管理员登出"""
|
||
session.pop('admin_id', None)
|
||
session.pop('admin_username', None)
|
||
flash('已安全退出', 'info')
|
||
return redirect(url_for('admin.login'))
|
||
|
||
|
||
@admin_bp.route('/dashboard')
|
||
@admin_required
|
||
def dashboard():
|
||
"""管理员仪表板"""
|
||
try:
|
||
# 获取统计数据
|
||
stats = {
|
||
'total_users': User.query.count(),
|
||
'active_users': User.query.filter_by(status=1).count(),
|
||
'total_admins': AdminUser.query.count(),
|
||
'recent_logs_count': OperationLog.query.filter(
|
||
OperationLog.created_at >= datetime.now() - timedelta(days=7)
|
||
).count()
|
||
}
|
||
|
||
# 获取最近的操作日志
|
||
recent_logs = OperationLog.query.order_by(
|
||
OperationLog.created_at.desc()
|
||
).limit(10).all()
|
||
|
||
# 用户注册趋势(最近7天)
|
||
user_trend = []
|
||
for i in range(6, -1, -1):
|
||
date = datetime.now() - timedelta(days=i)
|
||
date_start = date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
date_end = date_start + timedelta(days=1)
|
||
|
||
count = User.query.filter(
|
||
User.created_at >= date_start,
|
||
User.created_at < date_end
|
||
).count()
|
||
|
||
user_trend.append({
|
||
'date': date.strftime('%m-%d'),
|
||
'count': count
|
||
})
|
||
|
||
return render_template('admin/dashboard.html',
|
||
stats=stats,
|
||
recent_logs=recent_logs,
|
||
user_trend=user_trend)
|
||
|
||
except Exception as e:
|
||
flash(f'加载仪表板数据失败: {str(e)}', 'error')
|
||
return render_template('admin/dashboard.html',
|
||
stats={},
|
||
recent_logs=[],
|
||
user_trend=[])
|
||
|
||
|
||
@admin_bp.route('/profile')
|
||
@admin_required
|
||
def profile():
|
||
"""管理员个人资料"""
|
||
return render_template('admin/profile.html', admin=g.current_admin)
|
||
|
||
|
||
@admin_bp.route('/profile/edit', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('修改管理员资料')
|
||
def edit_profile():
|
||
"""编辑管理员个人资料"""
|
||
try:
|
||
real_name = request.form.get('real_name', '').strip()
|
||
email = request.form.get('email', '').strip()
|
||
phone = request.form.get('phone', '').strip()
|
||
|
||
# 更新信息
|
||
if real_name:
|
||
g.current_admin.real_name = real_name
|
||
if email:
|
||
g.current_admin.email = email
|
||
if phone:
|
||
g.current_admin.phone = phone
|
||
|
||
db.session.commit()
|
||
flash('个人资料更新成功', 'success')
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(f'更新失败: {str(e)}', 'error')
|
||
|
||
return redirect(url_for('admin.profile'))
|
||
|
||
|
||
@admin_bp.route('/change-password', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('修改管理员密码')
|
||
def change_password():
|
||
"""修改管理员密码"""
|
||
try:
|
||
current_password = request.form.get('current_password', '').strip()
|
||
new_password = request.form.get('new_password', '').strip()
|
||
confirm_password = request.form.get('confirm_password', '').strip()
|
||
|
||
# 验证当前密码
|
||
if not g.current_admin.check_password(current_password):
|
||
flash('当前密码错误', 'error')
|
||
return redirect(url_for('admin.profile'))
|
||
|
||
# 验证新密码
|
||
if len(new_password) < 6:
|
||
flash('新密码长度至少6位', 'error')
|
||
return redirect(url_for('admin.profile'))
|
||
|
||
if new_password != confirm_password:
|
||
flash('新密码和确认密码不一致', 'error')
|
||
return redirect(url_for('admin.profile'))
|
||
|
||
# 更新密码
|
||
g.current_admin.set_password(new_password)
|
||
db.session.commit()
|
||
|
||
flash('密码修改成功', 'success')
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(f'密码修改失败: {str(e)}', 'error')
|
||
|
||
return redirect(url_for('admin.profile'))
|
||
|
||
|
||
@admin_bp.route('/users')
|
||
@admin_required
|
||
def users():
|
||
"""用户管理"""
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = 20
|
||
|
||
query = User.query.order_by(User.created_at.desc())
|
||
|
||
# 搜索功能
|
||
search = request.args.get('search', '').strip()
|
||
if search:
|
||
query = query.filter(
|
||
db.or_(
|
||
User.username.like(f'%{search}%'),
|
||
User.email.like(f'%{search}%'),
|
||
User.phone.like(f'%{search}%'),
|
||
User.nickname.like(f'%{search}%')
|
||
)
|
||
)
|
||
|
||
# 状态筛选
|
||
status = request.args.get('status', '', type=str)
|
||
if status:
|
||
query = query.filter(User.status == int(status))
|
||
|
||
users = query.paginate(page=page, per_page=per_page, error_out=False)
|
||
|
||
return render_template('admin/users.html', users=users, search=search, status=status)
|
||
|
||
|
||
@admin_bp.route('/logs')
|
||
@admin_required
|
||
def logs():
|
||
"""操作日志"""
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = 50
|
||
|
||
query = OperationLog.query.order_by(OperationLog.created_at.desc())
|
||
|
||
# 用户类型筛选
|
||
user_type = request.args.get('user_type', '', type=str)
|
||
if user_type:
|
||
query = query.filter(OperationLog.user_type == int(user_type))
|
||
|
||
# 操作类型筛选
|
||
action = request.args.get('action', '').strip()
|
||
if action:
|
||
query = query.filter(OperationLog.action.like(f'%{action}%'))
|
||
|
||
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)
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/auth.py
|
||
📊 大小: 4911 bytes (4.80 KB)
|
||
🕒 修改时间: 2025-07-03 03:42:09
|
||
🔸==============================================================================
|
||
|
||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
|
||
from app.forms import LoginForm, RegisterForm
|
||
from app.models.user import User
|
||
from app.models.verification import EmailVerification
|
||
from app.utils.email_service import send_verification_email
|
||
from config.database import db
|
||
|
||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||
|
||
|
||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||
def login():
|
||
"""用户登录"""
|
||
if 'user_id' in session:
|
||
return redirect(url_for('main.index'))
|
||
|
||
form = LoginForm()
|
||
if form.validate_on_submit():
|
||
username = form.username.data
|
||
password = form.password.data
|
||
|
||
# 支持用户名、手机号、邮箱登录
|
||
user = User.query.filter(
|
||
(User.username == username) |
|
||
(User.phone == username) |
|
||
(User.email == username)
|
||
).first()
|
||
|
||
if user and user.check_password(password):
|
||
if user.status == 0:
|
||
flash('账户已被禁用,请联系管理员', 'error')
|
||
return render_template('user/login.html', form=form)
|
||
|
||
# 登录成功,设置session
|
||
session['user_id'] = user.id
|
||
session['username'] = user.username
|
||
session['nickname'] = user.nickname or user.username
|
||
session.permanent = form.remember_me.data
|
||
|
||
flash(f'欢迎回来,{user.nickname or user.username}!', 'success')
|
||
|
||
# 获取登录前的页面
|
||
next_page = request.args.get('next')
|
||
if next_page:
|
||
return redirect(next_page)
|
||
return redirect(url_for('main.index'))
|
||
else:
|
||
flash('用户名或密码错误', 'error')
|
||
|
||
return render_template('user/login.html', form=form)
|
||
|
||
|
||
@auth_bp.route('/send_email_code', methods=['POST'])
|
||
def send_email_code():
|
||
"""发送邮箱验证码"""
|
||
try:
|
||
data = request.get_json()
|
||
email = data.get('email')
|
||
code_type = data.get('type', 1) # 默认为注册类型
|
||
|
||
if not email:
|
||
return jsonify({'success': False, 'message': '邮箱地址不能为空'})
|
||
|
||
# 检查邮箱格式
|
||
import re
|
||
email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
|
||
if not re.match(email_pattern, email):
|
||
return jsonify({'success': False, 'message': '邮箱格式不正确'})
|
||
|
||
# 如果是注册,检查邮箱是否已被注册
|
||
if code_type == 1:
|
||
existing_user = User.query.filter_by(email=email).first()
|
||
if existing_user:
|
||
return jsonify({'success': False, 'message': '该邮箱已被注册'})
|
||
|
||
# 检查是否频繁发送(1分钟内只能发送一次)
|
||
from datetime import datetime, timedelta
|
||
recent_code = EmailVerification.query.filter_by(
|
||
email=email,
|
||
type=code_type
|
||
).filter(
|
||
EmailVerification.created_at > datetime.utcnow() - timedelta(minutes=1)
|
||
).first()
|
||
|
||
if recent_code:
|
||
return jsonify({'success': False, 'message': '发送过于频繁,请稍后再试'})
|
||
|
||
# 创建验证码
|
||
verification = EmailVerification.create_verification(email, code_type)
|
||
|
||
# 发送邮件
|
||
send_verification_email(email, verification.code, code_type)
|
||
|
||
return jsonify({'success': True, 'message': '验证码已发送'})
|
||
|
||
except Exception as e:
|
||
print(f"发送邮箱验证码错误: {e}")
|
||
return jsonify({'success': False, 'message': '发送失败,请重试'})
|
||
|
||
|
||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||
def register():
|
||
"""用户注册"""
|
||
if 'user_id' in session:
|
||
return redirect(url_for('main.index'))
|
||
|
||
form = RegisterForm()
|
||
if form.validate_on_submit():
|
||
try:
|
||
# 验证邮箱验证码
|
||
if not EmailVerification.verify_code(form.email.data, form.email_code.data, 1):
|
||
flash('邮箱验证码错误或已过期', 'error')
|
||
return render_template('user/register.html', form=form)
|
||
|
||
user = User(
|
||
username=form.username.data,
|
||
email=form.email.data,
|
||
phone=form.phone.data,
|
||
nickname=form.username.data
|
||
)
|
||
user.set_password(form.password.data)
|
||
|
||
db.session.add(user)
|
||
db.session.commit()
|
||
|
||
flash('注册成功!请登录', 'success')
|
||
return redirect(url_for('auth.login'))
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash('注册失败,请重试', 'error')
|
||
print(f"注册错误: {e}")
|
||
|
||
return render_template('user/register.html', form=form)
|
||
|
||
|
||
@auth_bp.route('/logout')
|
||
def logout():
|
||
"""用户登出"""
|
||
session.clear()
|
||
flash('您已成功登出', 'info')
|
||
return redirect(url_for('main.index'))
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/cart.py
|
||
📊 大小: 7602 bytes (7.42 KB)
|
||
🕒 修改时间: 2025-07-04 02:47:10
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
购物车视图
|
||
"""
|
||
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash
|
||
from app.models.cart import Cart
|
||
from app.models.product import Product, ProductInventory
|
||
from app.models.user import User
|
||
from app.utils.decorators import login_required
|
||
from config.database import db
|
||
|
||
cart_bp = Blueprint('cart', __name__, url_prefix='/cart')
|
||
|
||
|
||
@cart_bp.route('/')
|
||
@login_required
|
||
def index():
|
||
"""购物车页面"""
|
||
user_id = session['user_id']
|
||
cart_items = Cart.get_user_cart(user_id)
|
||
|
||
# 计算总价和可用商品数量
|
||
total_price = 0
|
||
available_count = 0
|
||
|
||
for item in cart_items:
|
||
if item.is_available():
|
||
total_price += item.get_total_price()
|
||
available_count += 1
|
||
|
||
return render_template('cart/index.html',
|
||
cart_items=cart_items,
|
||
total_price=total_price,
|
||
available_count=available_count)
|
||
|
||
|
||
@cart_bp.route('/add', methods=['POST'])
|
||
@login_required
|
||
def add():
|
||
"""添加商品到购物车"""
|
||
try:
|
||
user_id = session['user_id']
|
||
product_id = request.json.get('product_id')
|
||
sku_code = request.json.get('sku_code')
|
||
spec_combination = request.json.get('spec_combination', '')
|
||
quantity = request.json.get('quantity', 1)
|
||
|
||
# 验证参数
|
||
if not product_id or quantity <= 0:
|
||
return jsonify({'success': False, 'message': '参数错误'})
|
||
|
||
# 检查商品是否存在且上架
|
||
product = Product.query.filter_by(id=product_id, status=1).first()
|
||
if not product:
|
||
return jsonify({'success': False, 'message': '商品不存在或已下架'})
|
||
|
||
# 检查库存
|
||
if sku_code:
|
||
sku_info = ProductInventory.query.filter_by(sku_code=sku_code).first()
|
||
if not sku_info:
|
||
return jsonify({'success': False, 'message': 'SKU不存在'})
|
||
|
||
if sku_info.stock < quantity:
|
||
return jsonify({'success': False, 'message': f'库存不足,仅剩{sku_info.stock}件'})
|
||
else:
|
||
# 如果没有指定SKU,检查默认库存
|
||
default_sku = ProductInventory.query.filter_by(
|
||
product_id=product_id,
|
||
is_default=1
|
||
).first()
|
||
if default_sku and default_sku.stock < quantity:
|
||
return jsonify({'success': False, 'message': f'库存不足,仅剩{default_sku.stock}件'})
|
||
|
||
# 添加到购物车
|
||
Cart.add_to_cart(
|
||
user_id=user_id,
|
||
product_id=product_id,
|
||
sku_code=sku_code,
|
||
spec_combination=spec_combination,
|
||
quantity=quantity
|
||
)
|
||
|
||
# 获取购物车数量
|
||
cart_count = Cart.get_cart_count(user_id)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '已添加到购物车',
|
||
'cart_count': cart_count
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'添加失败: {str(e)}'})
|
||
|
||
|
||
@cart_bp.route('/update', methods=['POST'])
|
||
@login_required
|
||
def update():
|
||
"""更新购物车商品数量"""
|
||
try:
|
||
user_id = session['user_id']
|
||
cart_id = request.json.get('cart_id')
|
||
quantity = request.json.get('quantity')
|
||
|
||
if not cart_id or quantity is None or quantity < 0:
|
||
return jsonify({'success': False, 'message': '参数错误'})
|
||
|
||
# 获取购物车项目
|
||
cart_item = Cart.query.filter_by(id=cart_id, user_id=user_id).first()
|
||
if not cart_item:
|
||
return jsonify({'success': False, 'message': '购物车项目不存在'})
|
||
|
||
if quantity == 0:
|
||
# 删除商品
|
||
db.session.delete(cart_item)
|
||
else:
|
||
# 检查库存
|
||
if cart_item.get_stock() < quantity:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'库存不足,仅剩{cart_item.get_stock()}件'
|
||
})
|
||
|
||
# 更新数量
|
||
cart_item.quantity = quantity
|
||
cart_item.updated_at = db.func.now()
|
||
|
||
db.session.commit()
|
||
|
||
# 返回更新后的信息
|
||
cart_count = Cart.get_cart_count(user_id)
|
||
total_price = Cart.get_cart_total(user_id)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '更新成功',
|
||
'cart_count': cart_count,
|
||
'total_price': total_price,
|
||
'item_total': cart_item.get_total_price() if quantity > 0 else 0
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'更新失败: {str(e)}'})
|
||
|
||
|
||
@cart_bp.route('/remove', methods=['POST'])
|
||
@login_required
|
||
def remove():
|
||
"""删除购物车商品"""
|
||
try:
|
||
user_id = session['user_id']
|
||
cart_id = request.json.get('cart_id')
|
||
|
||
if not cart_id:
|
||
return jsonify({'success': False, 'message': '参数错误'})
|
||
|
||
# 获取购物车项目
|
||
cart_item = Cart.query.filter_by(id=cart_id, user_id=user_id).first()
|
||
if not cart_item:
|
||
return jsonify({'success': False, 'message': '购物车项目不存在'})
|
||
|
||
db.session.delete(cart_item)
|
||
db.session.commit()
|
||
|
||
# 返回更新后的信息
|
||
cart_count = Cart.get_cart_count(user_id)
|
||
total_price = Cart.get_cart_total(user_id)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '删除成功',
|
||
'cart_count': cart_count,
|
||
'total_price': total_price
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
|
||
|
||
|
||
@cart_bp.route('/clear', methods=['POST'])
|
||
@login_required
|
||
def clear():
|
||
"""清空购物车"""
|
||
try:
|
||
user_id = session['user_id']
|
||
|
||
Cart.query.filter_by(user_id=user_id).delete()
|
||
db.session.commit()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '购物车已清空'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'清空失败: {str(e)}'})
|
||
|
||
|
||
@cart_bp.route('/count')
|
||
@login_required
|
||
def count():
|
||
"""获取购物车商品数量"""
|
||
user_id = session['user_id']
|
||
cart_count = Cart.get_cart_count(user_id)
|
||
return jsonify({'cart_count': cart_count})
|
||
|
||
|
||
@cart_bp.route('/checkout')
|
||
@login_required
|
||
def checkout():
|
||
"""去结算"""
|
||
user_id = session['user_id']
|
||
selected_items = request.args.getlist('items')
|
||
|
||
if not selected_items:
|
||
flash('请选择要购买的商品', 'error')
|
||
return redirect(url_for('cart.index'))
|
||
|
||
# 获取选中的购物车项目
|
||
cart_items = Cart.query.filter(
|
||
Cart.id.in_(selected_items),
|
||
Cart.user_id == user_id
|
||
).all()
|
||
|
||
if not cart_items:
|
||
flash('选中的商品不存在', 'error')
|
||
return redirect(url_for('cart.index'))
|
||
|
||
# 检查商品可用性
|
||
unavailable_items = []
|
||
for item in cart_items:
|
||
if not item.is_available():
|
||
unavailable_items.append(item.product.name)
|
||
|
||
if unavailable_items:
|
||
flash(f'以下商品库存不足或已下架:{", ".join(unavailable_items)}', 'error')
|
||
return redirect(url_for('cart.index'))
|
||
|
||
# 跳转到订单结算页面
|
||
items_param = '&'.join([f'items={item_id}' for item_id in selected_items])
|
||
return redirect(url_for('order.checkout') + '?' + items_param)
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/main.py
|
||
📊 大小: 6561 bytes (6.41 KB)
|
||
🕒 修改时间: 2025-07-03 15:30:57
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
主页面视图
|
||
"""
|
||
from flask import Blueprint, render_template, session, current_app, request, redirect, url_for
|
||
from app.models.user import User
|
||
from app.models.product import Product, Category
|
||
from sqlalchemy import func
|
||
|
||
main_bp = Blueprint('main', __name__)
|
||
|
||
|
||
@main_bp.route('/')
|
||
def index():
|
||
"""首页"""
|
||
user = None
|
||
if 'user_id' in session:
|
||
try:
|
||
user = User.query.get(session['user_id'])
|
||
if user and user.status != 1:
|
||
# 用户被禁用,清除session
|
||
session.pop('user_id', None)
|
||
user = None
|
||
except Exception as e:
|
||
current_app.logger.error(f"获取用户信息失败: {str(e)}")
|
||
session.pop('user_id', None)
|
||
user = None
|
||
|
||
# 获取热门商品(按销量排序,取前8个)
|
||
hot_products = Product.query.filter_by(status=1)\
|
||
.order_by(Product.sales_count.desc())\
|
||
.limit(8).all()
|
||
|
||
# 获取最新商品(按创建时间排序,取前8个)
|
||
new_products = Product.query.filter_by(status=1)\
|
||
.order_by(Product.created_at.desc())\
|
||
.limit(8).all()
|
||
|
||
# 获取活跃的顶级分类(用于导航)
|
||
top_categories = Category.query.filter_by(is_active=1, parent_id=0)\
|
||
.order_by(Category.sort_order)\
|
||
.limit(6).all()
|
||
|
||
return render_template('index.html',
|
||
user=user,
|
||
hot_products=hot_products,
|
||
new_products=new_products,
|
||
top_categories=top_categories)
|
||
|
||
|
||
@main_bp.route('/products')
|
||
def product_list():
|
||
"""商品列表页面"""
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = 20
|
||
|
||
# 基础查询:只显示上架商品
|
||
query = Product.query.filter_by(status=1)
|
||
|
||
# 分类筛选
|
||
category_id = request.args.get('category_id', type=int)
|
||
if category_id:
|
||
# 获取该分类及其所有子分类的商品
|
||
category = Category.query.get_or_404(category_id)
|
||
if category.level == 1: # 一级分类,查找所有子分类
|
||
subcategory_ids = [c.id for c in Category.query.filter_by(parent_id=category_id).all()]
|
||
subcategory_ids.append(category_id)
|
||
query = query.filter(Product.category_id.in_(subcategory_ids))
|
||
else:
|
||
query = query.filter_by(category_id=category_id)
|
||
|
||
# 搜索功能
|
||
search = request.args.get('search', '').strip()
|
||
if search:
|
||
query = query.filter(Product.name.like(f'%{search}%'))
|
||
|
||
# 价格筛选
|
||
min_price = request.args.get('min_price', type=float)
|
||
max_price = request.args.get('max_price', type=float)
|
||
if min_price is not None:
|
||
query = query.filter(Product.price >= min_price)
|
||
if max_price is not None:
|
||
query = query.filter(Product.price <= max_price)
|
||
|
||
# 排序
|
||
sort = request.args.get('sort', 'default')
|
||
if sort == 'price_asc':
|
||
query = query.order_by(Product.price.asc())
|
||
elif sort == 'price_desc':
|
||
query = query.order_by(Product.price.desc())
|
||
elif sort == 'sales':
|
||
query = query.order_by(Product.sales_count.desc())
|
||
elif sort == 'newest':
|
||
query = query.order_by(Product.created_at.desc())
|
||
else: # default
|
||
query = query.order_by(Product.created_at.desc())
|
||
|
||
# 分页
|
||
products = query.paginate(page=page, per_page=per_page, error_out=False)
|
||
|
||
# 获取所有分类用于侧边栏
|
||
categories = Category.query.filter_by(is_active=1, parent_id=0)\
|
||
.order_by(Category.sort_order).all()
|
||
|
||
# 当前分类信息
|
||
current_category = None
|
||
if category_id:
|
||
current_category = Category.query.get(category_id)
|
||
|
||
return render_template('product/list.html',
|
||
products=products,
|
||
categories=categories,
|
||
current_category=current_category,
|
||
search=search,
|
||
category_id=category_id,
|
||
sort=sort,
|
||
min_price=min_price,
|
||
max_price=max_price)
|
||
|
||
|
||
@main_bp.route('/products/<int:product_id>')
|
||
def product_detail(product_id):
|
||
"""商品详情页面"""
|
||
product = Product.query.filter_by(id=product_id, status=1).first_or_404()
|
||
|
||
# 增加浏览量
|
||
try:
|
||
product.view_count += 1
|
||
from config.database import db
|
||
db.session.commit()
|
||
except Exception as e:
|
||
current_app.logger.error(f"更新浏览量失败: {str(e)}")
|
||
|
||
# 获取商品图片(按排序)
|
||
images = product.images
|
||
if images:
|
||
images = sorted(images, key=lambda x: x.sort_order)
|
||
|
||
# 获取商品库存信息并转换为字典
|
||
inventory_list = product.inventory
|
||
inventory_data = []
|
||
if inventory_list:
|
||
for inventory in inventory_list:
|
||
inventory_data.append({
|
||
'id': inventory.id,
|
||
'sku_code': inventory.sku_code,
|
||
'spec_combination': inventory.spec_combination,
|
||
'price_adjustment': float(inventory.price_adjustment) if inventory.price_adjustment else 0,
|
||
'stock': inventory.stock,
|
||
'warning_stock': inventory.warning_stock,
|
||
'is_default': inventory.is_default,
|
||
'status': inventory.status,
|
||
'final_price': inventory.get_final_price()
|
||
})
|
||
|
||
# 获取推荐商品(同分类的其他商品)
|
||
recommended_products = Product.query.filter(
|
||
Product.category_id == product.category_id,
|
||
Product.id != product.id,
|
||
Product.status == 1
|
||
).order_by(Product.sales_count.desc()).limit(4).all()
|
||
|
||
return render_template('product/detail.html',
|
||
product=product,
|
||
images=images,
|
||
inventory_list=inventory_list,
|
||
inventory_data=inventory_data,
|
||
recommended_products=recommended_products)
|
||
|
||
|
||
@main_bp.route('/category/<int:category_id>')
|
||
def category_products(category_id):
|
||
"""分类商品页面(重定向到商品列表)"""
|
||
return redirect(url_for('main.product_list', category_id=category_id))
|
||
|
||
|
||
@main_bp.route('/search')
|
||
def search():
|
||
"""搜索页面(重定向到商品列表)"""
|
||
search_query = request.args.get('q', '').strip()
|
||
return redirect(url_for('main.product_list', search=search_query))
|
||
|
||
|
||
@main_bp.route('/about')
|
||
def about():
|
||
"""关于我们"""
|
||
return render_template('about.html')
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/order.py
|
||
📊 大小: 10973 bytes (10.72 KB)
|
||
🕒 修改时间: 2025-07-04 02:27:02
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
订单视图
|
||
"""
|
||
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash, g
|
||
from app.models.order import Order, OrderItem
|
||
from app.models.cart import Cart
|
||
from app.models.address import UserAddress
|
||
from app.models.product import ProductInventory
|
||
from app.models.payment import Payment
|
||
from app.forms import CheckoutForm
|
||
from app.utils.decorators import login_required
|
||
from config.database import db
|
||
import json
|
||
|
||
order_bp = Blueprint('order', __name__, url_prefix='/order')
|
||
|
||
|
||
@order_bp.route('/checkout')
|
||
@login_required
|
||
def checkout():
|
||
"""订单结算页面"""
|
||
user_id = session['user_id']
|
||
selected_items = request.args.getlist('items')
|
||
|
||
if not selected_items:
|
||
flash('请选择要购买的商品', 'error')
|
||
return redirect(url_for('cart.index'))
|
||
|
||
# 获取选中的购物车项目
|
||
cart_items = Cart.query.filter(
|
||
Cart.id.in_(selected_items),
|
||
Cart.user_id == user_id
|
||
).all()
|
||
|
||
if not cart_items:
|
||
flash('选中的商品不存在', 'error')
|
||
return redirect(url_for('cart.index'))
|
||
|
||
# 检查商品可用性和库存
|
||
unavailable_items = []
|
||
total_amount = 0
|
||
|
||
for item in cart_items:
|
||
if not item.is_available():
|
||
unavailable_items.append(item.product.name)
|
||
else:
|
||
total_amount += item.get_total_price()
|
||
|
||
if unavailable_items:
|
||
flash(f'以下商品库存不足或已下架:{", ".join(unavailable_items)}', 'error')
|
||
return redirect(url_for('cart.index'))
|
||
|
||
# 获取用户地址
|
||
addresses = UserAddress.get_user_addresses(user_id)
|
||
if not addresses:
|
||
flash('请先添加收货地址', 'warning')
|
||
return redirect(url_for('address.add'))
|
||
|
||
# 计算运费
|
||
shipping_fee = 0 # 默认免运费
|
||
|
||
# 创建表单并设置地址选项
|
||
form = CheckoutForm()
|
||
form.address_id.choices = [(addr.id, f"{addr.receiver_name} - {addr.get_full_address()}")
|
||
for addr in addresses]
|
||
|
||
# 设置默认地址
|
||
default_address = UserAddress.get_default_address(user_id)
|
||
if default_address:
|
||
form.address_id.data = default_address.id
|
||
|
||
return render_template('order/checkout.html',
|
||
cart_items=cart_items,
|
||
addresses=addresses,
|
||
form=form,
|
||
total_amount=total_amount,
|
||
shipping_fee=shipping_fee,
|
||
final_amount=total_amount + shipping_fee)
|
||
|
||
|
||
@order_bp.route('/create', methods=['POST'])
|
||
@login_required
|
||
def create():
|
||
"""创建订单"""
|
||
try:
|
||
user_id = session['user_id']
|
||
data = request.get_json()
|
||
|
||
selected_items = data.get('selected_items', [])
|
||
address_id = data.get('address_id')
|
||
shipping_method = data.get('shipping_method', 'standard')
|
||
payment_method = data.get('payment_method', 'wechat')
|
||
remark = data.get('remark', '')
|
||
|
||
if not selected_items or not address_id:
|
||
return jsonify({'success': False, 'message': '参数错误'})
|
||
|
||
# 获取购物车商品
|
||
cart_items = Cart.query.filter(
|
||
Cart.id.in_(selected_items),
|
||
Cart.user_id == user_id
|
||
).all()
|
||
|
||
if not cart_items:
|
||
return jsonify({'success': False, 'message': '购物车商品不存在'})
|
||
|
||
# 验证地址
|
||
address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first()
|
||
if not address:
|
||
return jsonify({'success': False, 'message': '收货地址不存在'})
|
||
|
||
# 再次检查库存和计算总价
|
||
total_amount = 0
|
||
order_items_data = []
|
||
|
||
for cart_item in cart_items:
|
||
if not cart_item.is_available():
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'商品"{cart_item.product.name}"库存不足或已下架'
|
||
})
|
||
|
||
# 检查库存是否足够
|
||
current_stock = cart_item.get_stock()
|
||
if current_stock < cart_item.quantity:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'商品"{cart_item.product.name}"库存不足,仅剩{current_stock}件'
|
||
})
|
||
|
||
item_total = cart_item.get_total_price()
|
||
total_amount += item_total
|
||
|
||
order_items_data.append({
|
||
'product_id': cart_item.product_id,
|
||
'sku_code': cart_item.sku_code,
|
||
'product_name': cart_item.product.name,
|
||
'product_image': cart_item.product.main_image,
|
||
'spec_combination': cart_item.spec_combination,
|
||
'price': cart_item.get_price(),
|
||
'quantity': cart_item.quantity,
|
||
'total_price': item_total
|
||
})
|
||
|
||
# 计算运费
|
||
shipping_fee_map = {
|
||
'standard': 0,
|
||
'express': 10,
|
||
'same_day': 20
|
||
}
|
||
shipping_fee = shipping_fee_map.get(shipping_method, 0)
|
||
actual_amount = total_amount + shipping_fee
|
||
|
||
# 创建订单
|
||
order = Order(
|
||
user_id=user_id,
|
||
order_sn=Order.generate_order_sn(),
|
||
total_amount=total_amount,
|
||
actual_amount=actual_amount,
|
||
shipping_fee=shipping_fee,
|
||
payment_method=payment_method,
|
||
shipping_method=shipping_method,
|
||
remark=remark
|
||
)
|
||
|
||
# 设置收货人信息
|
||
order.set_receiver_info({
|
||
'receiver_name': address.receiver_name,
|
||
'receiver_phone': address.receiver_phone,
|
||
'province': address.province,
|
||
'city': address.city,
|
||
'district': address.district,
|
||
'detail_address': address.detail_address,
|
||
'postal_code': address.postal_code,
|
||
'full_address': address.get_full_address()
|
||
})
|
||
|
||
db.session.add(order)
|
||
db.session.flush() # 获取订单ID
|
||
|
||
# 创建订单商品明细
|
||
for item_data in order_items_data:
|
||
order_item = OrderItem(
|
||
order_id=order.id,
|
||
**item_data
|
||
)
|
||
db.session.add(order_item)
|
||
|
||
# 扣减库存
|
||
for cart_item in cart_items:
|
||
if cart_item.sku_code:
|
||
sku_info = ProductInventory.query.filter_by(sku_code=cart_item.sku_code).first()
|
||
if sku_info:
|
||
sku_info.stock -= cart_item.quantity
|
||
|
||
# 增加销量
|
||
cart_item.product.sales_count += cart_item.quantity
|
||
|
||
# 删除购物车商品
|
||
for cart_item in cart_items:
|
||
db.session.delete(cart_item)
|
||
|
||
# 创建支付记录
|
||
payment = Payment(
|
||
order_id=order.id,
|
||
payment_sn=Payment.generate_payment_sn(),
|
||
payment_method=payment_method,
|
||
amount=actual_amount
|
||
)
|
||
db.session.add(payment)
|
||
|
||
db.session.commit()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '订单创建成功',
|
||
'order_id': order.id,
|
||
'order_sn': order.order_sn,
|
||
'payment_sn': payment.payment_sn
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'创建订单失败: {str(e)}'})
|
||
|
||
|
||
@order_bp.route('/list')
|
||
@login_required
|
||
def list():
|
||
"""订单列表"""
|
||
user_id = session['user_id']
|
||
status = request.args.get('status', type=int)
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = 10
|
||
|
||
query = Order.query.filter_by(user_id=user_id)
|
||
|
||
if status:
|
||
query = query.filter_by(status=status)
|
||
|
||
orders = query.order_by(Order.created_at.desc()).paginate(
|
||
page=page, per_page=per_page, error_out=False
|
||
)
|
||
|
||
return render_template('user/orders.html', orders=orders, current_status=status)
|
||
|
||
|
||
@order_bp.route('/detail/<int:order_id>')
|
||
@login_required
|
||
def detail(order_id):
|
||
"""订单详情"""
|
||
user_id = session['user_id']
|
||
order = Order.query.filter_by(id=order_id, user_id=user_id).first_or_404()
|
||
|
||
return render_template('order/detail.html', order=order)
|
||
|
||
|
||
@order_bp.route('/cancel/<int:order_id>', methods=['POST'])
|
||
@login_required
|
||
def cancel(order_id):
|
||
"""取消订单"""
|
||
try:
|
||
user_id = session['user_id']
|
||
order = Order.query.filter_by(id=order_id, user_id=user_id).first()
|
||
|
||
if not order:
|
||
return jsonify({'success': False, 'message': '订单不存在'})
|
||
|
||
if not order.can_cancel():
|
||
return jsonify({'success': False, 'message': '订单状态不允许取消'})
|
||
|
||
# 更新订单状态
|
||
order.status = Order.STATUS_CANCELLED
|
||
|
||
# 恢复库存
|
||
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)}'})
|
||
|
||
|
||
@order_bp.route('/confirm_receipt/<int:order_id>', methods=['POST'])
|
||
@login_required
|
||
def confirm_receipt(order_id):
|
||
"""确认收货"""
|
||
try:
|
||
user_id = session['user_id']
|
||
order = Order.query.filter_by(id=order_id, user_id=user_id).first()
|
||
|
||
if not order:
|
||
return jsonify({'success': False, 'message': '订单不存在'})
|
||
|
||
if not order.can_confirm_receipt():
|
||
return jsonify({'success': False, 'message': '订单状态不允许确认收货'})
|
||
|
||
# 更新订单状态
|
||
order.status = Order.STATUS_PENDING_REVIEW
|
||
order.received_at = db.func.now()
|
||
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '确认收货成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'确认收货失败: {str(e)}'})
|
||
|
||
|
||
@order_bp.route('/pay/<payment_sn>')
|
||
@login_required
|
||
def pay(payment_sn):
|
||
"""支付页面"""
|
||
user_id = session['user_id']
|
||
payment = Payment.query.filter_by(payment_sn=payment_sn).first_or_404()
|
||
order = payment.order
|
||
|
||
# 验证订单所有权
|
||
if order.user_id != user_id:
|
||
flash('订单不存在', 'error')
|
||
return redirect(url_for('order.list'))
|
||
|
||
# 检查是否可以支付
|
||
if not order.can_pay():
|
||
flash('订单不可支付', 'error')
|
||
return redirect(url_for('order.detail', order_id=order.id))
|
||
|
||
return render_template('order/pay.html', order=order, payment=payment)
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/payment.py
|
||
📊 大小: 6712 bytes (6.55 KB)
|
||
🕒 修改时间: 2025-07-04 02:27:33
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
支付视图
|
||
"""
|
||
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash
|
||
from app.models.payment import Payment
|
||
from app.models.order import Order
|
||
from app.utils.decorators import login_required
|
||
from config.database import db
|
||
from datetime import datetime
|
||
|
||
payment_bp = Blueprint('payment', __name__, url_prefix='/payment')
|
||
|
||
|
||
@payment_bp.route('/process', methods=['POST'])
|
||
@login_required
|
||
def process():
|
||
"""处理支付请求"""
|
||
try:
|
||
user_id = session['user_id']
|
||
payment_sn = request.json.get('payment_sn')
|
||
payment_method = request.json.get('payment_method')
|
||
|
||
if not payment_sn:
|
||
return jsonify({'success': False, 'message': '支付流水号不能为空'})
|
||
|
||
# 获取支付记录
|
||
payment = Payment.query.filter_by(payment_sn=payment_sn).first()
|
||
if not payment:
|
||
return jsonify({'success': False, 'message': '支付记录不存在'})
|
||
|
||
order = payment.order
|
||
if order.user_id != user_id:
|
||
return jsonify({'success': False, 'message': '订单不存在'})
|
||
|
||
if not order.can_pay():
|
||
return jsonify({'success': False, 'message': '订单不可支付'})
|
||
|
||
# 根据支付方式处理
|
||
if payment_method == 'wechat':
|
||
# 微信支付
|
||
result = process_wechat_pay(payment)
|
||
elif payment_method == 'alipay':
|
||
# 支付宝支付
|
||
result = process_alipay(payment)
|
||
elif payment_method == 'bank':
|
||
# 银行卡支付
|
||
result = process_bank_pay(payment)
|
||
else:
|
||
return jsonify({'success': False, 'message': '不支持的支付方式'})
|
||
|
||
return jsonify(result)
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': f'支付处理失败: {str(e)}'})
|
||
|
||
|
||
def process_wechat_pay(payment):
|
||
"""处理微信支付"""
|
||
# TODO: 接入真实的微信支付API
|
||
# 目前返回模拟的支付二维码
|
||
|
||
# 模拟生成支付二维码数据
|
||
qr_code_url = f"weixin://wxpay/bizpayurl?pr={payment.payment_sn}"
|
||
|
||
return {
|
||
'success': True,
|
||
'payment_type': 'qrcode',
|
||
'qr_code_url': qr_code_url,
|
||
'payment_sn': payment.payment_sn,
|
||
'amount': float(payment.amount),
|
||
'message': '请使用微信扫码支付'
|
||
}
|
||
|
||
|
||
def process_alipay(payment):
|
||
"""处理支付宝支付"""
|
||
# TODO: 接入真实的支付宝API
|
||
# 目前返回模拟的跳转链接
|
||
|
||
pay_url = f"https://mapi.alipay.com/gateway.do?service=create_direct_pay_by_user&payment_sn={payment.payment_sn}"
|
||
|
||
return {
|
||
'success': True,
|
||
'payment_type': 'redirect',
|
||
'pay_url': pay_url,
|
||
'payment_sn': payment.payment_sn,
|
||
'amount': float(payment.amount),
|
||
'message': '正在跳转到支付宝...'
|
||
}
|
||
|
||
|
||
def process_bank_pay(payment):
|
||
"""处理银行卡支付"""
|
||
# TODO: 接入银行支付网关
|
||
# 目前返回模拟的网银链接
|
||
|
||
bank_url = f"https://pay.bank.com/pay?order={payment.payment_sn}"
|
||
|
||
return {
|
||
'success': True,
|
||
'payment_type': 'redirect',
|
||
'pay_url': bank_url,
|
||
'payment_sn': payment.payment_sn,
|
||
'amount': float(payment.amount),
|
||
'message': '正在跳转到网银...'
|
||
}
|
||
|
||
|
||
@payment_bp.route('/callback/wechat', methods=['POST'])
|
||
def wechat_callback():
|
||
"""微信支付回调"""
|
||
try:
|
||
# TODO: 验证微信支付回调签名
|
||
# 目前模拟处理
|
||
|
||
callback_data = request.get_data()
|
||
# 解析回调数据,获取支付结果
|
||
|
||
# 模拟成功的回调处理
|
||
return handle_payment_success(request.form.get('payment_sn'), 'wechat_success_' + str(datetime.now().timestamp()))
|
||
|
||
except Exception as e:
|
||
return f"FAIL: {str(e)}"
|
||
|
||
|
||
@payment_bp.route('/callback/alipay', methods=['POST'])
|
||
def alipay_callback():
|
||
"""支付宝支付回调"""
|
||
try:
|
||
# TODO: 验证支付宝回调签名
|
||
# 目前模拟处理
|
||
|
||
return handle_payment_success(request.form.get('payment_sn'), 'alipay_success_' + str(datetime.now().timestamp()))
|
||
|
||
except Exception as e:
|
||
return f"FAIL: {str(e)}"
|
||
|
||
|
||
def handle_payment_success(payment_sn, third_party_sn):
|
||
"""处理支付成功"""
|
||
try:
|
||
payment = Payment.query.filter_by(payment_sn=payment_sn).first()
|
||
if not payment:
|
||
return "FAIL: Payment not found"
|
||
|
||
if payment.status == Payment.STATUS_SUCCESS:
|
||
return "SUCCESS" # 已经处理过的支付
|
||
|
||
# 更新支付状态
|
||
payment.status = Payment.STATUS_SUCCESS
|
||
payment.third_party_sn = third_party_sn
|
||
payment.paid_at = datetime.utcnow()
|
||
|
||
# 更新订单状态
|
||
order = payment.order
|
||
order.status = Order.STATUS_PENDING_SHIPMENT
|
||
|
||
db.session.commit()
|
||
|
||
return "SUCCESS"
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return f"FAIL: {str(e)}"
|
||
|
||
|
||
@payment_bp.route('/check_status/<payment_sn>')
|
||
@login_required
|
||
def check_status(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': '支付记录不存在'})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'status': payment.status,
|
||
'status_text': payment.get_status_text(),
|
||
'paid_at': payment.paid_at.isoformat() if payment.paid_at else None
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': f'查询失败: {str(e)}'})
|
||
|
||
|
||
@payment_bp.route('/simulate_success/<payment_sn>', methods=['POST'])
|
||
@login_required
|
||
def simulate_success(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': '订单已支付'})
|
||
|
||
# 模拟支付成功
|
||
result = handle_payment_success(payment_sn, f'SIMULATE_{datetime.now().timestamp()}')
|
||
|
||
if result == "SUCCESS":
|
||
return jsonify({'success': True, 'message': '支付成功'})
|
||
else:
|
||
return jsonify({'success': False, 'message': result})
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': f'模拟支付失败: {str(e)}'})
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/product.py
|
||
📊 大小: 23747 bytes (23.19 KB)
|
||
🕒 修改时间: 2025-07-03 15:17:18
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
商品管理视图
|
||
"""
|
||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, g
|
||
from werkzeug.utils import secure_filename
|
||
from app.models.product import Product, Category, ProductImage, SpecName, SpecValue, ProductInventory
|
||
from app.models.admin import AdminUser
|
||
from app.utils.decorators import admin_required, log_operation
|
||
from app.utils.cos_client import cos_client
|
||
from config.database import db
|
||
from sqlalchemy import func
|
||
import time
|
||
import uuid
|
||
import json
|
||
|
||
product_bp = Blueprint('product', __name__, url_prefix='/admin/products')
|
||
|
||
|
||
@product_bp.route('/')
|
||
@admin_required
|
||
def index():
|
||
"""商品列表"""
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = 20
|
||
|
||
query = Product.query.order_by(Product.created_at.desc())
|
||
|
||
# 搜索功能
|
||
search = request.args.get('search', '').strip()
|
||
if search:
|
||
query = query.filter(Product.name.like(f'%{search}%'))
|
||
|
||
# 分类筛选
|
||
category_id = request.args.get('category_id', '', type=str)
|
||
if category_id:
|
||
query = query.filter(Product.category_id == int(category_id))
|
||
|
||
# 状态筛选
|
||
status = request.args.get('status', '', type=str)
|
||
if status:
|
||
query = query.filter(Product.status == int(status))
|
||
|
||
products = query.paginate(page=page, per_page=per_page, error_out=False)
|
||
|
||
# 获取所有分类用于筛选
|
||
categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all()
|
||
|
||
return render_template('admin/products.html',
|
||
products=products,
|
||
categories=categories,
|
||
search=search,
|
||
category_id=category_id,
|
||
status=status)
|
||
|
||
|
||
|
||
|
||
@product_bp.route('/add')
|
||
@admin_required
|
||
def add():
|
||
"""添加商品页面"""
|
||
categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all()
|
||
spec_names = SpecName.query.order_by(SpecName.sort_order).all()
|
||
|
||
return render_template('admin/product_form.html',
|
||
product=None,
|
||
categories=categories,
|
||
spec_names=spec_names)
|
||
|
||
|
||
@product_bp.route('/edit/<int:product_id>')
|
||
@admin_required
|
||
def edit(product_id):
|
||
"""编辑商品页面"""
|
||
product = Product.query.get_or_404(product_id)
|
||
categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all()
|
||
spec_names = SpecName.query.order_by(SpecName.sort_order).all()
|
||
|
||
return render_template('admin/product_form.html',
|
||
product=product,
|
||
categories=categories,
|
||
spec_names=spec_names)
|
||
|
||
|
||
@product_bp.route('/save', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('保存商品信息', 'product')
|
||
def save():
|
||
"""保存商品信息"""
|
||
try:
|
||
product_id = request.form.get('product_id', type=int)
|
||
|
||
# 基本信息
|
||
name = request.form.get('name', '').strip()
|
||
category_id = request.form.get('category_id', type=int)
|
||
brand = request.form.get('brand', '').strip()
|
||
price = request.form.get('price', type=float)
|
||
original_price = request.form.get('original_price', type=float)
|
||
description = request.form.get('description', '').strip()
|
||
weight = request.form.get('weight', type=float)
|
||
status = request.form.get('status', 1, type=int)
|
||
|
||
# 验证必填字段
|
||
if not name or not category_id or not price:
|
||
flash('请填写完整的商品基本信息', 'error')
|
||
return redirect(request.referrer)
|
||
|
||
# 创建或更新商品
|
||
if product_id:
|
||
product = Product.query.get_or_404(product_id)
|
||
else:
|
||
product = Product()
|
||
|
||
product.name = name
|
||
product.category_id = category_id
|
||
product.brand = brand
|
||
product.price = price
|
||
product.original_price = original_price
|
||
product.description = description
|
||
product.weight = weight
|
||
product.status = status
|
||
|
||
# 处理库存类型
|
||
inventory_type = request.form.get('inventory_type', 'single')
|
||
|
||
if inventory_type == 'single':
|
||
product.has_specs = 0
|
||
else:
|
||
product.has_specs = 1
|
||
|
||
if not product_id:
|
||
db.session.add(product)
|
||
db.session.flush() # 获取product.id
|
||
|
||
# 处理库存信息
|
||
if inventory_type == 'single':
|
||
# 单规格处理
|
||
single_stock = request.form.get('single_stock', 0, type=int)
|
||
warning_stock = request.form.get('warning_stock', 10, type=int)
|
||
|
||
# 删除现有库存记录(如果是编辑模式)
|
||
if product_id:
|
||
ProductInventory.query.filter_by(product_id=product.id).delete()
|
||
|
||
# 创建单个SKU
|
||
sku_code = f"{product.name[:3].upper()}-DEFAULT-{product.id}"
|
||
inventory = ProductInventory(
|
||
product_id=product.id,
|
||
sku_code=sku_code,
|
||
spec_combination=None,
|
||
price_adjustment=0,
|
||
stock=single_stock,
|
||
warning_stock=warning_stock,
|
||
is_default=1,
|
||
status=1
|
||
)
|
||
db.session.add(inventory)
|
||
|
||
else:
|
||
# 多规格处理
|
||
sku_codes = request.form.getlist('sku_codes[]')
|
||
spec_combinations = request.form.getlist('spec_combinations[]')
|
||
price_adjustments = request.form.getlist('price_adjustments[]')
|
||
stocks = request.form.getlist('stocks[]')
|
||
warning_stocks = request.form.getlist('warning_stocks[]')
|
||
default_sku_index = request.form.get('default_sku', 0, type=int)
|
||
|
||
if not sku_codes:
|
||
flash('请至少添加一个SKU', 'error')
|
||
return redirect(request.referrer)
|
||
|
||
# 删除现有库存记录(如果是编辑模式)
|
||
if product_id:
|
||
ProductInventory.query.filter_by(product_id=product.id).delete()
|
||
|
||
# 创建多个SKU
|
||
for i, sku_code in enumerate(sku_codes):
|
||
try:
|
||
spec_combination = json.loads(spec_combinations[i]) if spec_combinations[i] else None
|
||
except:
|
||
spec_combination = None
|
||
|
||
inventory = ProductInventory(
|
||
product_id=product.id,
|
||
sku_code=sku_code,
|
||
spec_combination=spec_combination,
|
||
price_adjustment=float(price_adjustments[i]) if price_adjustments[i] else 0,
|
||
stock=int(stocks[i]) if stocks[i] else 0,
|
||
warning_stock=int(warning_stocks[i]) if warning_stocks[i] else 10,
|
||
is_default=1 if i == default_sku_index else 0,
|
||
status=1
|
||
)
|
||
db.session.add(inventory)
|
||
|
||
db.session.commit()
|
||
|
||
flash('商品信息保存成功', 'success')
|
||
return redirect(url_for('product.edit', product_id=product.id))
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(f'保存失败: {str(e)}', 'error')
|
||
return redirect(request.referrer)
|
||
|
||
|
||
@product_bp.route('/upload-images/<int:product_id>', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('上传商品图片', 'productdef index():')
|
||
def upload_images(product_id):
|
||
"""上传商品图片"""
|
||
try:
|
||
product = Product.query.get_or_404(product_id)
|
||
if 'files' not in request.files:
|
||
return jsonify({'success': False, 'message': '没有选择文件'})
|
||
files = request.files.getlist('files')
|
||
uploaded_images = []
|
||
for file in files:
|
||
if file.filename == '':
|
||
continue
|
||
# 检查文件类型
|
||
allowed_extensions = {'jpg', 'jpeg', 'png', 'gif', 'webp'}
|
||
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
|
||
if file_ext not in allowed_extensions:
|
||
continue
|
||
# 上传到COS
|
||
result = cos_client.upload_file(file, 'product', file.filename)
|
||
if result['success']:
|
||
# 检查是否是第一张图片
|
||
existing_images_count = ProductImage.query.filter_by(product_id=product_id).count()
|
||
is_first_image = (existing_images_count == 0)
|
||
|
||
# 保存图片记录
|
||
image = ProductImage(
|
||
product_id=product_id,
|
||
image_url=result['url'],
|
||
sort_order=existing_images_count,
|
||
is_main=1 if is_first_image else 0 # 第一张图片自动设为主图
|
||
)
|
||
db.session.add(image)
|
||
|
||
# 如果是第一张图片,同时更新商品主图
|
||
if is_first_image:
|
||
product.main_image = result['url']
|
||
uploaded_images.append({
|
||
'id': None, # 临时ID,提交后会更新
|
||
'url': result['url'],
|
||
'sort_order': image.sort_order,
|
||
'is_main': is_first_image
|
||
})
|
||
db.session.commit()
|
||
# 更新图片ID
|
||
for i, uploaded_image in enumerate(uploaded_images):
|
||
image = ProductImage.query.filter_by(
|
||
product_id=product_id,
|
||
image_url=uploaded_image['url']
|
||
).first()
|
||
if image:
|
||
uploaded_images[i]['id'] = image.id
|
||
return jsonify({
|
||
'success': True,
|
||
'message': f'成功上传 {len(uploaded_images)} 张图片',
|
||
'images': uploaded_images
|
||
})
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'上传失败: {str(e)}'})
|
||
|
||
|
||
@product_bp.route('/delete-image/<int:image_id>', methods=['DELETE'])
|
||
@admin_required
|
||
@log_operation('删除商品图片', 'product_image')
|
||
def delete_image(image_id):
|
||
"""删除商品图片"""
|
||
try:
|
||
image = ProductImage.query.get_or_404(image_id)
|
||
|
||
# 从COS删除文件
|
||
if image.image_url:
|
||
file_key = image.image_url.split('/')[-4:] # 提取文件路径
|
||
file_key = '/'.join(file_key)
|
||
cos_client.delete_file(file_key)
|
||
|
||
db.session.delete(image)
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '图片删除成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
|
||
|
||
|
||
@product_bp.route('/set-main-image/<int:image_id>', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('设置主图', 'product')
|
||
def set_main_image(image_id):
|
||
"""设置主图"""
|
||
try:
|
||
image = ProductImage.query.get_or_404(image_id)
|
||
product = image.product
|
||
|
||
# 清除当前主图
|
||
ProductImage.query.filter_by(product_id=product.id, is_main=1).update({'is_main': 0})
|
||
|
||
# 设置新主图
|
||
image.is_main = 1
|
||
product.main_image = image.image_url
|
||
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '主图设置成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'设置失败: {str(e)}'})
|
||
|
||
|
||
@product_bp.route('/sort-images/<int:product_id>', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('排序商品图片', 'product')
|
||
def sort_images(product_id):
|
||
"""图片排序"""
|
||
try:
|
||
image_ids = request.json.get('image_ids', [])
|
||
|
||
for index, image_id in enumerate(image_ids):
|
||
ProductImage.query.filter_by(id=image_id).update({'sort_order': index})
|
||
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '排序保存成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'排序失败: {str(e)}'})
|
||
|
||
|
||
@product_bp.route('/delete/<int:product_id>', methods=['DELETE'])
|
||
@admin_required
|
||
@log_operation('删除商品', 'product')
|
||
def delete(product_id):
|
||
"""删除商品"""
|
||
try:
|
||
product = Product.query.get_or_404(product_id)
|
||
|
||
# 删除商品图片
|
||
for image in product.images:
|
||
if image.image_url:
|
||
file_key = image.image_url.split('/')[-4:]
|
||
file_key = '/'.join(file_key)
|
||
cos_client.delete_file(file_key)
|
||
|
||
# 删除商品记录
|
||
db.session.delete(product)
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '商品删除成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
|
||
|
||
|
||
# 分类管理相关路由
|
||
@product_bp.route('/categories')
|
||
@admin_required
|
||
def categories():
|
||
"""分类管理"""
|
||
categories = Category.query.order_by(Category.sort_order).all()
|
||
return render_template('admin/categories.html', categories=categories)
|
||
|
||
|
||
@product_bp.route('/categories/save', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('保存商品分类', 'category')
|
||
def save_category():
|
||
"""保存分类"""
|
||
try:
|
||
category_id = request.form.get('category_id', type=int)
|
||
name = request.form.get('name', '').strip()
|
||
parent_id = request.form.get('parent_id', 0, type=int)
|
||
sort_order = request.form.get('sort_order', 0, type=int)
|
||
is_active = request.form.get('is_active', 1, type=int)
|
||
|
||
if not name:
|
||
flash('分类名称不能为空', 'error')
|
||
return redirect(url_for('product.categories'))
|
||
|
||
# 检查分类名称是否重复
|
||
existing = Category.query.filter_by(name=name, parent_id=parent_id).first()
|
||
if existing and (not category_id or existing.id != category_id):
|
||
flash('同一层级下分类名称不能重复', 'error')
|
||
return redirect(url_for('product.categories'))
|
||
|
||
if category_id:
|
||
category = Category.query.get_or_404(category_id)
|
||
|
||
# 防止将分类设为自己的子分类
|
||
if parent_id == category_id:
|
||
flash('不能将分类设为自己的子分类', 'error')
|
||
return redirect(url_for('product.categories'))
|
||
|
||
# 防止循环引用
|
||
if parent_id != 0:
|
||
parent = Category.query.get(parent_id)
|
||
temp_parent = parent
|
||
while temp_parent and temp_parent.parent_id != 0:
|
||
if temp_parent.parent_id == category_id:
|
||
flash('不能创建循环引用的分类层级', 'error')
|
||
return redirect(url_for('product.categories'))
|
||
temp_parent = Category.query.get(temp_parent.parent_id)
|
||
else:
|
||
category = Category()
|
||
|
||
category.name = name
|
||
category.parent_id = parent_id
|
||
category.sort_order = sort_order
|
||
category.is_active = is_active
|
||
|
||
# 设置层级
|
||
if parent_id == 0:
|
||
category.level = 1
|
||
else:
|
||
parent = Category.query.get(parent_id)
|
||
if parent:
|
||
category.level = parent.level + 1
|
||
if category.level > 3:
|
||
flash('分类层级不能超过3级', 'error')
|
||
return redirect(url_for('product.categories'))
|
||
else:
|
||
category.level = 1
|
||
|
||
# 处理图标上传
|
||
if 'icon' in request.files:
|
||
icon_file = request.files['icon']
|
||
if icon_file and icon_file.filename:
|
||
# 删除旧图标
|
||
if category.icon_url:
|
||
old_file_key = category.icon_url.split('/')[-4:]
|
||
old_file_key = '/'.join(old_file_key)
|
||
cos_client.delete_file(old_file_key)
|
||
|
||
# 上传新图标
|
||
result = cos_client.upload_file(icon_file, 'category', icon_file.filename)
|
||
if result['success']:
|
||
category.icon_url = result['url']
|
||
else:
|
||
flash(f'图标上传失败: {result["error"]}', 'error')
|
||
return redirect(url_for('product.categories'))
|
||
|
||
if not category_id:
|
||
db.session.add(category)
|
||
|
||
db.session.commit()
|
||
flash('分类保存成功', 'success')
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(f'保存失败: {str(e)}', 'error')
|
||
|
||
return redirect(url_for('product.categories'))
|
||
|
||
|
||
@product_bp.route('/categories/<int:category_id>', methods=['GET'])
|
||
@admin_required
|
||
def get_category(category_id):
|
||
"""获取分类详情"""
|
||
try:
|
||
category = Category.query.get_or_404(category_id)
|
||
return jsonify({
|
||
'success': True,
|
||
'category': category.to_dict()
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)})
|
||
|
||
|
||
@product_bp.route('/categories/<int:category_id>', methods=['DELETE'])
|
||
@admin_required
|
||
@log_operation('删除商品分类', 'category')
|
||
def delete_category(category_id):
|
||
"""删除分类"""
|
||
try:
|
||
category = Category.query.get_or_404(category_id)
|
||
|
||
# 检查是否有子分类
|
||
children = Category.query.filter_by(parent_id=category_id).count()
|
||
if children > 0:
|
||
return jsonify({'success': False, 'message': '该分类下还有子分类,无法删除'})
|
||
|
||
# 检查是否有商品使用此分类
|
||
products = Product.query.filter_by(category_id=category_id).count()
|
||
if products > 0:
|
||
return jsonify({'success': False, 'message': f'该分类下还有 {products} 个商品,无法删除'})
|
||
|
||
# 删除分类图标
|
||
if category.icon_url:
|
||
file_key = category.icon_url.split('/')[-4:]
|
||
file_key = '/'.join(file_key)
|
||
cos_client.delete_file(file_key)
|
||
|
||
db.session.delete(category)
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '分类删除成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
|
||
|
||
|
||
@product_bp.route('/categories/sort', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('分类排序', 'category')
|
||
def sort_categories():
|
||
"""分类排序"""
|
||
try:
|
||
category_orders = request.json.get('orders', [])
|
||
|
||
for item in category_orders:
|
||
category_id = item.get('id')
|
||
sort_order = item.get('sort_order')
|
||
|
||
Category.query.filter_by(id=category_id).update({'sort_order': sort_order})
|
||
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '排序保存成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'排序失败: {str(e)}'})
|
||
|
||
|
||
# 库存管理相关路由
|
||
@product_bp.route('/inventory/<int:product_id>')
|
||
@admin_required
|
||
def get_inventory(product_id):
|
||
"""获取商品库存详情"""
|
||
try:
|
||
product = Product.query.get_or_404(product_id)
|
||
inventory_list = ProductInventory.query.filter_by(product_id=product_id) \
|
||
.order_by(ProductInventory.is_default.desc(), ProductInventory.id).all()
|
||
|
||
inventory_data = []
|
||
for inventory in inventory_list:
|
||
inventory_data.append({
|
||
'id': inventory.id,
|
||
'sku_code': inventory.sku_code,
|
||
'spec_combination': inventory.spec_combination,
|
||
'stock': inventory.stock,
|
||
'warning_stock': inventory.warning_stock,
|
||
'price_adjustment': float(inventory.price_adjustment) if inventory.price_adjustment else 0,
|
||
'is_default': inventory.is_default,
|
||
'status': inventory.status,
|
||
'final_price': inventory.get_final_price()
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'inventory': inventory_data,
|
||
'product_name': product.name
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)})
|
||
|
||
|
||
@product_bp.route('/inventory/update', methods=['POST'])
|
||
@admin_required
|
||
@log_operation('更新库存', 'inventory')
|
||
def update_inventory():
|
||
"""批量更新库存"""
|
||
try:
|
||
inventory_data = request.json.get('inventory_list', [])
|
||
|
||
for item in inventory_data:
|
||
inventory_id = item.get('id')
|
||
new_stock = item.get('stock')
|
||
|
||
if inventory_id and new_stock is not None:
|
||
inventory = ProductInventory.query.get(inventory_id)
|
||
if inventory:
|
||
old_stock = inventory.stock
|
||
inventory.stock = new_stock
|
||
|
||
# 记录库存变更日志
|
||
from app.models.product import InventoryLog
|
||
InventoryLog.create_log(
|
||
product_id=inventory.product_id,
|
||
sku_code=inventory.sku_code,
|
||
change_type=3, # 调整
|
||
change_quantity=new_stock - old_stock,
|
||
before_stock=old_stock,
|
||
after_stock=new_stock,
|
||
remark='管理员手动调整'
|
||
)
|
||
|
||
db.session.commit()
|
||
return jsonify({'success': True, 'message': '库存更新成功'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'message': f'更新失败: {str(e)}'})
|
||
|
||
|
||
@product_bp.route('/inventory/log/<int:product_id>')
|
||
@admin_required
|
||
def inventory_log(product_id):
|
||
"""查看库存变更日志"""
|
||
product = Product.query.get_or_404(product_id)
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = 20
|
||
|
||
from app.models.product import InventoryLog
|
||
logs = InventoryLog.query.filter_by(product_id=product_id) \
|
||
.order_by(InventoryLog.created_at.desc()) \
|
||
.paginate(page=page, per_page=per_page, error_out=False)
|
||
|
||
return render_template('admin/inventory_log.html',
|
||
product=product,
|
||
logs=logs)
|
||
|
||
|
||
@product_bp.route('/generate-sku-code', methods=['POST'])
|
||
@admin_required
|
||
def generate_sku_code():
|
||
"""生成SKU编码"""
|
||
try:
|
||
product_name = request.json.get('product_name', '')
|
||
spec_combination = request.json.get('spec_combination', {})
|
||
|
||
# 生成SKU编码逻辑
|
||
short_name = product_name[:3].upper() if product_name else 'PRD'
|
||
spec_code = ''.join([v[:2].upper() for v in spec_combination.values()]) if spec_combination else 'DEFAULT'
|
||
timestamp = str(int(time.time()))[-4:]
|
||
|
||
sku_code = f"{short_name}-{spec_code}-{timestamp}"
|
||
|
||
return jsonify({'success': True, 'sku_code': sku_code})
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)})
|
||
|
||
|
||
@product_bp.route('/check-sku-code', methods=['POST'])
|
||
@admin_required
|
||
def check_sku_code():
|
||
"""检查SKU编码是否重复"""
|
||
try:
|
||
sku_code = request.json.get('sku_code', '')
|
||
product_id = request.json.get('product_id', None)
|
||
|
||
query = ProductInventory.query.filter_by(sku_code=sku_code)
|
||
if product_id:
|
||
query = query.filter(ProductInventory.product_id != product_id)
|
||
|
||
exists = query.first() is not None
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'exists': exists,
|
||
'message': 'SKU编码已存在' if exists else 'SKU编码可用'
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)})
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/upload.py
|
||
📊 大小: 5464 bytes (5.34 KB)
|
||
🕒 修改时间: 2025-07-03 04:48:37
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
文件上传视图
|
||
"""
|
||
from flask import Blueprint, request, jsonify, session, current_app
|
||
from werkzeug.utils import secure_filename
|
||
from app.utils.decorators import login_required
|
||
from app.models.user import User
|
||
from app.utils.cos_client import cos_client
|
||
from config.database import db
|
||
from config.cos_config import COSConfig
|
||
import os
|
||
|
||
upload_bp = Blueprint('upload', __name__)
|
||
|
||
|
||
@upload_bp.route('/avatar', methods=['POST'])
|
||
@login_required
|
||
def upload_avatar():
|
||
"""
|
||
上传用户头像
|
||
"""
|
||
try:
|
||
# 检查是否有文件
|
||
if 'avatar' not in request.files:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '没有选择文件'
|
||
}), 400
|
||
|
||
file = request.files['avatar']
|
||
|
||
# 检查文件名
|
||
if file.filename == '':
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '没有选择文件'
|
||
}), 400
|
||
|
||
# 验证文件类型
|
||
if not allowed_file(file.filename, COSConfig.ALLOWED_IMAGE_EXTENSIONS):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'不支持的文件格式,只支持: {", ".join(COSConfig.ALLOWED_IMAGE_EXTENSIONS)}'
|
||
}), 400
|
||
|
||
# 验证文件大小
|
||
file.seek(0, 2) # 移动到文件末尾
|
||
file_size = file.tell()
|
||
file.seek(0) # 重置文件指针
|
||
|
||
if file_size > COSConfig.MAX_IMAGE_SIZE:
|
||
size_mb = COSConfig.MAX_IMAGE_SIZE / 1024 / 1024
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'文件大小超过限制,最大允许 {size_mb:.1f}MB'
|
||
}), 400
|
||
|
||
# 获取当前用户
|
||
user = User.query.get(session['user_id'])
|
||
if not user:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '用户不存在'
|
||
}), 404
|
||
|
||
# 上传到COS
|
||
upload_result = cos_client.upload_file(
|
||
file_obj=file,
|
||
folder_type='avatar',
|
||
original_filename=file.filename
|
||
)
|
||
|
||
if not upload_result['success']:
|
||
current_app.logger.error(f"COS上传失败: {upload_result['error']}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '文件上传失败,请重试'
|
||
}), 500
|
||
|
||
# 删除旧头像(如果存在)
|
||
if user.avatar_url:
|
||
try:
|
||
# 从URL中提取文件路径
|
||
old_file_key = extract_file_key_from_url(user.avatar_url)
|
||
if old_file_key:
|
||
cos_client.delete_file(old_file_key)
|
||
current_app.logger.info(f"删除旧头像: {old_file_key}")
|
||
except Exception as e:
|
||
current_app.logger.warning(f"删除旧头像失败: {str(e)}")
|
||
|
||
# 更新用户头像URL
|
||
user.avatar_url = upload_result['url']
|
||
db.session.commit()
|
||
|
||
current_app.logger.info(f"用户 {user.username} 头像上传成功: {upload_result['file_key']}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '头像上传成功',
|
||
'avatar_url': upload_result['url'],
|
||
'file_key': upload_result['file_key']
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"头像上传异常: {str(e)}")
|
||
db.session.rollback()
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
|
||
def allowed_file(filename, allowed_extensions):
|
||
"""
|
||
检查文件扩展名是否允许
|
||
"""
|
||
return '.' in filename and \
|
||
filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
||
|
||
|
||
def extract_file_key_from_url(url):
|
||
"""
|
||
从COS URL中提取文件路径
|
||
"""
|
||
try:
|
||
if not url:
|
||
return None
|
||
|
||
# 移除域名部分,只保留文件路径
|
||
if COSConfig.BUCKET_DOMAIN in url:
|
||
return url.split(COSConfig.BUCKET_DOMAIN + '/')[-1]
|
||
|
||
return None
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
@upload_bp.route('/test', methods=['GET', 'POST'])
|
||
@login_required
|
||
def test_upload():
|
||
"""
|
||
测试上传功能
|
||
"""
|
||
if request.method == 'GET':
|
||
return '''
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>测试上传</title>
|
||
<meta charset="utf-8">
|
||
</head>
|
||
<body>
|
||
<h2>测试文件上传</h2>
|
||
<form method="post" enctype="multipart/form-data">
|
||
<input type="file" name="test_file" accept="image/*" required>
|
||
<button type="submit">上传测试</button>
|
||
</form>
|
||
</body>
|
||
</html>
|
||
'''
|
||
|
||
# POST 请求处理
|
||
if 'test_file' not in request.files:
|
||
return '没有文件'
|
||
|
||
file = request.files['test_file']
|
||
if file.filename == '':
|
||
return '没有选择文件'
|
||
|
||
# 上传到COS
|
||
result = cos_client.upload_file(
|
||
file_obj=file,
|
||
folder_type='temp',
|
||
original_filename=file.filename
|
||
)
|
||
|
||
if result['success']:
|
||
return f'''
|
||
<h2>上传成功!</h2>
|
||
<p>文件路径: {result['file_key']}</p>
|
||
<p>访问URL: <a href="{result['url']}" target="_blank">{result['url']}</a></p>
|
||
<img src="{result['url']}" style="max-width: 300px;">
|
||
'''
|
||
else:
|
||
return f'上传失败: {result["error"]}'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: app/views/user.py
|
||
📊 大小: 1046 bytes (1.02 KB)
|
||
🕒 修改时间: 2025-07-03 03:00:41
|
||
🔸==============================================================================
|
||
|
||
from flask import Blueprint, render_template, session, redirect, url_for, flash
|
||
from app.models.user import User
|
||
|
||
user_bp = Blueprint('user', __name__, url_prefix='/user')
|
||
|
||
|
||
def login_required(f):
|
||
"""登录验证装饰器"""
|
||
|
||
def decorated_function(*args, **kwargs):
|
||
if 'user_id' not in session:
|
||
flash('请先登录', 'warning')
|
||
return redirect(url_for('auth.login'))
|
||
return f(*args, **kwargs)
|
||
|
||
decorated_function.__name__ = f.__name__
|
||
return decorated_function
|
||
|
||
|
||
@user_bp.route('/profile')
|
||
@login_required
|
||
def profile():
|
||
"""用户个人中心"""
|
||
user = User.query.get(session['user_id'])
|
||
if not user:
|
||
session.clear()
|
||
flash('用户不存在,请重新登录', 'error')
|
||
return redirect(url_for('auth.login'))
|
||
|
||
return render_template('user/profile.html', user=user)
|
||
|
||
|
||
@user_bp.route('/orders')
|
||
@login_required
|
||
def orders():
|
||
"""用户订单"""
|
||
user = User.query.get(session['user_id'])
|
||
return render_template('user/orders.html', user=user)
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: check_avatar.py
|
||
📊 大小: 616 bytes (0.60 KB)
|
||
🕒 修改时间: 2025-07-03 05:21:18
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
检查用户头像URL
|
||
"""
|
||
import sys
|
||
import os
|
||
|
||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from app import create_app
|
||
from app.models.user import User
|
||
|
||
app = create_app()
|
||
|
||
with app.app_context():
|
||
# 查看所有用户的头像信息
|
||
users = User.query.all()
|
||
|
||
print("=" * 60)
|
||
print("用户头像信息检查")
|
||
print("=" * 60)
|
||
|
||
for user in users:
|
||
print(f"用户: {user.username}")
|
||
print(f"头像URL: {user.avatar_url}")
|
||
print(f"完整URL: {user.avatar_url if user.avatar_url else '无头像'}")
|
||
print("-" * 40)
|
||
"""
|
||
检查用户头像URL
|
||
"""
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: config/__init__.py
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: config/config.py
|
||
📊 大小: 1541 bytes (1.50 KB)
|
||
🕒 修改时间: 2025-07-03 04:02:34
|
||
🔸==============================================================================
|
||
|
||
import os
|
||
from datetime import timedelta
|
||
|
||
|
||
class Config:
|
||
# 基础配置
|
||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here-change-in-production'
|
||
|
||
# 数据库配置
|
||
MYSQL_HOST = '27.124.22.104'
|
||
MYSQL_USER = 'taibai'
|
||
MYSQL_PASSWORD = 'taibaishopping'
|
||
MYSQL_DB = 'online_shopping'
|
||
MYSQL_PORT = 3306
|
||
|
||
# SQLAlchemy配置
|
||
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}'
|
||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||
'pool_size': 10,
|
||
'pool_timeout': 20,
|
||
'pool_recycle': -1,
|
||
'pool_pre_ping': True
|
||
}
|
||
|
||
# Session配置
|
||
SESSION_TYPE = 'filesystem'
|
||
SESSION_PERMANENT = False
|
||
PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
|
||
|
||
# 文件上传配置
|
||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
||
UPLOAD_FOLDER = 'app/static/uploads'
|
||
|
||
# 分页配置
|
||
POSTS_PER_PAGE = 20
|
||
|
||
# 邮件配置
|
||
MAIL_SERVER = 'mail.sq0715.com'
|
||
MAIL_PORT = 587
|
||
MAIL_USE_TLS = True
|
||
MAIL_USE_SSL = False
|
||
MAIL_USERNAME = 'vip@sq0715.com'
|
||
MAIL_PASSWORD = 'Aalsq12350501.'
|
||
MAIL_DEFAULT_SENDER = 'vip@sq0715.com'
|
||
|
||
# 验证码配置
|
||
EMAIL_CODE_EXPIRE_MINUTES = 10 # 邮箱验证码有效期(分钟)
|
||
|
||
|
||
class DevelopmentConfig(Config):
|
||
DEBUG = True
|
||
|
||
|
||
class ProductionConfig(Config):
|
||
DEBUG = False
|
||
|
||
|
||
config = {
|
||
'development': DevelopmentConfig,
|
||
'production': ProductionConfig,
|
||
'default': DevelopmentConfig
|
||
}
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: config/cos_config.py
|
||
📊 大小: 2805 bytes (2.74 KB)
|
||
🕒 修改时间: 2025-07-03 07:11:25
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
腾讯云COS配置
|
||
"""
|
||
import os
|
||
|
||
|
||
class COSConfig:
|
||
"""COS配置类"""
|
||
|
||
# 腾讯云密钥信息
|
||
SECRET_ID = 'AKIDWu3xbz7zbw1qpeDWZLs99tMYUAZiaBVZ'
|
||
SECRET_KEY = 'qQjlX2GEvMWQ3PUIq77qIUP3RZQ0KBtL'
|
||
|
||
# 存储桶信息
|
||
BUCKET_NAME = 'taibai-1328510989'
|
||
REGION = 'ap-guangzhou'
|
||
|
||
# 存储桶域名
|
||
BUCKET_DOMAIN = f'{BUCKET_NAME}.cos.{REGION}.myqcloud.com'
|
||
|
||
# 文件存储路径配置
|
||
UPLOAD_FOLDERS = {
|
||
'avatar': 'uploads/avatars/', # 用户头像
|
||
'product': 'uploads/products/', # 商品图片
|
||
'review': 'uploads/reviews/', # 评价图片
|
||
'temp': 'uploads/temp/', # 临时文件
|
||
}
|
||
|
||
# 允许上传的文件类型
|
||
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||
ALLOWED_FILE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'pdf', 'doc', 'docx'}
|
||
|
||
# 文件大小限制 (字节)
|
||
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
||
MAX_IMAGE_SIZE = 2 * 1024 * 1024 # 2MB
|
||
|
||
# 图片处理配置
|
||
IMAGE_QUALITY = 85 # 压缩质量
|
||
THUMBNAIL_SIZE = (200, 200) # 缩略图尺寸
|
||
|
||
@classmethod
|
||
def get_full_url(cls, file_path):
|
||
"""获取文件完整访问URL"""
|
||
if not file_path:
|
||
return None
|
||
if file_path.startswith('http'):
|
||
return file_path
|
||
return f'https://{cls.BUCKET_DOMAIN}/{file_path}'
|
||
|
||
|
||
"""
|
||
腾讯云COS配置
|
||
"""
|
||
import os
|
||
|
||
|
||
class COSConfig:
|
||
"""COS配置类"""
|
||
|
||
# 腾讯云密钥信息
|
||
SECRET_ID = 'AKIDWu3xbz7zbw1qpeDWZLs99tMYUAZiaBVZ'
|
||
SECRET_KEY = 'qQjlX2GEvMWQ3PUIq77qIUP3RZQ0KBtL'
|
||
|
||
# 存储桶信息
|
||
BUCKET_NAME = 'taibai-1328510989'
|
||
REGION = 'ap-guangzhou'
|
||
|
||
# 存储桶域名
|
||
BUCKET_DOMAIN = f'{BUCKET_NAME}.cos.{REGION}.myqcloud.com'
|
||
|
||
# 文件存储路径配置
|
||
UPLOAD_FOLDERS = {
|
||
'avatar': 'uploads/avatars/', # 用户头像
|
||
'product': 'uploads/products/', # 商品图片
|
||
'category': 'uploads/categories/', # 分类图标
|
||
'review': 'uploads/reviews/', # 评价图片
|
||
'temp': 'uploads/temp/', # 临时文件
|
||
}
|
||
|
||
# 允许上传的文件类型
|
||
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||
ALLOWED_FILE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'pdf', 'doc', 'docx'}
|
||
|
||
# 文件大小限制 (字节)
|
||
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
||
MAX_IMAGE_SIZE = 2 * 1024 * 1024 # 2MB
|
||
|
||
# 图片处理配置
|
||
IMAGE_QUALITY = 85 # 压缩质量
|
||
THUMBNAIL_SIZE = (200, 200) # 缩略图尺寸
|
||
|
||
@classmethod
|
||
def get_full_url(cls, file_path):
|
||
"""获取文件完整访问URL"""
|
||
if not file_path:
|
||
return None
|
||
if file_path.startswith('http'):
|
||
return file_path
|
||
return f'https://{cls.BUCKET_DOMAIN}/{file_path}'
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: config/database.py
|
||
📊 大小: 770 bytes (0.75 KB)
|
||
🕒 修改时间: 2025-07-03 03:08:46
|
||
🔸==============================================================================
|
||
|
||
from flask_sqlalchemy import SQLAlchemy
|
||
import sys
|
||
|
||
db = SQLAlchemy()
|
||
|
||
|
||
def init_db(app):
|
||
"""初始化数据库"""
|
||
db.init_app(app)
|
||
|
||
try:
|
||
with app.app_context():
|
||
# 测试数据库连接
|
||
result = db.session.execute(db.text('SELECT 1'))
|
||
print("✅ 数据库连接成功")
|
||
|
||
# 由于表已存在,我们只需要确保模型与数据库同步
|
||
# 不需要重新创建表
|
||
print("✅ 数据库初始化完成")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 数据库初始化失败: {e}")
|
||
print("请检查数据库配置和网络连接")
|
||
# 在开发环境中不退出,允许继续运行
|
||
print("⚠️ 继续运行,但可能会有数据库相关问题")
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: create_admin.py
|
||
📊 大小: 5307 bytes (5.18 KB)
|
||
🕒 修改时间: 2025-07-03 05:51:05
|
||
🔸==============================================================================
|
||
|
||
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
管理员账号创建工具
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import getpass
|
||
import re
|
||
from datetime import datetime
|
||
|
||
# 添加项目路径
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from app import create_app
|
||
from app.models.admin import AdminUser
|
||
from config.database import db
|
||
|
||
|
||
def validate_email(email):
|
||
"""验证邮箱格式"""
|
||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||
return re.match(pattern, email) is not None
|
||
|
||
|
||
def validate_phone(phone):
|
||
"""验证手机号格式"""
|
||
pattern = r'^1[3-9]\d{9}$'
|
||
return re.match(pattern, phone) is not None
|
||
|
||
|
||
def validate_password(password):
|
||
"""验证密码强度"""
|
||
if len(password) < 6:
|
||
return False, "密码长度至少6位"
|
||
|
||
if not re.search(r'[a-zA-Z]', password):
|
||
return False, "密码必须包含字母"
|
||
|
||
if not re.search(r'\d', password):
|
||
return False, "密码必须包含数字"
|
||
|
||
return True, "密码符合要求"
|
||
|
||
|
||
def create_admin():
|
||
"""创建管理员账号"""
|
||
app = create_app()
|
||
|
||
with app.app_context():
|
||
print("=" * 50)
|
||
print("🛠️ 太白购物商城 - 管理员账号创建工具")
|
||
print("=" * 50)
|
||
print()
|
||
|
||
# 检查是否已有管理员
|
||
existing_count = AdminUser.query.count()
|
||
if existing_count > 0:
|
||
print(f"⚠️ 当前已有 {existing_count} 个管理员账号")
|
||
confirm = input("是否继续创建新的管理员账号?(y/N): ").strip().lower()
|
||
if confirm != 'y':
|
||
print("❌ 取消创建")
|
||
return
|
||
print()
|
||
|
||
# 输入用户名
|
||
while True:
|
||
username = input("请输入管理员用户名: ").strip()
|
||
if not username:
|
||
print("❌ 用户名不能为空")
|
||
continue
|
||
|
||
if len(username) < 3:
|
||
print("❌ 用户名长度至少3位")
|
||
continue
|
||
|
||
# 检查用户名是否已存在
|
||
if AdminUser.query.filter_by(username=username).first():
|
||
print("❌ 用户名已存在")
|
||
continue
|
||
|
||
break
|
||
|
||
# 输入真实姓名
|
||
real_name = input("请输入真实姓名: ").strip()
|
||
|
||
# 输入邮箱
|
||
while True:
|
||
email = input("请输入邮箱地址: ").strip()
|
||
if not email:
|
||
break
|
||
|
||
if not validate_email(email):
|
||
print("❌ 邮箱格式不正确")
|
||
continue
|
||
|
||
# 检查邮箱是否已存在
|
||
if AdminUser.query.filter_by(email=email).first():
|
||
print("❌ 邮箱已被使用")
|
||
continue
|
||
|
||
break
|
||
|
||
# 输入手机号
|
||
while True:
|
||
phone = input("请输入手机号: ").strip()
|
||
if not phone:
|
||
break
|
||
|
||
if not validate_phone(phone):
|
||
print("❌ 手机号格式不正确")
|
||
continue
|
||
|
||
# 检查手机号是否已存在
|
||
if AdminUser.query.filter_by(phone=phone).first():
|
||
print("❌ 手机号已被使用")
|
||
continue
|
||
|
||
break
|
||
|
||
# 输入密码
|
||
while True:
|
||
password = getpass.getpass("请输入密码: ")
|
||
|
||
is_valid, message = validate_password(password)
|
||
if not is_valid:
|
||
print(f"❌ {message}")
|
||
continue
|
||
|
||
confirm_password = getpass.getpass("请确认密码: ")
|
||
|
||
if password != confirm_password:
|
||
print("❌ 密码不一致,请重新输入")
|
||
continue
|
||
|
||
break
|
||
|
||
print()
|
||
print("=" * 30)
|
||
print("📋 管理员信息确认")
|
||
print("=" * 30)
|
||
print(f"用户名: {username}")
|
||
print(f"真实姓名: {real_name if real_name else '未填写'}")
|
||
print(f"邮箱: {email if email else '未填写'}")
|
||
print(f"手机号: {phone if phone else '未填写'}")
|
||
print(f"创建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
print()
|
||
|
||
confirm = input("确认创建?(y/N): ").strip().lower()
|
||
|
||
if confirm != 'y':
|
||
print("❌ 取消创建")
|
||
return
|
||
|
||
try:
|
||
# 创建管理员
|
||
admin = AdminUser(
|
||
username=username,
|
||
real_name=real_name if real_name else None,
|
||
email=email if email else None,
|
||
phone=phone if phone else None,
|
||
status=1
|
||
)
|
||
admin.set_password(password)
|
||
|
||
db.session.add(admin)
|
||
db.session.commit()
|
||
|
||
print()
|
||
print("✅ 管理员账号创建成功!")
|
||
print("=" * 30)
|
||
print("📌 登录信息")
|
||
print("=" * 30)
|
||
print(f"登录地址: http://localhost:5000/admin/login")
|
||
print(f"用户名: {username}")
|
||
print(f"密码: [已设置]")
|
||
print()
|
||
print("🔐 请妥善保管登录信息")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 创建失败: {str(e)}")
|
||
db.session.rollback()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
create_admin()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: create_sample_categories.py
|
||
📊 大小: 4784 bytes (4.67 KB)
|
||
🕒 修改时间: 2025-07-03 07:12:00
|
||
🔸==============================================================================
|
||
|
||
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
创建示例分类数据
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
|
||
# 添加项目路径
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from app import create_app
|
||
from app.models.product import Category
|
||
from config.database import db
|
||
|
||
|
||
def create_sample_categories():
|
||
"""创建示例分类数据"""
|
||
app = create_app()
|
||
|
||
with app.app_context():
|
||
print("=== 创建示例分类数据 ===\n")
|
||
|
||
# 检查是否已有分类
|
||
if Category.query.count() > 0:
|
||
print("⚠️ 数据库中已有分类数据,是否继续添加?(y/N): ", end="")
|
||
if input().strip().lower() != 'y':
|
||
return
|
||
|
||
# 创建示例分类数据
|
||
categories_data = [
|
||
# 一级分类
|
||
{'name': '数码电子', 'parent_id': 0, 'level': 1, 'sort_order': 1},
|
||
{'name': '服装鞋帽', 'parent_id': 0, 'level': 1, 'sort_order': 2},
|
||
{'name': '食品饮料', 'parent_id': 0, 'level': 1, 'sort_order': 3},
|
||
{'name': '家居生活', 'parent_id': 0, 'level': 1, 'sort_order': 4},
|
||
{'name': '图书文具', 'parent_id': 0, 'level': 1, 'sort_order': 5},
|
||
]
|
||
|
||
# 创建一级分类
|
||
level1_categories = {}
|
||
for cat_data in categories_data:
|
||
category = Category(**cat_data)
|
||
db.session.add(category)
|
||
db.session.flush() # 获取ID
|
||
level1_categories[cat_data['name']] = category.id
|
||
print(f"✅ 创建一级分类: {cat_data['name']}")
|
||
|
||
# 二级分类数据
|
||
level2_data = [
|
||
# 数码电子子分类
|
||
{'name': '手机通讯', 'parent_id': level1_categories['数码电子'], 'level': 2, 'sort_order': 1},
|
||
{'name': '电脑办公', 'parent_id': level1_categories['数码电子'], 'level': 2, 'sort_order': 2},
|
||
{'name': '相机摄像', 'parent_id': level1_categories['数码电子'], 'level': 2, 'sort_order': 3},
|
||
|
||
# 服装鞋帽子分类
|
||
{'name': '男装', 'parent_id': level1_categories['服装鞋帽'], 'level': 2, 'sort_order': 1},
|
||
{'name': '女装', 'parent_id': level1_categories['服装鞋帽'], 'level': 2, 'sort_order': 2},
|
||
{'name': '运动鞋', 'parent_id': level1_categories['服装鞋帽'], 'level': 2, 'sort_order': 3},
|
||
|
||
# 食品饮料子分类
|
||
{'name': '零食小食', 'parent_id': level1_categories['食品饮料'], 'level': 2, 'sort_order': 1},
|
||
{'name': '饮料冲调', 'parent_id': level1_categories['食品饮料'], 'level': 2, 'sort_order': 2},
|
||
{'name': '生鲜食品', 'parent_id': level1_categories['食品饮料'], 'level': 2, 'sort_order': 3},
|
||
]
|
||
|
||
# 创建二级分类
|
||
level2_categories = {}
|
||
for cat_data in level2_data:
|
||
category = Category(**cat_data)
|
||
db.session.add(category)
|
||
db.session.flush()
|
||
level2_categories[cat_data['name']] = category.id
|
||
print(f" ├─ 创建二级分类: {cat_data['name']}")
|
||
|
||
# 三级分类数据
|
||
level3_data = [
|
||
# 手机通讯子分类
|
||
{'name': '智能手机', 'parent_id': level2_categories['手机通讯'], 'level': 3, 'sort_order': 1},
|
||
{'name': '手机配件', 'parent_id': level2_categories['手机通讯'], 'level': 3, 'sort_order': 2},
|
||
|
||
# 男装子分类
|
||
{'name': 'T恤', 'parent_id': level2_categories['男装'], 'level': 3, 'sort_order': 1},
|
||
{'name': '衬衫', 'parent_id': level2_categories['男装'], 'level': 3, 'sort_order': 2},
|
||
{'name': '牛仔裤', 'parent_id': level2_categories['男装'], 'level': 3, 'sort_order': 3},
|
||
|
||
# 零食小食子分类
|
||
{'name': '饼干糕点', 'parent_id': level2_categories['零食小食'], 'level': 3, 'sort_order': 1},
|
||
{'name': '坚果炒货', 'parent_id': level2_categories['零食小食'], 'level': 3, 'sort_order': 2},
|
||
]
|
||
|
||
# 创建三级分类
|
||
for cat_data in level3_data:
|
||
category = Category(**cat_data)
|
||
db.session.add(category)
|
||
print(f" └─ 创建三级分类: {cat_data['name']}")
|
||
|
||
# 提交数据
|
||
db.session.commit()
|
||
|
||
print(f"\n✅ 示例分类数据创建完成!")
|
||
print(f"一级分类: {len(categories_data)} 个")
|
||
print(f"二级分类: {len(level2_data)} 个")
|
||
print(f"三级分类: {len(level3_data)} 个")
|
||
print(f"总计: {len(categories_data) + len(level2_data) + len(level3_data)} 个分类")
|
||
print(f"\n访问地址: http://localhost:5000/admin/products/categories")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
create_sample_categories()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: create_sample_specs.py
|
||
📊 大小: 2010 bytes (1.96 KB)
|
||
🕒 修改时间: 2025-07-03 15:07:45
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
创建示例规格数据
|
||
"""
|
||
from app import create_app
|
||
from config.database import db
|
||
from app.models.product import SpecName, SpecValue
|
||
|
||
def create_sample_specs():
|
||
"""创建示例规格数据"""
|
||
app = create_app()
|
||
|
||
with app.app_context():
|
||
# 检查是否已有数据
|
||
if SpecName.query.count() > 0:
|
||
print("规格数据已存在,跳过创建")
|
||
return
|
||
|
||
print("开始创建规格数据...")
|
||
|
||
# 创建规格名称
|
||
specs_data = [
|
||
{
|
||
'name': '颜色',
|
||
'values': ['红色', '蓝色', '黑色', '白色', '金色', '银色']
|
||
},
|
||
{
|
||
'name': '尺寸',
|
||
'values': ['S', 'M', 'L', 'XL', 'XXL']
|
||
},
|
||
{
|
||
'name': '内存',
|
||
'values': ['4GB', '8GB', '16GB', '32GB']
|
||
},
|
||
{
|
||
'name': '存储容量',
|
||
'values': ['64GB', '128GB', '256GB', '512GB', '1TB']
|
||
},
|
||
{
|
||
'name': '型号',
|
||
'values': ['标准版', '升级版', '专业版', '旗舰版']
|
||
}
|
||
]
|
||
|
||
for i, spec_data in enumerate(specs_data):
|
||
# 创建规格名称
|
||
spec_name = SpecName(
|
||
name=spec_data['name'],
|
||
sort_order=i + 1
|
||
)
|
||
db.session.add(spec_name)
|
||
db.session.flush() # 获取ID
|
||
|
||
# 创建规格值
|
||
for j, value in enumerate(spec_data['values']):
|
||
spec_value = SpecValue(
|
||
spec_name_id=spec_name.id,
|
||
value=value,
|
||
sort_order=j + 1
|
||
)
|
||
db.session.add(spec_value)
|
||
|
||
print(f"✅ 创建规格:{spec_data['name']} - {len(spec_data['values'])} 个值")
|
||
|
||
db.session.commit()
|
||
print("✅ 规格数据创建完成!")
|
||
|
||
if __name__ == '__main__':
|
||
create_sample_specs()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: create_test_order.py
|
||
📊 大小: 2823 bytes (2.76 KB)
|
||
🕒 修改时间: 2025-07-04 02:48:05
|
||
🔸==============================================================================
|
||
|
||
#!/usr/bin/env python
|
||
"""
|
||
创建测试订单数据
|
||
"""
|
||
|
||
from app import create_app
|
||
from config.database import db
|
||
from app.models.user import User
|
||
from app.models.address import UserAddress
|
||
from app.models.product import Product, ProductInventory
|
||
from app.models.cart import Cart
|
||
|
||
|
||
def create_test_data():
|
||
app = create_app()
|
||
|
||
with app.app_context():
|
||
try:
|
||
# 检查是否有测试用户
|
||
test_user = User.query.filter_by(username='testuser').first()
|
||
if not test_user:
|
||
print("请先运行 create_test_user.py 创建测试用户")
|
||
return
|
||
|
||
print(f"测试用户: {test_user.username} (ID: {test_user.id})")
|
||
|
||
# 创建测试地址
|
||
if not UserAddress.query.filter_by(user_id=test_user.id).first():
|
||
address = UserAddress(
|
||
user_id=test_user.id,
|
||
receiver_name='张三',
|
||
receiver_phone='13800138000',
|
||
province='广东省',
|
||
city='广州市',
|
||
district='天河区',
|
||
detail_address='天河路123号',
|
||
postal_code='510000',
|
||
is_default=1
|
||
)
|
||
db.session.add(address)
|
||
print("创建测试地址")
|
||
|
||
# 添加商品到购物车
|
||
products = Product.query.filter_by(status=1).limit(3).all()
|
||
for product in products:
|
||
# 检查是否已在购物车
|
||
existing_cart = Cart.query.filter_by(
|
||
user_id=test_user.id,
|
||
product_id=product.id
|
||
).first()
|
||
|
||
if not existing_cart:
|
||
# 获取默认SKU
|
||
default_sku = ProductInventory.query.filter_by(
|
||
product_id=product.id,
|
||
is_default=1
|
||
).first()
|
||
|
||
cart_item = Cart(
|
||
user_id=test_user.id,
|
||
product_id=product.id,
|
||
sku_code=default_sku.sku_code if default_sku else None,
|
||
quantity=1
|
||
)
|
||
db.session.add(cart_item)
|
||
print(f"添加商品到购物车: {product.name}")
|
||
|
||
db.session.commit()
|
||
print("测试数据创建完成!")
|
||
print("\n测试步骤:")
|
||
print("1. 使用 testuser / 123456 登录")
|
||
print("2. 访问购物车页面")
|
||
print("3. 选择商品进行结算")
|
||
print("4. 测试订单流程")
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
print(f"创建测试数据失败: {e}")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
create_test_data()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: create_test_user.py
|
||
📊 大小: 2050 bytes (2.00 KB)
|
||
🕒 修改时间: 2025-07-03 03:09:13
|
||
🔸==============================================================================
|
||
|
||
import pymysql
|
||
import sys
|
||
from werkzeug.security import generate_password_hash
|
||
from datetime import datetime
|
||
|
||
# 数据库配置
|
||
config = {
|
||
'host': '27.124.22.104',
|
||
'user': 'taibai',
|
||
'password': 'taibaishopping',
|
||
'database': 'online_shopping',
|
||
'port': 3306,
|
||
'charset': 'utf8mb4'
|
||
}
|
||
|
||
|
||
def create_test_user():
|
||
try:
|
||
# 连接数据库
|
||
connection = pymysql.connect(**config)
|
||
print("✅ 数据库连接成功")
|
||
|
||
with connection.cursor() as cursor:
|
||
# 检查是否已存在测试用户
|
||
cursor.execute("SELECT id FROM users WHERE username = %s", ('testuser',))
|
||
existing_user = cursor.fetchone()
|
||
|
||
if existing_user:
|
||
print("✅ 测试用户已存在!")
|
||
print("用户名: testuser")
|
||
print("密码: 123456")
|
||
print("邮箱: test@example.com")
|
||
print("手机: 13800138000")
|
||
return
|
||
|
||
# 创建测试用户
|
||
password_hash = generate_password_hash('123456')
|
||
now = datetime.now()
|
||
|
||
sql = """
|
||
INSERT INTO users (username, phone, email, password_hash, nickname, status, created_at, updated_at)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||
"""
|
||
|
||
cursor.execute(sql, (
|
||
'testuser',
|
||
'13800138000',
|
||
'test@example.com',
|
||
password_hash,
|
||
'测试用户',
|
||
1,
|
||
now,
|
||
now
|
||
))
|
||
|
||
connection.commit()
|
||
print("✅ 测试用户创建成功!")
|
||
print("用户名: testuser")
|
||
print("密码: 123456")
|
||
print("邮箱: test@example.com")
|
||
print("手机: 13800138000")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 创建测试用户失败: {e}")
|
||
sys.exit(1)
|
||
finally:
|
||
if 'connection' in locals():
|
||
connection.close()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
create_test_user()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: docker/.dockerignore
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: docker/Dockerfile
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: docker/docker-compose.yml
|
||
📊 大小: 0 bytes (0.00 KB)
|
||
🕒 修改时间: 2025-07-03 02:46:14
|
||
🔸==============================================================================
|
||
|
||
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: export_code.py
|
||
📊 大小: 10115 bytes (9.88 KB)
|
||
🕒 修改时间: 2025-07-04 03:35:25
|
||
🔸==============================================================================
|
||
|
||
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
项目代码导出工具
|
||
用于将整个电商项目的代码导出到文本文件中
|
||
"""
|
||
|
||
import os
|
||
import datetime
|
||
from pathlib import Path
|
||
|
||
|
||
class CodeExporter:
|
||
def __init__(self, project_root=None):
|
||
"""
|
||
初始化代码导出器
|
||
:param project_root: 项目根目录,默认为当前目录
|
||
"""
|
||
self.project_root = Path(project_root) if project_root else Path('.')
|
||
self.output_file = None
|
||
|
||
# 需要导出的文件扩展名
|
||
self.include_extensions = {
|
||
'.py', '.html', '.css', '.js', '.sql', '.txt', '.md',
|
||
'.yml', '.yaml', '.json', '.xml', '.ini', '.cfg'
|
||
}
|
||
|
||
# 需要排除的目录
|
||
self.exclude_dirs = {
|
||
'venv', '.venv', 'env', '.env', '__pycache__', '.git',
|
||
'.idea', '.vscode', 'node_modules', 'logs', 'temp', 'tmp',
|
||
'.pytest_cache', '.coverage', 'htmlcov', 'dist', 'build'
|
||
}
|
||
|
||
# 需要排除的文件
|
||
self.exclude_files = {
|
||
'.DS_Store', 'Thumbs.db', '.gitignore', '*.pyc', '*.pyo',
|
||
'*.log', '*.tmp', '*.bak', '*.swp', '*.swo'
|
||
}
|
||
|
||
# 特殊处理的文件(即使没有扩展名也要包含)
|
||
self.special_files = {
|
||
'Dockerfile', 'requirements.txt', 'README', 'LICENSE',
|
||
'Makefile', 'Procfile', '.dockerignore'
|
||
}
|
||
|
||
def should_include_file(self, file_path):
|
||
"""
|
||
判断文件是否应该被包含在导出中
|
||
:param file_path: 文件路径
|
||
:return: bool
|
||
"""
|
||
file_name = file_path.name
|
||
file_suffix = file_path.suffix.lower()
|
||
|
||
# 检查特殊文件
|
||
if file_name in self.special_files:
|
||
return True
|
||
|
||
# 检查扩展名
|
||
if file_suffix in self.include_extensions:
|
||
return True
|
||
|
||
return False
|
||
|
||
def should_exclude_dir(self, dir_path):
|
||
"""
|
||
判断目录是否应该被排除
|
||
:param dir_path: 目录路径
|
||
:return: bool
|
||
"""
|
||
dir_name = dir_path.name
|
||
return dir_name in self.exclude_dirs or dir_name.startswith('.')
|
||
|
||
def get_file_info(self, file_path):
|
||
"""
|
||
获取文件信息
|
||
:param file_path: 文件路径
|
||
:return: dict
|
||
"""
|
||
try:
|
||
stat = file_path.stat()
|
||
return {
|
||
'size': stat.st_size,
|
||
'modified': datetime.datetime.fromtimestamp(stat.st_mtime),
|
||
'relative_path': file_path.relative_to(self.project_root)
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
'size': 0,
|
||
'modified': datetime.datetime.now(),
|
||
'relative_path': file_path.relative_to(self.project_root),
|
||
'error': str(e)
|
||
}
|
||
|
||
def read_file_content(self, file_path):
|
||
"""
|
||
读取文件内容
|
||
:param file_path: 文件路径
|
||
:return: str
|
||
"""
|
||
try:
|
||
# 尝试用UTF-8编码读取
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
return f.read()
|
||
except UnicodeDecodeError:
|
||
try:
|
||
# 如果UTF-8失败,尝试GBK编码
|
||
with open(file_path, 'r', encoding='gbk') as f:
|
||
return f.read()
|
||
except UnicodeDecodeError:
|
||
try:
|
||
# 如果还是失败,尝试latin-1编码
|
||
with open(file_path, 'r', encoding='latin-1') as f:
|
||
return f.read()
|
||
except Exception as e:
|
||
return f"[无法读取文件内容: {str(e)}]"
|
||
except Exception as e:
|
||
return f"[读取文件时发生错误: {str(e)}]"
|
||
|
||
def scan_project(self):
|
||
"""
|
||
扫描项目目录,获取所有需要导出的文件
|
||
:return: list
|
||
"""
|
||
files_to_export = []
|
||
|
||
for root, dirs, files in os.walk(self.project_root):
|
||
root_path = Path(root)
|
||
|
||
# 过滤掉需要排除的目录
|
||
dirs[:] = [d for d in dirs if not self.should_exclude_dir(root_path / d)]
|
||
|
||
for file in files:
|
||
file_path = root_path / file
|
||
|
||
if self.should_include_file(file_path):
|
||
file_info = self.get_file_info(file_path)
|
||
files_to_export.append({
|
||
'path': file_path,
|
||
'info': file_info
|
||
})
|
||
|
||
# 按相对路径排序
|
||
files_to_export.sort(key=lambda x: str(x['info']['relative_path']))
|
||
return files_to_export
|
||
|
||
def export_to_file(self, output_filename=None):
|
||
"""
|
||
导出代码到文件
|
||
:param output_filename: 输出文件名
|
||
"""
|
||
if not output_filename:
|
||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
output_filename = f"project_code_export_{timestamp}.txt"
|
||
|
||
self.output_file = output_filename
|
||
files_to_export = self.scan_project()
|
||
|
||
print(f"开始导出项目代码...")
|
||
print(f"项目根目录: {self.project_root.absolute()}")
|
||
print(f"找到 {len(files_to_export)} 个文件需要导出")
|
||
print(f"输出文件: {output_filename}")
|
||
|
||
with open(output_filename, 'w', encoding='utf-8') as output:
|
||
# 写入文件头
|
||
self.write_header(output, files_to_export)
|
||
|
||
# 写入每个文件的内容
|
||
for i, file_data in enumerate(files_to_export, 1):
|
||
file_path = file_data['path']
|
||
file_info = file_data['info']
|
||
|
||
print(f"正在处理 ({i}/{len(files_to_export)}): {file_info['relative_path']}")
|
||
|
||
self.write_file_section(output, file_path, file_info)
|
||
|
||
# 写入文件尾
|
||
self.write_footer(output)
|
||
|
||
print(f"\n✅ 导出完成!")
|
||
print(f"输出文件: {output_filename}")
|
||
print(f"文件大小: {os.path.getsize(output_filename) / 1024:.2f} KB")
|
||
|
||
def write_header(self, output, files_to_export):
|
||
"""
|
||
写入文件头部信息
|
||
"""
|
||
output.write("=" * 80 + "\n")
|
||
output.write("项目代码导出文件\n")
|
||
output.write("=" * 80 + "\n")
|
||
output.write(f"项目名称: 基于Python的线上电商系统\n")
|
||
output.write(f"导出时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||
output.write(f"项目路径: {self.project_root.absolute()}\n")
|
||
output.write(f"文件总数: {len(files_to_export)}\n")
|
||
output.write("=" * 80 + "\n\n")
|
||
|
||
# 写入文件目录
|
||
output.write("📁 文件目录:\n")
|
||
output.write("-" * 50 + "\n")
|
||
for file_data in files_to_export:
|
||
file_info = file_data['info']
|
||
size_kb = file_info['size'] / 1024 if file_info['size'] > 0 else 0
|
||
output.write(f"{file_info['relative_path']} ({size_kb:.1f} KB)\n")
|
||
output.write("\n" + "=" * 80 + "\n\n")
|
||
|
||
def write_file_section(self, output, file_path, file_info):
|
||
"""
|
||
写入单个文件的内容
|
||
"""
|
||
relative_path = file_info['relative_path']
|
||
|
||
# 文件分隔符
|
||
output.write("🔸" + "=" * 78 + "\n")
|
||
output.write(f"📄 文件: {relative_path}\n")
|
||
output.write(f"📊 大小: {file_info['size']} bytes ({file_info['size'] / 1024:.2f} KB)\n")
|
||
output.write(f"🕒 修改时间: {file_info['modified'].strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||
|
||
if 'error' in file_info:
|
||
output.write(f"⚠️ 错误: {file_info['error']}\n")
|
||
|
||
output.write("🔸" + "=" * 78 + "\n\n")
|
||
|
||
# 文件内容
|
||
content = self.read_file_content(file_path)
|
||
output.write(content)
|
||
|
||
# 确保文件结尾有换行
|
||
if not content.endswith('\n'):
|
||
output.write('\n')
|
||
|
||
output.write("\n\n")
|
||
|
||
def write_footer(self, output):
|
||
"""
|
||
写入文件尾部信息
|
||
"""
|
||
output.write("=" * 80 + "\n")
|
||
output.write("导出完成\n")
|
||
output.write(f"导出时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||
output.write("=" * 80 + "\n")
|
||
|
||
def export_summary(self):
|
||
"""
|
||
导出项目摘要信息
|
||
"""
|
||
files_to_export = self.scan_project()
|
||
|
||
# 按文件类型统计
|
||
type_stats = {}
|
||
total_size = 0
|
||
|
||
for file_data in files_to_export:
|
||
file_path = file_data['path']
|
||
file_info = file_data['info']
|
||
|
||
ext = file_path.suffix.lower() or '无扩展名'
|
||
if ext not in type_stats:
|
||
type_stats[ext] = {'count': 0, 'size': 0}
|
||
|
||
type_stats[ext]['count'] += 1
|
||
type_stats[ext]['size'] += file_info['size']
|
||
total_size += file_info['size']
|
||
|
||
print("\n📊 项目统计信息:")
|
||
print("-" * 50)
|
||
print(f"总文件数: {len(files_to_export)}")
|
||
print(f"总大小: {total_size / 1024:.2f} KB")
|
||
print("\n📋 文件类型统计:")
|
||
|
||
for ext, stats in sorted(type_stats.items(), key=lambda x: x[1]['count'], reverse=True):
|
||
print(f"{ext:>10}: {stats['count']:>3} 个文件, {stats['size'] / 1024:>6.1f} KB")
|
||
|
||
|
||
def main():
|
||
"""
|
||
主函数
|
||
"""
|
||
print("🚀 项目代码导出工具")
|
||
print("=" * 50)
|
||
|
||
# 创建导出器
|
||
exporter = CodeExporter()
|
||
|
||
# 显示项目摘要
|
||
exporter.export_summary()
|
||
|
||
# 询问是否继续导出
|
||
print("\n" + "=" * 50)
|
||
choice = input("是否继续导出完整代码到文件? (y/n): ").lower().strip()
|
||
|
||
if choice in ['y', 'yes', '是']:
|
||
# 询问输出文件名
|
||
output_name = input("请输入输出文件名 (直接回车使用默认名称): ").strip()
|
||
if not output_name:
|
||
output_name = None
|
||
|
||
# 开始导出
|
||
exporter.export_to_file(output_name)
|
||
else:
|
||
print("取消导出。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: requirements.txt
|
||
📊 大小: 245 bytes (0.24 KB)
|
||
🕒 修改时间: 2025-07-03 04:23:09
|
||
🔸==============================================================================
|
||
|
||
Flask==2.3.3
|
||
Flask-SQLAlchemy==3.0.5
|
||
Flask-WTF==1.1.1
|
||
WTForms==3.0.1
|
||
PyMySQL==1.1.0
|
||
Werkzeug==2.3.7
|
||
python-dotenv==1.0.0
|
||
Flask-Session==0.5.0
|
||
email-validator==2.0.0
|
||
Flask-Mail==0.9.1
|
||
cos-python-sdk-v5==1.9.24
|
||
Pillow==10.0.1
|
||
python-magic==0.4.27
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: run.py
|
||
📊 大小: 175 bytes (0.17 KB)
|
||
🕒 修改时间: 2025-07-03 03:10:14
|
||
🔸==============================================================================
|
||
|
||
from app import create_app
|
||
import os
|
||
|
||
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
|
||
|
||
if __name__ == '__main__':
|
||
app.run(host='0.0.0.0', port=50400, debug=True)
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: test_cos_connection.py
|
||
📊 大小: 6619 bytes (6.46 KB)
|
||
🕒 修改时间: 2025-07-03 04:26:42
|
||
🔸==============================================================================
|
||
|
||
"""
|
||
测试腾讯云COS连接 - 独立测试脚本
|
||
"""
|
||
import sys
|
||
import os
|
||
from datetime import datetime
|
||
|
||
# 添加项目根目录到Python路径
|
||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
# 直接导入COS相关模块,避免导入Flask应用
|
||
from qcloud_cos import CosConfig, CosS3Client
|
||
from qcloud_cos.cos_exception import CosClientError, CosServiceError
|
||
from config.cos_config import COSConfig
|
||
|
||
class COSTestClient:
|
||
"""COS测试客户端"""
|
||
|
||
def __init__(self):
|
||
"""初始化COS客户端"""
|
||
try:
|
||
# 配置COS
|
||
config = CosConfig(
|
||
Region=COSConfig.REGION,
|
||
SecretId=COSConfig.SECRET_ID,
|
||
SecretKey=COSConfig.SECRET_KEY,
|
||
Token=None,
|
||
Scheme='https'
|
||
)
|
||
|
||
# 创建客户端
|
||
self.client = CosS3Client(config)
|
||
self.bucket = COSConfig.BUCKET_NAME
|
||
|
||
print("✅ COS客户端初始化成功")
|
||
|
||
except Exception as e:
|
||
print(f"❌ COS客户端初始化失败: {str(e)}")
|
||
raise
|
||
|
||
def test_connection(self):
|
||
"""测试COS连接"""
|
||
try:
|
||
# 尝试列出存储桶
|
||
response = self.client.list_objects(
|
||
Bucket=self.bucket,
|
||
MaxKeys=1
|
||
)
|
||
|
||
return {
|
||
'success': True,
|
||
'message': 'COS连接测试成功',
|
||
'bucket': self.bucket,
|
||
'region': COSConfig.REGION
|
||
}
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'message': f'COS连接测试失败: {str(e)}',
|
||
'bucket': self.bucket,
|
||
'region': COSConfig.REGION
|
||
}
|
||
|
||
def list_files(self, prefix='', max_keys=10):
|
||
"""列出文件"""
|
||
try:
|
||
response = self.client.list_objects(
|
||
Bucket=self.bucket,
|
||
Prefix=prefix,
|
||
MaxKeys=max_keys
|
||
)
|
||
|
||
files = []
|
||
if 'Contents' in response:
|
||
for obj in response['Contents']:
|
||
files.append({
|
||
'key': obj['Key'],
|
||
'size': obj['Size'],
|
||
'last_modified': obj['LastModified'],
|
||
'url': COSConfig.get_full_url(obj['Key'])
|
||
})
|
||
|
||
return files
|
||
|
||
except Exception as e:
|
||
print(f"❌ 列出文件失败: {str(e)}")
|
||
return []
|
||
|
||
def upload_test_file(self):
|
||
"""上传测试文件"""
|
||
test_content = f"COS上传测试文件\n创建时间: {datetime.now()}\n测试内容: Hello COS!"
|
||
test_file_key = f"test/test_upload_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
||
|
||
try:
|
||
# 上传文件
|
||
response = self.client.put_object(
|
||
Bucket=self.bucket,
|
||
Body=test_content.encode('utf-8'),
|
||
Key=test_file_key,
|
||
StorageClass='STANDARD'
|
||
)
|
||
|
||
file_url = COSConfig.get_full_url(test_file_key)
|
||
|
||
return {
|
||
'success': True,
|
||
'file_key': test_file_key,
|
||
'url': file_url,
|
||
'etag': response['ETag']
|
||
}
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': str(e)
|
||
}
|
||
|
||
def delete_file(self, file_key):
|
||
"""删除文件"""
|
||
try:
|
||
response = self.client.delete_object(
|
||
Bucket=self.bucket,
|
||
Key=file_key
|
||
)
|
||
|
||
return {'success': True}
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': str(e)
|
||
}
|
||
|
||
def main():
|
||
"""主测试函数"""
|
||
print("=" * 60)
|
||
print("🚀 腾讯云COS连接测试")
|
||
print("=" * 60)
|
||
|
||
# 显示配置信息
|
||
print(f"📦 存储桶名称: {COSConfig.BUCKET_NAME}")
|
||
print(f"🌍 所属地域: {COSConfig.REGION}")
|
||
print(f"🔗 访问域名: {COSConfig.BUCKET_DOMAIN}")
|
||
print(f"🔑 SecretId: {COSConfig.SECRET_ID[:8]}***")
|
||
print("-" * 60)
|
||
|
||
try:
|
||
# 初始化测试客户端
|
||
cos_test = COSTestClient()
|
||
|
||
# 1. 测试连接
|
||
print("1️⃣ 测试COS连接...")
|
||
result = cos_test.test_connection()
|
||
|
||
if result['success']:
|
||
print("✅ COS连接测试成功!")
|
||
print(f" 存储桶: {result['bucket']}")
|
||
print(f" 地域: {result['region']}")
|
||
else:
|
||
print("❌ COS连接测试失败!")
|
||
print(f" 错误信息: {result['message']}")
|
||
return False
|
||
|
||
print("-" * 60)
|
||
|
||
# 2. 测试文件列表
|
||
print("2️⃣ 测试文件列表功能...")
|
||
files = cos_test.list_files(max_keys=5)
|
||
print(f"✅ 文件列表获取成功,共找到 {len(files)} 个文件")
|
||
|
||
if files:
|
||
print("📁 最近的文件:")
|
||
for i, file_info in enumerate(files[:3], 1):
|
||
size_mb = file_info['size'] / 1024 / 1024
|
||
print(f" {i}. {file_info['key']}")
|
||
print(f" 大小: {size_mb:.2f}MB")
|
||
print(f" 修改时间: {file_info['last_modified']}")
|
||
else:
|
||
print("📭 存储桶为空")
|
||
|
||
print("-" * 60)
|
||
|
||
# 3. 测试文件上传
|
||
print("3️⃣ 测试文件上传功能...")
|
||
upload_result = cos_test.upload_test_file()
|
||
|
||
if upload_result['success']:
|
||
print("✅ 文件上传测试成功!")
|
||
print(f" 文件路径: {upload_result['file_key']}")
|
||
print(f" 访问URL: {upload_result['url']}")
|
||
print(f" ETag: {upload_result['etag']}")
|
||
|
||
# 4. 测试文件删除
|
||
print("-" * 60)
|
||
print("4️⃣ 测试文件删除功能...")
|
||
delete_result = cos_test.delete_file(upload_result['file_key'])
|
||
|
||
if delete_result['success']:
|
||
print("✅ 文件删除测试成功!")
|
||
else:
|
||
print(f"❌ 文件删除测试失败: {delete_result['error']}")
|
||
|
||
else:
|
||
print(f"❌ 文件上传测试失败: {upload_result['error']}")
|
||
|
||
print("=" * 60)
|
||
print("🎉 COS功能测试完成!")
|
||
print("=" * 60)
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ 测试过程中发生异常: {str(e)}")
|
||
return False
|
||
|
||
if __name__ == '__main__':
|
||
main()
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: test_db_connection.py
|
||
📊 大小: 3366 bytes (3.29 KB)
|
||
🕒 修改时间: 2025-07-03 03:06:50
|
||
🔸==============================================================================
|
||
|
||
import pymysql
|
||
import sys
|
||
|
||
# 数据库配置
|
||
config = {
|
||
'host': '27.124.22.104',
|
||
'user': 'taibai',
|
||
'password': 'taibaishopping',
|
||
'database': 'online_shopping',
|
||
'port': 3306,
|
||
'charset': 'utf8mb4',
|
||
'connect_timeout': 10, # 设置连接超时时间
|
||
'read_timeout': 10,
|
||
'write_timeout': 10
|
||
}
|
||
|
||
|
||
def test_connection():
|
||
try:
|
||
print("正在测试数据库连接...")
|
||
print(f"主机: {config['host']}")
|
||
print(f"端口: {config['port']}")
|
||
print(f"用户: {config['user']}")
|
||
print(f"数据库: {config['database']}")
|
||
|
||
# 尝试连接数据库
|
||
connection = pymysql.connect(**config)
|
||
print("✅ 数据库连接成功!")
|
||
|
||
# 测试查询
|
||
with connection.cursor() as cursor:
|
||
cursor.execute("SELECT VERSION()")
|
||
version = cursor.fetchone()
|
||
print(f"MySQL版本: {version[0]}")
|
||
|
||
cursor.execute("SHOW TABLES")
|
||
tables = cursor.fetchall()
|
||
print(f"当前数据库中的表数量: {len(tables)}")
|
||
if tables:
|
||
print("现有表:")
|
||
for table in tables:
|
||
print(f" - {table[0]}")
|
||
|
||
connection.close()
|
||
return True
|
||
|
||
except pymysql.Error as e:
|
||
print(f"❌ 数据库连接失败: {e}")
|
||
return False
|
||
except Exception as e:
|
||
print(f"❌ 连接过程中发生错误: {e}")
|
||
return False
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if test_connection():
|
||
print("\n数据库连接测试通过,可以继续运行应用。")
|
||
else:
|
||
print("\n请检查数据库配置或网络连接。")
|
||
sys.exit(1)
|
||
import pymysql
|
||
import sys
|
||
|
||
# 数据库配置
|
||
config = {
|
||
'host': '27.124.22.104',
|
||
'user': 'taibai',
|
||
'password': 'taibaishopping',
|
||
'database': 'online_shopping',
|
||
'port': 3306,
|
||
'charset': 'utf8mb4',
|
||
'connect_timeout': 10, # 设置连接超时时间
|
||
'read_timeout': 10,
|
||
'write_timeout': 10
|
||
}
|
||
|
||
|
||
def test_connection():
|
||
try:
|
||
print("正在测试数据库连接...")
|
||
print(f"主机: {config['host']}")
|
||
print(f"端口: {config['port']}")
|
||
print(f"用户: {config['user']}")
|
||
print(f"数据库: {config['database']}")
|
||
|
||
# 尝试连接数据库
|
||
connection = pymysql.connect(**config)
|
||
print("✅ 数据库连接成功!")
|
||
|
||
# 测试查询
|
||
with connection.cursor() as cursor:
|
||
cursor.execute("SELECT VERSION()")
|
||
version = cursor.fetchone()
|
||
print(f"MySQL版本: {version[0]}")
|
||
|
||
cursor.execute("SHOW TABLES")
|
||
tables = cursor.fetchall()
|
||
print(f"当前数据库中的表数量: {len(tables)}")
|
||
if tables:
|
||
print("现有表:")
|
||
for table in tables:
|
||
print(f" - {table[0]}")
|
||
|
||
connection.close()
|
||
return True
|
||
|
||
except pymysql.Error as e:
|
||
print(f"❌ 数据库连接失败: {e}")
|
||
return False
|
||
except Exception as e:
|
||
print(f"❌ 连接过程中发生错误: {e}")
|
||
return False
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if test_connection():
|
||
print("\n数据库连接测试通过,可以继续运行应用。")
|
||
else:
|
||
print("\n请检查数据库配置或网络连接。")
|
||
sys.exit(1)
|
||
|
||
|
||
🔸==============================================================================
|
||
📄 文件: test_email_detailed.py
|
||
📊 大小: 8512 bytes (8.31 KB)
|
||
🕒 修改时间: 2025-07-03 04:02:12
|
||
🔸==============================================================================
|
||
|
||
import smtplib
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
|
||
|
||
def test_smtp_detailed():
|
||
"""详细测试mail.sq0715.com的不同配置"""
|
||
server = 'mail.sq0715.com'
|
||
username = 'vip@sq0715.com'
|
||
password = 'Aalsq12350501.'
|
||
|
||
configs = [
|
||
{
|
||
'name': '587端口 + STARTTLS',
|
||
'port': 587,
|
||
'use_tls': True,
|
||
'use_ssl': False
|
||
},
|
||
{
|
||
'name': '465端口 + SSL',
|
||
'port': 465,
|
||
'use_tls': False,
|
||
'use_ssl': True
|
||
},
|
||
{
|
||
'name': '25端口 + STARTTLS',
|
||
'port': 25,
|
||
'use_tls': True,
|
||
'use_ssl': False
|
||
},
|
||
{
|
||
'name': '25端口 无加密',
|
||
'port': 25,
|
||
'use_tls': False,
|
||
'use_ssl': False
|
||
},
|
||
{
|
||
'name': '993端口 + SSL',
|
||
'port': 993,
|
||
'use_tls': False,
|
||
'use_ssl': True
|
||
}
|
||
]
|
||
|
||
for config in configs:
|
||
print(f"\n{'=' * 50}")
|
||
print(f"测试配置: {config['name']}")
|
||
print(f"服务器: {server}:{config['port']}")
|
||
print(f"TLS: {config['use_tls']}, SSL: {config['use_ssl']}")
|
||
print('=' * 50)
|
||
|
||
try:
|
||
# 创建SMTP连接
|
||
if config['use_ssl']:
|
||
print("使用SSL连接...")
|
||
smtp_server = smtplib.SMTP_SSL(server, config['port'], timeout=30)
|
||
else:
|
||
print("使用普通连接...")
|
||
smtp_server = smtplib.SMTP(server, config['port'], timeout=30)
|
||
|
||
# 开启调试模式
|
||
smtp_server.set_debuglevel(1)
|
||
|
||
print("连接建立成功,发送EHLO...")
|
||
smtp_server.ehlo()
|
||
|
||
# 如果需要STARTTLS
|
||
if config['use_tls']:
|
||
print("启动TLS加密...")
|
||
smtp_server.starttls()
|
||
smtp_server.ehlo() # 重新发送EHLO
|
||
|
||
print("尝试登录...")
|
||
smtp_server.login(username, password)
|
||
print("✅ 登录成功!")
|
||
|
||
# 发送测试邮件
|
||
print("发送测试邮件...")
|
||
msg = MIMEMultipart()
|
||
msg['From'] = username
|
||
msg['To'] = username # 发送给自己
|
||
msg['Subject'] = f'测试邮件 - {config["name"]}'
|
||
|
||
body = f"这是使用 {config['name']} 配置发送的测试邮件"
|
||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||
|
||
smtp_server.send_message(msg)
|
||
print("✅ 邮件发送成功!")
|
||
|
||
smtp_server.quit()
|
||
print(f"🎉 配置 '{config['name']}' 完全成功!")
|
||
|
||
return config # 返回成功的配置
|
||
|
||
except smtplib.SMTPAuthenticationError as e:
|
||
print(f"❌ 认证失败: {e}")
|
||
except smtplib.SMTPConnectError as e:
|
||
print(f"❌ 连接失败: {e}")
|
||
except smtplib.SMTPServerDisconnected as e:
|
||
print(f"❌ 服务器断开连接: {e}")
|
||
except smtplib.SMTPRecipientsRefused as e:
|
||
print(f"❌ 收件人被拒绝: {e}")
|
||
except Exception as e:
|
||
print(f"❌ 其他错误: {type(e).__name__}: {e}")
|
||
|
||
return None
|
||
|
||
|
||
if __name__ == '__main__':
|
||
print("开始测试 mail.sq0715.com 的SMTP配置...")
|
||
successful_config = test_smtp_detailed()
|
||
|
||
if successful_config:
|
||
print(f"\n🎉 找到可用配置!")
|
||
print("请在config.py中使用以下配置:")
|
||
print("-" * 40)
|
||
print(f"MAIL_SERVER = 'mail.sq0715.com'")
|
||
print(f"MAIL_PORT = {successful_config['port']}")
|
||
print(f"MAIL_USE_TLS = {successful_config['use_tls']}")
|
||
print(f"MAIL_USE_SSL = {successful_config['use_ssl']}")
|
||
print(f"MAIL_USERNAME = 'vip@sq0715.com'")
|
||
print(f"MAIL_PASSWORD = 'Aalsq12350501.'")
|
||
print(f"MAIL_DEFAULT_SENDER = 'vip@sq0715.com'")
|
||
else:
|
||
print("\n❌ 所有配置都失败了")
|
||
print("可能的原因:")
|
||
print("1. 邮箱密码不正确")
|
||
print("2. 邮箱服务器不支持SMTP")
|
||
print("3. 需要在邮箱设置中开启SMTP服务")
|
||
print("4. 服务器防火墙阻止了连接")
|
||
import smtplib
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
|
||
|
||
def test_smtp_detailed():
|
||
"""详细测试mail.sq0715.com的不同配置"""
|
||
server = 'mail.sq0715.com'
|
||
username = 'vip@sq0715.com'
|
||
password = 'Aalsq12350501.'
|
||
|
||
configs = [
|
||
{
|
||
'name': '587端口 + STARTTLS',
|
||
'port': 587,
|
||
'use_tls': True,
|
||
'use_ssl': False
|
||
},
|
||
{
|
||
'name': '465端口 + SSL',
|
||
'port': 465,
|
||
'use_tls': False,
|
||
'use_ssl': True
|
||
},
|
||
{
|
||
'name': '25端口 + STARTTLS',
|
||
'port': 25,
|
||
'use_tls': True,
|
||
'use_ssl': False
|
||
},
|
||
{
|
||
'name': '25端口 无加密',
|
||
'port': 25,
|
||
'use_tls': False,
|
||
'use_ssl': False
|
||
},
|
||
{
|
||
'name': '993端口 + SSL',
|
||
'port': 993,
|
||
'use_tls': False,
|
||
'use_ssl': True
|
||
}
|
||
]
|
||
|
||
for config in configs:
|
||
print(f"\n{'=' * 50}")
|
||
print(f"测试配置: {config['name']}")
|
||
print(f"服务器: {server}:{config['port']}")
|
||
print(f"TLS: {config['use_tls']}, SSL: {config['use_ssl']}")
|
||
print('=' * 50)
|
||
|
||
try:
|
||
# 创建SMTP连接
|
||
if config['use_ssl']:
|
||
print("使用SSL连接...")
|
||
smtp_server = smtplib.SMTP_SSL(server, config['port'], timeout=30)
|
||
else:
|
||
print("使用普通连接...")
|
||
smtp_server = smtplib.SMTP(server, config['port'], timeout=30)
|
||
|
||
# 开启调试模式
|
||
smtp_server.set_debuglevel(1)
|
||
|
||
print("连接建立成功,发送EHLO...")
|
||
smtp_server.ehlo()
|
||
|
||
# 如果需要STARTTLS
|
||
if config['use_tls']:
|
||
print("启动TLS加密...")
|
||
smtp_server.starttls()
|
||
smtp_server.ehlo() # 重新发送EHLO
|
||
|
||
print("尝试登录...")
|
||
smtp_server.login(username, password)
|
||
print("✅ 登录成功!")
|
||
|
||
# 发送测试邮件
|
||
print("发送测试邮件...")
|
||
msg = MIMEMultipart()
|
||
msg['From'] = username
|
||
msg['To'] = username # 发送给自己
|
||
msg['Subject'] = f'测试邮件 - {config["name"]}'
|
||
|
||
body = f"这是使用 {config['name']} 配置发送的测试邮件"
|
||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||
|
||
smtp_server.send_message(msg)
|
||
print("✅ 邮件发送成功!")
|
||
|
||
smtp_server.quit()
|
||
print(f"🎉 配置 '{config['name']}' 完全成功!")
|
||
|
||
return config # 返回成功的配置
|
||
|
||
except smtplib.SMTPAuthenticationError as e:
|
||
print(f"❌ 认证失败: {e}")
|
||
except smtplib.SMTPConnectError as e:
|
||
print(f"❌ 连接失败: {e}")
|
||
except smtplib.SMTPServerDisconnected as e:
|
||
print(f"❌ 服务器断开连接: {e}")
|
||
except smtplib.SMTPRecipientsRefused as e:
|
||
print(f"❌ 收件人被拒绝: {e}")
|
||
except Exception as e:
|
||
print(f"❌ 其他错误: {type(e).__name__}: {e}")
|
||
|
||
return None
|
||
|
||
|
||
if __name__ == '__main__':
|
||
print("开始测试 mail.sq0715.com 的SMTP配置...")
|
||
successful_config = test_smtp_detailed()
|
||
|
||
if successful_config:
|
||
print(f"\n🎉 找到可用配置!")
|
||
print("请在config.py中使用以下配置:")
|
||
print("-" * 40)
|
||
print(f"MAIL_SERVER = 'mail.sq0715.com'")
|
||
print(f"MAIL_PORT = {successful_config['port']}")
|
||
print(f"MAIL_USE_TLS = {successful_config['use_tls']}")
|
||
print(f"MAIL_USE_SSL = {successful_config['use_ssl']}")
|
||
print(f"MAIL_USERNAME = 'vip@sq0715.com'")
|
||
print(f"MAIL_PASSWORD = 'Aalsq12350501.'")
|
||
print(f"MAIL_DEFAULT_SENDER = 'vip@sq0715.com'")
|
||
else:
|
||
print("\n❌ 所有配置都失败了")
|
||
print("可能的原因:")
|
||
print("1. 邮箱密码不正确")
|
||
print("2. 邮箱服务器不支持SMTP")
|
||
print("3. 需要在邮箱设置中开启SMTP服务")
|
||
print("4. 服务器防火墙阻止了连接")
|
||
|
||
|
||
================================================================================
|
||
导出完成
|
||
导出时间: 2025-07-04 03:35:45
|
||
================================================================================
|