================================================================================ 项目代码导出文件 ================================================================================ 项目名称: 基于Python的线上电商系统 导出时间: 2025-07-09 05:29:12 项目路径: /Users/lishunqin/Desktop/Online_shopping_platform 文件总数: 140 ================================================================================ 📁 文件目录: -------------------------------------------------- README.md (7.4 KB) app/__init__.py (1.3 KB) app/forms.py (5.2 KB) app/models/__init__.py (0.9 KB) app/models/address.py (2.8 KB) app/models/admin.py (1.8 KB) app/models/browse_history.py (3.8 KB) app/models/cart.py (4.5 KB) app/models/favorite.py (3.4 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/css/address_form.css (1.4 KB) app/static/css/addresses.css (1.3 KB) app/static/css/admin_base.css (2.0 KB) app/static/css/admin_categories.css (2.7 KB) app/static/css/admin_dashboard.css (0.6 KB) app/static/css/admin_login.css (1.4 KB) app/static/css/admin_logs.css (5.0 KB) app/static/css/admin_orders.css (3.7 KB) app/static/css/admin_profile.css (4.3 KB) app/static/css/admin_users.css (6.9 KB) app/static/css/auth.css (1.2 KB) app/static/css/base.css (0.6 KB) app/static/css/cart.css (0.4 KB) app/static/css/checkout.css (2.7 KB) app/static/css/favorites.css (2.0 KB) app/static/css/history.css (2.3 KB) app/static/css/index.css (1.6 KB) app/static/css/order_detail.css (2.1 KB) app/static/css/orders.css (2.8 KB) app/static/css/pay.css (2.1 KB) app/static/css/product_detail.css (6.5 KB) app/static/css/product_list.css (0.3 KB) app/static/css/profile.css (5.4 KB) app/static/css/register.css (0.5 KB) app/static/css/review.css (14.2 KB) app/static/js/address_form.js (7.2 KB) app/static/js/addresses.js (2.0 KB) app/static/js/admin_categories.js (9.0 KB) app/static/js/admin_dashboard.js (2.3 KB) app/static/js/admin_logs.js (11.3 KB) app/static/js/admin_orders.js (6.7 KB) app/static/js/admin_users.js (12.5 KB) app/static/js/base.js (5.3 KB) app/static/js/cart.js (6.1 KB) app/static/js/checkout.js (4.7 KB) app/static/js/city_data.js (54.3 KB) app/static/js/favorites.js (6.6 KB) app/static/js/history.js (8.0 KB) app/static/js/order_detail.js (1.5 KB) app/static/js/orders.js (3.4 KB) app/static/js/pay.js (10.3 KB) app/static/js/product_detail.js (13.2 KB) app/static/js/product_list.js (0.2 KB) app/static/js/profile.js (10.7 KB) app/static/js/register.js (4.0 KB) app/static/js/review.js (21.6 KB) app/templates/admin/base.html (5.0 KB) app/templates/admin/categories.html (17.6 KB) app/templates/admin/dashboard.html (7.3 KB) app/templates/admin/login.html (2.7 KB) app/templates/admin/logs.html (11.0 KB) app/templates/admin/order_detail.html (19.8 KB) app/templates/admin/orders.html (17.4 KB) app/templates/admin/product_form.html (32.9 KB) app/templates/admin/products.html (17.6 KB) app/templates/admin/profile.html (6.3 KB) app/templates/admin/users.html (12.7 KB) app/templates/base.html (5.8 KB) app/templates/cart/index.html (9.9 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.1 KB) app/templates/order/checkout.html (12.9 KB) app/templates/order/detail.html (12.6 KB) app/templates/order/pay.html (7.9 KB) app/templates/product/detail.html (18.1 KB) app/templates/product/list.html (13.5 KB) app/templates/review/my_reviews.html (9.0 KB) app/templates/review/write.html (6.0 KB) app/templates/test_upload.html (14.5 KB) app/templates/user/address_form.html (7.4 KB) app/templates/user/addresses.html (6.4 KB) app/templates/user/favorites.html (11.3 KB) app/templates/user/history.html (12.3 KB) app/templates/user/login.html (2.7 KB) app/templates/user/orders.html (13.6 KB) app/templates/user/profile.html (12.4 KB) app/templates/user/register.html (5.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 (3.1 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 (17.9 KB) app/views/auth.py (4.8 KB) app/views/cart.py (7.4 KB) app/views/favorite.py (7.3 KB) app/views/history.py (4.7 KB) app/views/main.py (6.4 KB) app/views/order.py (10.7 KB) app/views/payment.py (8.0 KB) app/views/product.py (23.2 KB) app/views/review.py (8.9 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_product_with_specs.py (4.3 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 📊 大小: 7559 bytes (7.38 KB) 🕒 修改时间: 2025-07-04 19:06:52 🔸==============================================================================
# 🛍️ Python Flask 电商系统 🛍️ **一个功能完善、技术栈现代、可直接部署的线上电商平台毕业设计项目。** ![Python](https://img.shields.io/badge/Python-3.8%2B-blue?style=for-the-badge&logo=python)![Flask](https://img.shields.io/badge/Flask-2.x-black?style=for-the-badge&logo=flask)![MySQL](https://img.shields.io/badge/MySQL-5.7%2B-orange?style=for-the-badge&logo=mysql)![Docker](https://img.shields.io/badge/Docker-Ready-blue?style=for-the-badge&logo=docker)![Tencent Cloud](https://img.shields.io/badge/Tencent_Cloud-COS_%26_CDN-red?style=for-the-badge&logo=tencent-cloud)
--- ## ✨ 项目简介 本项目是一个基于 **Python + Flask** 构建的全功能线上电商系统。它涵盖了从用户注册登录、商品浏览、购物车、下单支付到后台管理的全流程。项目架构清晰,代码结构规范,并集成了**腾讯云COS**、**CDN**等云服务,最终可通过 **Docker** 实现快速、标准化的部署。 ## 🚀 技术栈概览 | 分类 | 技术 | 描述 | | :--- | :--- | :--- | | 🖥️ **后端** | `Python`, `Flask`, `SQLAlchemy` | 核心开发语言、Web框架与ORM | | 🗃️ **数据库** | `MySQL` | 持久化数据存储 | | 🎨 **前端** | `HTML`, `CSS`, `JavaScript`, `Bootstrap` | 页面构建、样式与交互逻辑 | | ☁️ **云服务** | `腾讯云COS`, `腾讯云CDN`, `微信支付` | 对象存储、内容分发网络与支付服务 | | 🐳 **部署** | `Docker`, `Nginx` | 容器化部署与反向代理 | | ⚙️ **工具** | `Flask-Login`, `Flask-WTF`, `Flask-Mail` | 用户认证、表单处理、邮件服务 | ## 🌟 系统核心功能
🛍️ 用户端功能 (点击展开) - **👤 用户中心** - 手机/邮箱注册登录,支持短信/邮箱验证码 - 微信授权登录(可选) - 个人信息编辑(昵称、头像、性别等) - 头像上传至腾讯云COS - 收货地址管理(增删改查、设为默认) - 我的收藏夹 & 浏览历史 - **🛒 购物流程** - 多级商品分类导航 - 商品列表(分页、排序、筛选、搜索) - 商品详情页(轮播图、规格选择、用户评价) - 购物车(添加、修改数量、删除、结算) - 未登录用户购物车(`localStorage` 支持) - **💳 订单与支付** - 创建订单,填写备注 - **微信支付**(PC端扫码、移动端JSAPI) - 订单状态跟踪(待支付、待发货、待收货、待评价...) - 查看订单详情与物流信息 - 取消订单、申请退款、确认收货 - **✍️ 评价系统** - 对已完成订单的商品进行评价(评分、文字、图片) - 匿名评价选项
🔧 管理后台功能 (点击展开) - **📊 数据看板 (Dashboard)** - 销售额、订单量、用户增长等核心指标可视化 - **📦 商品管理** - 商品分类的增删改查 - 商品信息管理(上架/下架、编辑、库存、价格) - 商品规格与属性管理 - **📋 订单管理** - 按状态筛选和搜索订单 - 查看订单详情 - 执行发货操作(填写物流信息) - 处理用户退款申请 - **👥 用户管理** - 查看用户列表 - 禁用/启用用户账户 - **⚙️ 系统设置** - 网站基本信息配置 - 支付接口与云存储配置
## 📂 项目结构 ``` Online_shopping_platform/ ├── app/ # 核心应用目录 │ ├── models/ # 📦 数据模型 (ORM) │ ├── views/ # 👁️ 视图函数 (Controllers) │ ├── templates/ # 📄 HTML模板 │ ├── static/ # 🎨 静态资源 (CSS, JS, Images) │ ├── utils/ # 🛠️ 工具模块 (支付, COS, 邮件等) │ ├── forms.py # 📝 表单定义 │ └── __init__.py # 🚀 应用工厂函数 ├── config/ # ⚙️ 配置文件 ├── docker/ # 🐳 Docker 相关配置 ├── logs/ # 📜 日志文件 ├── tests/ # 🧪 测试用例 ├── requirements.txt # 📦 Python 依赖 ├── run.py # ▶️ 应用启动脚本 └── README.md # 📖 你正在阅读的文件 ``` ## 🗄️ 数据库设计 项目数据库设计遵循电商业务逻辑,结构清晰,关系明确。
查看核心数据表 (点击展开) | 表名 | 用途 | | :--- | :--- | | `users` | 存储用户信息 | | `user_addresses` | 用户收货地址 | | `products` | 商品基本信息 (SPU) | | `product_inventory` | 商品库存单元 (SKU) | | `categories` | 商品分类 | | `cart` | 购物车 | | `orders` | 订单主表 | | `order_items` | 订单详情表 | | `payments` | 支付记录 | | `reviews` | 商品评价 | | `admin_users` | 后台管理员 |
> 完整的 `CREATE TABLE` SQL语句已在项目文件中提供,包含了详细的字段、索引和外键设计。 ## 🛠️ 本地运行与部署 ### 1. 环境准备 - 克隆项目到本地 ```bash git clone cd Online_shopping_platform ``` - 创建并激活Python虚拟环境 ```bash python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows ``` - 安装项目依赖 ```bash pip install -r requirements.txt ``` ### 2. 配置 - 复制 `.env.example` 文件为 `.env` (如果提供)。 - 根据提示修改 `config/` 目录下的配置文件,或设置环境变量,填入你的: - **数据库连接信息** - **Flask `SECRET_KEY`** - **腾讯云 `COS` 和 `CDN` 配置** - **微信支付商户信息** - **邮件服务器配置** ### 3. 初始化 - 初始化数据库表结构 ```bash # 在Flask应用上下文中执行 flask db init # 如果使用Flask-Migrate flask db migrate flask db upgrade # 或者使用自定义脚本 python -c "from app import create_app, db; app = create_app(); app.app_context().push(); db.create_all()" ``` - 创建第一个管理员账户 ```bash python create_admin.py ``` ### 4. 启动! - 运行开发服务器 ```bash python run.py ``` - 🎉 恭喜!现在可以访问 `http://127.0.0.1:5000` 查看你的电商网站了。 ### 5. Docker 部署 (推荐) - 确保已安装 Docker 和 Docker Compose。 - 在项目根目录下执行: ```bash docker-compose up --build -d ``` - 服务将在后台启动,并通过 Nginx 代理对外提供服务。 ## 💡 项目亮点 - **🛡️ 安全性**: 全面考虑了常见的Web安全问题,如 `SQL注入`、`XSS`、`CSRF`,并进行了有效防护。 - **⚡ 性能优化**: 集成了 `CDN加速`、`图片懒加载`、`数据库查询优化` 等多种性能优化手段。 - **📱 响应式设计**: 基于 Bootstrap 框架,完美适配PC和移动设备,提供一致的用户体验。 - **🧩 模块化设计**: 代码结构清晰,功能高度解耦,易于维护和二次开发。 - **☁️ 云原生支持**: 无缝集成云存储和CDN,为高并发和大数据量场景打下基础。 ---

本项目为毕业设计作品,旨在展示一个完整的Web应用开发流程。

作者:林金兴 | 指导老师:[指导老师姓名]

🔸============================================================================== 📄 文件: app/__init__.py 📊 大小: 1326 bytes (1.29 KB) 🕒 修改时间: 2025-07-09 02:21:01 🔸============================================================================== from flask import Flask from config.database import init_db from config.config import Config def create_app(config_name=None): app = Flask(__name__) app.config.from_object(Config) # 初始化数据库 init_db(app) # 注册蓝图 from app.views.auth import auth_bp from app.views.main import main_bp from app.views.user import user_bp from app.views.product import product_bp from app.views.cart import cart_bp from app.views.order import order_bp from app.views.payment import payment_bp from app.views.admin import admin_bp from app.views.address import address_bp from app.views.upload import upload_bp from app.views.review import review_bp from app.views.favorite import favorite_bp from app.views.history import history_bp app.register_blueprint(auth_bp) app.register_blueprint(main_bp) app.register_blueprint(user_bp) app.register_blueprint(product_bp) app.register_blueprint(cart_bp) app.register_blueprint(order_bp) app.register_blueprint(payment_bp) app.register_blueprint(admin_bp) app.register_blueprint(address_bp) app.register_blueprint(upload_bp) app.register_blueprint(review_bp) app.register_blueprint(favorite_bp) app.register_blueprint(history_bp) return app 🔸============================================================================== 📄 文件: 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 📊 大小: 952 bytes (0.93 KB) 🕒 修改时间: 2025-07-09 02:20:50 🔸============================================================================== 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 from app.models.favorite import UserFavorite from app.models.browse_history import BrowseHistory __all__ = [ 'User', 'EmailVerification', 'AdminUser', 'OperationLog', 'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue', 'ProductInventory', 'InventoryLog', 'ProductSpecRelation', 'Cart', 'UserAddress', 'Order', 'OrderItem', 'ShippingInfo', 'Payment', 'Review', 'UserFavorite', 'BrowseHistory' ] 🔸============================================================================== 📄 文件: 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'' 🔸============================================================================== 📄 文件: 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'' 🔸============================================================================== 📄 文件: app/models/browse_history.py 📊 大小: 3903 bytes (3.81 KB) 🕒 修改时间: 2025-07-09 02:19:42 🔸============================================================================== """ 浏览历史模型 """ from datetime import datetime from config.database import db from app.models.product import Product from app.models.user import User class BrowseHistory(db.Model): __tablename__ = 'browse_history' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) viewed_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关联关系 user = db.relationship('User', backref='browse_history') product = db.relationship('Product', backref='viewed_by') # 唯一约束 __table_args__ = (db.UniqueConstraint('user_id', 'product_id', name='uk_user_product'),) def to_dict(self): """转换为字典""" return { 'id': self.id, 'user_id': self.user_id, 'product_id': self.product_id, 'viewed_at': self.viewed_at.isoformat() if self.viewed_at else None, 'product': { 'id': self.product.id, 'name': self.product.name, 'price': float(self.product.price), 'main_image': self.product.main_image, 'status': self.product.status, 'sales_count': self.product.sales_count, 'category': self.product.category.name if self.product.category else None } if self.product else None } @classmethod def add_history(cls, user_id, product_id): """添加浏览记录""" # 检查商品是否存在 product = Product.query.get(product_id) if not product: return False, "商品不存在" # 查找已有记录 history = cls.query.filter_by(user_id=user_id, product_id=product_id).first() if history: # 更新浏览时间 history.viewed_at = datetime.utcnow() else: # 创建新记录 history = cls(user_id=user_id, product_id=product_id) db.session.add(history) try: db.session.commit() return True, "浏览记录添加成功" except Exception as e: db.session.rollback() return False, f"添加浏览记录失败: {str(e)}" @classmethod def get_user_history(cls, user_id, page=1, per_page=20): """获取用户浏览历史""" return cls.query.filter_by(user_id=user_id) \ .join(Product) \ .filter(Product.status == 1) \ .order_by(cls.viewed_at.desc()) \ .paginate(page=page, per_page=per_page, error_out=False) @classmethod def get_user_history_count(cls, user_id): """获取用户浏览历史数量""" return cls.query.filter_by(user_id=user_id).count() @classmethod def clear_user_history(cls, user_id): """清空用户浏览历史""" try: cls.query.filter_by(user_id=user_id).delete() db.session.commit() return True, "浏览历史清空成功" except Exception as e: db.session.rollback() return False, f"清空浏览历史失败: {str(e)}" @classmethod def remove_history_item(cls, user_id, product_id): """删除单个浏览记录""" history = cls.query.filter_by(user_id=user_id, product_id=product_id).first() if not history: return False, "浏览记录不存在" db.session.delete(history) try: db.session.commit() return True, "浏览记录删除成功" except Exception as e: db.session.rollback() return False, f"删除浏览记录失败: {str(e)}" def __repr__(self): return f'' 🔸============================================================================== 📄 文件: 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'' 🔸============================================================================== 📄 文件: app/models/favorite.py 📊 大小: 3502 bytes (3.42 KB) 🕒 修改时间: 2025-07-09 02:19:33 🔸============================================================================== """ 用户收藏模型 """ from datetime import datetime from config.database import db from app.models.product import Product from app.models.user import User class UserFavorite(db.Model): __tablename__ = 'user_favorites' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 关联关系 user = db.relationship('User', backref='favorites') product = db.relationship('Product', backref='favorited_by') # 唯一约束 __table_args__ = (db.UniqueConstraint('user_id', 'product_id', name='uk_user_product'),) def to_dict(self): """转换为字典""" return { 'id': self.id, 'user_id': self.user_id, 'product_id': self.product_id, 'created_at': self.created_at.isoformat() if self.created_at else None, 'product': { 'id': self.product.id, 'name': self.product.name, 'price': float(self.product.price), 'main_image': self.product.main_image, 'status': self.product.status, 'sales_count': self.product.sales_count } if self.product else None } @classmethod def is_favorited(cls, user_id, product_id): """检查用户是否收藏了某商品""" return cls.query.filter_by(user_id=user_id, product_id=product_id).first() is not None @classmethod def add_favorite(cls, user_id, product_id): """添加收藏""" # 检查是否已存在 existing = cls.query.filter_by(user_id=user_id, product_id=product_id).first() if existing: return False, "商品已在收藏夹中" # 检查商品是否存在 product = Product.query.get(product_id) if not product: return False, "商品不存在" # 添加收藏 favorite = cls(user_id=user_id, product_id=product_id) db.session.add(favorite) try: db.session.commit() return True, "收藏成功" except Exception as e: db.session.rollback() return False, f"收藏失败: {str(e)}" @classmethod def remove_favorite(cls, user_id, product_id): """取消收藏""" favorite = cls.query.filter_by(user_id=user_id, product_id=product_id).first() if not favorite: return False, "商品未收藏" db.session.delete(favorite) try: db.session.commit() return True, "取消收藏成功" except Exception as e: db.session.rollback() return False, f"取消收藏失败: {str(e)}" @classmethod def get_user_favorites(cls, user_id, page=1, per_page=20): """获取用户收藏列表""" return cls.query.filter_by(user_id=user_id) \ .join(Product) \ .filter(Product.status == 1) \ .order_by(cls.created_at.desc()) \ .paginate(page=page, per_page=per_page, error_out=False) @classmethod def get_user_favorites_count(cls, user_id): """获取用户收藏数量""" return cls.query.filter_by(user_id=user_id).count() def __repr__(self): return f'' 🔸============================================================================== 📄 文件: 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'' 🔸============================================================================== 📄 文件: 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'' 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'' 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'' 🔸============================================================================== 📄 文件: 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'' 🔸============================================================================== 📄 文件: 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'' 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'' 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'' 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'' 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'' 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'' 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'' 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'' 🔸============================================================================== 📄 文件: 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'' 🔸============================================================================== 📄 文件: 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'' 🔸============================================================================== 📄 文件: 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/css/address_form.css 📊 大小: 1445 bytes (1.41 KB) 🕒 修改时间: 2025-07-04 04:02:17 🔸============================================================================== /* 地址表单页面样式 */ .form-label { font-weight: 500; margin-bottom: 0.5rem; } .form-label .text-danger { font-size: 0.9em; } .form-select:focus, .form-control:focus { border-color: #86b7fe; box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); } .text-danger { font-size: 0.875em; margin-top: 0.25rem; } /* 调试信息样式 */ .alert-info { border-left: 4px solid #0dcaf0; background-color: #cff4fc; border-color: #b8daff; } #debugInfo { font-family: 'Courier New', monospace; font-size: 0.9em; margin-top: 0.5rem; } /* 表单布局优化 */ .row .col-md-4, .row .col-md-6, .row .col-md-8 { margin-bottom: 0; } .mb-3 { margin-bottom: 1rem !important; } /* 按钮组样式 */ .d-flex.gap-2 { gap: 0.5rem !important; } .btn { padding: 0.5rem 1rem; font-weight: 500; } /* 复选框样式 */ .form-check { padding-left: 1.5em; } .form-check-input { margin-top: 0.25em; } .form-check-label { font-weight: 500; cursor: pointer; } /* 响应式设计 */ @media (max-width: 768px) { .col-md-4, .col-md-6, .col-md-8 { margin-bottom: 1rem; } .d-flex.gap-2 { flex-direction: column; } .d-flex.gap-2 .btn { width: 100%; } } /* 加载状态样式 */ .form-select:disabled { background-color: #e9ecef; opacity: 0.65; } .loading-text { color: #6c757d; font-style: italic; } 🔸============================================================================== 📄 文件: app/static/css/addresses.css 📊 大小: 1295 bytes (1.26 KB) 🕒 修改时间: 2025-07-04 04:00:58 🔸============================================================================== /* 地址管理页面样式 */ .address-card { transition: all 0.3s ease; cursor: pointer; } .address-card:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.1); } .address-card.border-primary { border-width: 2px !important; } .address-card .card-body { position: relative; } .dropdown-toggle::after { display: none; } /* 空状态样式 */ .empty-state { padding: 3rem 0; } .empty-state i { opacity: 0.5; } /* 地址卡片内容样式 */ .address-card .card-title { font-size: 1.1rem; margin-bottom: 0.5rem; } .address-card .badge { font-size: 0.75rem; padding: 0.25rem 0.5rem; } .address-card .text-muted { font-size: 0.9rem; } /* 下拉菜单样式 */ .dropdown-menu { min-width: 120px; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); border: 1px solid rgba(0,0,0,.125); } .dropdown-item { padding: 0.5rem 1rem; font-size: 0.9rem; } .dropdown-item:hover { background-color: #f8f9fa; } .dropdown-item.text-danger:hover { background-color: #f8d7da; color: #721c24 !important; } /* 响应式设计 */ @media (max-width: 768px) { .address-card { margin-bottom: 1rem; } .col-md-6 { padding-left: 0.75rem; padding-right: 0.75rem; } } 🔸============================================================================== 📄 文件: app/static/css/admin_base.css 📊 大小: 2094 bytes (2.04 KB) 🕒 修改时间: 2025-07-04 14:51:53 🔸============================================================================== :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); } 🔸============================================================================== 📄 文件: app/static/css/admin_categories.css 📊 大小: 2763 bytes (2.70 KB) 🕒 修改时间: 2025-07-04 18:44:46 🔸============================================================================== /* 分类管理页面样式 */ .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; } 🔸============================================================================== 📄 文件: app/static/css/admin_dashboard.css 📊 大小: 627 bytes (0.61 KB) 🕒 修改时间: 2025-07-04 14:51:53 🔸============================================================================== /* Dashboard specific styles */ .dashboard-stats { margin-bottom: 30px; } .chart-container { position: relative; height: 300px; } .system-status-item { display: flex; justify-content: between; align-items: center; margin-bottom: 15px; padding: 10px 0; border-bottom: 1px solid #f0f0f0; } .system-status-item:last-child { border-bottom: none; margin-bottom: 0; } .log-table-container { margin-top: 30px; } .empty-state { text-align: center; padding: 40px 20px; color: #6c757d; } .empty-state i { font-size: 3rem; margin-bottom: 15px; opacity: 0.5; } 🔸============================================================================== 📄 文件: app/static/css/admin_login.css 📊 大小: 1444 bytes (1.41 KB) 🕒 修改时间: 2025-07-04 14:51:53 🔸============================================================================== 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; } 🔸============================================================================== 📄 文件: app/static/css/admin_logs.css 📊 大小: 5113 bytes (4.99 KB) 🕒 修改时间: 2025-07-09 01:54:54 🔸============================================================================== /* 操作日志页面样式 */ .admin-logs { padding: 0; } /* 统计卡片样式 */ .stats-card { border: none; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s; } .stats-card:hover { transform: translateY(-2px); } .stats-card .card-title { font-size: 1.8rem; font-weight: 600; color: #333; margin-bottom: 0.25rem; } .stats-card .card-text { color: #666; font-size: 0.9rem; margin-bottom: 0; } .icon-wrapper { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; color: white; } .icon-wrapper.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .icon-wrapper.success { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); } .icon-wrapper.warning { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); } .icon-wrapper.info { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); color: #333; } /* 表格样式 */ .table th { background-color: #f8f9fa; border-bottom: 2px solid #dee2e6; font-weight: 600; color: #495057; font-size: 0.9rem; } .table td { vertical-align: middle; padding: 1rem 0.75rem; font-size: 0.875rem; } .table-hover tbody tr:hover { background-color: #f8f9fa; } /* 操作类型样式 */ .operation-action { display: inline-block; padding: 0.25rem 0.5rem; background-color: #e9ecef; border-radius: 6px; font-size: 0.8rem; font-weight: 500; color: #495057; } /* 资源类型样式 */ .resource-type { background-color: #d4edda; color: #155724; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.75rem; font-weight: 500; } .resource-id { color: #6c757d; font-size: 0.8rem; margin-left: 0.25rem; } /* 用户代理样式 */ .user-agent-wrapper { max-width: 200px; } .user-agent { display: block; font-size: 0.8rem; color: #6c757d; cursor: help; line-height: 1.2; } /* 徽章样式 */ .badge { font-size: 0.7rem; font-weight: 500; padding: 0.3em 0.6em; } /* 时间显示样式 */ .table td:first-child { white-space: nowrap; min-width: 110px; } .table td:first-child small { font-size: 0.75rem; } /* 空状态样式 */ .empty-state { text-align: center; padding: 3rem 1rem; color: #6c757d; } .empty-state i { font-size: 3rem; margin-bottom: 1rem; color: #dee2e6; } .empty-state div { font-size: 1.1rem; margin-bottom: 0.5rem; } /* 响应式设计 */ @media (max-width: 768px) { .table-responsive { font-size: 0.8rem; } .table th, .table td { padding: 0.75rem 0.5rem; } .stats-card .card-title { font-size: 1.5rem; } .icon-wrapper { width: 40px; height: 40px; font-size: 1.2rem; } .user-agent-wrapper { max-width: 150px; } } /* 筛选表单样式 */ .card .form-label { font-weight: 500; color: #495057; } .form-control:focus, .form-select:focus { border-color: #667eea; box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); } /* 分页样式 */ .pagination .page-link { color: #667eea; border-color: #dee2e6; } .pagination .page-link:hover { color: #495057; background-color: #f8f9fa; border-color: #dee2e6; } .pagination .page-item.active .page-link { background-color: #667eea; border-color: #667eea; } /* 代码样式 */ code { background-color: #f8f9fa; color: #e83e8c; padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.8rem; } /* 表格滚动条样式 */ .table-responsive::-webkit-scrollbar { height: 8px; } .table-responsive::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .table-responsive::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; } .table-responsive::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } /* 卡片头部样式 */ .card-header h5 { color: #333; font-weight: 600; } .card-header small { font-weight: 400; } /* 筛选区域样式 */ .card-body form { margin-bottom: 0; } .card-body .btn { height: 38px; margin-top: 0.5rem; } /* 日志详情样式 */ .log-detail-btn { font-size: 0.8rem; padding: 0.2rem 0.4rem; border-radius: 4px; } /* 操作者信息样式 */ .badge.bg-warning { background-color: #ffc107 !important; color: #212529 !important; } .badge.bg-info { background-color: #0dcaf0 !important; color: #000 !important; } /* 分页信息样式 */ .card-footer { padding: 1rem 1.5rem; background-color: #f8f9fa !important; border-top: 1px solid #dee2e6; } /* 加载状态 */ .loading { text-align: center; padding: 2rem; color: #6c757d; } .loading i { font-size: 2rem; margin-bottom: 1rem; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } 🔸============================================================================== 📄 文件: app/static/css/admin_orders.css 📊 大小: 3820 bytes (3.73 KB) 🕒 修改时间: 2025-07-08 19:56:29 🔸============================================================================== /* 订单管理样式 */ .admin-orders { padding: 0; } /* 统计卡片 - 修复颜色问题,使用更高优先级 */ .admin-orders .stats-card { background: #ffffff !important; color: #2c3e50 !important; border: 1px solid #e9ecef !important; border-radius: 0.5rem !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important; transition: transform 0.2s, box-shadow 0.2s; padding: 1.25rem !important; } .admin-orders .stats-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important; } .admin-orders .stats-card .card-body { padding: 0 !important; text-align: center; } .stats-number { font-size: 2.2rem; font-weight: bold; color: #2c3e50 !important; line-height: 1.2; margin-bottom: 0.25rem; } .stats-label { font-size: 0.9rem; color: #6c757d !important; font-weight: 500; } /* 状态特定颜色 - 使用更明显的颜色对比 */ .admin-orders .stats-card.pending-payment { border-left: 4px solid #ffc107 !important; } .admin-orders .stats-card.pending-payment .stats-number { color: #f39c12 !important; } .admin-orders .stats-card.pending-shipment { border-left: 4px solid #17a2b8 !important; } .admin-orders .stats-card.pending-shipment .stats-number { color: #17a2b8 !important; } .admin-orders .stats-card.shipped { border-left: 4px solid #28a745 !important; } .admin-orders .stats-card.shipped .stats-number { color: #28a745 !important; } .admin-orders .stats-card.completed { border-left: 4px solid #6f42c1 !important; } .admin-orders .stats-card.completed .stats-number { color: #6f42c1 !important; } .admin-orders .stats-card.cancelled { border-left: 4px solid #dc3545 !important; } .admin-orders .stats-card.cancelled .stats-number { color: #dc3545 !important; } /* 订单状态徽章 */ .order-status-1 { background-color: #ffc107; color: #212529; } .order-status-2 { background-color: #17a2b8; color: #fff; } .order-status-3 { background-color: #28a745; color: #fff; } .order-status-4 { background-color: #fd7e14; color: #fff; } .order-status-5 { background-color: #6f42c1; color: #fff; } .order-status-6 { background-color: #dc3545; color: #fff; } .order-status-7 { background-color: #e83e8c; color: #fff; } /* 表格样式 */ .table th { background-color: #f8f9fa; font-weight: 600; border-bottom: 2px solid #dee2e6; } .table td { vertical-align: middle; } .table-hover tbody tr:hover { background-color: #f8f9fa; } /* 操作按钮组 */ .btn-group .btn { border-radius: 0.375rem; margin-right: 0.25rem; } .btn-group .btn:last-child { margin-right: 0; } /* 商品缩略图 */ .product-thumb { border-radius: 0.375rem; border: 1px solid #dee2e6; } /* 订单详情页面 */ .admin-order-detail .card { border: none; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } .admin-order-detail .card-header { background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; } .admin-order-detail .table th { background-color: transparent; border-bottom: 1px solid #dee2e6; font-weight: 600; } .admin-order-detail .table td { border-bottom: 1px solid #dee2e6; } /* 模态框样式 */ .modal-header { background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; } .modal-body .form-label { font-weight: 600; } /* 响应式设计 */ @media (max-width: 768px) { .admin-orders .stats-card { margin-bottom: 1rem; } .stats-number { font-size: 1.8rem; } .btn-group { flex-direction: column; gap: 0.25rem; } .btn-group .btn { margin-right: 0; } .table-responsive { font-size: 0.875rem; } } 🔸============================================================================== 📄 文件: app/static/css/admin_profile.css 📊 大小: 4383 bytes (4.28 KB) 🕒 修改时间: 2025-07-04 18:44:46 🔸============================================================================== /* 管理员个人资料页面样式 */ .profile-container { padding: 20px 0; } .profile-card { border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); margin-bottom: 20px; } .profile-card .card-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px 12px 0 0 !important; padding: 15px 20px; } .profile-card .card-header h5 { margin: 0; font-weight: 600; } .profile-card .card-header i { margin-right: 8px; } .profile-card .card-body { padding: 25px; } .form-label { font-weight: 600; color: #495057; margin-bottom: 8px; } .form-control { border-radius: 8px; border: 1px solid #e0e6ed; padding: 12px 15px; transition: all 0.3s ease; } .form-control:focus { border-color: #667eea; box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); } .form-control[readonly] { background-color: #f8f9fa; color: #6c757d; } .form-text { font-size: 12px; color: #6c757d; margin-top: 5px; } .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 8px; padding: 12px 24px; font-weight: 600; transition: all 0.3s ease; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } .btn-warning { background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); border: none; border-radius: 8px; padding: 12px 24px; font-weight: 600; color: #8b4513; transition: all 0.3s ease; } .btn-warning:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(252, 182, 159, 0.4); color: #8b4513; } .btn i { margin-right: 6px; } /* 账号信息卡片 */ .info-card { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); border: none; border-radius: 12px; margin-bottom: 20px; } .info-card .card-header { background: rgba(255, 255, 255, 0.2); border: none; border-radius: 12px 12px 0 0 !important; } .info-card .card-body { background: rgba(255, 255, 255, 0.1); border-radius: 0 0 12px 12px; } .info-item { margin-bottom: 15px; padding: 10px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.2); } .info-item:last-child { border-bottom: none; margin-bottom: 0; } .info-item strong { color: #2c3e50; font-weight: 600; } .badge { font-size: 12px; padding: 6px 12px; border-radius: 20px; font-weight: 500; } .badge.bg-success { background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%) !important; color: #2c3e50; } .badge.bg-danger { background: linear-gradient(135deg, #fc466b 0%, #3f5efb 100%) !important; color: white; } /* 密码修改卡片 */ .password-card { background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); border: none; border-radius: 12px; } .password-card .card-header { background: rgba(255, 255, 255, 0.2); border: none; border-radius: 12px 12px 0 0 !important; } .password-card .card-body { background: rgba(255, 255, 255, 0.1); border-radius: 0 0 12px 12px; } .password-card .form-control { background: rgba(255, 255, 255, 0.8); border: 1px solid rgba(255, 255, 255, 0.3); } .password-card .form-control:focus { background: white; border-color: #fcb69f; box-shadow: 0 0 0 0.2rem rgba(252, 182, 159, 0.25); } /* 响应式设计 */ @media (max-width: 768px) { .profile-container { padding: 10px 0; } .profile-card .card-body { padding: 20px 15px; } .row .col-md-6 { margin-bottom: 15px; } } /* 动画效果 */ .profile-card { animation: fadeInUp 0.6s ease-out; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } /* 表单验证样式 */ .form-control.is-invalid { border-color: #dc3545; box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .form-control.is-valid { border-color: #28a745; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .invalid-feedback { display: block; color: #dc3545; font-size: 12px; margin-top: 5px; } .valid-feedback { display: block; color: #28a745; font-size: 12px; margin-top: 5px; } 🔸============================================================================== 📄 文件: app/static/css/admin_users.css 📊 大小: 7104 bytes (6.94 KB) 🕒 修改时间: 2025-07-09 02:08:32 🔸============================================================================== /* 用户管理页面样式 */ .admin-users { padding: 0; } /* 统计卡片样式 */ .stats-card { border: none; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s; } .stats-card:hover { transform: translateY(-2px); } .stats-card .card-title { font-size: 1.8rem; font-weight: 600; color: #333; margin-bottom: 0.25rem; } .stats-card .card-text { color: #666; font-size: 0.9rem; margin-bottom: 0; } .icon-wrapper { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; color: white; } .icon-wrapper.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .icon-wrapper.success { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); } .icon-wrapper.danger { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); } .icon-wrapper.info { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); color: #333; } /* 用户头像样式 - 表格中的头像 */ .avatar-wrapper { width: 48px !important; height: 48px !important; position: relative; overflow: hidden !important; flex-shrink: 0 !important; flex-grow: 0 !important; } .user-avatar { width: 48px !important; height: 48px !important; border-radius: 50% !important; object-fit: cover !important; border: 2px solid #f8f9fa !important; display: block !important; max-width: 48px !important; max-height: 48px !important; min-width: 48px !important; min-height: 48px !important; flex-shrink: 0 !important; flex-grow: 0 !important; } .user-avatar-placeholder { width: 48px !important; height: 48px !important; border-radius: 50% !important; background: #e9ecef !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: 1.2rem !important; color: #6c757d !important; flex-shrink: 0 !important; flex-grow: 0 !important; } /* 用户详情模态框中的头像容器 */ .user-avatar-large-wrapper { width: 80px !important; height: 80px !important; margin: 0 auto !important; overflow: hidden !important; border-radius: 50% !important; position: relative !important; flex-shrink: 0 !important; flex-grow: 0 !important; } /* 用户详情模态框中的头像 */ .avatar-large { width: 80px !important; height: 80px !important; border-radius: 50% !important; object-fit: cover !important; border: 3px solid #f8f9fa !important; display: block !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; flex-shrink: 0 !important; flex-grow: 0 !important; } .avatar-placeholder-large { width: 80px !important; height: 80px !important; border-radius: 50% !important; background: #e9ecef !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: 2rem !important; color: #6c757d !important; flex-shrink: 0 !important; flex-grow: 0 !important; } /* 强制覆盖Bootstrap的所有可能的图片样式 */ .user-detail img, .table img, .modal img { max-width: none !important; max-height: none !important; } .user-detail img.avatar-large, .modal img.avatar-large { width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; } .table img.user-avatar { width: 48px !important; height: 48px !important; max-width: 48px !important; max-height: 48px !important; min-width: 48px !important; min-height: 48px !important; } /* 表格样式 */ .table th { background-color: #f8f9fa; border-bottom: 2px solid #dee2e6; font-weight: 600; color: #495057; } .table td { vertical-align: middle; padding: 1rem 0.75rem; } .table-hover tbody tr:hover { background-color: #f8f9fa; } /* 按钮组样式 */ .btn-group .btn { padding: 0.375rem 0.75rem; font-size: 0.875rem; } /* 空状态样式 */ .empty-state { text-align: center; padding: 3rem 1rem; color: #6c757d; } .empty-state i { font-size: 3rem; margin-bottom: 1rem; color: #dee2e6; } .empty-state div { font-size: 1.1rem; margin-bottom: 0.5rem; } /* 用户详情信息样式 */ .user-detail { padding: 1rem; } .user-info-list { margin-top: 1rem; } .user-info-item { display: flex; align-items: center; padding: 0.75rem 0; border-bottom: 1px solid #f8f9fa; } .user-info-item:last-child { border-bottom: none; } .user-info-label { font-weight: 500; color: #495057; width: 120px; flex-shrink: 0; } .user-info-value { color: #333; flex: 1; } /* 响应式设计 */ @media (max-width: 768px) { .table-responsive { font-size: 0.875rem; } .btn-group .btn { padding: 0.25rem 0.5rem; font-size: 0.75rem; } .stats-card .card-title { font-size: 1.5rem; } .icon-wrapper { width: 40px; height: 40px; font-size: 1.2rem; } .user-avatar { width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; } .avatar-wrapper { width: 40px !important; height: 40px !important; } .user-avatar-placeholder { width: 40px !important; height: 40px !important; font-size: 1rem !important; } } /* 筛选表单样式 */ .card .form-label { font-weight: 500; color: #495057; } .form-control:focus, .form-select:focus { border-color: #667eea; box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); } /* 分页样式 */ .pagination .page-link { color: #667eea; border-color: #dee2e6; } .pagination .page-link:hover { color: #495057; background-color: #f8f9fa; border-color: #dee2e6; } .pagination .page-item.active .page-link { background-color: #667eea; border-color: #667eea; } /* 用户详情模态框样式 */ .modal-content { border-radius: 12px; border: none; box-shadow: 0 10px 30px rgba(0,0,0,0.15); } .modal-header { background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; border-radius: 12px 12px 0 0; } .modal-title { font-weight: 600; color: #333; } /* 徽章样式 */ .badge { font-size: 0.75rem; font-weight: 500; padding: 0.35em 0.65em; } /* 加载状态 */ .loading { text-align: center; padding: 2rem; color: #6c757d; } .loading i { font-size: 2rem; margin-bottom: 1rem; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } 🔸============================================================================== 📄 文件: app/static/css/auth.css 📊 大小: 1183 bytes (1.16 KB) 🕒 修改时间: 2025-07-04 03:54:59 🔸============================================================================== /* 认证页面样式 */ .auth-container { min-height: 60vh; display: flex; align-items: center; } .auth-card { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border: none; border-radius: 10px; } .auth-card .card-header { background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); color: white; border-radius: 10px 10px 0 0; border: none; } .auth-card .card-header h4 { margin: 0; font-weight: 500; } .auth-card .card-body { padding: 2rem; } /* 表单样式 */ .form-control:focus { border-color: #007bff; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .btn-primary { background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); border: none; padding: 0.75rem; font-weight: 500; } .btn-primary:hover { background: linear-gradient(135deg, #0056b3 0%, #004085 100%); transform: translateY(-1px); } /* 链接样式 */ .auth-link { color: #007bff; text-decoration: none; font-weight: 500; } .auth-link:hover { color: #0056b3; text-decoration: underline; } /* 响应式设计 */ @media (max-width: 576px) { .auth-card .card-body { padding: 1.5rem; } } 🔸============================================================================== 📄 文件: app/static/css/base.css 📊 大小: 658 bytes (0.64 KB) 🕒 修改时间: 2025-07-04 03:53:30 🔸============================================================================== /* 基础样式 */ .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; } .cart-badge { position: relative; top: -2px; font-size: 0.7rem; } /* 响应式设计 */ @media (max-width: 768px) { .search-form { max-width: 100%; margin: 10px 0; } } /* 返回顶部按钮 */ #backToTop { display: none; z-index: 1000; } /* 成功提示框样式 */ .success-toast { top: 20px; right: 20px; z-index: 9999; min-width: 300px; } 🔸============================================================================== 📄 文件: app/static/css/cart.css 📊 大小: 379 bytes (0.37 KB) 🕒 修改时间: 2025-07-04 14:40:23 🔸============================================================================== .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; } } 🔸============================================================================== 📄 文件: app/static/css/checkout.css 📊 大小: 2715 bytes (2.65 KB) 🕒 修改时间: 2025-07-08 17:14:27 🔸============================================================================== /* 订单结算页面样式 */ .checkout-section { margin-bottom: 1.5rem; } .address-card { cursor: pointer; transition: all 0.3s ease; border: 2px solid #e9ecef; } .address-card:hover { border-color: #007bff; box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1); } .address-card.selected { border-color: #007bff; background-color: #e7f3ff; } .product-item { padding: 1rem 0; border-bottom: 1px solid #e9ecef; } .product-item:last-child { border-bottom: none; } .order-summary { background: #f8f9fa; padding: 1rem; border-radius: 0.5rem; } .price-row { display: flex; justify-content: space-between; margin-bottom: 0.5rem; } .price-row.total-price { font-size: 1.1rem; font-weight: bold; color: #dc3545; } .form-check { padding: 0.75rem; border: 1px solid #e9ecef; border-radius: 0.5rem; margin-bottom: 0.5rem; transition: all 0.3s ease; } .form-check:hover { border-color: #007bff; background-color: #f8f9fa; } .form-check-input:checked + .form-check-label { color: #007bff; } /* 支付方式特殊样式 */ .form-check input[type="radio"][value="simulate"]:checked + label { color: #ffc107; } .form-check input[type="radio"][value="simulate"]:checked + label i { color: #ffc107 !important; } /* 模拟支付说明样式 */ .alert-warning { border-left: 4px solid #ffc107; } /* 响应式设计 */ @media (max-width: 768px) { .checkout-section .col-md-4, .checkout-section .col-md-3 { margin-bottom: 1rem; } .address-card { margin-bottom: 1rem; } .product-item .col-md-2, .product-item .col-md-6 { margin-bottom: 0.5rem; } } /* 动画效果 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .alert { animation: fadeIn 0.3s ease; } /* 按钮样式 */ .btn-lg { padding: 0.75rem 1.5rem; font-size: 1.1rem; } /* 卡片头部样式 */ .card-header h5 { margin-bottom: 0; color: #495057; } .card-header i { margin-right: 0.5rem; color: #007bff; } /* 表单标签样式 */ .form-check-label { cursor: pointer; width: 100%; } .form-check-label strong { display: block; margin-bottom: 0.25rem; } .form-check-label small { color: #6c757d; } /* 商品图片样式 */ .product-item img { max-height: 80px; object-fit: cover; } /* 价格显示样式 */ .fw-bold { color: #dc3545; } /* 面包屑导航样式 */ .breadcrumb { background: transparent; padding: 0; } .breadcrumb-item + .breadcrumb-item::before { color: #6c757d; } 🔸============================================================================== 📄 文件: app/static/css/favorites.css 📊 大小: 2094 bytes (2.04 KB) 🕒 修改时间: 2025-07-09 03:07:24 🔸============================================================================== /* 收藏页面样式 */ .favorite-item { transition: all 0.3s ease; border: 1px solid #e9ecef; } .favorite-item:hover { box-shadow: 0 4px 8px rgba(0,0,0,0.1); transform: translateY(-2px); } .favorite-image { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; border: 1px solid #e9ecef; } .favorite-image-placeholder { width: 80px; height: 80px; background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #6c757d; font-size: 2rem; } .favorite-checkbox { transform: scale(1.2); } .empty-state { min-height: 300px; display: flex; align-items: center; justify-content: center; } /* 图标按钮样式 */ .icon-buttons .btn { font-size: 1.1rem; padding: 0.5rem; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; margin: 0 2px; transition: all 0.2s ease; } .icon-buttons .btn:hover { transform: scale(1.1); } .icon-buttons .btn-outline-primary:hover { background-color: #007bff; border-color: #007bff; color: white; } .icon-buttons .btn-outline-danger:hover { background-color: #dc3545; border-color: #dc3545; color: white; } .card-title a { color: #212529; font-size: 0.95rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; } .card-title a:hover { color: #007bff; } .favorite-item .card-body { padding: 1rem; } .badge { font-size: 0.75rem; } /* 工具提示样式 */ .tooltip-inner { font-size: 0.8rem; padding: 0.25rem 0.5rem; } /* 响应式设计 */ @media (max-width: 768px) { .favorite-image, .favorite-image-placeholder { width: 60px; height: 60px; } .card-title a { font-size: 0.9rem; } .icon-buttons .btn { font-size: 1rem; width: 35px; height: 35px; } } 🔸============================================================================== 📄 文件: app/static/css/history.css 📊 大小: 2369 bytes (2.31 KB) 🕒 修改时间: 2025-07-09 03:07:52 🔸============================================================================== /* 浏览历史页面样式 */ .history-item { transition: all 0.3s ease; border: 1px solid #e9ecef; } .history-item:hover { box-shadow: 0 4px 8px rgba(0,0,0,0.1); transform: translateY(-2px); } .history-image { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; border: 1px solid #e9ecef; } .history-image-placeholder { width: 80px; height: 80px; background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #6c757d; font-size: 2rem; } .history-checkbox { transform: scale(1.2); } .empty-state { min-height: 300px; display: flex; align-items: center; justify-content: center; } /* 卡片底部按钮区域样式 */ .history-item .card-footer { background-color: #f8f9fa; border-top: 1px solid #e9ecef; padding: 0.75rem; margin-top: auto; } .history-item .card-footer .btn-group { display: flex; gap: 0.25rem; } .history-item .card-footer .btn { font-size: 0.8rem; padding: 0.375rem 0.75rem; border-radius: 4px; transition: all 0.2s ease; flex: 1; } .history-item .card-footer .btn:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .history-item .card-footer .btn-outline-primary:hover { background-color: #007bff; border-color: #007bff; color: white; } .history-item .card-footer .btn-outline-danger:hover { background-color: #dc3545; border-color: #dc3545; color: white; } .history-item .card-footer .btn-outline-secondary:hover { background-color: #6c757d; border-color: #6c757d; color: white; } .card-title a { color: #212529; font-size: 0.95rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; } .card-title a:hover { color: #007bff; } .history-item .card-body { padding: 1rem; } .badge { font-size: 0.75rem; } /* 响应式设计 */ @media (max-width: 768px) { .history-image, .history-image-placeholder { width: 60px; height: 60px; } .card-title a { font-size: 0.9rem; } .history-item .card-footer .btn { font-size: 0.75rem; padding: 0.25rem 0.5rem; } } 🔸============================================================================== 📄 文件: app/static/css/index.css 📊 大小: 1679 bytes (1.64 KB) 🕒 修改时间: 2025-07-09 04:24:10 🔸============================================================================== /* 首页样式 */ .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); } /* 欢迎横幅样式 */ /* 欢迎横幅样式 */ .jumbotron { background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important; color: white !important; border-radius: 0.5rem !important; } .jumbotron h1 { color: white !important; font-weight: bold !important; } .jumbotron p { color: white !important; opacity: 0.9; } .jumbotron .btn-light { background-color: white !important; color: #007bff !important; border: none !important; font-weight: bold !important; } .jumbotron .btn-light:hover { background-color: #f8f9fa !important; color: #0056b3 !important; } /* 商品图片样式 */ .product-image { height: 200px; object-fit: cover; } .product-image-placeholder { height: 200px; background-color: #f8f9fa; display: flex; align-items: center; justify-content: center; } /* 价格样式 */ .price-current { color: #dc3545; font-weight: bold; } .price-original { color: #6c757d; text-decoration: line-through; font-size: 0.875rem; } /* 服务特色卡片 */ .feature-card { transition: transform 0.2s; } .feature-card:hover { transform: translateY(-3px); } /* 用户专区卡片 */ .user-zone-card { transition: all 0.2s; } .user-zone-card:hover { transform: translateY(-2px); box-shadow: 0 2px 8px rgba(0,0,0,0.1); } 🔸============================================================================== 📄 文件: app/static/css/order_detail.css 📊 大小: 2123 bytes (2.07 KB) 🕒 修改时间: 2025-07-08 16:54:22 🔸============================================================================== /* 订单详情页面样式 */ /* 首先,重置所有可能影响的样式 */ .order-detail-card .product-item img { all: unset !important; } /* 然后重新定义我们需要的样式 */ .order-detail-card .product-item img { width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 4px !important; border: 1px solid #ddd !important; display: block !important; box-sizing: border-box !important; } /* 订单状态时间线 */ .order-status-timeline { position: relative; padding-left: 30px; } .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; } .info-row { display: flex; justify-content: space-between; margin-bottom: 8px; } .total-amount { color: #e74c3c; font-weight: bold; font-size: 1.2em; } /* 响应式设计 */ @media (max-width: 768px) { .order-detail-card .product-item img { width: 60px !important; height: 60px !important; max-width: 60px !important; max-height: 60px !important; min-width: 60px !important; min-height: 60px !important; } } 🔸============================================================================== 📄 文件: app/static/css/orders.css 📊 大小: 2905 bytes (2.84 KB) 🕒 修改时间: 2025-07-04 04:02:57 🔸============================================================================== /* 订单页面样式 */ .order-card { margin-bottom: 20px; border: 1px solid #dee2e6; border-radius: 8px; transition: box-shadow 0.3s ease; } .order-card:hover { box-shadow: 0 4px 15px rgba(0,0,0,0.1); } .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, img.product-image, .order-item .product-image, .order-item img.product-image { width: 80px !important; height: 80px !important; object-fit: cover !important; border-radius: 4px !important; display: block !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; } /* 防止任何外部样式影响 */ .col-md-2 .product-image, .order-item .col-md-2 img { width: 80px !important; height: 80px !important; object-fit: cover !important; border-radius: 4px !important; } /* 导航标签样式 */ .nav-pills .nav-link { border-radius: 20px; padding: 0.5rem 1rem; margin-right: 0.5rem; transition: all 0.3s ease; } .nav-pills .nav-link:hover { background-color: #e9ecef; color: #495057; } .nav-pills .nav-link.active { background-color: #007bff; color: white; } /* 空状态样式 */ .empty-state { padding: 3rem 0; } .empty-state i { opacity: 0.5; } /* 分页样式 */ .pagination { margin-top: 2rem; } .page-link { color: #007bff; border-color: #dee2e6; } .page-link:hover { color: #0056b3; background-color: #e9ecef; border-color: #dee2e6; } .page-item.active .page-link { background-color: #007bff; border-color: #007bff; } /* 按钮组样式 */ .btn-group .btn { margin-right: 0.5rem; } .btn-group .btn:last-child { margin-right: 0; } /* 响应式设计 */ @media (max-width: 768px) { .order-header .row, .order-footer .row { flex-direction: column; } .order-header .col-md-3, .order-footer .col-md-6 { margin-bottom: 0.5rem; text-align: left !important; } .order-item .row { flex-direction: column; text-align: center; } .order-item .col-md-2, .order-item .col-md-6 { margin-bottom: 1rem; } .nav-pills { flex-wrap: wrap; } .nav-pills .nav-link { margin-bottom: 0.5rem; font-size: 0.9rem; } } 🔸============================================================================== 📄 文件: app/static/css/pay.css 📊 大小: 2169 bytes (2.12 KB) 🕒 修改时间: 2025-07-08 17:11:04 🔸============================================================================== /* 支付页面样式 */ .pay-container { max-width: 600px; margin: 2rem auto; padding: 0 1rem; } .order-info { background: #f8f9fa; padding: 1rem; border-radius: 0.5rem; margin-bottom: 1.5rem; } .payment-method { border: 2px solid #e9ecef; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; cursor: pointer; transition: all 0.3s ease; } .payment-method:hover { border-color: #007bff; background-color: #f8f9fa; } .payment-method.selected { border-color: #007bff; background-color: #e7f3ff; } .qr-code { text-align: center; padding: 2rem; background: #fff; border: 1px solid #dee2e6; border-radius: 0.5rem; margin: 1rem 0; } .payment-status { text-align: center; padding: 3rem 1rem; } .countdown { font-weight: bold; color: #dc3545; font-size: 1.1rem; } .simulate-panel { background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); border: 2px solid #ffc107 !important; } .simulate-panel h6 { margin-bottom: 0.5rem; } .simulate-panel .btn { min-width: 140px; } .payment-tips { background: #f8f9fa; padding: 1rem; border-radius: 0.5rem; border-left: 4px solid #007bff; } .payment-tips ul { margin-bottom: 0; padding-left: 1.2rem; } .payment-tips li { margin-bottom: 0.3rem; } /* 响应式设计 */ @media (max-width: 768px) { .pay-container { margin: 1rem auto; padding: 0 0.5rem; } .d-md-flex .btn { margin-bottom: 0.5rem; } .simulate-panel .btn { min-width: auto; width: 100%; } } /* 动画效果 */ .payment-status i { animation: pulse 2s infinite; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } /* 按钮状态 */ .btn:disabled { opacity: 0.6; cursor: not-allowed; } /* 支付方式图标 */ .payment-method i { min-width: 60px; } /* 倒计时样式 */ .countdown { background: #fff; padding: 0.2rem 0.5rem; border-radius: 0.25rem; border: 1px solid #dc3545; } 🔸============================================================================== 📄 文件: app/static/css/product_detail.css 📊 大小: 6622 bytes (6.47 KB) 🕒 修改时间: 2025-07-09 05:23:08 🔸============================================================================== /* 商品详情页样式 */ .product-card { transition: transform 0.2s; } .product-card:hover { /* 规格选择样式 */ .specs-section { border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; background-color: #f8f9fa; } .spec-group { margin-bottom: 15px; } .spec-group:last-child { margin-bottom: 0; } .spec-group .form-label { font-weight: 600; margin-bottom: 10px; color: #495057; } .spec-options { display: flex; flex-wrap: wrap; gap: 8px; } .spec-option { min-width: 60px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; background-color: #fff; color: #495057; cursor: pointer; transition: all 0.2s ease; font-size: 14px; } .spec-option:hover { border-color: #007bff; background-color: #e3f2fd; } .spec-option.btn-primary { background-color: #007bff; border-color: #007bff; color: white; } .spec-option.btn-primary:hover { background-color: #0056b3; border-color: #0056b3; } .spec-option:disabled { opacity: 0.5; cursor: not-allowed; background-color: #e9ecef; } /* 库存信息样式 */ .stock-section { padding: 15px; background-color: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6; } #stockCount { font-weight: 600; font-size: 1.1em; } /* 数量选择样式 */ .quantity-section .input-group { max-width: 150px; } .quantity-section .form-control { text-align: center; font-weight: 600; } /* 操作按钮样式 */ .action-buttons .btn { padding: 12px 24px; font-weight: 600; border-radius: 8px; } .action-buttons .btn:disabled { opacity: 0.6; cursor: not-allowed; } /* 服务承诺样式 */ .service-promises { background-color: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #dee2e6; } .service-promises h6 { color: #495057; margin-bottom: 10px; } .service-promises li { margin-bottom: 5px; color: #6c757d; } .service-promises .bi-check-circle { margin-right: 8px; } 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; } /* 商品主图轮播样式修复 */ .carousel-inner img { /* 重置Bootstrap图片样式 */ all: unset !important; display: block !important; width: 100% !important; height: 400px !important; object-fit: cover !important; border-radius: 8px !important; } /* 缩略图样式修复 */ .thumbnail-image { /* 重置Bootstrap图片样式 */ all: unset !important; display: block !important; width: 100% !important; height: 80px !important; object-fit: cover !important; cursor: pointer !important; border-radius: 4px !important; border: 2px solid #dee2e6 !important; transition: all 0.2s ease !important; } .thumbnail-image:hover { transform: scale(1.05); box-shadow: 0 2px 8px rgba(0,0,0,0.2); border-color: #007bff; } /* 推荐商品图片样式修复 */ .product-card .card-img-top { /* 重置Bootstrap图片样式 */ all: unset !important; display: block !important; width: 100% !important; height: 200px !important; object-fit: cover !important; border-top-left-radius: 0.375rem !important; border-top-right-radius: 0.375rem !important; } /* 商品详情标签页内的图片样式 */ .tab-content img { /* 确保标签页内的图片不会过大 */ max-width: 100% !important; height: auto !important; border-radius: 4px !important; border: 1px solid #dee2e6 !important; } /* 评价图片在商品详情页中的特殊样式 */ .reviews-section img { /* 重置评价图片样式 */ all: unset !important; display: inline-block !important; max-width: 80px !important; max-height: 80px !important; width: auto !important; height: auto !important; object-fit: cover !important; border-radius: 6px !important; border: 1px solid #dee2e6 !important; cursor: pointer !important; transition: all 0.2s ease !important; margin-right: 8px !important; margin-bottom: 8px !important; } .reviews-section img:hover { transform: scale(1.05); box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-color: #007bff; } /* 用户头像图片样式 */ .reviewer-avatar { /* 重置用户头像样式 */ all: unset !important; display: block !important; width: 40px !important; height: 40px !important; border-radius: 50% !important; object-fit: cover !important; border: 2px solid #e9ecef !important; } /* 图片模态框样式 */ .modal-body img { /* 模态框中的图片样式 */ all: unset !important; display: block !important; max-width: 100% !important; max-height: 80vh !important; width: auto !important; height: auto !important; margin: 0 auto !important; border-radius: 8px !important; box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; } /* 响应式优化 */ @media (max-width: 768px) { .price-section { text-align: center; } .action-buttons .d-md-flex { flex-direction: column; } .action-buttons .btn { margin-bottom: 10px; } .carousel-inner img { height: 300px !important; } .thumbnail-image { height: 60px !important; } .reviews-section img { max-width: 60px !important; max-height: 60px !important; } } /* 无图片占位符样式 */ .bg-light.d-flex { background-color: #f8f9fa !important; border: 2px dashed #dee2e6 !important; } /* 确保所有图片都有基础的重置样式 */ .product-detail img:not(.reviewer-avatar):not(.thumbnail-image):not(.card-img-top) { max-width: 100% !important; height: auto !important; border-radius: 4px !important; } 🔸============================================================================== 📄 文件: app/static/css/product_list.css 📊 大小: 259 bytes (0.25 KB) 🕒 修改时间: 2025-07-04 14:41:00 🔸============================================================================== .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; } 🔸============================================================================== 📄 文件: app/static/css/profile.css 📊 大小: 5567 bytes (5.44 KB) 🕒 修改时间: 2025-07-04 03:59:34 🔸============================================================================== /* 个人中心页面样式 */ /* 头像上传相关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; } 🔸============================================================================== 📄 文件: app/static/css/register.css 📊 大小: 526 bytes (0.51 KB) 🕒 修改时间: 2025-07-04 03:58:30 🔸============================================================================== /* 注册页面样式 */ .is-valid { border-color: #28a745 !important; } .is-invalid { border-color: #dc3545 !important; } .text-success { color: #28a745 !important; } .text-danger { color: #dc3545 !important; } /* 验证码按钮样式 */ .btn.disabled { opacity: 0.6; cursor: not-allowed; } /* 表单增强样式 */ .form-text { font-size: 0.875em; margin-top: 0.25rem; } .input-group .btn { border-left: 0; } .input-group .form-control:focus + .btn { border-color: #86b7fe; } 🔸============================================================================== 📄 文件: app/static/css/review.css 📊 大小: 14587 bytes (14.25 KB) 🕒 修改时间: 2025-07-08 19:29:05 🔸============================================================================== /* 评价功能样式 */ /* 评价表单样式 */ .product-info { background-color: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #e9ecef; } .product-info img { /* 图片重置样式 - 解决Bootstrap冲突 */ all: unset !important; display: block !important; max-width: 100% !important; max-height: 80px !important; width: auto !important; height: auto !important; object-fit: cover !important; border-radius: 4px !important; border: 1px solid #dee2e6 !important; } /* 星级评分样式 - 简化版本 */ .rating-container { display: flex; align-items: center; gap: 15px; } .star-rating { display: flex; gap: 3px; } .star { font-size: 2.5rem; cursor: pointer; transition: all 0.2s ease; user-select: none; line-height: 1; /* 默认样式:灰色 */ color: #ddd !important; } .star:hover { transform: scale(1.1); } /* 填充状态:橙色 */ .star.filled { color: #ff6b35 !important; } /* 评分文字样式 */ .rating-text { font-weight: 600; color: #666; font-size: 1.1rem; padding: 10px 15px; background-color: #f8f9fa; border-radius: 8px; border: 2px solid #e9ecef; min-width: 120px; text-align: center; transition: all 0.2s ease; } .rating-text.selected { background-color: #ff6b35; color: white; border-color: #ff6b35; } /* 图片上传样式 */ .image-upload-container { border: 2px dashed #ddd; border-radius: 8px; padding: 20px; text-align: center; transition: border-color 0.3s ease; } .image-upload-container:hover { border-color: #007bff; } .upload-area { cursor: pointer; padding: 20px; } .upload-area i { font-size: 3rem; color: #666; display: block; margin-bottom: 10px; } /* 上传图片预览容器 - 强制控制布局 */ .uploaded-images { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; margin-top: 15px !important; justify-content: flex-start !important; align-items: flex-start !important; } /* 上传图片预览尺寸 - 使用最强的样式规则 */ .image-preview { position: relative !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; border-radius: 8px !important; overflow: hidden !important; border: 2px solid #e9ecef !important; flex-shrink: 0 !important; flex-grow: 0 !important; display: inline-block !important; box-sizing: border-box !important; } /* 强制重置上传预览图片的所有样式 */ .image-preview img { all: unset !important; display: block !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; box-sizing: border-box !important; position: relative !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; margin: 0 !important; padding: 0 !important; border: none !important; outline: none !important; background: none !important; } /* 针对上传图片容器内的所有img标签 */ .uploaded-images img { all: unset !important; display: block !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; box-sizing: border-box !important; position: relative !important; flex-shrink: 0 !important; flex-grow: 0 !important; } /* 上传图片容器的直接img子元素 */ .uploaded-images > .image-preview > img { all: unset !important; display: block !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; } .image-preview .remove-btn { position: absolute !important; top: 2px !important; right: 2px !important; background: rgba(255, 255, 255, 0.9) !important; border: none !important; border-radius: 50% !important; width: 20px !important; height: 20px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; font-size: 12px !important; color: #dc3545 !important; box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important; z-index: 10 !important; } .image-preview .remove-btn:hover { background: rgba(255, 255, 255, 1) !important; transform: scale(1.1) !important; } /* 评价列表样式 */ .review-item { border-bottom: 1px solid #e9ecef; padding: 20px 0; margin-bottom: 20px; } .review-item:last-child { border-bottom: none; margin-bottom: 0; } .rating-display .stars { color: #ff6b35 !important; font-size: 1.2rem; margin-right: 8px; } .review-content { line-height: 1.6; margin: 10px 0; word-wrap: break-word; } .review-images { display: flex; gap: 8px; flex-wrap: wrap; } .review-image-thumb { /* 图片重置样式 - 解决Bootstrap冲突 */ all: unset !important; display: block !important; width: 60px !important; height: 60px !important; object-fit: cover !important; border-radius: 4px !important; border: 1px solid #e9ecef !important; cursor: pointer !important; transition: transform 0.2s ease !important; } .review-image-thumb:hover { transform: scale(1.05); box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .review-meta { margin-top: 10px; padding-top: 10px; border-top: 1px solid #f8f9fa; } /* 商品详情页评价标签页样式 */ .reviews-section { padding: 20px 0; } .reviews-stats { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px; } .rating-summary { display: flex; align-items: center; gap: 20px; margin-bottom: 15px; } .overall-rating { text-align: center; } .overall-rating .score { font-size: 3rem; font-weight: bold; color: #ff6b35; line-height: 1; } .overall-rating .stars { color: #ff6b35 !important; font-size: 1.5rem; } .overall-rating .total { color: #666; margin-top: 5px; } .rating-breakdown { flex: 1; } .rating-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } .rating-bar .label { width: 40px; font-size: 14px; } .rating-bar .progress { flex: 1; height: 8px; } .rating-bar .count { width: 40px; text-align: right; font-size: 14px; color: #666; } .reviews-filter { margin-bottom: 20px; } .reviews-filter .btn { margin-right: 10px; margin-bottom: 10px; } .review-list-item { border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; margin-bottom: 15px; background: #fff; } .reviewer-info { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; } /* 用户头像样式 - 重点修复区域 */ .reviewer-avatar { /* 头像图片重置样式 - 强制重置所有样式 */ all: unset !important; display: block !important; width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; border-radius: 50% !important; object-fit: cover !important; border: 2px solid #e9ecef !important; box-sizing: border-box !important; flex-shrink: 0 !important; vertical-align: top !important; } /* 针对商品详情页评价容器中的头像 */ #reviewsContainer .reviewer-avatar { /* 强制重置商品详情页评价容器中的头像 */ all: unset !important; display: block !important; width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; border-radius: 50% !important; object-fit: cover !important; border: 2px solid #e9ecef !important; box-sizing: border-box !important; flex-shrink: 0 !important; vertical-align: top !important; } /* 针对评价标签页中的头像 */ #reviews .reviewer-avatar { /* 评价标签页中的头像 */ all: unset !important; display: block !important; width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; border-radius: 50% !important; object-fit: cover !important; border: 2px solid #e9ecef !important; box-sizing: border-box !important; flex-shrink: 0 !important; vertical-align: top !important; } .reviewer-name { font-weight: 500; } .review-time { color: #666; font-size: 14px; } .empty-state { padding: 60px 20px; } .empty-state i { opacity: 0.3; } /* 商品详情页评价图片展示 - 重点修复区域 */ .product-review-images { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; } .product-review-image { /* 商品评价图片重置样式 - 强制重置所有样式 */ all: unset !important; display: inline-block !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; border: 1px solid #dee2e6 !important; cursor: pointer !important; transition: all 0.2s ease !important; box-sizing: border-box !important; vertical-align: top !important; } .product-review-image:hover { transform: scale(1.05) !important; box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; border-color: #007bff !important; } /* 特殊针对商品详情页面的评价容器 */ #reviewsContainer img:not(.reviewer-avatar) { /* 强制重置商品详情页评价容器中的所有图片(除了头像) */ all: unset !important; display: inline-block !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; border: 1px solid #dee2e6 !important; cursor: pointer !important; transition: all 0.2s ease !important; margin-right: 8px !important; margin-bottom: 8px !important; box-sizing: border-box !important; vertical-align: top !important; } #reviewsContainer img:not(.reviewer-avatar):hover { transform: scale(1.05) !important; box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; border-color: #007bff !important; } /* 评价标签页特殊处理 */ #reviews img:not(.reviewer-avatar) { /* 评价标签页中的图片(除了头像) */ all: unset !important; display: inline-block !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; border: 1px solid #dee2e6 !important; cursor: pointer !important; transition: all 0.2s ease !important; margin-right: 8px !important; margin-bottom: 8px !important; box-sizing: border-box !important; } #reviews img:not(.reviewer-avatar):hover { transform: scale(1.05) !important; box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; border-color: #007bff !important; } /* 响应式设计 */ @media (max-width: 768px) { .star { font-size: 2rem; } .rating-summary { flex-direction: column; align-items: flex-start; gap: 15px; } .uploaded-images { justify-content: flex-start !important; } /* 移动端上传图片预览更小 */ .image-preview { width: 60px !important; height: 60px !important; max-width: 60px !important; max-height: 60px !important; min-width: 60px !important; min-height: 60px !important; } .image-preview img, .uploaded-images img, .uploaded-images > .image-preview > img { width: 60px !important; height: 60px !important; max-width: 60px !important; max-height: 60px !important; min-width: 60px !important; min-height: 60px !important; } .image-preview .remove-btn { width: 16px !important; height: 16px !important; font-size: 10px !important; top: 1px !important; right: 1px !important; } .review-image-thumb { width: 50px !important; height: 50px !important; } .product-review-image, #reviewsContainer img:not(.reviewer-avatar), #reviews img:not(.reviewer-avatar) { width: 60px !important; height: 60px !important; max-width: 60px !important; max-height: 60px !important; min-width: 60px !important; min-height: 60px !important; } .reviewer-avatar, #reviewsContainer .reviewer-avatar, #reviews .reviewer-avatar { width: 35px !important; height: 35px !important; max-width: 35px !important; max-height: 35px !important; min-width: 35px !important; min-height: 35px !important; } } /* 加载状态 */ .loading { opacity: 0.6; pointer-events: none; } .loading::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20px; height: 20px; border: 2px solid #f3f3f3; border-top: 2px solid #007bff; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } } /* 动画效果 */ .review-item { animation: fadeInUp 0.3s ease; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } 🔸============================================================================== 📄 文件: app/static/js/address_form.js 📊 大小: 7374 bytes (7.20 KB) 🕒 修改时间: 2025-07-04 04:02:31 🔸============================================================================== // 地址表单页面JavaScript功能 // 全局变量,避免重复初始化 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 = '❌ 地址数据加载失败'; showAlert('地址数据加载失败,请刷新页面重试', 'error'); return; } console.log('✅ cityData 已加载,省份数量:', Object.keys(cityData).length); document.getElementById('debugInfo').innerHTML = '✅ 地址数据已加载,省份数量: ' + Object.keys(cityData).length + ''; // 避免重复初始化 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 = ''; // 添加所有省份 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 = ''; 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 = ''; 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 = ''; 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); } } 🔸============================================================================== 📄 文件: app/static/js/addresses.js 📊 大小: 2037 bytes (1.99 KB) 🕒 修改时间: 2025-07-04 04:01:18 🔸============================================================================== // 地址管理页面JavaScript功能 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'); }); } } // 页面加载完成后的处理 document.addEventListener('DOMContentLoaded', function() { // 为地址卡片添加点击效果 const addressCards = document.querySelectorAll('.address-card'); addressCards.forEach(card => { card.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 15px rgba(0,0,0,0.1)'; }); card.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = ''; }); }); }); 🔸============================================================================== 📄 文件: app/static/js/admin_categories.js 📊 大小: 9228 bytes (9.01 KB) 🕒 修改时间: 2025-07-04 18:44:46 🔸============================================================================== // 分类管理页面JavaScript // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', function() { initIconUpload(); initEditIconUpload(); initFormSubmission(); }); // 初始化图标上传 function initIconUpload() { const uploadArea = document.getElementById('iconUploadArea'); const iconInput = document.getElementById('iconInput'); const iconPreview = document.getElementById('iconPreview'); if (uploadArea && iconInput && 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'); if (uploadArea && iconInput && 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 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 => { console.error('Error:', error); alert('获取分类信息失败: ' + error); }); } // 添加子分类 function addSubCategory(parentId) { const parentSelect = document.getElementById('parent_id'); const nameInput = document.getElementById('name'); if (parentSelect) { parentSelect.value = parentId; } if (nameInput) { nameInput.focus(); } // 滚动到添加表单 const addForm = document.querySelector('.add-category-form'); if (addForm) { addForm.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 => { console.error('Error:', error); alert('删除失败: ' + error); }); } } // 切换分类展开/收起 function toggleCategory(categoryId) { const categoryItem = document.querySelector(`[data-id="${categoryId}"]`); if (!categoryItem) return; const childrenDiv = categoryItem.querySelector('.children-categories'); const toggleBtn = categoryItem.querySelector('.category-header .bi-chevron-down, .category-header .bi-chevron-up'); if (childrenDiv && toggleBtn) { 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'; }); } // 初始化表单提交 function initFormSubmission() { const addForm = document.getElementById('addCategoryForm'); if (addForm) { addForm.addEventListener('submit', function(e) { setTimeout(() => { if (!document.querySelector('.alert-danger')) { // 重置表单 this.reset(); const iconPreview = document.getElementById('iconPreview'); const uploadIcon = document.getElementById('iconUploadArea').querySelector('i'); if (iconPreview) iconPreview.style.display = 'none'; if (uploadIcon) uploadIcon.style.display = 'block'; } }, 100); }); } } // 表单验证 function validateCategoryForm(formId) { const form = document.getElementById(formId); if (!form) return false; const nameInput = form.querySelector('input[name="name"]'); if (!nameInput || !nameInput.value.trim()) { alert('请输入分类名称'); if (nameInput) nameInput.focus(); return false; } return true; } // 工具函数:显示加载状态 function showLoading(element) { if (element) { element.disabled = true; const originalText = element.innerHTML; element.innerHTML = ' 处理中...'; setTimeout(() => { element.disabled = false; element.innerHTML = originalText; }, 2000); } } // 工具函数:显示成功消息 function showSuccess(message) { const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-success alert-dismissible fade show'; alertDiv.innerHTML = ` ${message} `; const container = document.querySelector('.container-fluid'); if (container) { container.insertBefore(alertDiv, container.firstChild); setTimeout(() => { alertDiv.remove(); }, 3000); } } // 工具函数:显示错误消息 function showError(message) { const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-danger alert-dismissible fade show'; alertDiv.innerHTML = ` ${message} `; const container = document.querySelector('.container-fluid'); if (container) { container.insertBefore(alertDiv, container.firstChild); setTimeout(() => { alertDiv.remove(); }, 5000); } } 🔸============================================================================== 📄 文件: app/static/js/admin_dashboard.js 📊 大小: 2361 bytes (2.31 KB) 🕒 修改时间: 2025-07-04 14:51:53 🔸============================================================================== // Dashboard JavaScript functionality document.addEventListener('DOMContentLoaded', function() { // Initialize user trend chart if canvas exists const chartCanvas = document.getElementById('userTrendChart'); if (chartCanvas) { initUserTrendChart(); } // Auto refresh dashboard data every 5 minutes setInterval(function() { refreshDashboardStats(); }, 300000); // 5 minutes }); function initUserTrendChart() { const ctx = document.getElementById('userTrendChart').getContext('2d'); // Get data from template variables (these will be rendered by Jinja2) const labels = window.userTrendLabels || []; const data = window.userTrendData || []; const userTrendChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: '注册用户数', data: data, 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 } } } } }); } function refreshDashboardStats() { // This function could be used to refresh dashboard statistics via AJAX // For now, it's a placeholder for future implementation console.log('Refreshing dashboard stats...'); } // Utility function to format numbers function formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } // Function to update stats cards (for future AJAX updates) function updateStatsCard(cardSelector, value) { const card = document.querySelector(cardSelector); if (card) { const valueElement = card.querySelector('h3'); if (valueElement) { valueElement.textContent = formatNumber(value); } } } 🔸============================================================================== 📄 文件: app/static/js/admin_logs.js 📊 大小: 11576 bytes (11.30 KB) 🕒 修改时间: 2025-07-09 01:54:54 🔸============================================================================== // 操作日志页面JavaScript document.addEventListener('DOMContentLoaded', function() { // 初始化 initializeLogManagement(); }); // 初始化日志管理功能 function initializeLogManagement() { // 添加事件监听器 setupEventListeners(); // 初始化工具提示 initializeTooltips(); // 初始化表格 initializeTable(); } // 设置事件监听器 function setupEventListeners() { // 搜索表单提交 const searchForm = document.querySelector('form[method="GET"]'); if (searchForm) { searchForm.addEventListener('submit', function(e) { // 可以在这里添加搜索前的验证 }); } // 用户类型筛选变更 const userTypeSelect = document.getElementById('user_type'); if (userTypeSelect) { userTypeSelect.addEventListener('change', function() { // 自动提交表单 this.form.submit(); }); } // 操作类型输入框 const actionInput = document.getElementById('action'); if (actionInput) { // 添加防抖搜索 let searchTimer; actionInput.addEventListener('input', function() { clearTimeout(searchTimer); searchTimer = setTimeout(() => { // 可以实现实时搜索 }, 500); }); } } // 初始化工具提示 function initializeTooltips() { // 为用户代理字段添加工具提示 const userAgentElements = document.querySelectorAll('.user-agent'); userAgentElements.forEach(element => { if (element.title) { // 使用Bootstrap的tooltip new bootstrap.Tooltip(element); } }); } // 初始化表格 function initializeTable() { // 添加表格行点击事件 const tableRows = document.querySelectorAll('.table tbody tr'); tableRows.forEach(row => { row.addEventListener('click', function(e) { // 如果点击的是按钮,不触发行点击事件 if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { return; } // 高亮选中行 tableRows.forEach(r => r.classList.remove('table-active')); this.classList.add('table-active'); }); }); } // 查看日志详情 function viewLogDetail(logId) { // 发送AJAX请求获取日志详情 fetch(`/admin/logs/${logId}/detail`) .then(response => response.json()) .then(data => { if (data.success) { showLogDetailModal(data.log); } else { showError('获取日志详情失败: ' + data.message); } }) .catch(error => { console.error('Error:', error); showError('网络错误,请重试'); }); } // 显示日志详情模态框 function showLogDetailModal(log) { const modalHtml = ` `; // 移除现有的模态框 const existingModal = document.getElementById('logDetailModal'); if (existingModal) { existingModal.remove(); } // 添加新的模态框 document.body.insertAdjacentHTML('beforeend', modalHtml); // 显示模态框 const modal = new bootstrap.Modal(document.getElementById('logDetailModal')); modal.show(); } // 导出日志 function exportLogs() { // 获取当前筛选条件 const userType = document.getElementById('user_type').value; const action = document.getElementById('action').value; // 构建导出URL const params = new URLSearchParams(); if (userType) params.append('user_type', userType); if (action) params.append('action', action); const exportUrl = `/admin/logs/export?${params.toString()}`; // 下载文件 window.location.href = exportUrl; } // 清理日志 function clearLogs() { if (!confirm('确定要清理历史日志吗?此操作不可逆!')) { return; } const daysToKeep = prompt('请输入要保留的天数(例如:30):', '30'); if (!daysToKeep || isNaN(daysToKeep) || daysToKeep <= 0) { showError('请输入有效的天数'); return; } // 显示加载状态 showLoading(); // 发送AJAX请求 fetch('/admin/logs/clear', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ days_to_keep: parseInt(daysToKeep) }) }) .then(response => response.json()) .then(data => { hideLoading(); if (data.success) { showSuccess(data.message); // 刷新页面 setTimeout(() => { location.reload(); }, 1000); } else { showError(data.message); } }) .catch(error => { hideLoading(); console.error('Error:', error); showError('网络错误,请重试'); }); } // 搜索日志 function searchLogs() { const searchForm = document.querySelector('form[method="GET"]'); if (searchForm) { searchForm.submit(); } } // 重置搜索 function resetSearch() { window.location.href = '/admin/logs'; } // 格式化日期时间 function formatDateTime(dateTimeString) { if (!dateTimeString) return '未知'; const date = new Date(dateTimeString); return date.toLocaleString('zh-CN'); } // 显示成功消息 function showSuccess(message) { showAlert(message, 'success'); } // 显示错误消息 function showError(message) { showAlert(message, 'danger'); } // 显示提示消息 function showAlert(message, type) { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show`; alertDiv.innerHTML = ` ${message} `; // 插入到页面顶部 const container = document.querySelector('.admin-content'); container.insertBefore(alertDiv, container.firstChild); // 3秒后自动关闭 setTimeout(() => { alertDiv.remove(); }, 3000); } // 显示加载状态 function showLoading() { const loadingDiv = document.createElement('div'); loadingDiv.id = 'loading-overlay'; loadingDiv.innerHTML = `
处理中...
`; loadingDiv.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; color: white; `; document.body.appendChild(loadingDiv); } // 隐藏加载状态 function hideLoading() { const loadingDiv = document.getElementById('loading-overlay'); if (loadingDiv) { loadingDiv.remove(); } } // 表格排序功能 function sortTable(column) { // 实现表格排序功能 console.log('Sort by:', column); } // 批量操作功能(可选) function bulkOperation() { // 实现批量操作功能 const selectedLogs = document.querySelectorAll('input[type="checkbox"]:checked'); if (selectedLogs.length === 0) { showError('请选择要操作的日志'); return; } // 实现批量操作逻辑 } 🔸============================================================================== 📄 文件: app/static/js/admin_orders.js 📊 大小: 6855 bytes (6.69 KB) 🕒 修改时间: 2025-07-09 05:11:36 🔸============================================================================== // 订单管理JavaScript功能 document.addEventListener('DOMContentLoaded', function() { // 初始化所有模态框 const shipModal = new bootstrap.Modal(document.getElementById('shipModal')); const refundModal = new bootstrap.Modal(document.getElementById('refundModal')); const cancelModal = new bootstrap.Modal(document.getElementById('cancelModal')); // 当前操作的订单ID let currentOrderId = null; // 显示发货模态框 window.showShipModal = function(orderId, orderSn) { currentOrderId = orderId; document.getElementById('shipOrderSn').value = orderSn; shipModal.show(); }; // 显示退款模态框 window.showRefundModal = function(orderId, orderSn) { currentOrderId = orderId; document.getElementById('refundOrderSn').value = orderSn; refundModal.show(); }; // 显示取消模态框 window.showCancelModal = function(orderId, orderSn) { currentOrderId = orderId; document.getElementById('cancelOrderSn').value = orderSn; cancelModal.show(); }; // 处理发货表单提交 document.getElementById('shipForm').addEventListener('submit', function(e) { e.preventDefault(); if (!currentOrderId) { showAlert('错误', '订单ID不能为空', 'danger'); return; } const formData = new FormData(this); const submitBtn = this.querySelector('button[type="submit"]'); // 显示加载状态 const originalText = submitBtn.innerHTML; submitBtn.innerHTML = ' 处理中...'; submitBtn.disabled = true; fetch(`/admin/orders/${currentOrderId}/ship`, { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { showAlert('成功', '发货成功!', 'success'); shipModal.hide(); setTimeout(() => { location.reload(); }, 1000); } else { showAlert('错误', data.message || '发货失败', 'danger'); } }) .catch(error => { console.error('Error:', error); showAlert('错误', '网络请求失败', 'danger'); }) .finally(() => { // 恢复按钮状态 submitBtn.innerHTML = originalText; submitBtn.disabled = false; }); }); // 处理退款表单提交 document.getElementById('refundForm').addEventListener('submit', function(e) { e.preventDefault(); if (!currentOrderId) { showAlert('错误', '订单ID不能为空', 'danger'); return; } const formData = new FormData(this); const submitBtn = this.querySelector('button[type="submit"]'); // 显示加载状态 const originalText = submitBtn.innerHTML; submitBtn.innerHTML = ' 处理中...'; submitBtn.disabled = true; fetch(`/admin/orders/${currentOrderId}/refund`, { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { showAlert('成功', '退款成功!', 'success'); refundModal.hide(); setTimeout(() => { location.reload(); }, 1000); } else { showAlert('错误', data.message || '退款失败', 'danger'); } }) .catch(error => { console.error('Error:', error); showAlert('错误', '网络请求失败', 'danger'); }) .finally(() => { // 恢复按钮状态 submitBtn.innerHTML = originalText; submitBtn.disabled = false; }); }); // 处理取消表单提交 document.getElementById('cancelForm').addEventListener('submit', function(e) { e.preventDefault(); if (!currentOrderId) { showAlert('错误', '订单ID不能为空', 'danger'); return; } const formData = new FormData(this); const submitBtn = this.querySelector('button[type="submit"]'); // 显示加载状态 const originalText = submitBtn.innerHTML; submitBtn.innerHTML = ' 处理中...'; submitBtn.disabled = true; fetch(`/admin/orders/${currentOrderId}/cancel`, { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { showAlert('成功', '订单取消成功!', 'success'); cancelModal.hide(); setTimeout(() => { location.reload(); }, 1000); } else { showAlert('错误', data.message || '取消失败', 'danger'); } }) .catch(error => { console.error('Error:', error); showAlert('错误', '网络请求失败', 'danger'); }) .finally(() => { // 恢复按钮状态 submitBtn.innerHTML = originalText; submitBtn.disabled = false; }); }); // 通用提示函数 function showAlert(title, message, type) { // 创建提示框 const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`; alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;'; alertDiv.innerHTML = ` ${title} ${message} `; document.body.appendChild(alertDiv); // 3秒后自动关闭 setTimeout(() => { if (alertDiv.parentNode) { alertDiv.parentNode.removeChild(alertDiv); } }, 3000); } }); // 旋转动画CSS(如果需要) if (!document.querySelector('#admin-orders-style')) { const style = document.createElement('style'); style.id = 'admin-orders-style'; style.textContent = ` .spin { animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } 🔸============================================================================== 📄 文件: app/static/js/admin_users.js 📊 大小: 12828 bytes (12.53 KB) 🕒 修改时间: 2025-07-09 02:08:32 🔸============================================================================== // 用户管理页面JavaScript document.addEventListener('DOMContentLoaded', function() { // 初始化 initializeUserManagement(); }); // 初始化用户管理功能 function initializeUserManagement() { // 添加事件监听器 setupEventListeners(); // 初始化头像显示 initializeAvatars(); // 强制设置头像样式 forceAvatarStyles(); } // 设置事件监听器 function setupEventListeners() { // 搜索表单提交 const searchForm = document.querySelector('form[method="GET"]'); if (searchForm) { searchForm.addEventListener('submit', function(e) { // 可以在这里添加搜索前的验证 }); } // 状态筛选变更 const statusSelect = document.getElementById('status'); if (statusSelect) { statusSelect.addEventListener('change', function() { // 自动提交表单 this.form.submit(); }); } } // 初始化头像显示 function initializeAvatars() { const avatars = document.querySelectorAll('.user-avatar'); avatars.forEach(avatar => { avatar.onerror = function() { // 如果头像加载失败,替换为默认头像 this.style.display = 'none'; const placeholder = this.parentElement.querySelector('.user-avatar-placeholder'); if (placeholder) { placeholder.style.display = 'flex'; } }; }); } // 强制设置头像样式,覆盖Bootstrap function forceAvatarStyles() { // 设置表格中的头像样式 const tableAvatars = document.querySelectorAll('.table .user-avatar'); tableAvatars.forEach(avatar => { setAvatarStyles(avatar, '48px'); }); } // 设置单个头像样式 function setAvatarStyles(avatar, size) { if (!avatar) return; // 强制设置所有相关样式 avatar.style.setProperty('width', size, 'important'); avatar.style.setProperty('height', size, 'important'); avatar.style.setProperty('max-width', size, 'important'); avatar.style.setProperty('max-height', size, 'important'); avatar.style.setProperty('min-width', size, 'important'); avatar.style.setProperty('min-height', size, 'important'); avatar.style.setProperty('border-radius', '50%', 'important'); avatar.style.setProperty('object-fit', 'cover', 'important'); avatar.style.setProperty('border', '2px solid #f8f9fa', 'important'); avatar.style.setProperty('display', 'block', 'important'); avatar.style.setProperty('flex-shrink', '0', 'important'); avatar.style.setProperty('flex-grow', '0', 'important'); // 移除可能影响的Bootstrap类 avatar.classList.remove('img-fluid', 'img-responsive', 'img-thumbnail'); // 设置父元素样式 if (avatar.parentElement) { avatar.parentElement.style.setProperty('width', size, 'important'); avatar.parentElement.style.setProperty('height', size, 'important'); avatar.parentElement.style.setProperty('overflow', 'hidden', 'important'); avatar.parentElement.style.setProperty('flex-shrink', '0', 'important'); avatar.parentElement.style.setProperty('flex-grow', '0', 'important'); } } // 查看用户详情 function viewUser(userId) { // 显示模态框 const modal = new bootstrap.Modal(document.getElementById('userDetailModal')); const content = document.getElementById('userDetailContent'); // 显示加载状态 content.innerHTML = `
加载中...
`; modal.show(); // 发送AJAX请求获取用户详情 fetch(`/admin/users/${userId}/detail`) .then(response => response.json()) .then(data => { if (data.success) { renderUserDetail(data.user); // 立即强制设置头像样式 setTimeout(() => { forceModalAvatarStyles(); }, 50); // 再次确保样式正确应用 setTimeout(() => { forceModalAvatarStyles(); }, 200); } else { showError('获取用户详情失败: ' + data.message); } }) .catch(error => { console.error('Error:', error); showError('网络错误,请重试'); }); } // 渲染用户详情 function renderUserDetail(user) { const content = document.getElementById('userDetailContent'); content.innerHTML = `
${user.avatar_url ? `用户头像` : `
` }
${user.nickname || user.username}
${user.status === 1 ? '正常' : '禁用'}
`; } // 强制设置模态框中的头像样式 function forceModalAvatarStyles() { const modalAvatar = document.getElementById('modalAvatar'); if (modalAvatar) { setAvatarStyles(modalAvatar, '80px'); // 设置容器样式 const wrapper = document.querySelector('.user-avatar-large-wrapper'); if (wrapper) { wrapper.style.setProperty('width', '80px', 'important'); wrapper.style.setProperty('height', '80px', 'important'); wrapper.style.setProperty('margin', '0 auto', 'important'); wrapper.style.setProperty('overflow', 'hidden', 'important'); wrapper.style.setProperty('border-radius', '50%', 'important'); wrapper.style.setProperty('position', 'relative', 'important'); wrapper.style.setProperty('flex-shrink', '0', 'important'); wrapper.style.setProperty('flex-grow', '0', 'important'); } } // 通用的模态框头像处理 const modalAvatars = document.querySelectorAll('.modal .avatar-large'); modalAvatars.forEach(avatar => { setAvatarStyles(avatar, '80px'); }); } // 切换用户状态 function toggleUserStatus(userId, currentStatus) { const action = currentStatus === 1 ? '禁用' : '启用'; const newStatus = currentStatus === 1 ? 0 : 1; if (!confirm(`确定要${action}此用户吗?`)) { return; } // 显示加载状态 showLoading(); // 发送AJAX请求 fetch(`/admin/users/${userId}/toggle-status`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ status: newStatus }) }) .then(response => response.json()) .then(data => { hideLoading(); if (data.success) { showSuccess(data.message); // 刷新页面 setTimeout(() => { location.reload(); }, 1000); } else { showError(data.message); } }) .catch(error => { hideLoading(); console.error('Error:', error); showError('网络错误,请重试'); }); } // 获取性别文本 function getGenderText(gender) { switch (gender) { case 1: return '男'; case 2: return '女'; default: return '未知'; } } // 格式化日期时间 function formatDateTime(dateTimeString) { if (!dateTimeString) return '未知'; const date = new Date(dateTimeString); return date.toLocaleString('zh-CN'); } // 显示成功消息 function showSuccess(message) { showAlert(message, 'success'); } // 显示错误消息 function showError(message) { showAlert(message, 'danger'); } // 显示提示消息 function showAlert(message, type) { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show`; alertDiv.innerHTML = ` ${message} `; // 插入到页面顶部 const container = document.querySelector('.admin-content'); container.insertBefore(alertDiv, container.firstChild); // 3秒后自动关闭 setTimeout(() => { alertDiv.remove(); }, 3000); } // 显示加载状态 function showLoading() { const loadingDiv = document.createElement('div'); loadingDiv.id = 'loading-overlay'; loadingDiv.innerHTML = `
处理中...
`; loadingDiv.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; color: white; `; document.body.appendChild(loadingDiv); } // 隐藏加载状态 function hideLoading() { const loadingDiv = document.getElementById('loading-overlay'); if (loadingDiv) { loadingDiv.remove(); } } // 页面加载完成后强制设置头像样式 window.addEventListener('load', function() { forceAvatarStyles(); }); // 定时检查并修复头像样式 setInterval(function() { // 检查并修复表格头像 const tableAvatars = document.querySelectorAll('.table .user-avatar'); tableAvatars.forEach(avatar => { if (avatar.offsetWidth > 60 || avatar.offsetHeight > 60) { setAvatarStyles(avatar, '48px'); } }); // 检查并修复模态框头像 const modalAvatars = document.querySelectorAll('.modal .avatar-large'); modalAvatars.forEach(avatar => { if (avatar.offsetWidth > 100 || avatar.offsetHeight > 100) { setAvatarStyles(avatar, '80px'); } }); }, 500); 🔸============================================================================== 📄 文件: app/static/js/base.js 📊 大小: 5415 bytes (5.29 KB) 🕒 修改时间: 2025-07-08 17:18:51 🔸============================================================================== // 基础JavaScript功能 // 返回顶部功能 window.addEventListener('scroll', function() { const backToTop = document.getElementById('backToTop'); if (backToTop) { if (window.pageYOffset > 300) { backToTop.style.display = 'block'; } else { backToTop.style.display = 'none'; } } }); function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } // 购物车数量更新 function updateCartBadge(count) { const badge = document.getElementById('cartBadge'); if (badge) { if (count > 0) { badge.textContent = count; badge.style.display = 'inline-block'; } else { badge.style.display = 'none'; } } } // 通用提示函数 function showAlert(message, type = 'info', duration = 3000) { // 移除现有的提示框 const existingAlerts = document.querySelectorAll('.custom-alert'); existingAlerts.forEach(alert => alert.remove()); // 创建新的提示框 const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${getBootstrapAlertType(type)} alert-dismissible fade show custom-alert`; alertDiv.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px; max-width: 500px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); `; const icon = getAlertIcon(type); alertDiv.innerHTML = `
${message}
`; document.body.appendChild(alertDiv); // 自动消失 if (duration > 0) { setTimeout(() => { if (alertDiv.parentNode) { alertDiv.classList.remove('show'); setTimeout(() => { if (alertDiv.parentNode) { alertDiv.remove(); } }, 150); } }, duration); } return alertDiv; } // 获取Bootstrap警告类型 function getBootstrapAlertType(type) { const typeMap = { 'success': 'success', 'error': 'danger', 'warning': 'warning', 'info': 'info' }; return typeMap[type] || 'info'; } // 获取警告图标 function getAlertIcon(type) { const iconMap = { 'success': 'bi-check-circle-fill', 'error': 'bi-exclamation-triangle-fill', 'warning': 'bi-exclamation-triangle-fill', 'info': 'bi-info-circle-fill' }; return iconMap[type] || 'bi-info-circle-fill'; } // 确认对话框 function showConfirm(message, callback) { if (confirm(message)) { if (typeof callback === 'function') { callback(); } return true; } return false; } // 页面加载完成后的初始化 document.addEventListener('DOMContentLoaded', function() { // 当前页面高亮 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'); } }); // 初始化购物车数量 updateCartCount(); }); // 通用AJAX错误处理 function handleAjaxError(xhr, defaultMessage = '操作失败,请稍后再试') { if (xhr.status === 401) { showAlert('请先登录', 'warning'); setTimeout(() => { window.location.href = '/auth/login'; }, 1500); } else if (xhr.status === 403) { showAlert('没有权限执行此操作', 'error'); } else if (xhr.status === 404) { showAlert('请求的资源不存在', 'error'); } else if (xhr.status >= 500) { showAlert('服务器错误,请稍后再试', 'error'); } else { showAlert(defaultMessage, 'error'); } } // 通用成功提示(保持向后兼容) function showSuccessMessage(message) { showAlert(message, 'success'); } // 更新购物车数量 function updateCartCount() { fetch('/cart/count') .then(response => response.json()) .then(data => { if (data.success) { updateCartBadge(data.count); } }) .catch(error => { console.log('获取购物车数量失败:', error); }); } // 格式化价格 function formatPrice(price) { return '¥' + parseFloat(price).toFixed(2); } // 格式化数字 function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } // 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 节流函数 function throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } } 🔸============================================================================== 📄 文件: app/static/js/cart.js 📊 大小: 6282 bytes (6.13 KB) 🕒 修改时间: 2025-07-04 14:40:34 🔸============================================================================== 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()}`; } 🔸============================================================================== 📄 文件: app/static/js/checkout.js 📊 大小: 4771 bytes (4.66 KB) 🕒 修改时间: 2025-07-08 17:18:51 🔸============================================================================== // 订单结算页面脚本 let selectedAddressId = 0; let subtotal = 0; // 初始化页面 document.addEventListener('DOMContentLoaded', function() { // 从页面获取初始数据 const defaultAddress = document.querySelector('input[name="address_id"]:checked'); if (defaultAddress) { selectedAddressId = parseInt(defaultAddress.value); } // 获取商品总价 const subtotalElement = document.getElementById('subtotal'); if (subtotalElement) { subtotal = parseFloat(subtotalElement.textContent.replace('¥', '')); } }); // 选择地址 function selectAddress(addressId) { selectedAddressId = addressId; // 更新UI document.querySelectorAll('.address-card').forEach(card => { card.classList.remove('selected'); }); const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`); if (selectedCard) { selectedCard.classList.add('selected'); } // 更新单选按钮 const radioButton = document.querySelector(`input[value="${addressId}"]`); if (radioButton) { radioButton.checked = true; } } // 更新运费 function updateShippingFee() { const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked'); if (!shippingMethodElement) return; const shippingMethod = shippingMethodElement.value; let fee = 0; switch(shippingMethod) { case 'express': fee = 10; break; case 'same_day': fee = 20; break; default: fee = 0; } const shippingFeeElement = document.getElementById('shippingFee'); const totalAmountElement = document.getElementById('totalAmount'); if (shippingFeeElement) { shippingFeeElement.textContent = `¥${fee.toFixed(2)}`; } if (totalAmountElement) { totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`; } } // 提交订单 function submitOrder() { // 验证地址选择 if (!selectedAddressId) { showAlert('请选择收货地址', 'warning'); return; } // 获取表单数据 const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked'); const paymentMethodElement = document.querySelector('input[name="payment_method"]:checked'); const remarkElement = document.getElementById('orderRemark'); if (!shippingMethodElement) { showAlert('请选择配送方式', 'warning'); return; } if (!paymentMethodElement) { showAlert('请选择支付方式', 'warning'); return; } const shippingMethod = shippingMethodElement.value; const paymentMethod = paymentMethodElement.value; const remark = remarkElement ? remarkElement.value : ''; // 获取选中的购物车商品ID const urlParams = new URLSearchParams(window.location.search); 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'); if (!submitBtn) { showAlert('提交按钮未找到', 'error'); return; } const originalText = submitBtn.innerHTML; submitBtn.innerHTML = ' 提交中...'; submitBtn.disabled = true; // 提交订单 fetch('/order/create', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(orderData) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return 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 => { console.error('提交订单错误:', error); showAlert('提交订单失败,请重试', 'error'); // 恢复按钮状态 submitBtn.innerHTML = originalText; submitBtn.disabled = false; }); } 🔸============================================================================== 📄 文件: app/static/js/city_data.js 📊 大小: 55620 bytes (54.32 KB) 🕒 修改时间: 2025-07-04 03:24:59 🔸============================================================================== // 中国省市区数据 const cityData = { '北京市': { '北京市': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '门头沟区', '房山区', '通州区', '顺义区', '昌平区', '大兴区', '怀柔区', '平谷区', '密云区', '延庆区'] }, '上海市': { '上海市': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '虹口区', '杨浦区', '闵行区', '宝山区', '嘉定区', '浦东新区', '金山区', '松江区', '青浦区', '奉贤区', '崇明区'] }, '天津市': { '天津市': ['和平区', '河东区', '河西区', '南开区', '河北区', '红桥区', '东丽区', '西青区', '津南区', '北辰区', '武清区', '宝坻区', '滨海新区', '宁河区', '静海区', '蓟州区'] }, '重庆市': { '重庆市': ['万州区', '涪陵区', '渝中区', '大渡口区', '江北区', '沙坪坝区', '九龙坡区', '南岸区', '北碚区', '綦江区', '大足区', '渝北区', '巴南区', '黔江区', '长寿区', '江津区', '合川区', '永川区', '南川区', '璧山区', '铜梁区', '潼南区', '荣昌区', '开州区', '梁平区', '武隆区', '城口县', '丰都县', '垫江县', '忠县', '云阳县', '奉节县', '巫山县', '巫溪县', '石柱土家族自治县', '秀山土家族苗族自治县', '酉阳土家族苗族自治县', '彭水苗族土家族自治县'] }, '河北省': { '石家庄市': ['长安区', '桥西区', '新华区', '井陉矿区', '裕华区', '藁城区', '鹿泉区', '栾城区', '井陉县', '正定县', '行唐县', '灵寿县', '高邑县', '深泽县', '赞皇县', '无极县', '平山县', '元氏县', '赵县', '辛集市', '晋州市', '新乐市'], '唐山市': ['路南区', '路北区', '古冶区', '开平区', '丰南区', '丰润区', '曹妃甸区', '滦州市', '滦南县', '乐亭县', '迁西县', '玉田县', '遵化市', '迁安市'], '秦皇岛市': ['海港区', '山海关区', '北戴河区', '抚宁区', '青龙满族自治县', '昌黎县', '卢龙县'], '邯郸市': ['邯山区', '丛台区', '复兴区', '峰峰矿区', '肥乡区', '永年区', '临漳县', '成安县', '大名县', '涉县', '磁县', '邱县', '鸡泽县', '广平县', '馆陶县', '魏县', '曲周县', '武安市'], '邢台市': ['桥东区', '桥西区', '邢台县', '临城县', '内丘县', '柏乡县', '隆尧县', '任县', '南和县', '宁晋县', '巨鹿县', '新河县', '广宗县', '平乡县', '威县', '清河县', '临西县', '南宫市', '沙河市'], '保定市': ['竞秀区', '莲池区', '满城区', '清苑区', '徐水区', '涞水县', '阜平县', '定兴县', '唐县', '高阳县', '容城县', '涞源县', '望都县', '安新县', '易县', '曲阳县', '蠡县', '顺平县', '博野县', '雄县', '涿州市', '定州市', '安国市', '高碑店市'], '张家口市': ['桥东区', '桥西区', '宣化区', '下花园区', '万全区', '崇礼区', '张北县', '康保县', '沽源县', '尚义县', '蔚县', '阳原县', '怀安县', '怀来县', '涿鹿县', '赤城县'], '承德市': ['双桥区', '双滦区', '鹰手营子矿区', '承德县', '兴隆县', '平泉市', '滦平县', '隆化县', '丰宁满族自治县', '宽城满族自治县', '围场满族蒙古族自治县'], '沧州市': ['新华区', '运河区', '沧县', '青县', '东光县', '海兴县', '盐山县', '肃宁县', '南皮县', '吴桥县', '献县', '孟村回族自治县', '泊头市', '任丘市', '黄骅市', '河间市'], '廊坊市': ['安次区', '广阳区', '固安县', '永清县', '香河县', '大城县', '文安县', '大厂回族自治县', '霸州市', '三河市'], '衡水市': ['桃城区', '冀州区', '枣强县', '武邑县', '武强县', '饶阳县', '安平县', '故城县', '景县', '阜城县', '深州市'] }, '山西省': { '太原市': ['小店区', '迎泽区', '杏花岭区', '尖草坪区', '万柏林区', '晋源区', '清徐县', '阳曲县', '娄烦县', '古交市'], '大同市': ['平城区', '云冈区', '新荣区', '左云县', '阳高县', '天镇县', '广灵县', '灵丘县', '浑源县', '云州区'], '阳泉市': ['城区', '矿区', '郊区', '平定县', '盂县'], '长治市': ['潞州区', '上党区', '屯留区', '潞城区', '襄垣县', '平顺县', '黎城县', '壶关县', '长子县', '武乡县', '沁县', '沁源县'], '晋城市': ['城区', '沁水县', '阳城县', '陵川县', '泽州县', '高平市'], '朔州市': ['朔城区', '平鲁区', '山阴县', '应县', '右玉县', '怀仁市'], '晋中市': ['榆次区', '榆社县', '左权县', '和顺县', '昔阳县', '寿阳县', '太谷县', '祁县', '平遥县', '灵石县', '介休市'], '运城市': ['盐湖区', '临猗县', '万荣县', '闻喜县', '稷山县', '新绛县', '绛县', '垣曲县', '夏县', '平陆县', '芮城县', '永济市', '河津市'], '忻州市': ['忻府区', '定襄县', '五台县', '代县', '繁峙县', '宁武县', '静乐县', '神池县', '五寨县', '岢岚县', '河曲县', '保德县', '偏关县', '原平市'], '临汾市': ['尧都区', '曲沃县', '翼城县', '襄汾县', '洪洞县', '古县', '安泽县', '浮山县', '吉县', '乡宁县', '大宁县', '隰县', '永和县', '蒲县', '汾西县', '侯马市', '霍州市'], '吕梁市': ['离石区', '文水县', '交城县', '兴县', '临县', '柳林县', '石楼县', '岚县', '方山县', '中阳县', '交口县', '孝义市', '汾阳市'] }, '内蒙古自治区': { '呼和浩特市': ['新城区', '回民区', '玉泉区', '赛罕区', '土默特左旗', '托克托县', '和林格尔县', '清水河县', '武川县'], '包头市': ['东河区', '昆都仑区', '青山区', '石拐区', '白云鄂博矿区', '九原区', '土默特右旗', '固阳县', '达尔罕茂明安联合旗'], '乌海市': ['海勃湾区', '海南区', '乌达区'], '赤峰市': ['红山区', '元宝山区', '松山区', '阿鲁科尔沁旗', '巴林左旗', '巴林右旗', '林西县', '克什克腾旗', '翁牛特旗', '喀喇沁旗', '宁城县', '敖汉旗'], '通辽市': ['科尔沁区', '科尔沁左翼中旗', '科尔沁左翼后旗', '开鲁县', '库伦旗', '奈曼旗', '扎鲁特旗', '霍林郭勒市'], '鄂尔多斯市': ['东胜区', '康巴什区', '达拉特旗', '准格尔旗', '鄂托克前旗', '鄂托克旗', '杭锦旗', '乌审旗', '伊金霍洛旗'], '呼伦贝尔市': ['海拉尔区', '扎赉诺尔区', '阿荣旗', '莫力达瓦达斡尔族自治旗', '鄂伦春自治旗', '鄂温克族自治旗', '陈巴尔虎旗', '新巴尔虎左旗', '新巴尔虎右旗', '满洲里市', '牙克石市', '扎兰屯市', '额尔古纳市', '根河市'], '巴彦淖尔市': ['临河区', '五原县', '磴口县', '乌拉特前旗', '乌拉特中旗', '乌拉特后旗', '杭锦后旗'], '乌兰察布市': ['集宁区', '卓资县', '化德县', '商都县', '兴和县', '凉城县', '察哈尔右翼前旗', '察哈尔右翼中旗', '察哈尔右翼后旗', '四子王旗', '丰镇市'], '兴安盟': ['乌兰浩特市', '阿尔山市', '科尔沁右翼前旗', '科尔沁右翼中旗', '扎赉特旗', '突泉县'], '锡林郭勒盟': ['锡林浩特市', '阿巴嘎旗', '苏尼特左旗', '苏尼特右旗', '东乌珠穆沁旗', '西乌珠穆沁旗', '太仆寺旗', '镶黄旗', '正镶白旗', '正蓝旗', '多伦县', '二连浩特市'], '阿拉善盟': ['阿拉善左旗', '阿拉善右旗', '额济纳旗'] }, '辽宁省': { '沈阳市': ['和平区', '沈河区', '大东区', '皇姑区', '铁西区', '苏家屯区', '浑南区', '沈北新区', '于洪区', '辽中区', '康平县', '法库县', '新民市'], '大连市': ['中山区', '西岗区', '沙河口区', '甘井子区', '旅顺口区', '金州区', '普兰店区', '长海县', '瓦房店市', '庄河市'], '鞍山市': ['铁东区', '铁西区', '立山区', '千山区', '台安县', '岫岩满族自治县', '海城市'], '抚顺市': ['新抚区', '东洲区', '望花区', '顺城区', '抚顺县', '新宾满族自治县', '清原满族自治县'], '本溪市': ['平山区', '溪湖区', '明山区', '南芬区', '本溪满族自治县', '桓仁满族自治县'], '丹东市': ['元宝区', '振兴区', '振安区', '宽甸满族自治县', '东港市', '凤城市'], '锦州市': ['古塔区', '凌河区', '太和区', '黑山县', '义县', '凌海市', '北镇市'], '营口市': ['站前区', '西市区', '鲅鱼圈区', '老边区', '盖州市', '大石桥市'], '阜新市': ['海州区', '新邱区', '太平区', '清河门区', '细河区', '阜新蒙古族自治县', '彰武县'], '辽阳市': ['白塔区', '文圣区', '宏伟区', '弓长岭区', '太子河区', '辽阳县', '灯塔市'], '盘锦市': ['双台子区', '兴隆台区', '大洼区', '盘山县'], '铁岭市': ['银州区', '清河区', '铁岭县', '西丰县', '昌图县', '调兵山市', '开原市'], '朝阳市': ['双塔区', '龙城区', '朝阳县', '建平县', '喀喇沁左翼蒙古族自治县', '北票市', '凌源市'], '葫芦岛市': ['连山区', '龙港区', '南票区', '绥中县', '建昌县', '兴城市'] }, '吉林省': { '长春市': ['南关区', '宽城区', '朝阳区', '二道区', '绿园区', '双阳区', '九台区', '农安县', '榆树市', '德惠市'], '吉林市': ['昌邑区', '龙潭区', '船营区', '丰满区', '永吉县', '蛟河市', '桦甸市', '舒兰市', '磐石市'], '四平市': ['铁西区', '铁东区', '梨树县', '伊通满族自治县', '公主岭市', '双辽市'], '辽源市': ['龙山区', '西安区', '东丰县', '东辽县'], '通化市': ['东昌区', '二道江区', '通化县', '辉南县', '柳河县', '梅河口市', '集安市'], '白山市': ['浑江区', '江源区', '抚松县', '靖宇县', '长白朝鲜族自治县', '临江市'], '松原市': ['宁江区', '前郭尔罗斯蒙古族自治县', '长岭县', '乾安县', '扶余市'], '白城市': ['洮北区', '镇赖县', '通榆县', '洮南市', '大安市'], '延边朝鲜族自治州': ['延吉市', '图们市', '敦化市', '珲春市', '龙井市', '和龙市', '汪清县', '安图县'] }, '黑龙江省': { '哈尔滨市': ['道里区', '南岗区', '道外区', '平房区', '松北区', '香坊区', '呼兰区', '阿城区', '双城区', '依兰县', '方正县', '宾县', '巴彦县', '木兰县', '通河县', '延寿县', '尚志市', '五常市'], '齐齐哈尔市': ['龙沙区', '建华区', '铁锋区', '昂昂溪区', '富拉尔基区', '碾子山区', '梅里斯达斡尔族区', '龙江县', '依安县', '泰来县', '甘南县', '富裕县', '克山县', '克东县', '拜泉县', '讷河市'], '鸡西市': ['鸡冠区', '恒山区', '滴道区', '梨树区', '城子河区', '麻山区', '鸡东县', '虎林市', '密山市'], '鹤岗市': ['向阳区', '工农区', '南山区', '兴安区', '东山区', '兴山区', '萝北县', '绥滨县'], '双鸭山市': ['尖山区', '岭东区', '四方台区', '宝山区', '集贤县', '友谊县', '宝清县', '饶河县'], '大庆市': ['萨尔图区', '龙凤区', '让胡路区', '红岗区', '大同区', '肇州县', '肇源县', '林甸县', '杜尔伯特蒙古族自治县'], '伊春市': ['伊春区', '南岔区', '友好区', '西林区', '翠峦区', '新青区', '美溪区', '金山屯区', '五营区', '乌马河区', '汤旺河区', '带岭区', '乌伊岭区', '红星区', '上甘岭区', '嘉荫县', '铁力市'], '佳木斯市': ['向阳区', '前进区', '东风区', '郊区', '桦南县', '桦川县', '汤原县', '抚远市', '同江市', '富锦市'], '七台河市': ['新兴区', '桃山区', '茄子河区', '勃利县'], '牡丹江市': ['东安区', '阳明区', '爱民区', '西安区', '林口县', '绥芬河市', '海林市', '宁安市', '穆棱市', '东宁市'], '黑河市': ['爱辉区', '嫩江县', '逊克县', '孙吴县', '北安市', '五大连池市'], '绥化市': ['北林区', '望奎县', '兰西县', '青冈县', '庆安县', '明水县', '绥棱县', '安达市', '肇东市', '海伦市'], '大兴安岭地区': ['呼玛县', '塔河县', '漠河市'] }, '江苏省': { '南京市': ['玄武区', '秦淮区', '建邺区', '鼓楼区', '浦口区', '栖霞区', '雨花台区', '江宁区', '六合区', '溧水区', '高淳区'], '无锡市': ['锡山区', '惠山区', '滨湖区', '梁溪区', '新吴区', '江阴市', '宜兴市'], '徐州市': ['鼓楼区', '云龙区', '贾汪区', '泉山区', '铜山区', '丰县', '沛县', '睢宁县', '新沂市', '邳州市'], '常州市': ['天宁区', '钟楼区', '新北区', '武进区', '金坛区', '溧阳市'], '苏州市': ['虎丘区', '吴中区', '相城区', '姑苏区', '吴江区', '常熟市', '张家港市', '昆山市', '太仓市'], '南通市': ['崇川区', '港闸区', '通州区', '海安市', '如东县', '启东市', '如皋市', '海门市'], '连云港市': ['连云区', '海州区', '赣榆区', '东海县', '灌云县', '灌南县'], '淮安市': ['淮安区', '淮阴区', '清江浦区', '洪泽区', '涟水县', '盱眙县', '金湖县'], '盐城市': ['亭湖区', '盐都区', '大丰区', '响水县', '滨海县', '阜宁县', '射阳县', '建湖县', '东台市'], '扬州市': ['广陵区', '邗江区', '江都区', '宝应县', '仪征市', '高邮市'], '镇江市': ['京口区', '润州区', '丹徒区', '丹阳市', '扬中市', '句容市'], '泰州市': ['海陵区', '高港区', '姜堰区', '兴化市', '靖江市', '泰兴市'], '宿迁市': ['宿城区', '宿豫区', '沭阳县', '泗阳县', '泗洪县'] }, '浙江省': { '杭州市': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区', '萧山区', '余杭区', '富阳区', '临安区', '桐庐县', '淳安县', '建德市'], '宁波市': ['海曙区', '江北区', '北仑区', '镇海区', '鄞州区', '奉化区', '象山县', '宁海县', '余姚市', '慈溪市'], '温州市': ['鹿城区', '龙湾区', '瓯海区', '洞头区', '永嘉县', '平阳县', '苍南县', '文成县', '泰顺县', '瑞安市', '乐清市'], '嘉兴市': ['南湖区', '秀洲区', '嘉善县', '海盐县', '海宁市', '平湖市', '桐乡市'], '湖州市': ['吴兴区', '南浔区', '德清县', '长兴县', '安吉县'], '绍兴市': ['越城区', '柯桥区', '上虞区', '新昌县', '诸暨市', '嵊州市'], '金华市': ['婺城区', '金东区', '武义县', '浦江县', '磐安县', '兰溪市', '义乌市', '东阳市', '永康市'], '衢州市': ['柯城区', '衢江区', '常山县', '开化县', '龙游县', '江山市'], '舟山市': ['定海区', '普陀区', '岱山县', '嵊泗县'], '台州市': ['椒江区', '黄岩区', '路桥区', '三门县', '天台县', '仙居县', '温岭市', '临海市', '玉环市'], '丽水市': ['莲都区', '青田县', '缙云县', '遂昌县', '松阳县', '云和县', '庆元县', '景宁畲族自治县', '龙泉市'] }, '安徽省': { '合肥市': ['瑶海区', '庐阳区', '蜀山区', '包河区', '长丰县', '肥东县', '肥西县', '庐江县', '巢湖市'], '芜湖市': ['镜湖区', '弋江区', '鸠江区', '三山区', '芜湖县', '繁昌县', '南陵县', '无为市'], '蚌埠市': ['龙子湖区', '蚌山区', '禹会区', '淮上区', '怀远县', '五河县', '固镇县'], '淮南市': ['大通区', '田家庵区', '谢家集区', '八公山区', '潘集区', '凤台县', '寿县'], '马鞍山市': ['花山区', '雨山区', '博望区', '当涂县', '含山县', '和县'], '淮北市': ['杜集区', '相山区', '烈山区', '濉溪县'], '铜陵市': ['铜官区', '义安区', '郊区', '枞阳县'], '安庆市': ['迎江区', '大观区', '宜秀区', '怀宁县', '潜山市', '太湖县', '宿松县', '望江县', '岳西县', '桐城市'], '黄山市': ['屯溪区', '黄山区', '徽州区', '歙县', '休宁县', '黟县', '祁门县'], '滁州市': ['琅琊区', '南谯区', '来安县', '全椒县', '定远县', '凤阳县', '天长市', '明光市'], '阜阳市': ['颍州区', '颍东区', '颍泉区', '临泉县', '太和县', '阜南县', '颍上县', '界首市'], '宿州市': ['埇桥区', '砀山县', '萧县', '灵璧县', '泗县'], '六安市': ['金安区', '裕安区', '叶集区', '霍邱县', '舒城县', '金寨县', '霍山县'], '亳州市': ['谯城区', '涡阳县', '蒙城县', '利辛县'], '池州市': ['贵池区', '东至县', '石台县', '青阳县'], '宣城市': ['宣州区', '郎溪县', '广德市', '泾县', '绩溪县', '旌德县', '宁国市'] }, '福建省': { '福州市': ['鼓楼区', '台江区', '仓山区', '马尾区', '晋安区', '长乐区', '闽侯县', '连江县', '罗源县', '闽清县', '永泰县', '平潭县', '福清市'], '厦门市': ['思明区', '海沧区', '湖里区', '集美区', '同安区', '翔安区'], '莆田市': ['城厢区', '涵江区', '荔城区', '秀屿区', '仙游县'], '三明市': ['梅列区', '三元区', '明溪县', '清流县', '宁化县', '大田县', '尤溪县', '沙县', '将乐县', '泰宁县', '建宁县', '永安市'], '泉州市': ['鲤城区', '丰泽区', '洛江区', '泉港区', '惠安县', '安溪县', '永春县', '德化县', '金门县', '石狮市', '晋江市', '南安市'], '漳州市': ['芗城区', '龙文区', '云霄县', '漳浦县', '诏安县', '长泰县', '东山县', '南靖县', '平和县', '华安县', '龙海市'], '南平市': ['延平区', '建阳区', '顺昌县', '浦城县', '光泽县', '松溪县', '政和县', '邵武市', '武夷山市', '建瓯市'], '龙岩市': ['新罗区', '永定区', '长汀县', '上杭县', '武平县', '连城县', '漳平市'], '宁德市': ['蕉城区', '霞浦县', '古田县', '屏南县', '寿宁县', '周宁县', '柘荣县', '福安市', '福鼎市'] }, '江西省': { '南昌市': ['东湖区', '西湖区', '青云谱区', '湾里区', '青山湖区', '新建区', '南昌县', '安义县', '进贤县'], '景德镇市': ['昌江区', '珠山区', '浮梁县', '乐平市'], '萍乡市': ['安源区', '湘东区', '莲花县', '上栗县', '芦溪县'], '九江市': ['濂溪区', '浔阳区', '柴桑区', '武宁县', '修水县', '永修县', '德安县', '都昌县', '湖口县', '彭泽县', '瑞昌市', '共青城市', '庐山市'], '新余市': ['渝水区', '分宜县'], '鹰潭市': ['月湖区', '余江区', '贵溪市'], '赣州市': ['章贡区', '南康区', '赣县区', '信丰县', '大余县', '上犹县', '崇义县', '安远县', '龙南县', '定南县', '全南县', '宁都县', '于都县', '兴国县', '会昌县', '寻乌县', '石城县', '瑞金市'], '吉安市': ['吉州区', '青原区', '吉安县', '吉水县', '峡江县', '新干县', '永丰县', '泰和县', '遂川县', '万安县', '安福县', '永新县', '井冈山市'], '宜春市': ['袁州区', '奉新县', '万载县', '上高县', '宜丰县', '靖安县', '铜鼓县', '丰城市', '樟树市', '高安市'], '抚州市': ['临川区', '东乡区', '南城县', '黎川县', '南丰县', '崇仁县', '乐安县', '宜黄县', '金溪县', '资溪县', '广昌县'], '上饶市': ['信州区', '广丰区', '广信区', '玉山县', '铅山县', '横峰县', '弋阳县', '余干县', '鄱阳县', '万年县', '婺源县', '德兴市'] }, '山东省': { '济南市': ['历下区', '市中区', '槐荫区', '天桥区', '历城区', '长清区', '章丘区', '济阳区', '莱芜区', '钢城区', '平阴县', '商河县'], '青岛市': ['市南区', '市北区', '黄岛区', '崂山区', '李沧区', '城阳区', '即墨区', '胶州市', '平度市', '莱西市'], '淄博市': ['淄川区', '张店区', '博山区', '临淄区', '周村区', '桓台县', '高青县', '沂源县'], '枣庄市': ['市中区', '薛城区', '峄城区', '台儿庄区', '山亭区', '滕州市'], '东营市': ['东营区', '河口区', '垦利区', '利津县', '广饶县'], '烟台市': ['芝罘区', '福山区', '牟平区', '莱山区', '长岛县', '龙口市', '莱阳市', '莱州市', '蓬莱市', '招远市', '栖霞市', '海阳市'], '潍坊市': ['潍城区', '寒亭区', '坊子区', '奎文区', '临朐县', '昌乐县', '青州市', '诸城市', '寿光市', '安丘市', '高密市', '昌邑市'], '济宁市': ['任城区', '兖州区', '微山县', '鱼台县', '金乡县', '嘉祥县', '汶上县', '泗水县', '梁山县', '曲阜市', '邹城市'], '泰安市': ['泰山区', '岱岳区', '宁阳县', '东平县', '新泰市', '肥城市'], '威海市': ['环翠区', '文登区', '荣成市', '乳山市'], '日照市': ['东港区', '岚山区', '五莲县', '莒县'], '临沂市': ['兰山区', '罗庄区', '河东区', '沂南县', '郯城县', '沂水县', '兰陵县', '费县', '平邑县', '莒南县', '蒙阴县', '临沭县'], '德州市': ['德城区', '陵城区', '宁津县', '庆云县', '临邑县', '齐河县', '平原县', '夏津县', '武城县', '乐陵市', '禹城市'], '聊城市': ['东昌府区', '茌平区', '阳谷县', '莘县', '茌平县', '东阿县', '冠县', '高唐县', '临清市'], '滨州市': ['滨城区', '沾化区', '惠民县', '阳信县', '无棣县', '博兴县', '邹平市'], '菏泽市': ['牡丹区', '定陶区', '曹县', '单县', '成武县', '巨野县', '郓城县', '鄄城县', '东明县'] }, '河南省': { '郑州市': ['中原区', '二七区', '管城回族区', '金水区', '上街区', '惠济区', '中牟县', '巩义市', '荥阳市', '新密市', '新郑市', '登封市'], '开封市': ['龙亭区', '顺河回族区', '鼓楼区', '禹王台区', '祥符区', '杞县', '通许县', '尉氏县', '兰考县'], '洛阳市': ['老城区', '西工区', '瀍河回族区', '涧西区', '吉利区', '洛龙区', '孟津县', '新安县', '栾川县', '嵩县', '汝阳县', '宜阳县', '洛宁县', '伊川县', '偃师市'], '平顶山市': ['新华区', '卫东区', '石龙区', '湛河区', '宝丰县', '叶县', '鲁山县', '郏县', '舞钢市', '汝州市'], '安阳市': ['文峰区', '北关区', '殷都区', '龙安区', '安阳县', '汤阴县', '滑县', '内黄县', '林州市'], '鹤壁市': ['鹤山区', '山城区', '淇滨区', '浚县', '淇县'], '新乡市': ['红旗区', '卫滨区', '凤泉区', '牧野区', '新乡县', '获嘉县', '原阳县', '延津县', '封丘县', '长垣市', '卫辉市', '辉县市'], '焦作市': ['解放区', '中站区', '马村区', '山阳区', '修武县', '博爱县', '武陟县', '温县', '沁阳市', '孟州市'], '濮阳市': ['华龙区', '清丰县', '南乐县', '范县', '台前县', '濮阳县'], '许昌市': ['魏都区', '建安区', '鄢陵县', '襄城县', '禹州市', '长葛市'], '漯河市': ['源汇区', '郾城区', '召陵区', '舞阳县', '临颍县'], '三门峡市': ['湖滨区', '陕州区', '渑池县', '卢氏县', '义马市', '灵宝市'], '南阳市': ['宛城区', '卧龙区', '南召县', '方城县', '西峡县', '镇平县', '内乡县', '淅川县', '社旗县', '唐河县', '新野县', '桐柏县', '邓州市'], '商丘市': ['梁园区', '睢阳区', '民权县', '睢县', '宁陵县', '柘城县', '虞城县', '夏邑县', '永城市'], '信阳市': ['浉河区', '平桥区', '罗山县', '光山县', '新县', '商城县', '固始县', '潢川县', '淮滨县', '息县'], '周口市': ['川汇区', '扶沟县', '西华县', '商水县', '沈丘县', '郸城县', '淮阳区', '太康县', '鹿邑县', '项城市'], '驻马店市': ['驿城区', '西平县', '上蔡县', '平舆县', '正阳县', '确山县', '泌阳县', '汝南县', '遂平县', '新蔡县'], '济源市': ['济源市'] }, '湖北省': { '武汉市': ['江岸区', '江汉区', '硚口区', '汉阳区', '武昌区', '青山区', '洪山区', '东西湖区', '汉南区', '蔡甸区', '江夏区', '黄陂区', '新洲区'], '黄石市': ['黄石港区', '西塞山区', '下陆区', '铁山区', '阳新县', '大冶市'], '十堰市': ['茅箭区', '张湾区', '郧阳区', '郧西县', '竹山县', '竹溪县', '房县', '丹江口市'], '宜昌市': ['西陵区', '伍家岗区', '点军区', '猇亭区', '夷陵区', '远安县', '兴山县', '秭归县', '长阳土家族自治县', '五峰土家族自治县', '宜都市', '当阳市', '枝江市'], '襄阳市': ['襄城区', '樊城区', '襄州区', '南漳县', '谷城县', '保康县', '老河口市', '枣阳市', '宜城市'], '鄂州市': ['梁子湖区', '华容区', '鄂城区'], '荆门市': ['东宝区', '掇刀区', '京山市', '沙洋县', '钟祥市'], '孝感市': ['孝南区', '孝昌县', '大悟县', '云梦县', '应城市', '安陆市', '汉川市'], '荆州市': ['沙市区', '荆州区', '公安县', '监利县', '江陵县', '石首市', '洪湖市', '松滋市'], '黄冈市': ['黄州区', '团风县', '红安县', '罗田县', '英山县', '浠水县', '蕲春县', '黄梅县', '麻城市', '武穴市'], '咸宁市': ['咸安区', '嘉鱼县', '通城县', '崇阳县', '通山县', '赤壁市'], '随州市': ['曾都区', '随县', '广水市'], '恩施土家族苗族自治州': ['恩施市', '利川市', '建始县', '巴东县', '宣恩县', '咸丰县', '来凤县', '鹤峰县'], '仙桃市': ['仙桃市'], '潜江市': ['潜江市'], '天门市': ['天门市'], '神农架林区': ['神农架林区'] }, '湖南省': { '长沙市': ['芙蓉区', '天心区', '岳麓区', '开福区', '雨花区', '望城区', '长沙县', '宁乡市', '浏阳市'], '株洲市': ['荷塘区', '芦淞区', '石峰区', '天元区', '渌口区', '攸县', '茶陵县', '炎陵县', '醴陵市'], '湘潭市': ['雨湖区', '岳塘区', '湘潭县', '湘乡市', '韶山市'], '衡阳市': ['珠晖区', '雁峰区', '石鼓区', '蒸湘区', '南岳区', '衡阳县', '衡南县', '衡山县', '衡东县', '祁东县', '耒阳市', '常宁市'], '邵阳市': ['双清区', '大祥区', '北塔区', '邵东市', '新邵县', '邵阳县', '隆回县', '洞口县', '绥宁县', '新宁县', '城步苗族自治县', '武冈市'], '岳阳市': ['岳阳楼区', '云溪区', '君山区', '岳阳县', '华容县', '湘阴县', '平江县', '汨罗市', '临湘市'], '常德市': ['武陵区', '鼎城区', '安乡县', '汉寿县', '澧县', '临澧县', '桃源县', '石门县', '津市市'], '张家界市': ['永定区', '武陵源区', '慈利县', '桑植县'], '益阳市': ['资阳区', '赫山区', '南县', '桃江县', '安化县', '沅江市'], '郴州市': ['北湖区', '苏仙区', '桂阳县', '宜章县', '永兴县', '嘉禾县', '临武县', '汝城县', '桂东县', '安仁县', '资兴市'], '永州市': ['零陵区', '冷水滩区', '祁阳县', '东安县', '双牌县', '道县', '江永县', '宁远县', '蓝山县', '新田县', '江华瑶族自治县'], '怀化市': ['鹤城区', '中方县', '沅陵县', '辰溪县', '溆浦县', '会同县', '麻阳苗族自治县', '新晃侗族自治县', '芷江侗族自治县', '靖州苗族侗族自治县', '通道侗族自治县', '洪江市'], '娄底市': ['娄星区', '双峰县', '新化县', '冷水江市', '涟源市'], '湘西土家族苗族自治州': ['吉首市', '泸溪县', '凤凰县', '花垣县', '保靖县', '古丈县', '永顺县', '龙山县'] }, '广东省': { '广州市': ['荔湾区', '越秀区', '海珠区', '天河区', '白云区', '黄埔区', '番禺区', '花都区', '南沙区', '从化区', '增城区'], '深圳市': ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区', '龙华区', '坪山区', '光明区', '大鹏新区'], '珠海市': ['香洲区', '斗门区', '金湾区'], '汕头市': ['龙湖区', '金平区', '濠江区', '潮阳区', '潮南区', '澄海区', '南澳县'], '佛山市': ['禅城区', '南海区', '顺德区', '三水区', '高明区'], '韶关市': ['武江区', '浈江区', '曲江区', '始兴县', '仁化县', '翁源县', '乳源瑶族自治县', '新丰县', '乐昌市', '南雄市'], '湛江市': ['赤坎区', '霞山区', '坡头区', '麻章区', '遂溪县', '徐闻县', '廉江市', '雷州市', '吴川市'], '肇庆市': ['端州区', '鼎湖区', '高要区', '广宁县', '怀集县', '封开县', '德庆县', '四会市'], '江门市': ['蓬江区', '江海区', '新会区', '台山市', '开平市', '鹤山市', '恩平市'], '茂名市': ['茂南区', '电白区', '高州市', '化州市', '信宜市'], '惠州市': ['惠城区', '惠阳区', '博罗县', '惠东县', '龙门县'], '梅州市': ['梅江区', '梅县区', '大埔县', '丰顺县', '五华县', '平远县', '蕉岭县', '兴宁市'], '汕尾市': ['城区', '海丰县', '陆河县', '陆丰市'], '河源市': ['源城区', '紫金县', '龙川县', '连平县', '和平县', '东源县'], '阳江市': ['江城区', '阳东区', '阳西县', '阳春市'], '清远市': ['清城区', '清新区', '佛冈县', '阳山县', '连山壮族瑶族自治县', '连南瑶族自治县', '英德市', '连州市'], '东莞市': ['东莞市'], '中山市': ['中山市'], '潮州市': ['湘桥区', '潮安区', '饶平县'], '揭阳市': ['榕城区', '揭东区', '揭西县', '惠来县', '普宁市'], '云浮市': ['云城区', '云安区', '新兴县', '郁南县', '罗定市'] }, '广西壮族自治区': { '南宁市': ['兴宁区', '青秀区', '江南区', '西乡塘区', '良庆区', '邕宁区', '武鸣区', '隆安县', '马山县', '上林县', '宾阳县', '横县'], '柳州市': ['城中区', '鱼峰区', '柳南区', '柳北区', '柳江区', '柳城县', '鹿寨县', '融安县', '融水苗族自治县', '三江侗族自治县'], '桂林市': ['秀峰区', '叠彩区', '象山区', '七星区', '雁山区', '临桂区', '阳朔县', '灵川县', '全州县', '兴安县', '永福县', '灌阳县', '龙胜各族自治县', '资源县', '平乐县', '荔浦市', '恭城瑶族自治县'], '梧州市': ['万秀区', '长洲区', '龙圩区', '苍梧县', '藤县', '蒙山县', '岑溪市'], '北海市': ['海城区', '银海区', '铁山港区', '合浦县'], '防城港市': ['港口区', '防城区', '上思县', '东兴市'], '钦州市': ['钦南区', '钦北区', '灵山县', '浦北县'], '贵港市': ['港北区', '港南区', '覃塘区', '平南县', '桂平市'], '玉林市': ['玉州区', '福绵区', '容县', '陆川县', '博白县', '兴业县', '北流市'], '百色市': ['右江区', '田阳区', '田东县', '平果市', '德保县', '那坡县', '凌云县', '乐业县', '田林县', '西林县', '隆林各族自治县', '靖西市'], '贺州市': ['八步区', '平桂区', '昭平县', '钟山县', '富川瑶族自治县'], '河池市': ['金城江区', '宜州区', '南丹县', '天峨县', '凤山县', '东兰县', '罗城仫佬族自治县', '环江毛南族自治县', '巴马瑶族自治县', '都安瑶族自治县', '大化瑶族自治县'], '来宾市': ['兴宾区', '忻城县', '象州县', '武宣县', '金秀瑶族自治县', '合山市'], '崇左市': ['江州区', '扶绥县', '宁明县', '龙州县', '大新县', '天等县', '凭祥市'] }, '海南省': { '海口市': ['秀英区', '龙华区', '琼山区', '美兰区'], '三亚市': ['海棠区', '吉阳区', '天涯区', '崖州区'], '三沙市': ['西沙群岛', '南沙群岛', '中沙群岛'], '儋州市': ['儋州市'], '五指山市': ['五指山市'], '琼海市': ['琼海市'], '文昌市': ['文昌市'], '万宁市': ['万宁市'], '东方市': ['东方市'], '定安县': ['定安县'], '屯昌县': ['屯昌县'], '澄迈县': ['澄迈县'], '临高县': ['临高县'], '白沙黎族自治县': ['白沙黎族自治县'], '昌江黎族自治县': ['昌江黎族自治县'], '乐东黎族自治县': ['乐东黎族自治县'], '陵水黎族自治县': ['陵水黎族自治县'], '保亭黎族苗族自治县': ['保亭黎族苗族自治县'], '琼中黎族苗族自治县': ['琼中黎族苗族自治县'] }, '四川省': { '成都市': ['锦江区', '青羊区', '金牛区', '武侯区', '成华区', '龙泉驿区', '青白江区', '新都区', '温江区', '双流区', '郫都区', '新津区', '金堂县', '大邑县', '蒲江县', '都江堰市', '彭州市', '邛崃市', '崇州市', '简阳市'], '自贡市': ['自流井区', '贡井区', '大安区', '沿滩区', '荣县', '富顺县'], '攀枝花市': ['东区', '西区', '仁和区', '米易县', '盐边县'], '泸州市': ['江阳区', '纳溪区', '龙马潭区', '泸县', '合江县', '叙永县', '古蔺县'], '德阳市': ['旌阳区', '罗江区', '中江县', '广汉市', '什邡市', '绵竹市'], '绵阳市': ['涪城区', '游仙区', '安州区', '三台县', '盐亭县', '梓潼县', '北川羌族自治县', '平武县', '江油市'], '广元市': ['利州区', '昭化区', '朝天区', '旺苍县', '青川县', '剑阁县', '苍溪县'], '遂宁市': ['船山区', '安居区', '蓬溪县', '射洪市', '大英县'], '内江市': ['市中区', '东兴区', '威远县', '资中县', '隆昌市'], '乐山市': ['市中区', '沙湾区', '五通桥区', '金口河区', '犍为县', '井研县', '夹江县', '沐川县', '峨边彝族自治县', '马边彝族自治县', '峨眉山市'], '南充市': ['顺庆区', '高坪区', '嘉陵区', '南部县', '营山县', '蓬安县', '仪陇县', '西充县', '阆中市'], '眉山市': ['东坡区', '彭山区', '仁寿县', '洪雅县', '丹棱县', '青神县'], '宜宾市': ['翠屏区', '南溪区', '叙州区', '江安县', '长宁县', '高县', '珙县', '筠连县', '兴文县', '屏山县'], '广安市': ['广安区', '前锋区', '岳池县', '武胜县', '邻水县', '华蓥市'], '达州市': ['通川区', '达川区', '宣汉县', '开江县', '大竹县', '渠县', '万源市'], '雅安市': ['雨城区', '名山区', '荥经县', '汉源县', '石棉县', '天全县', '芦山县', '宝兴县'], '巴中市': ['巴州区', '恩阳区', '通江县', '南江县', '平昌县'], '资阳市': ['雁江区', '安岳县', '乐至县'], '阿坝藏族羌族自治州': ['马尔康市', '汶川县', '理县', '茂县', '松潘县', '九寨沟县', '金川县', '小金县', '黑水县', '壤塘县', '阿坝县', '若尔盖县', '红原县'], '甘孜藏族自治州': ['康定市', '泸定县', '丹巴县', '九龙县', '雅江县', '道孚县', '炉霍县', '甘孜县', '新龙县', '德格县', '白玉县', '石渠县', '色达县', '理塘县', '巴塘县', '乡城县', '稻城县', '得荣县'], '凉山彝族自治州': ['西昌市', '木里藏族自治县', '盐源县', '德昌县', '会理市', '会东县', '宁南县', '普格县', '布拖县', '金阳县', '昭觉县', '喜德县', '冕宁县', '越西县', '甘洛县', '美姑县', '雷波县'] }, '贵州省': { '贵阳市': ['南明区', '云岩区', '花溪区', '乌当区', '白云区', '观山湖区', '开阳县', '息烽县', '修文县', '清镇市'], '六盘水市': ['钟山区', '六枝特区', '水城区', '盘州市'], '遵义市': ['红花岗区', '汇川区', '播州区', '桐梓县', '绥阳县', '正安县', '道真仡佬族苗族自治县', '务川仡佬族苗族自治县', '凤冈县', '湄潭县', '余庆县', '习水县', '赤水市', '仁怀市'], '安顺市': ['西秀区', '平坝区', '普定县', '镇宁布依族苗族自治县', '关岭布依族苗族自治县', '紫云苗族布依族自治县'], '毕节市': ['七星关区', '大方县', '黔西市', '金沙县', '织金县', '纳雍县', '威宁彝族回族苗族自治县', '赫章县'], '铜仁市': ['碧江区', '万山区', '江口县', '玉屏侗族自治县', '石阡县', '思南县', '印江土家族苗族自治县', '德江县', '沿河土家族自治县', '松桃苗族自治县'], '黔西南布依族苗族自治州': ['兴义市', '兴仁市', '普安县', '晴隆县', '贞丰县', '望谟县', '册亨县', '安龙县'], '黔东南苗族侗族自治州': ['凯里市', '黄平县', '施秉县', '三穗县', '镇远县', '岑巩县', '天柱县', '锦屏县', '剑河县', '台江县', '黎平县', '榕江县', '从江县', '雷山县', '麻江县', '丹寨县'], '黔南布依族苗族自治州': ['都匀市', '福泉市', '荔波县', '贵定县', '瓮安县', '独山县', '平塘县', '罗甸县', '长顺县', '龙里县', '惠水县', '三都水族自治县'] }, '云南省': { '昆明市': ['五华区', '盘龙区', '官渡区', '西山区', '东川区', '呈贡区', '晋宁区', '富民县', '宜良县', '石林彝族自治县', '嵩明县', '禄劝彝族苗族自治县', '寻甸回族彝族自治县', '安宁市'], '曲靖市': ['麒麟区', '沾益区', '马龙区', '陆良县', '师宗县', '罗平县', '富源县', '会泽县', '宣威市'], '玉溪市': ['红塔区', '江川区', '澄江市', '通海县', '华宁县', '易门县', '峨山彝族自治县', '新平彝族傣族自治县', '元江哈尼族彝族傣族自治县'], '保山市': ['隆阳区', '施甸县', '龙陵县', '昌宁县', '腾冲市'], '昭通市': ['昭阳区', '鲁甸县', '巧家县', '盐津县', '大关县', '永善县', '绥江县', '镇雄县', '彝良县', '威信县', '水富市'], '丽江市': ['古城区', '玉龙纳西族自治县', '永胜县', '华坪县', '宁蒗彝族自治县'], '普洱市': ['思茅区', '宁洱哈尼族彝族自治县', '墨江哈尼族自治县', '景东彝族自治县', '景谷傣族彝族自治县', '镇沅彝族哈尼族拉祜族自治县', '江城哈尼族彝族自治县', '孟连傣族拉祜族佤族自治县', '澜沧拉祜族自治县', '西盟佤族自治县'], '临沧市': ['临翔区', '凤庆县', '云县', '永德县', '镇康县', '双江拉祜族佤族布朗族傣族自治县', '耿马傣族佤族自治县', '沧源佤族自治县'], '楚雄彝族自治州': ['楚雄市', '双柏县', '牟定县', '南华县', '姚安县', '大姚县', '永仁县', '元谋县', '武定县', '禄丰市'], '红河哈尼族彝族自治州': ['个旧市', '开远市', '蒙自市', '弥勒市', '屏边苗族自治县', '建水县', '石屏县', '泸西县', '元阳县', '红河县', '金平苗族瑶族傣族自治县', '绿春县', '河口瑶族自治县'], '文山壮族苗族自治州': ['文山市', '砚山县', '西畴县', '麻栗坡县', '马关县', '丘北县', '广南县', '富宁县'], '西双版纳傣族自治州': ['景洪市', '勐海县', '勐腊县'], '大理白族自治州': ['大理市', '漾濞彝族自治县', '祥云县', '宾川县', '弥渡县', '南涧彝族自治县', '巍山彝族回族自治县', '永平县', '云龙县', '洱源县', '剑川县', '鹤庆县'], '德宏傣族景颇族自治州': ['瑞丽市', '芒市', '梁河县', '盈江县', '陇川县'], '怒江傈僳族自治州': ['泸水市', '福贡县', '贡山独龙族怒族自治县', '兰坪白族普米族自治县'], '迪庆藏族自治州': ['香格里拉市', '德钦县', '维西傈僳族自治县'] }, '西藏自治区': { '拉萨市': ['城关区', '堆龙德庆区', '达孜区', '林周县', '当雄县', '尼木县', '曲水县', '墨竹工卡县'], '日喀则市': ['桑珠孜区', '南木林县', '江孜县', '定日县', '萨迦县', '拉孜县', '昂仁县', '谢通门县', '白朗县', '仁布县', '康马县', '定结县', '仲巴县', '亚东县', '吉隆县', '聂拉木县', '萨嘎县', '岗巴县'], '昌都市': ['卡若区', '江达县', '贡觉县', '类乌齐县', '丁青县', '察雅县', '八宿县', '左贡县', '芒康县', '洛隆县', '边坝县'], '林芝市': ['巴宜区', '工布江达县', '米林县', '墨脱县', '波密县', '察隅县', '朗县'], '山南市': ['乃东区', '扎囊县', '贡嘎县', '桑日县', '琼结县', '曲松县', '措美县', '洛扎县', '加查县', '隆子县', '错那县', '浪卡子县'], '那曲市': ['色尼区', '嘉黎县', '比如县', '聂荣县', '安多县', '申扎县', '索县', '班戈县', '巴青县', '尼玛县', '双湖县'], '阿里地区': ['普兰县', '札达县', '噶尔县', '日土县', '革吉县', '改则县', '措勤县'] }, '陕西省': { '西安市': ['新城区', '碑林区', '莲湖区', '灞桥区', '未央区', '雁塔区', '阎良区', '临潼区', '长安区', '高陵区', '鄠邑区', '蓝田县', '周至县'], '铜川市': ['王益区', '印台区', '耀州区', '宜君县'], '宝鸡市': ['渭滨区', '金台区', '陈仓区', '凤翔区', '岐山县', '扶风县', '眉县', '陇县', '千阳县', '麟游县', '凤县', '太白县'], '咸阳市': ['秦都区', '杨陵区', '渭城区', '三原县', '泾阳县', '乾县', '礼泉县', '永寿县', '长武县', '旬邑县', '淳化县', '武功县', '兴平市', '彬州市'], '渭南市': ['临渭区', '华州区', '潼关县', '大荔县', '合阳县', '澄城县', '蒲城县', '白水县', '富平县', '韩城市', '华阴市'], '延安市': ['宝塔区', '安塞区', '延长县', '延川县', '志丹县', '吴起县', '甘泉县', '富县', '洛川县', '宜川县', '黄龙县', '黄陵县', '子长市'], '汉中市': ['汉台区', '南郑区', '城固县', '洋县', '西乡县', '勉县', '宁强县', '略阳县', '镇巴县', '留坝县', '佛坪县'], '榆林市': ['榆阳区', '横山区', '府谷县', '靖边县', '定边县', '绥德县', '米脂县', '佳县', '吴堡县', '清涧县', '子洲县', '神木市'], '安康市': ['汉滨区', '汉阴县', '石泉县', '宁陕县', '紫阳县', '岚皋县', '平利县', '镇坪县', '旬阳县', '白河县'], '商洛市': ['商州区', '洛南县', '丹凤县', '商南县', '山阳县', '镇安县', '柞水县'] }, '甘肃省': { '兰州市': ['城关区', '七里河区', '西固区', '安宁区', '红古区', '永登县', '皋兰县', '榆中县'], '嘉峪关市': ['嘉峪关市'], '金昌市': ['金川区', '永昌县'], '白银市': ['白银区', '平川区', '靖远县', '会宁县', '景泰县'], '天水市': ['秦州区', '麦积区', '清水县', '秦安县', '甘谷县', '武山县', '张家川回族自治县'], '武威市': ['凉州区', '民勤县', '古浪县', '天祝藏族自治县'], '张掖市': ['甘州区', '肃南裕固族自治县', '民乐县', '临泽县', '高台县', '山丹县'], '平凉市': ['崆峒区', '泾川县', '灵台县', '崇信县', '华亭市', '庄浪县', '静宁县'], '酒泉市': ['肃州区', '金塔县', '瓜州县', '肃北蒙古族自治县', '阿克塞哈萨克族自治县', '玉门市', '敦煌市'], '庆阳市': ['西峰区', '庆城县', '环县', '华池县', '合水县', '正宁县', '宁县', '镇原县'], '定西市': ['安定区', '通渭县', '陇西县', '渭源县', '临洮县', '漳县', '岷县'], '陇南市': ['武都区', '成县', '文县', '宕昌县', '康县', '西和县', '礼县', '徽县', '两当县'], '临夏回族自治州': ['临夏市', '临夏县', '康乐县', '永靖县', '广河县', '和政县', '东乡族自治县', '积石山保安族东乡族撒拉族自治县'], '甘南藏族自治州': ['合作市', '临潭县', '卓尼县', '舟曲县', '迭部县', '玛曲县', '碌曲县', '夏河县'] }, '青海省': { '西宁市': ['城东区', '城中区', '城西区', '城北区', '大通回族土族自治县', '湟中区', '湟源县'], '海东市': ['乐都区', '平安区', '民和回族土族自治县', '互助土族自治县', '化隆回族自治县', '循化撒拉族自治县'], '海北藏族自治州': ['门源回族自治县', '祁连县', '海晏县', '刚察县'], '黄南藏族自治州': ['同仁市', '尖扎县', '泽库县', '河南蒙古族自治县'], '海南藏族自治州': ['共和县', '同德县', '贵德县', '兴海县', '贵南县'], '果洛藏族自治州': ['玛沁县', '班玛县', '甘德县', '达日县', '久治县', '玛多县'], '玉树藏族自治州': ['玉树市', '杂多县', '称多县', '治多县', '囊谦县', '曲麻莱县'], '海西蒙古族藏族自治州': ['德令哈市', '格尔木市', '茫崖市', '乌兰县', '都兰县', '天峻县', '大柴旦行委'] }, '宁夏回族自治区': { '银川市': ['兴庆区', '西夏区', '金凤区', '永宁县', '贺兰县', '灵武市'], '石嘴山市': ['大武口区', '惠农区', '平罗县'], '吴忠市': ['利通区', '红寺堡区', '盐池县', '同心县', '青铜峡市'], '固原市': ['原州区', '西吉县', '隆德县', '泾源县', '彭阳县'], '中卫市': ['沙坡头区', '中宁县', '海原县'] }, '新疆维吾尔自治区': { '乌鲁木齐市': ['天山区', '沙依巴克区', '新市区', '水磨沟区', '头屯河区', '达坂城区', '米东区', '乌鲁木齐县'], '克拉玛依市': ['独山子区', '克拉玛依区', '白碱滩区', '乌尔禾区'], '吐鲁番市': ['高昌区', '鄯善县', '托克逊县'], '哈密市': ['伊州区', '巴里坤哈萨克自治县', '伊吾县'], '昌吉回族自治州': ['昌吉市', '阜康市', '呼图壁县', '玛纳斯县', '奇台县', '吉木萨尔县', '木垒哈萨克自治县'], '博尔塔拉蒙古自治州': ['博乐市', '阿拉山口市', '精河县', '温泉县'], '巴音郭楞蒙古自治州': ['库尔勒市', '轮台县', '尉犁县', '若羌县', '且末县', '焉耆回族自治县', '和静县', '和硕县', '博湖县'], '阿克苏地区': ['阿克苏市', '温宿县', '库车市', '沙雅县', '新和县', '拜城县', '乌什县', '阿瓦提县', '柯坪县'], '克孜勒苏柯尔克孜自治州': ['阿图什市', '阿克陶县', '阿合奇县', '乌恰县'], '喀什地区': ['喀什市', '疏附县', '疏勒县', '英吉沙县', '泽普县', '莎车县', '叶城县', '麦盖提县', '岳普湖县', '伽师县', '巴楚县', '塔什库尔干塔吉克自治县'], '和田地区': ['和田市', '和田县', '墨玉县', '皮山县', '洛浦县', '策勒县', '于田县', '民丰县'], '伊犁哈萨克自治州': ['伊宁市', '奎屯市', '霍尔果斯市', '伊宁县', '察布查尔锡伯自治县', '霍城县', '巩留县', '新源县', '昭苏县', '特克斯县', '尼勒克县'], '塔城地区': ['塔城市', '乌苏市', '额敏县', '沙湾市', '托里县', '裕民县', '和布克赛尔蒙古自治县'], '阿勒泰地区': ['阿勒泰市', '布尔津县', '富蕴县', '福海县', '哈巴河县', '青河县', '吉木乃县'], '石河子市': ['石河子市'], '阿拉尔市': ['阿拉尔市'], '图木舒克市': ['图木舒克市'], '五家渠市': ['五家渠市'], '北屯市': ['北屯市'], '铁门关市': ['铁门关市'], '双河市': ['双河市'], '可克达拉市': ['可克达拉市'], '昆玉市': ['昆玉市'], '胡杨河市': ['胡杨河市'] }, '香港特别行政区': { '香港岛': ['中西区', '湾仔区', '东区', '南区'], '九龙': ['油尖旺区', '深水埗区', '九龙城区', '黄大仙区', '观塘区'], '新界': ['北区', '大埔区', '沙田区', '西贡区', '荃湾区', '屯门区', '元朗区', '葵青区', '离岛区'] }, '澳门特别行政区': { '澳门半岛': ['花地玛堂区', '圣安多尼堂区', '大堂区', '望德堂区', '风顺堂区'], '氹仔': ['氹仔'], '路环': ['路环'] }, '台湾省': { '台北市': ['中正区', '大同区', '中山区', '松山区', '大安区', '万华区', '信义区', '士林区', '北投区', '内湖区', '南港区', '文山区'], '新北市': ['万里区', '金山区', '板桥区', '汐止区', '深坑区', '石碇区', '瑞芳区', '平溪区', '双溪区', '贡寮区', '新店区', '坪林区', '乌来区', '永和区', '中和区', '土城区', '三峡区', '树林区', '莺歌区', '三重区', '新庄区', '泰山区', '林口区', '芦洲区', '五股区', '八里区', '淡水区', '三芝区', '石门区'], '桃园市': ['中坜区', '平镇区', '龙潭区', '杨梅区', '新屋区', '观音区', '桃园区', '龟山区', '八德区', '大溪区', '复兴区', '大园区', '芦竹区'], '台中市': ['中区', '东区', '南区', '西区', '北区', '北屯区', '西屯区', '南屯区', '太平区', '大里区', '雾峰区', '乌日区', '丰原区', '后里区', '石冈区', '东势区', '和平区', '新社区', '潭子区', '大雅区', '神冈区', '大肚区', '沙鹿区', '龙井区', '梧栖区', '清水区', '大甲区', '外埔区', '大安区'], '台南市': ['中西区', '东区', '南区', '北区', '安平区', '安南区', '永康区', '归仁区', '新化区', '左镇区', '玉井区', '楠西区', '南化区', '仁德区', '关庙区', '龙崎区', '官田区', '麻豆区', '佳里区', '西港区', '七股区', '将军区', '学甲区', '北门区', '新营区', '后壁区', '白河区', '东山区', '六甲区', '下营区', '柳营区', '盐水区', '善化区', '大内区', '山上区', '新市区', '安定区'], '高雄市': ['新兴区', '前金区', '苓雅区', '盐埕区', '鼓山区', '旗津区', '前镇区', '三民区', '楠梓区', '小港区', '左营区', '仁武区', '大社区', '冈山区', '路竹区', '阿莲区', '田寮区', '燕巢区', '桥头区', '梓官区', '弥陀区', '永安区', '湖内区', '凤山区', '大寮区', '林园区', '鸟松区', '大树区', '旗山区', '美浓区', '六龟区', '内门区', '杉林区', '甲仙区', '桃源区', '那玛夏区', '茂林区', '茄萣区'], '基隆市': ['仁爱区', '信义区', '中正区', '中山区', '安乐区', '暖暖区', '七堵区'], '新竹市': ['东区', '北区', '香山区'], '嘉义市': ['东区', '西区'], '新竹县': ['竹北市', '湖口乡', '新丰乡', '新埔镇', '关西镇', '芎林乡', '宝山乡', '竹东镇', '五峰乡', '横山乡', '尖石乡', '北埔乡', '峨眉乡'], '苗栗县': ['竹南镇', '头份市', '三湾乡', '南庄乡', '狮潭乡', '后龙镇', '通霄镇', '苑里镇', '苗栗市', '造桥乡', '头屋乡', '公馆乡', '大湖乡', '泰安乡', '铜锣乡', '三义乡', '西湖乡', '卓兰镇'], '彰化县': ['彰化市', '芬园乡', '花坛乡', '秀水乡', '鹿港镇', '福兴乡', '线西乡', '和美镇', '伸港乡', '员林市', '社头乡', '永靖乡', '埔心乡', '溪湖镇', '大村乡', '埔盐乡', '田中镇', '北斗镇', '田尾乡', '埤头乡', '溪州乡', '竹塘乡', '二林镇', '大城乡', '芳苑乡', '二水乡'], '南投县': ['南投市', '中寮乡', '草屯镇', '国姓乡', '埔里镇', '仁爱乡', '名间乡', '集集镇', '水里乡', '鱼池乡', '信义乡', '竹山镇', '鹿谷乡'], '云林县': ['斗南镇', '大埤乡', '虎尾镇', '土库镇', '褒忠乡', '东势乡', '台西乡', '仑背乡', '麦寮乡', '斗六市', '林内乡', '古坑乡', '莿桐乡', '西螺镇', '二仑乡', '北港镇', '水林乡', '口湖乡', '四湖乡', '元长乡'], '嘉义县': ['番路乡', '梅山乡', '竹崎乡', '阿里山乡', '中埔乡', '大埔乡', '水上乡', '鹿草乡', '太保市', '朴子市', '东石乡', '六脚乡', '新港乡', '民雄乡', '大林镇', '溪口乡', '义竹乡', '布袋镇'], '屏东县': ['屏东市', '三地门乡', '雾台乡', '玛家乡', '九如乡', '里港乡', '高树乡', '盐埔乡', '长治乡', '麟洛乡', '竹田乡', '内埔乡', '万丹乡', '潮州镇', '泰武乡', '来义乡', '万峦乡', '崁顶乡', '新埤乡', '南州乡', '林边乡', '东港镇', '琉球乡', '佳冬乡', '新园乡', '枋寮乡', '枋山乡', '春日乡', '狮子乡', '车城乡', '牡丹乡', '恒春镇', '满州乡'], '宜兰县': ['宜兰市', '头城镇', '礁溪乡', '壮围乡', '员山乡', '罗东镇', '三星乡', '大同乡', '五结乡', '冬山乡', '苏澳镇', '南澳乡'], '花莲县': ['花莲市', '新城乡', '秀林乡', '吉安乡', '寿丰乡', '凤林镇', '光复乡', '丰滨乡', '瑞穗乡', '万荣乡', '玉里镇', '卓溪乡', '富里乡'], '台东县': ['台东市', '绿岛乡', '兰屿乡', '延平乡', '卑南乡', '鹿野乡', '关山镇', '海端乡', '池上乡', '东河乡', '成功镇', '长滨乡', '太麻里乡', '金峰乡', '大武乡', '达仁乡'], '澎湖县': ['马公市', '西屿乡', '望安乡', '七美乡', '白沙乡', '湖西乡'], '金门县': ['金沙镇', '金湖镇', '金宁乡', '金城镇', '烈屿乡', '乌坵乡'], '连江县': ['南竿乡', '北竿乡', '莒光乡', '东引乡'] } }; 🔸============================================================================== 📄 文件: app/static/js/favorites.js 📊 大小: 6804 bytes (6.64 KB) 🕒 修改时间: 2025-07-09 02:20:37 🔸============================================================================== // 收藏页面JavaScript let selectedItems = []; let isSelectAll = false; document.addEventListener('DOMContentLoaded', function() { // 初始化事件监听 initEventListeners(); }); function initEventListeners() { // 复选框变化事件 document.querySelectorAll('.favorite-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function() { updateSelectedItems(); }); }); } function updateSelectedItems() { selectedItems = []; document.querySelectorAll('.favorite-checkbox:checked').forEach(checkbox => { selectedItems.push(parseInt(checkbox.value)); }); // 更新按钮状态 const batchBtn = document.querySelector('[onclick="batchRemove()"]'); if (batchBtn) { batchBtn.disabled = selectedItems.length === 0; } } function toggleSelectAll() { isSelectAll = !isSelectAll; const checkboxes = document.querySelectorAll('.favorite-checkbox'); checkboxes.forEach(checkbox => { checkbox.checked = isSelectAll; }); updateSelectedItems(); // 更新按钮文本 const selectAllBtn = document.querySelector('[onclick="toggleSelectAll()"]'); if (selectAllBtn) { selectAllBtn.innerHTML = isSelectAll ? ' 取消全选' : ' 全选'; } } function removeFavorite(productId) { if (confirm('确定要取消收藏这个商品吗?')) { fetch('/favorite/remove', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: productId }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); // 移除商品卡片 const itemElement = document.querySelector(`[data-product-id="${productId}"]`); if (itemElement) { itemElement.remove(); } // 更新收藏数量 updateFavoriteCount(); // 检查是否为空 checkEmptyState(); } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('操作失败,请稍后再试'); }); } } function batchRemove() { if (selectedItems.length === 0) { showErrorMessage('请选择要删除的商品'); return; } const modal = new bootstrap.Modal(document.getElementById('confirmModal')); document.getElementById('confirmMessage').textContent = `确定要取消收藏这 ${selectedItems.length} 个商品吗?`; document.getElementById('confirmBtn').onclick = function() { performBatchRemove(); modal.hide(); }; modal.show(); } function performBatchRemove() { fetch('/favorite/batch-remove', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_ids: selectedItems }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); // 移除选中的商品卡片 selectedItems.forEach(productId => { const itemElement = document.querySelector(`[data-product-id="${productId}"]`); if (itemElement) { itemElement.remove(); } }); // 重置选择状态 selectedItems = []; isSelectAll = false; updateSelectedItems(); // 更新收藏数量 updateFavoriteCount(); // 检查是否为空 checkEmptyState(); } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('批量删除失败,请稍后再试'); }); } function addToCart(productId) { // 调用购物车添加功能 fetch('/cart/add', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: productId, quantity: 1 }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); // 更新购物车数量 if (typeof updateCartBadge === 'function') { updateCartBadge(data.cart_count); } } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('加入购物车失败,请稍后再试'); }); } function updateFavoriteCount() { fetch('/favorite/count') .then(response => response.json()) .then(data => { if (data.success) { // 更新收藏数量显示 const badge = document.querySelector('.badge.bg-secondary'); if (badge) { badge.textContent = `共 ${data.favorite_count} 件商品`; } } }) .catch(error => { console.error('Error:', error); }); } function checkEmptyState() { const itemsContainer = document.querySelector('.row'); const items = itemsContainer.querySelectorAll('.favorite-item'); if (items.length === 0) { // 显示空状态 itemsContainer.innerHTML = `

还没有收藏任何商品

去逛逛,收藏心仪的商品吧~

去首页逛逛
`; } } function showSuccessMessage(message) { // 使用现有的消息提示函数 if (typeof showMessage === 'function') { showMessage(message, 'success'); } else { alert(message); } } function showErrorMessage(message) { // 使用现有的消息提示函数 if (typeof showMessage === 'function') { showMessage(message, 'error'); } else { alert(message); } } 🔸============================================================================== 📄 文件: app/static/js/history.js 📊 大小: 8170 bytes (7.98 KB) 🕒 修改时间: 2025-07-09 02:20:46 🔸============================================================================== // 浏览历史页面JavaScript let selectedItems = []; let isSelectAll = false; document.addEventListener('DOMContentLoaded', function() { // 初始化事件监听 initEventListeners(); }); function initEventListeners() { // 复选框变化事件 document.querySelectorAll('.history-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function() { updateSelectedItems(); }); }); } function updateSelectedItems() { selectedItems = []; document.querySelectorAll('.history-checkbox:checked').forEach(checkbox => { selectedItems.push(parseInt(checkbox.value)); }); // 更新按钮状态 const batchBtn = document.querySelector('[onclick="batchRemove()"]'); if (batchBtn) { batchBtn.disabled = selectedItems.length === 0; } } function toggleSelectAll() { isSelectAll = !isSelectAll; const checkboxes = document.querySelectorAll('.history-checkbox'); checkboxes.forEach(checkbox => { checkbox.checked = isSelectAll; }); updateSelectedItems(); // 更新按钮文本 const selectAllBtn = document.querySelector('[onclick="toggleSelectAll()"]'); if (selectAllBtn) { selectAllBtn.innerHTML = isSelectAll ? ' 取消全选' : ' 全选'; } } function removeHistory(productId) { if (confirm('确定要删除这个浏览记录吗?')) { fetch('/history/remove', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: productId }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); // 移除商品卡片 const itemElement = document.querySelector(`[data-product-id="${productId}"]`); if (itemElement) { itemElement.remove(); } // 更新历史数量 updateHistoryCount(); // 检查是否为空 checkEmptyState(); } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('操作失败,请稍后再试'); }); } } function batchRemove() { if (selectedItems.length === 0) { showErrorMessage('请选择要删除的商品'); return; } const modal = new bootstrap.Modal(document.getElementById('confirmModal')); document.getElementById('confirmMessage').textContent = `确定要删除这 ${selectedItems.length} 个浏览记录吗?`; document.getElementById('confirmBtn').onclick = function() { performBatchRemove(); modal.hide(); }; modal.show(); } function performBatchRemove() { fetch('/history/batch-remove', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_ids: selectedItems }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); // 移除选中的商品卡片 selectedItems.forEach(productId => { const itemElement = document.querySelector(`[data-product-id="${productId}"]`); if (itemElement) { itemElement.remove(); } }); // 重置选择状态 selectedItems = []; isSelectAll = false; updateSelectedItems(); // 更新历史数量 updateHistoryCount(); // 检查是否为空 checkEmptyState(); } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('批量删除失败,请稍后再试'); }); } function clearHistory() { if (confirm('确定要清空所有浏览历史吗?此操作不可恢复。')) { fetch('/history/clear', { method: 'POST', headers: { 'Content-Type': 'application/json', } }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); // 重新加载页面或显示空状态 location.reload(); } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('清空历史失败,请稍后再试'); }); } } function addToCart(productId) { // 调用购物车添加功能 fetch('/cart/add', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: productId, quantity: 1 }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); // 更新购物车数量 if (typeof updateCartBadge === 'function') { updateCartBadge(data.cart_count); } } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('加入购物车失败,请稍后再试'); }); } function addToFavorites(productId) { fetch('/favorite/add', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: productId }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); } else { showErrorMessage(data.message); } }) .catch(error => { console.error('Error:', error); showErrorMessage('收藏失败,请稍后再试'); }); } function updateHistoryCount() { fetch('/history/count') .then(response => response.json()) .then(data => { if (data.success) { // 更新历史数量显示 const badge = document.querySelector('.badge.bg-secondary'); if (badge) { badge.textContent = `共 ${data.history_count} 件商品`; } } }) .catch(error => { console.error('Error:', error); }); } function checkEmptyState() { const itemsContainer = document.querySelector('.row'); const items = itemsContainer.querySelectorAll('.history-item'); if (items.length === 0) { // 显示空状态 itemsContainer.innerHTML = `

还没有浏览任何商品

去逛逛,看看有什么好商品~

去首页逛逛
`; } } function showSuccessMessage(message) { // 使用现有的消息提示函数 if (typeof showMessage === 'function') { showMessage(message, 'success'); } else { alert(message); } } function showErrorMessage(message) { // 使用现有的消息提示函数 if (typeof showMessage === 'function') { showMessage(message, 'error'); } else { alert(message); } } 🔸============================================================================== 📄 文件: app/static/js/order_detail.js 📊 大小: 1505 bytes (1.47 KB) 🕒 修改时间: 2025-07-08 16:50:42 🔸============================================================================== // 订单详情页面脚本 - 只处理业务逻辑,不处理样式 // 取消订单 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'); }); } } 🔸============================================================================== 📄 文件: app/static/js/orders.js 📊 大小: 3465 bytes (3.38 KB) 🕒 修改时间: 2025-07-04 04:03:15 🔸============================================================================== // 订单页面JavaScript功能 // 强制设置商品图片样式的函数 function forceProductImageStyle(imgElement) { if (!imgElement) return; // 强制设置所有样式属性 imgElement.style.width = '80px'; imgElement.style.height = '80px'; imgElement.style.objectFit = 'cover'; imgElement.style.borderRadius = '4px'; imgElement.style.display = 'block'; imgElement.style.maxWidth = '80px'; imgElement.style.maxHeight = '80px'; imgElement.style.minWidth = '80px'; imgElement.style.minHeight = '80px'; // 设置属性避免被覆盖 imgElement.setAttribute('width', '80'); imgElement.setAttribute('height', '80'); } // 页面加载完成后的处理 document.addEventListener('DOMContentLoaded', function() { // 强制设置所有商品图片样式 const productImages = document.querySelectorAll('.product-image'); productImages.forEach(function(img) { forceProductImageStyle(img); // 图片加载完成后再次强制设置 if (img.complete) { forceProductImageStyle(img); } else { img.onload = function() { forceProductImageStyle(img); }; } }); // 为订单卡片添加悬停效果 const orderCards = document.querySelectorAll('.order-card'); orderCards.forEach(card => { card.addEventListener('mouseenter', function() { this.style.boxShadow = '0 4px 15px rgba(0,0,0,0.1)'; }); card.addEventListener('mouseleave', function() { this.style.boxShadow = ''; }); }); }); // 额外的保险措施:定期检查并修正商品图片样式 setInterval(function() { const productImages = document.querySelectorAll('.product-image'); productImages.forEach(function(img) { // 检查图片是否超出预期尺寸 const rect = img.getBoundingClientRect(); if (rect.width > 85 || rect.height > 85) { forceProductImageStyle(img); } }); }, 1000); // 每秒检查一次 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'); }); } } 🔸============================================================================== 📄 文件: app/static/js/pay.js 📊 大小: 10562 bytes (10.31 KB) 🕒 修改时间: 2025-07-08 17:18:51 🔸============================================================================== // 订单支付页面脚本 let countdownTimer; let statusCheckTimer; let timeLeft = 15 * 60; // 15分钟 // 页面加载时开始倒计时 document.addEventListener('DOMContentLoaded', function() { startCountdown(); }); // 页面卸载时清理定时器 window.addEventListener('beforeunload', function() { if (countdownTimer) clearInterval(countdownTimer); if (statusCheckTimer) clearInterval(statusCheckTimer); }); // 开始倒计时 function startCountdown() { const countdownElement = document.getElementById('countdown'); if (!countdownElement) return; countdownTimer = setInterval(() => { timeLeft--; const minutes = Math.floor(timeLeft / 60); const seconds = timeLeft % 60; countdownElement.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 = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; const paymentMethod = document.querySelector('[data-payment-method]')?.dataset.paymentMethod; if (!paymentSn || !paymentMethod) { showAlert('支付信息获取失败', 'error'); return; } // 如果是模拟支付,直接显示控制面板,不需要调用接口 if (paymentMethod === 'simulate') { showAlert('请使用下方控制面板完成模拟支付', 'info'); return; } fetch('/payment/process', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ payment_sn: paymentSn, payment_method: paymentMethod }) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (data.success) { if (data.payment_type === 'qrcode') { showQRCode(data.qr_code_url); startStatusCheck(); } else if (data.payment_type === 'redirect') { window.open(data.pay_url, '_blank'); startStatusCheck(); } else if (data.payment_type === 'simulate') { showAlert('模拟支付已准备就绪', 'success'); } } else { showAlert(data.message || '支付启动失败', 'error'); } }) .catch(error => { console.error('支付启动错误:', error); showAlert('支付启动失败,请重试', 'error'); }); } // 显示二维码 function showQRCode(qrUrl) { const qrArea = document.getElementById('qrCodeArea'); const qrImage = document.getElementById('qrCodeImage'); if (!qrArea || !qrImage) return; // 这里应该使用真实的二维码生成库,现在用文本模拟 qrImage.innerHTML = `

微信支付二维码
`; qrArea.style.display = 'block'; } // 开始检查支付状态 function startStatusCheck() { statusCheckTimer = setInterval(() => { checkPaymentStatus(); }, 3000); // 每3秒检查一次 } // 检查支付状态 function checkPaymentStatus() { const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; if (!paymentSn) return; fetch(`/payment/check_status/${paymentSn}`) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (data.success) { if (data.status === 2) { // 支付成功 clearInterval(statusCheckTimer); clearInterval(countdownTimer); showPaymentSuccess(); } else if (data.status === 3) { // 支付失败 clearInterval(statusCheckTimer); showPaymentFail(); } } }) .catch(error => { console.error('状态检查失败:', error); }); } // 显示支付成功 function showPaymentSuccess() { const paymentArea = document.getElementById('paymentArea'); const actionButtons = document.getElementById('actionButtons'); const paymentStatus = document.getElementById('paymentStatus'); const successStatus = document.getElementById('successStatus'); if (paymentArea) paymentArea.style.display = 'none'; if (actionButtons) actionButtons.style.display = 'none'; if (paymentStatus) paymentStatus.style.display = 'block'; if (successStatus) successStatus.style.display = 'block'; showAlert('支付成功!正在跳转到订单详情...', 'success'); const orderId = document.querySelector('[data-order-id]')?.dataset.orderId; setTimeout(() => { if (orderId) { window.location.href = `/order/detail/${orderId}`; } else { window.location.href = '/order/list'; } }, 2000); } // 显示支付失败 function showPaymentFail() { const paymentArea = document.getElementById('paymentArea'); const paymentStatus = document.getElementById('paymentStatus'); const failStatus = document.getElementById('failStatus'); if (paymentArea) paymentArea.style.display = 'none'; if (paymentStatus) paymentStatus.style.display = 'block'; if (failStatus) failStatus.style.display = 'block'; showAlert('支付失败,请重新尝试', 'error'); // 显示重试按钮 setTimeout(() => { if (paymentArea) paymentArea.style.display = 'block'; if (paymentStatus) paymentStatus.style.display = 'none'; }, 3000); } // 取消订单 function cancelOrder() { const orderId = document.querySelector('[data-order-id]')?.dataset.orderId; if (!orderId) { showAlert('订单信息获取失败', 'error'); return; } showConfirm('确定要取消这个订单吗?', () => { fetch(`/order/cancel/${orderId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', } }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (data.success) { showAlert('订单已取消,正在跳转...', 'success'); setTimeout(() => { window.location.href = '/order/list'; }, 1500); } else { showAlert(data.message || '取消订单失败', 'error'); } }) .catch(error => { console.error('取消订单错误:', error); showAlert('取消失败,请重试', 'error'); }); }); } // 模拟支付成功 function simulatePaymentSuccess() { const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; if (!paymentSn) { showAlert('支付信息获取失败', 'error'); return; } // 显示处理中状态 const button = event.target; const originalText = button.innerHTML; button.innerHTML = ' 处理中...'; button.disabled = true; fetch(`/payment/simulate_success/${paymentSn}`, { method: 'POST', headers: { 'Content-Type': 'application/json', } }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (data.success) { showAlert('模拟支付成功!', 'success'); setTimeout(() => { showPaymentSuccess(); }, 1000); } else { showAlert(data.message || '模拟支付失败', 'error'); button.innerHTML = originalText; button.disabled = false; } }) .catch(error => { console.error('模拟支付错误:', error); showAlert('模拟支付失败', 'error'); button.innerHTML = originalText; button.disabled = false; }); } // 模拟支付失败 function simulatePaymentFail() { const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; if (!paymentSn) { showAlert('支付信息获取失败', 'error'); return; } showConfirm('确定要模拟支付失败吗?这将导致订单支付失败。', () => { // 显示处理中状态 const button = event.target; const originalText = button.innerHTML; button.innerHTML = ' 处理中...'; button.disabled = true; fetch(`/payment/simulate_fail/${paymentSn}`, { method: 'POST', headers: { 'Content-Type': 'application/json', } }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (data.success) { showAlert('模拟支付失败!', 'warning'); setTimeout(() => { showPaymentFail(); }, 1000); } else { showAlert(data.message || '模拟操作失败', 'error'); button.innerHTML = originalText; button.disabled = false; } }) .catch(error => { console.error('模拟支付失败错误:', error); showAlert('模拟操作失败', 'error'); button.innerHTML = originalText; button.disabled = false; }); }); } // 兼容旧版本的模拟支付函数 function simulatePayment() { simulatePaymentSuccess(); } 🔸============================================================================== 📄 文件: app/static/js/product_detail.js 📊 大小: 13520 bytes (13.20 KB) 🕒 修改时间: 2025-07-09 05:22:56 🔸============================================================================== // 获取库存数据 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(); } else if (inventoryData.length > 1) { // 自动选择默认规格 const defaultSku = inventoryData.find(sku => sku.is_default); if (defaultSku && defaultSku.spec_combination) { // 自动选择默认规格 for (const [specName, specValue] of Object.entries(defaultSku.spec_combination)) { selectedSpecs[specName] = specValue; // 更新按钮状态 const button = document.querySelector(`[data-spec-name="${specName}"][data-spec-value="${specValue}"]`); if (button) { button.classList.remove('btn-outline-secondary'); button.classList.add('btn-primary'); } } currentSku = defaultSku; updateStockInfo(); } } // 绑定规格选择事件 document.querySelectorAll('.spec-option').forEach(button => { button.addEventListener('click', function() { selectSpec(this); }); }); // 初始化购物车数量显示 if (typeof loadCartCount === 'function') { loadCartCount(); } // 添加浏览历史记录 if (window.isLoggedIn && window.productId) { addBrowseHistory(window.productId); } // 检查收藏状态 if (window.isLoggedIn && window.productId) { checkFavoriteStatus(window.productId); } }); // 添加浏览历史记录 function addBrowseHistory(productId) { fetch('/history/add', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: productId }) }) .then(response => response.json()) .then(data => { // 静默添加,不需要用户感知 console.log('浏览历史记录已添加'); }) .catch(error => { console.error('添加浏览历史失败:', error); }); } // 检查收藏状态 function checkFavoriteStatus(productId) { fetch(`/favorite/check/${productId}`) .then(response => response.json()) .then(data => { if (data.success) { updateFavoriteButton(data.is_favorited); } }) .catch(error => { console.error('检查收藏状态失败:', error); }); } // 更新收藏按钮状态 function updateFavoriteButton(isFavorited) { const favoriteBtn = document.querySelector('[onclick="addToFavorites()"]'); if (favoriteBtn) { if (isFavorited) { favoriteBtn.innerHTML = ' 已收藏'; favoriteBtn.className = 'btn btn-outline-danger'; } else { favoriteBtn.innerHTML = ' 收藏商品'; favoriteBtn.className = 'btn btn-outline-secondary'; } } } // 规格选择 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() { // 获取所有需要选择的规格类型 const allSpecNames = new Set(); inventoryData.forEach(sku => { if (sku.spec_combination) { Object.keys(sku.spec_combination).forEach(specName => { allSpecNames.add(specName); }); } }); // 检查是否选择了所有必需的规格 const selectedSpecNames = Object.keys(selectedSpecs); if (selectedSpecNames.length < allSpecNames.size) { currentSku = null; updateStockInfo(); return; } // 查找完全匹配的SKU for (let sku of inventoryData) { if (sku.spec_combination) { let isMatch = true; // 检查规格数量是否匹配 if (Object.keys(sku.spec_combination).length !== selectedSpecNames.length) { continue; } // 检查每个规格是否匹配 for (let [specName, specValue] of Object.entries(selectedSpecs)) { if (sku.spec_combination[specName] !== specValue) { isMatch = false; break; } } if (isMatch) { 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) { // 有选中的SKU stockElement.textContent = currentSku.stock; if (currentSku.stock > 0) { stockElement.className = 'text-success'; addToCartBtn.disabled = false; buyNowBtn.disabled = false; quantityInput.max = currentSku.stock; quantityInput.value = Math.min(parseInt(quantityInput.value), currentSku.stock); } else { stockElement.className = 'text-danger'; addToCartBtn.disabled = true; buyNowBtn.disabled = true; quantityInput.max = 0; quantityInput.value = 1; } // 更新价格 priceElement.textContent = currentSku.final_price.toFixed(2); } else if (inventoryData.length > 1) { // 多规格商品但未完全选择 stockElement.textContent = '请选择规格'; stockElement.className = 'text-warning'; addToCartBtn.disabled = true; buyNowBtn.disabled = true; quantityInput.max = 999; } else { // 无库存或其他错误 stockElement.textContent = '暂无库存'; stockElement.className = 'text-danger'; addToCartBtn.disabled = true; buyNowBtn.disabled = true; quantityInput.max = 0; } } // 数量变更 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 updateCartBadge(count) { const badge = document.querySelector('.cart-badge'); if (badge) { badge.textContent = count; badge.style.display = count > 0 ? 'inline' : 'none'; } } // 加入购物车 function addToCart() { if (!currentSku) { alert('请选择商品规格'); return; } const quantity = parseInt(document.getElementById('quantity').value); if (quantity <= 0 || quantity > currentSku.stock) { alert('请选择正确的购买数量'); return; } // 禁用按钮,防止重复点击 const addToCartBtn = document.getElementById('addToCartBtn'); addToCartBtn.disabled = true; addToCartBtn.innerHTML = ' 添加中...'; // 准备规格组合数据 const specCombination = Object.keys(selectedSpecs).length > 0 ? Object.entries(selectedSpecs).map(([key, value]) => `${key}:${value}`).join(', ') : ''; // 提交到购物车 fetch('/cart/add', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: window.productId, sku_code: currentSku.sku_code, spec_combination: specCombination, 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 = ' 加入购物车'; }); } // 立即购买 function buyNow() { if (!currentSku) { alert('请选择商品规格'); return; } const quantity = parseInt(document.getElementById('quantity').value); if (quantity <= 0 || quantity > currentSku.stock) { alert('请选择正确的购买数量'); return; } // 禁用按钮,防止重复点击 const buyNowBtn = document.getElementById('buyNowBtn'); buyNowBtn.disabled = true; buyNowBtn.innerHTML = ' 处理中...'; // 准备规格组合数据 const specCombination = Object.keys(selectedSpecs).length > 0 ? Object.entries(selectedSpecs).map(([key, value]) => `${key}:${value}`).join(', ') : ''; // 先添加到购物车,然后跳转到结算页面 fetch('/cart/add', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: window.productId, sku_code: currentSku.sku_code, spec_combination: specCombination, 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('购买失败,请稍后再试'); }) .finally(() => { // 恢复按钮状态 buyNowBtn.disabled = false; buyNowBtn.innerHTML = ' 立即购买'; }); } // 收藏商品 function addToFavorites() { if (!window.isLoggedIn) { if (confirm('请先登录后再收藏,是否前往登录?')) { window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname); } return; } // 确保获取到商品ID const productId = window.productId || window.currentProductId; if (!productId) { alert('获取商品信息失败,请刷新页面重试'); return; } const favoriteBtn = document.querySelector('[onclick="addToFavorites()"]'); const isFavorited = favoriteBtn && favoriteBtn.innerHTML.includes('已收藏'); // 临时禁用按钮 if (favoriteBtn) { favoriteBtn.disabled = true; favoriteBtn.innerHTML = ' 处理中...'; } fetch('/favorite/toggle', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ product_id: parseInt(productId) }) }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); updateFavoriteButton(data.is_favorited); } else { alert(data.message); } }) .catch(error => { console.error('Error:', error); alert('操作失败,请稍后再试'); }) .finally(() => { // 恢复按钮状态 if (favoriteBtn) { favoriteBtn.disabled = false; } }); } // 显示成功消息 function showSuccessMessage(message) { // 这里可以使用Toast或其他方式显示消息 if (typeof showToast === 'function') { showToast(message, 'success'); } else { // 简单的成功提示 alert(message); } } 🔸============================================================================== 📄 文件: app/static/js/product_list.js 📊 大小: 224 bytes (0.22 KB) 🕒 修改时间: 2025-07-04 14:41:10 🔸============================================================================== function changeSort(sortType) { const url = new URL(window.location); url.searchParams.set('sort', sortType); url.searchParams.set('page', '1'); // 重置到第一页 window.location.href = url.toString(); } 🔸============================================================================== 📄 文件: app/static/js/profile.js 📊 大小: 10990 bytes (10.73 KB) 🕒 修改时间: 2025-07-04 04:00:03 🔸============================================================================== // 个人中心页面JavaScript功能 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 = ` ${message} `; 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); // 每秒检查一次 🔸============================================================================== 📄 文件: app/static/js/register.js 📊 大小: 4062 bytes (3.97 KB) 🕒 修改时间: 2025-07-04 03:58:52 🔸============================================================================== // 注册页面JavaScript功能 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; } }); }); 🔸============================================================================== 📄 文件: app/static/js/review.js 📊 大小: 22091 bytes (21.57 KB) 🕒 修改时间: 2025-07-08 19:31:57 🔸============================================================================== // 评价功能 JavaScript document.addEventListener('DOMContentLoaded', function() { initializeReviewForm(); initializeImageUpload(); }); // 初始化评价表单 function initializeReviewForm() { const starRating = document.getElementById('starRating'); const ratingInput = document.getElementById('rating'); const ratingText = document.getElementById('ratingText'); const reviewForm = document.getElementById('reviewForm'); if (starRating) { const stars = starRating.querySelectorAll('.star'); const ratingTexts = { 1: '很差', 2: '较差', 3: '一般', 4: '满意', 5: '非常满意' }; let currentRating = 0; // 当前选中的评分 // 初始化:设置所有星星为空心 stars.forEach(star => { star.textContent = '☆'; // 空心星星 }); // 星级点击事件 stars.forEach((star, index) => { star.addEventListener('click', function() { const rating = index + 1; setRating(rating); }); // 鼠标悬停事件 star.addEventListener('mouseenter', function() { const rating = index + 1; showHoverStars(rating); // 显示临时评分文字 const tempText = ratingTexts[rating] || '请选择评分'; ratingText.textContent = tempText; ratingText.style.backgroundColor = '#ff6b35'; ratingText.style.color = 'white'; ratingText.style.borderColor = '#ff6b35'; }); }); // 鼠标离开星级评分区域 starRating.addEventListener('mouseleave', function() { showSelectedStars(currentRating); // 恢复原来的评分文字 if (currentRating > 0) { ratingText.textContent = ratingTexts[currentRating]; ratingText.classList.add('selected'); ratingText.style.backgroundColor = '#ff6b35'; ratingText.style.color = 'white'; ratingText.style.borderColor = '#ff6b35'; } else { ratingText.textContent = '请选择评分'; ratingText.classList.remove('selected'); ratingText.style.backgroundColor = '#f8f9fa'; ratingText.style.color = '#666'; ratingText.style.borderColor = '#e9ecef'; } }); // 设置评分 function setRating(rating) { currentRating = rating; ratingInput.value = rating; ratingText.textContent = ratingTexts[rating] || '请选择评分'; ratingText.classList.add('selected'); ratingText.style.backgroundColor = '#ff6b35'; ratingText.style.color = 'white'; ratingText.style.borderColor = '#ff6b35'; showSelectedStars(rating); } // 显示悬停状态的星星 function showHoverStars(rating) { stars.forEach((star, index) => { star.classList.remove('filled'); if (index < rating) { star.textContent = '★'; // 实心星星 star.classList.add('filled'); } else { star.textContent = '☆'; // 空心星星 } }); } // 显示选中状态的星星 function showSelectedStars(rating) { stars.forEach((star, index) => { star.classList.remove('filled'); if (index < rating) { star.textContent = '★'; // 实心星星 star.classList.add('filled'); } else { star.textContent = '☆'; // 空心星星 } }); } } // 表单提交 if (reviewForm) { reviewForm.addEventListener('submit', function(e) { e.preventDefault(); submitReview(); }); } } // 初始化图片上传 function initializeImageUpload() { const uploadArea = document.getElementById('uploadArea'); const imageInput = document.getElementById('imageInput'); const uploadedImages = document.getElementById('uploadedImages'); if (!uploadArea || !imageInput) return; let uploadedImageUrls = []; // 点击上传区域 uploadArea.addEventListener('click', function() { imageInput.click(); }); // 拖拽上传 uploadArea.addEventListener('dragover', function(e) { e.preventDefault(); this.style.borderColor = '#007bff'; }); uploadArea.addEventListener('dragleave', function(e) { e.preventDefault(); this.style.borderColor = '#ddd'; }); uploadArea.addEventListener('drop', function(e) { e.preventDefault(); this.style.borderColor = '#ddd'; const files = Array.from(e.dataTransfer.files); handleFiles(files); }); // 文件选择 imageInput.addEventListener('change', function() { const files = Array.from(this.files); handleFiles(files); }); // 处理文件上传 function handleFiles(files) { if (uploadedImageUrls.length + files.length > 5) { showAlert('最多只能上传5张图片', 'warning'); return; } files.forEach(file => { if (!file.type.startsWith('image/')) { showAlert('只能上传图片文件', 'warning'); return; } if (file.size > 5 * 1024 * 1024) { showAlert('图片大小不能超过5MB', 'warning'); return; } uploadImage(file); }); } // 上传图片到服务器 function uploadImage(file) { const formData = new FormData(); formData.append('file', file); // 显示上传进度 const previewElement = createImagePreview(URL.createObjectURL(file), true); uploadedImages.appendChild(previewElement); fetch('/review/upload_image', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { // 更新预览元素 const img = previewElement.querySelector('img'); img.src = data.url; // 强制设置图片样式 forceImageStyles(img); previewElement.classList.remove('uploading'); previewElement.dataset.url = data.url; uploadedImageUrls.push(data.url); } else { showAlert(data.message || '图片上传失败', 'error'); previewElement.remove(); } }) .catch(error => { showAlert('图片上传失败', 'error'); previewElement.remove(); }); } // 创建图片预览元素 function createImagePreview(src, isUploading = false) { const div = document.createElement('div'); div.className = `image-preview ${isUploading ? 'uploading' : ''}`; // 强制设置容器样式 div.style.cssText = ` position: relative !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; border-radius: 8px !important; overflow: hidden !important; border: 2px solid #e9ecef !important; flex-shrink: 0 !important; flex-grow: 0 !important; display: inline-block !important; box-sizing: border-box !important; margin: 0 !important; padding: 0 !important; vertical-align: top !important; `; const img = document.createElement('img'); img.src = src; img.alt = '评价图片'; // 强制设置图片样式 forceImageStyles(img); const removeBtn = document.createElement('button'); removeBtn.className = 'remove-btn'; removeBtn.innerHTML = '×'; removeBtn.type = 'button'; removeBtn.style.cssText = ` position: absolute !important; top: 2px !important; right: 2px !important; background: rgba(255, 255, 255, 0.9) !important; border: none !important; border-radius: 50% !important; width: 20px !important; height: 20px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; font-size: 12px !important; color: #dc3545 !important; box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important; z-index: 10 !important; `; removeBtn.onclick = function() { const url = div.dataset.url; if (url) { uploadedImageUrls = uploadedImageUrls.filter(u => u !== url); } div.remove(); }; div.appendChild(img); div.appendChild(removeBtn); return div; } // 强制设置图片样式的函数 function forceImageStyles(img) { img.style.cssText = ` display: block !important; width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; box-sizing: border-box !important; position: relative !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; margin: 0 !important; padding: 0 !important; border: none !important; outline: none !important; background: none !important; vertical-align: top !important; flex-shrink: 0 !important; flex-grow: 0 !important; `; // 图片加载完成后再次强制设置样式 img.onload = function() { forceImageStyles(this); }; } // 获取上传的图片URL列表 window.getUploadedImages = function() { return uploadedImageUrls; }; } // 提交评价 function submitReview() { const submitBtn = document.getElementById('submitBtn'); const orderId = document.getElementById('orderId').value; const productId = document.getElementById('productId').value; const rating = document.getElementById('rating').value; const content = document.getElementById('content').value; const isAnonymous = document.getElementById('isAnonymous').checked; // 验证 if (!rating) { showAlert('请选择评分', 'warning'); return; } // 禁用提交按钮 submitBtn.disabled = true; submitBtn.innerHTML = ' 提交中...'; const data = { order_id: parseInt(orderId), product_id: parseInt(productId), rating: parseInt(rating), content: content.trim(), is_anonymous: isAnonymous, images: window.getUploadedImages ? window.getUploadedImages() : [] }; fetch('/review/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.success) { showAlert(data.message, 'success'); setTimeout(() => { window.location.href = `/order/detail/${orderId}`; }, 1500); } else { showAlert(data.message, 'error'); } }) .catch(error => { showAlert('提交失败,请重试', 'error'); }) .finally(() => { // 恢复提交按钮 submitBtn.disabled = false; submitBtn.innerHTML = ' 提交评价'; }); } // 加载商品评价列表(用于商品详情页) function loadProductReviews(productId, page = 1, rating = null) { const reviewsContainer = document.getElementById('reviewsContainer'); if (!reviewsContainer) return; const params = new URLSearchParams({ page: page }); if (rating) { params.append('rating', rating); } reviewsContainer.innerHTML = '
加载中...
'; fetch(`/review/product/${productId}?${params}`) .then(response => response.json()) .then(data => { if (data.success) { renderReviews(data); } else { reviewsContainer.innerHTML = '
加载失败
'; } }) .catch(error => { reviewsContainer.innerHTML = '
加载失败
'; }); } // 渲染评价列表 function renderReviews(data) { const reviewsContainer = document.getElementById('reviewsContainer'); if (!reviewsContainer) return; let html = ''; // 评价统计 if (data.stats) { html += renderReviewStats(data.stats); } // 评价筛选 html += renderReviewFilter(); // 评价列表 if (data.reviews && data.reviews.length > 0) { data.reviews.forEach(review => { html += renderReviewItem(review); }); // 分页 if (data.pagination && data.pagination.pages > 1) { html += renderPagination(data.pagination); } } else { html += '
暂无评价
'; } reviewsContainer.innerHTML = html; } // 渲染评价统计 function renderReviewStats(stats) { const goodRate = stats.good_rate || 0; const totalReviews = stats.total_reviews || 0; let html = `
${goodRate}%
好评率 (${totalReviews}条评价)
`; for (let i = 5; i >= 1; i--) { const count = stats.rating_stats[i] || 0; const percentage = totalReviews > 0 ? (count / totalReviews * 100) : 0; html += `
${i}星
${count}
`; } html += `
`; return html; } // 渲染评价筛选 function renderReviewFilter() { return `
`; } // 渲染单个评价 - 修复图片和头像问题 function renderReviewItem(review) { let html = `
`; if (review.user_avatar) { // 用户头像 - 添加内联样式强制约束尺寸 html += `用户头像`; } else { html += `
`; } html += `
${review.username}
${new Date(review.created_at).toLocaleDateString()}
${review.rating_stars} ${review.rating}分
`; if (review.content) { html += `

${review.content}

`; } if (review.images && review.images.length > 0) { html += '
'; review.images.forEach(imageUrl => { // 评价图片 - 使用特殊的类名和内联样式确保图片尺寸正确 html += `评价图片`; }); html += '
'; } html += '
'; return html; } // 渲染分页 function renderPagination(pagination) { if (pagination.pages <= 1) return ''; let html = ''; return html; } // 筛选评价 function filterReviews(rating) { // 更新筛选按钮状态 const filterButtons = document.querySelectorAll('.reviews-filter .btn'); filterButtons.forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); // 重新加载评价 loadProductReviews(window.currentProductId, 1, rating); } // 显示图片模态框 function showImageModal(imageUrl) { const modal = document.getElementById('imageModal'); const modalImage = document.getElementById('modalImage'); if (modal && modalImage) { modalImage.src = imageUrl; new bootstrap.Modal(modal).show(); } } // 显示提示信息 function showAlert(message, type = 'info') { // 创建警告框 const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`; alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;'; alertDiv.innerHTML = ` ${message} `; // 添加到页面 document.body.appendChild(alertDiv); // 自动消失 setTimeout(() => { if (alertDiv.parentNode) { alertDiv.remove(); } }, 5000); } // 全局变量,用于存储当前商品ID window.currentProductId = null; 🔸============================================================================== 📄 文件: app/templates/admin/base.html 📊 大小: 5154 bytes (5.03 KB) 🕒 修改时间: 2025-07-08 17:54:21 🔸============================================================================== {% block title %}太白购物商城 - 管理后台{% endblock %} {% block extra_css %}{% endblock %}

{% block page_title %}管理后台{% endblock %}

{% block page_description %}{% endblock %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
{% block extra_js %}{% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/categories.html 📊 大小: 18047 bytes (17.62 KB) 🕒 修改时间: 2025-07-04 18:44:46 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}分类管理 - 太白购物商城管理后台{% endblock %} {% block page_title %}分类管理{% endblock %} {% block page_description %}商品分类层级管理{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
添加新分类
点击上传图标
{% if categories %}
分类结构 {{ categories|length }}
{% set top_categories = categories | selectattr('parent_id', 'equalto', 0) | sort(attribute='sort_order') %} {% for category in top_categories %}
{% if category.icon_url %} {{ category.name }} {% else %}
{% endif %}
{{ category.name }}
ID: {{ category.id }} | 层级: {{ category.level }} | 排序: {{ category.sort_order }} | {% if category.is_active %} 启用 {% else %} 禁用 {% endif %}
{% if category.level < 3 %} {% endif %}
{% set level2_categories = categories | selectattr('parent_id', 'equalto', category.id) | sort(attribute='sort_order') %} {% if level2_categories %}
{% for child in level2_categories %}
{% if child.icon_url %} {{ child.name }} {% else %}
{% endif %}
{{ child.name }}
ID: {{ child.id }} | 层级: {{ child.level }} | 排序: {{ child.sort_order }} | {% if child.is_active %} 启用 {% else %} 禁用 {% endif %}
{% if child.level < 3 %} {% endif %}
{% set level3_categories = categories | selectattr('parent_id', 'equalto', child.id) | sort(attribute='sort_order') %} {% if level3_categories %}
{% for grandchild in level3_categories %}
{% if grandchild.icon_url %} {{ grandchild.name }} {% else %}
{% endif %}
{{ grandchild.name }}
ID: {{ grandchild.id }} | 层级: {{ grandchild.level }} | 排序: {{ grandchild.sort_order }} | {% if grandchild.is_active %} 启用 {% else %} 禁用 {% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% else %}
还没有创建任何分类

点击上方的"添加新分类"来创建第一个商品分类

{% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/dashboard.html 📊 大小: 7525 bytes (7.35 KB) 🕒 修改时间: 2025-07-04 14:51:53 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}仪表板 - 太白购物商城管理后台{% endblock %} {% block page_title %}仪表板{% endblock %} {% block page_description %}系统概览和数据统计{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

{{ stats.total_users or 0 }}

总用户数

{{ stats.active_users or 0 }}

活跃用户

{{ stats.total_admins or 0 }}

管理员数

{{ stats.recent_logs_count or 0 }}

7天操作数

用户注册趋势(最近7天)
系统状态
数据库连接 正常
文件存储 正常
邮件服务 正常
系统版本 v1.0.0
最近操作日志
{% if recent_logs %} {% for log in recent_logs %} {% endfor %} {% else %} {% endif %}
时间 操作者 操作类型 操作内容 IP地址
{{ log.created_at.strftime('%m-%d %H:%M') if log.created_at else '' }} {% if log.user_type == 2 %} 管理员 {% else %} 用户 {% endif %} {{ log.user_id }} {{ log.action }} {% if log.resource_type %} {{ log.resource_type }} {% if log.resource_id %}#{{ log.resource_id }}{% endif %} {% else %} - {% endif %} {{ log.ip_address or '-' }}
暂无操作日志
{% if recent_logs %} {% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/login.html 📊 大小: 2726 bytes (2.66 KB) 🕒 修改时间: 2025-07-04 14:51:53 🔸============================================================================== 管理员登录 - 太白购物商城 🔸============================================================================== 📄 文件: app/templates/admin/logs.html 📊 大小: 11310 bytes (11.04 KB) 🕒 修改时间: 2025-07-09 02:00:00 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}操作日志 - 太白购物商城管理后台{% endblock %} {% block page_title %}操作日志{% endblock %} {% block page_description %}查看系统操作日志,监控用户和管理员行为{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
重置
{{ logs.total }}

总日志数

{{ logs.items | selectattr('user_type', 'equalto', 1) | list | length }}

用户操作

{{ logs.items | selectattr('user_type', 'equalto', 2) | list | length }}

管理员操作

{{ today_logs_count }}

今日操作

操作日志 共 {{ logs.total }} 条记录
{% if logs.items %} {% for log in logs.items %} {% endfor %} {% else %} {% endif %}
时间 操作者 操作类型 操作内容 IP地址 用户代理
{{ log.created_at.strftime('%Y-%m-%d') if log.created_at else '-' }}
{{ log.created_at.strftime('%H:%M:%S') if log.created_at else '' }}
{{ '管理员' if log.user_type == 2 else '用户' }} #{{ log.user_id or '-' }}
{{ log.action }}
{% if log.resource_type %} {{ log.resource_type }} {% if log.resource_id %} #{{ log.resource_id }} {% endif %} {% else %} - {% endif %}
{{ log.ip_address or '-' }}
{% if log.user_agent %} {{ log.user_agent[:50] }}{% if log.user_agent|length > 50 %}...{% endif %} {% else %} - {% endif %}
暂无操作日志
{% if user_type or action %} 尝试调整筛选条件 {% endif %}
{% if logs.pages > 1 %} {% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/order_detail.html 📊 大小: 20284 bytes (19.81 KB) 🕒 修改时间: 2025-07-08 17:54:21 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}订单详情 - 太白购物商城管理后台{% endblock %} {% block page_title %}订单详情{% endblock %} {% block page_description %}订单号:{{ order.order_sn }}{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
订单基本信息
订单号: {{ order.order_sn }}
用户信息:
{{ order.user.username }}
{% if order.user.phone %} {{ order.user.phone }} {% endif %}
订单状态: {{ order.get_status_text() }}
支付方式: {% if order.payment_method %} {{ order.payment_method }} {% else %} 未设置 {% endif %}
{% if order.shipped_at %} {% endif %} {% if order.received_at %} {% endif %}
创建时间: {{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
发货时间: {{ order.shipped_at.strftime('%Y-%m-%d %H:%M:%S') }}
收货时间: {{ order.received_at.strftime('%Y-%m-%d %H:%M:%S') }}
配送方式: {{ order.shipping_method or '标准配送' }}
{% if order.remark %}
订单备注:

{{ order.remark }}

{% endif %}
收货信息
{% set receiver = order.get_receiver_info() %} {% if receiver %}
收货人: {{ receiver.receiver_name }}
联系电话: {{ receiver.receiver_phone }}
{% if receiver.postal_code %} {% endif %}
收货地址: {{ receiver.full_address }}
邮政编码: {{ receiver.postal_code }}
{% else %}

暂无收货信息

{% endif %}
订单商品
{% for item in order.order_items %} {% endfor %}
商品信息 单价 数量 小计
{% if item.product_image %} {{ item.product_name }} {% endif %}
{{ item.product_name }}
{% if item.spec_combination %} {{ item.spec_combination }} {% endif %} {% if item.sku_code %} SKU: {{ item.sku_code }} {% endif %}
¥{{ "%.2f"|format(item.price) }} {{ item.quantity }} ¥{{ "%.2f"|format(item.total_price) }}
金额信息
商品总额: ¥{{ "%.2f"|format(order.total_amount) }}
运费: ¥{{ "%.2f"|format(order.shipping_fee) }}
实付金额: ¥{{ "%.2f"|format(order.actual_amount) }}
{% if payment %}
支付信息
{% if payment.paid_at %} {% endif %} {% if payment.third_party_sn %} {% endif %}
支付流水号: {{ payment.payment_sn }}
支付状态: {{ payment.get_status_text() }}
支付时间: {{ payment.paid_at.strftime('%Y-%m-%d %H:%M:%S') }}
第三方流水号: {{ payment.third_party_sn }}
{% endif %} {% if shipping_info %}
物流信息
物流公司: {{ shipping_info.shipping_company }}
快递单号: {{ shipping_info.tracking_number }}
物流状态: {% if shipping_info.shipping_status == 1 %}已发货 {% elif shipping_info.shipping_status == 2 %}运输中 {% elif shipping_info.shipping_status == 3 %}已送达 {% endif %}
{% endif %}
订单操作
{% if order.status == 2 %} {% endif %} {% if order.status in [2, 3] %} {% endif %} {% if order.can_cancel() %} {% endif %} 返回列表
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/orders.html 📊 大小: 17845 bytes (17.43 KB) 🕒 修改时间: 2025-07-08 17:54:21 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}订单管理 - 太白购物商城管理后台{% endblock %} {% block page_title %}订单管理{% endblock %} {% block page_description %}管理系统中的所有订单{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
{{ order_stats.get(0, {}).get('count', 0) + order_stats.get(1, {}).get('count', 0) + order_stats.get(2, {}).get('count', 0) + order_stats.get(3, {}).get('count', 0) + order_stats.get(4, {}).get('count', 0) + order_stats.get(5, {}).get('count', 0) + order_stats.get(6, {}).get('count', 0) + order_stats.get(7, {}).get('count', 0) }}
总订单
{{ order_stats.get(1, {}).get('count', 0) }}
待支付
{{ order_stats.get(2, {}).get('count', 0) }}
待发货
{{ order_stats.get(3, {}).get('count', 0) }}
待收货
{{ order_stats.get(5, {}).get('count', 0) }}
已完成
{{ order_stats.get(6, {}).get('count', 0) }}
已取消
重置
订单列表
{% if orders.items %}
{% for order in orders.items %} {% endfor %}
订单号 用户信息 订单金额 订单状态 支付方式 创建时间 操作
{{ order.order_sn }}
ID: {{ order.id }}
{{ order.user.username }}
{% if order.user.phone %} {{ order.user.phone }} {% endif %}
¥{{ "%.2f"|format(order.actual_amount) }}
{% if order.shipping_fee > 0 %} 含运费: ¥{{ "%.2f"|format(order.shipping_fee) }} {% endif %}
{{ order.get_status_text() }} {% if order.payment_method %} {{ order.payment_method }} {% else %} 未设置 {% endif %}
{{ order.created_at.strftime('%Y-%m-%d') }}
{{ order.created_at.strftime('%H:%M:%S') }}
详情 {% if order.status == 2 %} {% endif %} {% if order.status in [2, 3] %} {% endif %} {% if order.can_cancel() %} {% endif %}
{% if orders.pages > 1 %} {% endif %} {% else %}

暂无订单数据

{% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: 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 %} {% endblock %} {% block content %}
{% if product %} {% endif %}
基本信息
¥
¥
库存管理
{% if product and product.inventory %}
当前库存信息:
{% for inventory in product.inventory %} {% endfor %}
SKU编码 规格组合 库存 价格 状态
{{ inventory.sku_code }} {% if inventory.spec_combination %} {% for key, value in inventory.spec_combination.items() %} {{ key }}:{{ value }}{% if not loop.last %}, {% endif %} {% endfor %} {% else %} 默认规格 {% endif %} {{ inventory.stock }} ¥{{ "%.2f"|format(inventory.get_final_price()) }} {% if inventory.status %} 启用 {% else %} 禁用 {% endif %}
{% endif %}
{% if product %}
商品图片
拖拽图片到这里或点击选择

支持 JPG、PNG、GIF 格式,单张图片不超过 5MB

上传中...
{% endif %}
操作
{% if product %} 返回列表 {% else %} 返回列表 {% endif %}
{% if product %}
商品信息
商品ID:{{ product.id }}
销量:{{ product.sales_count }}
浏览量:{{ product.view_count }}
创建时间:
{{ product.created_at.strftime('%Y-%m-%d %H:%M:%S') if product.created_at else '' }}
更新时间:
{{ product.updated_at.strftime('%Y-%m-%d %H:%M:%S') if product.updated_at else '' }}
{% endif %}
{% endblock %} {% block extra_js %} {% 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 %}
{% if products.items %} {% for product in products.items %} {% endfor %} {% else %} {% endif %}
ID 商品图片 商品名称 分类 价格 库存 状态 销量 创建时间 操作
{{ product.id }} {% if product.main_image %} {% else %}
{% endif %}
{{ product.name[:40] }}{% if product.name|length > 40 %}...{% endif %} {% if product.brand %}
{{ product.brand }} {% endif %} {% if product.has_specs %}
多规格 {% endif %}
{{ product.category.name if product.category else '未分类' }} ¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %}
¥{{ "%.2f"|format(product.original_price) }} {% endif %}
{% set total_stock = product.inventory|sum(attribute='stock') if product.inventory else 0 %} {% set sku_count = product.inventory|length if product.inventory else 0 %}
{{ total_stock }} {% if sku_count > 1 %}
{{ sku_count }}个SKU {% endif %} {% if total_stock <= 0 %}
缺货 {% elif total_stock <= 10 %}
库存不足 {% endif %}
{% if product.status == 1 %} 上架 {% else %} 下架 {% endif %}
{{ product.sales_count }}
浏览:{{ product.view_count }}
{{ product.created_at.strftime('%m-%d') if product.created_at else '' }}
{{ product.created_at.strftime('%H:%M') if product.created_at else '' }}
编辑 {% if product.inventory %} {% endif %}

暂无商品数据

{% if search or category_id or status %} 清除筛选 {% endif %}
{% if products.pages > 1 %} {% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/profile.html 📊 大小: 6433 bytes (6.28 KB) 🕒 修改时间: 2025-07-04 18:44:46 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}个人资料 - 太白购物商城管理后台{% endblock %} {% block page_title %}个人资料{% endblock %} {% block page_description %}管理员个人信息设置{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
基本信息
用户名不可修改
账号信息
角色: {{ admin.role }}
状态: {% if admin.status == 1 %} 正常 {% else %} 禁用 {% endif %}
创建时间:
{{ admin.created_at.strftime('%Y-%m-%d %H:%M:%S') if admin.created_at else '' }}
最后登录:
{{ admin.last_login_at.strftime('%Y-%m-%d %H:%M:%S') if admin.last_login_at else '从未登录' }}
修改密码
密码长度至少6位,建议包含字母和数字
{% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/users.html 📊 大小: 12968 bytes (12.66 KB) 🕒 修改时间: 2025-07-09 02:00:00 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}用户管理 - 太白购物商城管理后台{% endblock %} {% block page_title %}用户管理{% endblock %} {% block page_description %}管理系统用户,查看用户信息和状态{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
重置
{{ users.total }}

总用户数

{{ users.items | selectattr('status', 'equalto', 1) | list | length }}

正常用户

{{ users.items | selectattr('status', 'equalto', 0) | list | length }}

禁用用户

{{ week_new_users }}

本周新增

用户列表 共 {{ users.total }} 条记录
{% if users.items %} {% for user in users.items %} {% endfor %} {% else %} {% endif %}
用户ID 用户信息 联系方式 注册时间 状态 操作
#{{ user.id }}
{% if user.avatar_url %} 头像 {% else %}
{% endif %}
{{ user.username }}
{% if user.nickname %} {{ user.nickname }} {% endif %}
{% if user.email %}
{{ user.email }}
{% endif %} {% if user.phone %}
{{ user.phone }}
{% endif %} {% if not user.email and not user.phone %} 未设置 {% endif %}
{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else '-' }}
{{ user.created_at.strftime('%H:%M:%S') if user.created_at else '' }}
{{ '正常' if user.status == 1 else '禁用' }}
暂无用户数据
{% if search or status %} 尝试调整搜索条件 {% endif %}
{% if users.pages > 1 %} {% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/base.html 📊 大小: 5960 bytes (5.82 KB) 🕒 修改时间: 2025-07-09 04:36:54 🔸============================================================================== {% block title %}太白购物商城{% endblock %} {% block head %}{% endblock %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
太白购物商城

您身边的购物专家

联系我们

service@taibai-mall.com
400-888-8888


© 2025 太白购物商城. 保留所有权利.
{% block scripts %}{% endblock %} 🔸============================================================================== 📄 文件: app/templates/cart/index.html 📊 大小: 10126 bytes (9.89 KB) 🕒 修改时间: 2025-07-04 14:42:12 🔸============================================================================== {% extends "base.html" %} {% block title %}购物车 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}

我的购物车


{% if cart_items %}
商品信息
单价
数量
操作
{% for item in cart_items %}
{% if item.product.main_image %} {{ item.product.name }} {% else %}
{% endif %}
{{ item.product.name }}
{% if item.product.brand %} 品牌:{{ item.product.brand }}
{% endif %} {% if item.spec_combination %} 规格:{{ item.spec_combination }}
{% endif %} 库存:{{ item.get_stock() }}件 {% if not item.is_available() %}
{% if item.product.status != 1 %} 商品已下架 {% elif item.get_stock() < item.quantity %} 库存不足 {% endif %}
{% endif %}
¥{{ "%.2f"|format(item.get_price()) }}
小计:¥{{ "%.2f"|format(item.get_total_price()) }}
{% endfor %}
结算信息
已选商品: 0
商品总价: ¥0.00

应付总额: ¥0.00
7天无理由退换
全国包邮
正品保证
{% else %}

购物车是空的

快去选购您喜欢的商品吧!

去购物
{% endif %}
{% endblock %} {% block scripts %} {% 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 📊 大小: 9352 bytes (9.13 KB) 🕒 修改时间: 2025-07-09 04:40:51 🔸============================================================================== {% extends "base.html" %} {% block title %}首页 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}

欢迎来到太白购物商城

{% if user %}

你好,{{ user.nickname or user.username }}!开始您的购物之旅吧!

{% else %}

发现优质商品,享受便捷购物体验

立即注册 {% endif %}
{% if top_categories %}

商品分类


{% for category in top_categories %} {% endfor %}
{% endif %} {% if hot_products %}

热门商品

查看更多

{% for product in hot_products %}
{% if product.main_image %} {{ product.name }} {% else %}
{% endif %}
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} {% endif %}
销量{{ product.sales_count }}
{% endfor %}
{% endif %} {% if new_products %}

最新商品

查看更多

{% for product in new_products %}
{% if product.main_image %} {{ product.name }} {% else %}
{% endif %}
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} {% endif %}
销量{{ product.sales_count }}
{% endfor %}
{% endif %} {% if user %}

我的专区


个人中心
进入
我的订单
查看
购物车
查看
我的收藏
查看
{% endif %}

服务特色


精选商品

汇聚全球优质商品,品质保证,价格实惠

浏览商品
快速配送

全国包邮,快速配送,让您尽快收到心仪商品

了解更多
安全保障

正品保证,售后无忧,让您购物更放心

服务保障
{% endblock %} 🔸============================================================================== 📄 文件: app/templates/order/checkout.html 📊 大小: 13242 bytes (12.93 KB) 🕒 修改时间: 2025-07-08 17:14:27 🔸============================================================================== {% extends "base.html" %} {% block title %}订单结算 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
收货地址
新增地址
{% for address in addresses %}
{{ address.receiver_name }}

{{ address.receiver_phone }}

{{ address.get_full_address() }}

{% endfor %}
商品信息
{% for item in cart_items %}
{{ item.product.name }}
{{ item.product.name }}
{% if item.spec_combination %}

{{ item.spec_combination }}

{% endif %} {% if item.product.brand %} {{ item.product.brand }} {% endif %}
× {{ item.quantity }}
¥{{ "%.2f"|format(item.get_total_price()) }}
{% endfor %}
配送方式
支付方式
订单备注
订单摘要
商品总价: ¥{{ "%.2f"|format(total_amount) }}
运费: ¥{{ "%.2f"|format(shipping_fee) }}

应付总额: ¥{{ "%.2f"|format(final_amount) }}
点击"提交订单"表示您同意 《用户协议》
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/order/detail.html 📊 大小: 12875 bytes (12.57 KB) 🕒 修改时间: 2025-07-08 17:35:20 🔸============================================================================== {% extends "base.html" %} {% block title %}订单详情 - 太白购物商城{% endblock %} {% block content %}
订单状态
订单已提交

{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}

等待买家付款
{% if order.status >= 2 %}

已完成

{% else %}

请在15分钟内完成支付

{% endif %}
卖家发货
{% if order.status >= 3 %}

{{ order.shipped_at.strftime('%Y-%m-%d %H:%M:%S') if order.shipped_at else '已发货' }}

{% else %}

等待卖家发货

{% endif %}
确认收货
{% if order.status >= 4 %}

{{ order.received_at.strftime('%Y-%m-%d %H:%M:%S') if order.received_at else '已确认收货' }}

{% else %}

等待买家确认收货

{% endif %}
交易完成
{% if order.status == 5 %}

交易成功

{% else %}

等待交易完成

{% endif %}
商品信息
{% for item in order.order_items %}
{{ item.product_name }}
{{ item.product_name }}
{% if item.spec_combination %}

{{ item.spec_combination }}

{% endif %} 单价:¥{{ "%.2f"|format(item.price) }}
× {{ item.quantity }}
¥{{ "%.2f"|format(item.total_price) }}
{% endfor %}
{% if order.shipping_info %}
物流信息
{% for shipping in order.shipping_info %}
物流公司: {{ shipping.shipping_company or '待发货' }}
快递单号: {{ shipping.tracking_number or '待发货' }}
{% endfor %}
{% endif %}
订单信息
订单号: {{ order.order_sn }}
下单时间: {{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
订单状态: {% if order.status == 1 %} {{ order.get_status_text() }} {% elif order.status == 2 %} {{ order.get_status_text() }} {% elif order.status == 3 %} {{ order.get_status_text() }} {% elif order.status == 5 %} {{ order.get_status_text() }} {% elif order.status == 6 %} {{ order.get_status_text() }} {% else %} {{ order.get_status_text() }} {% endif %}
支付方式: {{ order.payment_method or '未选择' }}
配送方式: {{ order.shipping_method or '标准配送' }}
{% if order.remark %}
备注: {{ order.remark }}
{% endif %}
收货信息
{% set receiver = order.get_receiver_info() %}
收货人: {{ receiver.receiver_name or '未知' }}
联系电话: {{ receiver.receiver_phone or '未知' }}
收货地址: {{ receiver.full_address or '未知' }}
费用明细
商品总价: ¥{{ "%.2f"|format(order.total_amount) }}
运费: ¥{{ "%.2f"|format(order.shipping_fee) }}

应付总额: ¥{{ "%.2f"|format(order.actual_amount) }}
{% if order.can_pay() %} 立即支付 {% endif %} {% if order.can_cancel() %} {% endif %} {% if order.can_confirm_receipt() %} {% endif %} {% if order.status == 4 %} {% if order.order_items|length == 1 %} 评价商品 {% else %} {% endif %} {% endif %} 返回订单列表
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/order/pay.html 📊 大小: 8132 bytes (7.94 KB) 🕒 修改时间: 2025-07-08 17:11:04 🔸============================================================================== {% extends "base.html" %} {% block title %}订单支付 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}

订单支付

订单信息
订单号:{{ order.order_sn }}
¥{{ "%.2f"|format(order.actual_amount) }}
支付方式: {% if order.payment_method == 'wechat' %}微信支付 {% elif order.payment_method == 'alipay' %}支付宝 {% elif order.payment_method == 'bank' %}银行卡支付 {% elif order.payment_method == 'simulate' %}模拟支付 {% else %}{{ order.payment_method }} {% endif %}
14:59
{% if order.payment_method == 'wechat' %}
微信支付

请使用微信扫描二维码完成支付

{% endif %} {% if order.payment_method == 'alipay' %}
支付宝

正在跳转到支付宝...

{% endif %} {% if order.payment_method == 'bank' %}
银行卡支付

正在跳转到网银...

{% endif %} {% if order.payment_method == 'simulate' %}
模拟支付

开发测试模式,可直接完成支付

模拟支付控制面板

这是开发测试功能,您可以模拟不同的支付结果

实际生产环境中,此面板将被真实支付接口替代
{% endif %}
支付说明:
  • 订单有效期为15分钟,请及时完成支付
  • 支付成功后,订单状态将自动更新
  • 如遇支付问题,请联系客服:400-123-4567
  • {% if order.payment_method == 'simulate' %}
  • 当前为模拟支付模式,仅用于开发测试
  • {% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/product/detail.html 📊 大小: 18487 bytes (18.05 KB) 🕒 修改时间: 2025-07-09 05:25:26 🔸============================================================================== {% extends "base.html" %} {% block title %}{{ product.name }} - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if images %} {% if images|length > 1 %}
{% for image in images %}
{{ product.name }}
{% endfor %}
{% endif %} {% else %}
{% endif %}

{{ product.name }}

{% if product.brand %}

品牌:{{ product.brand }}

{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} 省{{ "%.0f"|format(((product.original_price - product.price) / product.original_price * 100)) }}% {% endif %}
销量:{{ product.sales_count }} | 浏览:{{ product.view_count }}
{% if inventory_list and inventory_list|length > 1 %}
选择规格:
{% 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() %}
{% for spec_value in spec_values %} {% endfor %}
{% endfor %}
{% endif %}
库存: {% if inventory_list %} {% if inventory_list|length == 1 %} {{ inventory_list[0].stock }} {% else %} 请选择规格 {% endif %} {% else %} 暂无库存 {% endif %}
{% if product.weight %}
重量:{{ product.weight }}kg
{% endif %}
服务承诺:
  • 正品保证
  • 7天无理由退换
  • 全国包邮
  • 售后服务
{% if product.description %}
{{ product.description|replace('\n', '
')|safe }}
{% else %}

暂无详细描述

{% endif %}
{% if product.brand %} {% endif %} {% if product.weight %} {% endif %} {% if inventory_list %} {% endif %}
商品名称 {{ product.name }}
商品品牌 {{ product.brand }}
商品分类 {{ product.category.name }}
商品重量 {{ product.weight }}kg
上架时间 {{ product.created_at.strftime('%Y-%m-%d') }}
库存信息 {% if inventory_list|length == 1 %} {{ inventory_list[0].stock }}件 {% else %} 多规格商品,请选择具体规格查看库存 {% endif %}
点击标签页加载评价
{% if recommended_products %}

相关推荐


{% for rec_product in recommended_products %} {% endfor %}
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/product/list.html 📊 大小: 13860 bytes (13.54 KB) 🕒 修改时间: 2025-07-04 14:42:29 🔸============================================================================== {% extends "base.html" %} {% block title %} {% if current_category %}{{ current_category.name }} - {% endif %} {% if search %}搜索"{{ search }}" - {% endif %} 商品列表 - 太白购物商城 {% endblock %} {% block head %} {% endblock %} {% block content %}
商品搜索
{% if category_id %}{% endif %} {% if sort %}{% endif %}
商品分类
价格筛选
重置
{% if search %}{% endif %} {% if category_id %}{% endif %} {% if sort %}{% endif %}
{% set filtered_args = request.args.copy() %} {% set _ = filtered_args.pop('min_price', None) %} {% set _ = filtered_args.pop('max_price', None) %} 100元以下 100-500元 500-1000元 1000元以上
{% if current_category %} {{ current_category.name }} {% elif search %} 搜索"{{ search }}" {% else %} 全部商品 {% endif %} (共{{ products.total }}个商品)
{% if products.items %}
{% for product in products.items %}
{% if product.main_image %} {{ product.name }} {% else %}
{% endif %}
{{ product.name[:60] }}{% if product.name|length > 60 %}...{% endif %}
{% if product.brand %}

{{ product.brand }}

{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} {% endif %}
销量{{ product.sales_count }} 浏览{{ product.view_count }}
{% endfor %}
{% if products.pages > 1 %} {% endif %} {% else %}

暂无找到相关商品

请尝试调整搜索条件或浏览其他分类

返回全部商品
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/review/my_reviews.html 📊 大小: 9242 bytes (9.03 KB) 🕒 修改时间: 2025-07-09 04:48:22 🔸============================================================================== {% extends "base.html" %} {% block title %}我的评价 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
我的评价
{% if reviews.items %} {% for review in reviews.items %}
{{ review.product.name }}
{{ review.product.name }}
{{ review.get_rating_stars() }} {{ review.rating }}分
{% if review.content %}

{{ review.content }}

{% endif %} {% if review.get_images() %}
{% for image_url in review.get_images() %} 评价图片 {% endfor %}
{% endif %}
评价时间:{{ review.created_at.strftime('%Y-%m-%d %H:%M') }} {% if review.is_anonymous %} | 匿名评价 {% endif %}
{% endfor %} {% if reviews.pages > 1 %} {% endif %} {% else %}
暂无评价

您还没有发表过商品评价

去购物
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/review/write.html 📊 大小: 6165 bytes (6.02 KB) 🕒 修改时间: 2025-07-08 19:07:34 🔸============================================================================== {% extends "base.html" %} {% block title %}评价商品 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
评价商品
{{ order_item.product_name }}
{{ order_item.product_name }}
{% if order_item.spec_combination %}

{{ order_item.spec_combination }}

{% endif %}

单价:¥{{ "%.2f"|format(order_item.price) }} × {{ order_item.quantity }}

请选择评分
字数限制:500字以内

点击或拖拽上传图片

支持 JPG、PNG、GIF 格式,最大5MB
取消
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/test_upload.html 📊 大小: 14836 bytes (14.49 KB) 🕒 修改时间: 2025-07-03 04:21:18 🔸============================================================================== COS上传测试

腾讯云COS上传测试

头像上传测试

通用图片上传测试
上传历史
COS上传测试

腾讯云COS上传测试

头像上传测试

通用图片上传测试
上传历史
🔸============================================================================== 📄 文件: app/templates/user/address_form.html 📊 大小: 7575 bytes (7.40 KB) 🕒 修改时间: 2025-07-04 04:02:42 🔸============================================================================== {% extends "base.html" %} {% block title %}{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %} - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %}
{{ form.hidden_tag() }}
{{ form.receiver_name(class="form-control") }} {% if form.receiver_name.errors %}
{{ form.receiver_name.errors[0] }}
{% endif %}
{{ form.receiver_phone(class="form-control") }} {% if form.receiver_phone.errors %}
{{ form.receiver_phone.errors[0] }}
{% endif %}
{% if form.province.errors %}
{{ form.province.errors[0] }}
{% endif %}
{% if form.city.errors %}
{{ form.city.errors[0] }}
{% endif %}
{% if form.district.errors %}
{{ form.district.errors[0] }}
{% endif %}
{{ form.detail_address(class="form-control", placeholder="街道、门牌号等详细信息") }} {% if form.detail_address.errors %}
{{ form.detail_address.errors[0] }}
{% endif %}
{{ form.postal_code(class="form-control", placeholder="选填") }} {% if form.postal_code.errors %}
{{ form.postal_code.errors[0] }}
{% endif %}
{{ form.is_default(class="form-check-input") }}
{{ form.submit(class="btn btn-primary") }} 取消
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/addresses.html 📊 大小: 6585 bytes (6.43 KB) 🕒 修改时间: 2025-07-09 04:52:12 🔸============================================================================== {% extends "base.html" %} {% block title %}收货地址 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
收货地址
添加地址
{% if addresses %}
{% for address in addresses %}
{{ address.receiver_name }} {% if address.is_default %} 默认 {% endif %}

{{ address.receiver_phone }}

{{ address.get_full_address() }}

{% if address.postal_code %}

邮编:{{ address.postal_code }}

{% endif %}
{% endfor %}
{% else %}
暂无收货地址

请添加您的收货地址,方便下单购物

添加地址
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/favorites.html 📊 大小: 11617 bytes (11.34 KB) 🕒 修改时间: 2025-07-09 04:52:12 🔸============================================================================== {% extends "base.html" %} {% block title %}我的收藏 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
我的收藏
共 {{ total_count }} 件商品 {% if total_count > 0 %} {% endif %}
{% if favorites.items %}
{% for favorite in favorites.items %}
{{ favorite.product.name }}
¥{{ "%.2f"|format(favorite.product.price) }} 销量 {{ favorite.product.sales_count }}
{{ favorite.created_at.strftime('%Y-%m-%d') }} {% if favorite.product.status == 1 %} 有货 {% else %} 下架 {% endif %}
{% if favorite.product.status == 1 %} {% endif %}
{% endfor %}
{% if favorites.pages > 1 %} {% endif %} {% else %}

还没有收藏任何商品

去逛逛,收藏心仪的商品吧~

去首页逛逛
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/history.html 📊 大小: 12588 bytes (12.29 KB) 🕒 修改时间: 2025-07-09 04:52:12 🔸============================================================================== {% extends "base.html" %} {% block title %}浏览历史 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
浏览历史
共 {{ total_count }} 件商品 {% if total_count > 0 %} {% endif %}
{% if history.items %}
{% for item in history.items %}
{{ item.product.name }}
¥{{ "%.2f"|format(item.product.price) }} 销量 {{ item.product.sales_count }}
{{ item.product.category.name if item.product.category else "未分类" }}
{{ item.viewed_at.strftime('%Y-%m-%d %H:%M') }} {% if item.product.status == 1 %} 有货 {% else %} 下架 {% endif %}
{% if item.product.status == 1 %} {% endif %}
{% endfor %}
{% if history.pages > 1 %} {% endif %} {% else %}

还没有浏览任何商品

去逛逛,看看有什么好商品~

去首页逛逛
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/login.html 📊 大小: 2715 bytes (2.65 KB) 🕒 修改时间: 2025-07-04 03:55:20 🔸============================================================================== {% extends "base.html" %} {% block title %}用户登录 - 太白购物商城{% endblock %} {% block styles %} {% endblock %} {% block content %}

用户登录

{{ form.hidden_tag() }}
{{ form.username.label(class="form-label") }} {{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }} {% if form.username.errors %}
{% for error in form.username.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.password.label(class="form-label") }} {{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }} {% if form.password.errors %}
{% for error in form.password.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.remember_me(class="form-check-input") }} {{ form.remember_me.label(class="form-check-label") }}
{{ form.submit(class="btn btn-primary") }}

还没有账户? 立即注册

{% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/orders.html 📊 大小: 13975 bytes (13.65 KB) 🕒 修改时间: 2025-07-08 17:35:33 🔸============================================================================== {% extends "base.html" %} {% block title %}我的订单 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
我的订单
{% if orders.items %} {% for order in orders.items %}
订单号:{{ order.order_sn }}
下单时间:{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}
{% if order.status == 1 %} {{ order.get_status_text() }} {% elif order.status == 2 %} {{ order.get_status_text() }} {% elif order.status == 3 %} {{ order.get_status_text() }} {% elif order.status == 5 %} {{ order.get_status_text() }} {% elif order.status == 6 %} {{ order.get_status_text() }} {% else %} {{ order.get_status_text() }} {% endif %}
{% for item in order.order_items[:3] %}
{{ item.product_name }}
{{ item.product_name }}
{% if item.spec_combination %}

{{ item.spec_combination }}

{% endif %}
× {{ item.quantity }}
¥{{ "%.2f"|format(item.total_price) }}
{% endfor %} {% if order.order_items|length > 3 %}
还有 {{ order.order_items|length - 3 }} 件商品...
{% endif %}
{% endfor %} {% if orders.pages > 1 %} {% endif %} {% else %}
{% if current_status %} 暂无该状态订单 {% else %} 暂无订单 {% endif %}

您还没有任何订单,快去购物吧!

去购物
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/profile.html 📊 大小: 12649 bytes (12.35 KB) 🕒 修改时间: 2025-07-09 04:53:37 🔸============================================================================== {% extends "base.html" %} {% block title %}个人中心 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
基本信息
用户名: {{ user.username }}
昵称: {{ user.nickname or '未设置' }}
手机号: {{ user.phone or '未绑定' }}
邮箱: {{ user.email or '未绑定' }}
性别: {% if user.gender == 1 %}男 {% elif user.gender == 2 %}女 {% else %}未设置 {% endif %}
注册时间: {{ user.created_at.strftime('%Y-%m-%d %H:%M:%S') if user.created_at else '未知' }}
{% if user.avatar_url %} 头像 {% else %}
{% endif %}
上传中...
支持 JPG、PNG 格式,大小不超过 2MB
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/register.html 📊 大小: 5522 bytes (5.39 KB) 🕒 修改时间: 2025-07-04 03:59:03 🔸============================================================================== {% extends "base.html" %} {% block title %}用户注册 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}

用户注册

{{ form.hidden_tag() }}
{{ form.username.label(class="form-label") }} {{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }} {% if form.username.errors %}
{% for error in form.username.errors %} {{ error }} {% endfor %}
{% endif %}
用户名只能包含字母、数字和下划线,3-20个字符
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else ""), id="emailInput") }}
{% if form.email.errors %}
{% for error in form.email.errors %} {{ error }} {% endfor %}
{% endif %}
{{ 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 %}
{% for error in form.email_code.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.phone.label(class="form-label") }} {{ form.phone(class="form-control" + (" is-invalid" if form.phone.errors else "")) }} {% if form.phone.errors %}
{% for error in form.phone.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.password.label(class="form-label") }} {{ form.password(class="form-control" + (" is-invalid" if form.password.errors else ""), id="passwordInput") }} {% if form.password.errors %}
{% for error in form.password.errors %} {{ error }} {% endfor %}
{% endif %}
密码必须包含至少一个字母和一个数字,6-20个字符
{{ 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 %}
{% for error in form.confirm_password.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.submit(class="btn btn-success") }}

已有账户? 立即登录

{% endblock %} {% block scripts %} {% 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 📊 大小: 3125 bytes (3.05 KB) 🕒 修改时间: 2025-07-09 05:08:00 🔸============================================================================== """ 装饰器工具 """ from functools import wraps from flask import session, redirect, url_for, flash, request, g from app.models.admin import AdminUser from app.models.operation_log import OperationLog def admin_required(f): """管理员权限验证装饰器""" @wraps(f) def decorated_function(*args, **kwargs): if 'admin_id' not in session: flash('请先登录', 'warning') return redirect(url_for('admin.login')) # 获取管理员信息 admin = AdminUser.query.get(session['admin_id']) if not admin or admin.status != 1: session.clear() flash('账号已被禁用,请联系管理员', 'error') return redirect(url_for('admin.login')) g.current_admin = admin return f(*args, **kwargs) return decorated_function def login_required(f): """用户登录验证装饰器""" @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: flash('请先登录', 'warning') return redirect(url_for('auth.login')) return f(*args, **kwargs) return decorated_function def log_operation(action, resource_type=None): """操作日志记录装饰器""" def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): # 执行原函数 result = f(*args, **kwargs) # 记录操作日志 try: user_id = None user_type = None 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 # 普通用户 if user_id: # 获取资源ID resource_id = None if 'product_id' in kwargs: resource_id = kwargs['product_id'] elif 'category_id' in kwargs: resource_id = kwargs['category_id'] elif 'user_id' in kwargs: resource_id = kwargs['user_id'] # 记录日志 OperationLog.create_log( user_id=user_id, user_type=user_type, action=action, resource_type=resource_type, resource_id=resource_id, ip_address=request.remote_addr, user_agent=request.headers.get('User-Agent', ''), request_data=dict(request.form) if request.form else None ) except Exception as e: # 日志记录失败不应该影响主要功能 print(f"操作日志记录失败: {str(e)}") 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""" 验证码邮件

太白购物平台

您好!

您正在进行{type_map.get(code_type, "验证")}操作,验证码为:

{code}

验证码有效期为10分钟,请及时使用。

如果这不是您的操作,请忽略此邮件。

此邮件由系统自动发送,请勿回复。

© 2024 太白购物平台 版权所有

""" 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/', 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/', 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/', 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 📊 大小: 18310 bytes (17.88 KB) 🕒 修改时间: 2025-07-09 02:00:00 🔸============================================================================== """ 管理员视图 """ 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.order import Order, OrderItem, ShippingInfo from app.models.payment import Payment from app.models.operation_log import OperationLog from app.utils.decorators import admin_required, log_operation from config.database import db from datetime import datetime, timedelta from sqlalchemy import func, or_ import json 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( 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) # 计算本周新增用户数 week_start = datetime.now() - timedelta(days=7) week_new_users = User.query.filter(User.created_at >= week_start).count() return render_template('admin/users.html', users=users, search=search, status=status, week_new_users=week_new_users) @admin_bp.route('/users//detail') @admin_required def user_detail(user_id): """获取用户详情""" try: user = User.query.get_or_404(user_id) return jsonify({ 'success': True, 'user': user.to_dict() }) except Exception as e: return jsonify({ 'success': False, 'message': str(e) }) @admin_bp.route('/users//toggle-status', methods=['POST']) @admin_required @log_operation('切换用户状态') def toggle_user_status(user_id): """切换用户状态""" try: user = User.query.get_or_404(user_id) data = request.get_json() new_status = data.get('status') if new_status not in [0, 1]: return jsonify({ 'success': False, 'message': '无效的状态值' }) user.status = new_status db.session.commit() action_text = '启用' if new_status == 1 else '禁用' return jsonify({ 'success': True, 'message': f'用户已{action_text}' }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': str(e) }) @admin_bp.route('/orders') @admin_required def orders(): """订单管理""" page = request.args.get('page', 1, type=int) per_page = 20 query = Order.query.order_by(Order.created_at.desc()) # 搜索功能 search = request.args.get('search', '').strip() if search: query = query.filter( or_( Order.order_sn.like(f'%{search}%'), Order.user.has(User.username.like(f'%{search}%')), Order.user.has(User.phone.like(f'%{search}%')) ) ) # 状态筛选 status = request.args.get('status', '', type=str) if status: query = query.filter(Order.status == int(status)) # 日期筛选 start_date = request.args.get('start_date', '').strip() end_date = request.args.get('end_date', '').strip() if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d') query = query.filter(Order.created_at >= start_date_obj) except ValueError: pass if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1) query = query.filter(Order.created_at < end_date_obj) except ValueError: pass orders = query.paginate(page=page, per_page=per_page, error_out=False) # 获取订单统计 order_stats = {} for status_code, status_name in Order.STATUS_CHOICES.items(): count = Order.query.filter_by(status=status_code).count() order_stats[status_code] = {'name': status_name, 'count': count} return render_template('admin/orders.html', orders=orders, search=search, status=status, start_date=start_date, end_date=end_date, order_stats=order_stats, ORDER_STATUS=Order.STATUS_CHOICES) @admin_bp.route('/orders/') @admin_required def order_detail(order_id): """订单详情""" order = Order.query.get_or_404(order_id) # 获取支付记录 payment = Payment.query.filter_by(order_id=order_id).first() # 获取物流信息 shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first() return render_template('admin/order_detail.html', order=order, payment=payment, shipping_info=shipping_info) @admin_bp.route('/orders//ship', methods=['POST']) @admin_required @log_operation('订单发货') def ship_order(order_id): """订单发货""" try: order = Order.query.get_or_404(order_id) if order.status != Order.STATUS_PENDING_SHIPMENT: return jsonify({'success': False, 'message': '订单状态不允许发货'}) # 获取发货信息 shipping_company = request.form.get('shipping_company', '').strip() tracking_number = request.form.get('tracking_number', '').strip() if not shipping_company or not tracking_number: return jsonify({'success': False, 'message': '请填写完整的物流信息'}) # 更新订单状态 order.status = Order.STATUS_SHIPPED order.shipped_at = datetime.utcnow() # 创建或更新物流信息 shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first() if not shipping_info: shipping_info = ShippingInfo(order_id=order_id) db.session.add(shipping_info) shipping_info.shipping_company = shipping_company shipping_info.tracking_number = tracking_number shipping_info.shipping_status = 1 # 已发货 db.session.commit() return jsonify({'success': True, 'message': '发货成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'发货失败: {str(e)}'}) @admin_bp.route('/orders//refund', methods=['POST']) @admin_required @log_operation('订单退款') def refund_order(order_id): """订单退款""" try: order = Order.query.get_or_404(order_id) if order.status not in [Order.STATUS_PENDING_SHIPMENT, Order.STATUS_SHIPPED, Order.STATUS_REFUNDING]: return jsonify({'success': False, 'message': '订单状态不允许退款'}) # 获取退款信息 refund_reason = request.form.get('refund_reason', '').strip() if not refund_reason: return jsonify({'success': False, 'message': '请填写退款原因'}) # 更新订单状态 order.status = Order.STATUS_REFUNDING # 更新支付记录状态 payment = Payment.query.filter_by(order_id=order_id).first() if payment: payment.status = Payment.STATUS_REFUNDED # 恢复库存 from app.models.product import ProductInventory for item in order.order_items: if item.sku_code: sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first() if sku_info: sku_info.stock += item.quantity # 减少销量 if item.product: item.product.sales_count = max(0, item.product.sales_count - item.quantity) db.session.commit() return jsonify({'success': True, 'message': '退款处理成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'退款失败: {str(e)}'}) @admin_bp.route('/orders//cancel', methods=['POST']) @admin_required @log_operation('取消订单') def cancel_order(order_id): """取消订单""" try: order = Order.query.get_or_404(order_id) if not order.can_cancel(): return jsonify({'success': False, 'message': '订单状态不允许取消'}) # 获取取消原因 cancel_reason = request.form.get('cancel_reason', '').strip() # 更新订单状态 order.status = Order.STATUS_CANCELLED # 恢复库存 from app.models.product import ProductInventory for item in order.order_items: if item.sku_code: sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first() if sku_info: sku_info.stock += item.quantity # 减少销量 if item.product: item.product.sales_count = max(0, item.product.sales_count - item.quantity) db.session.commit() return jsonify({'success': True, 'message': '订单已取消'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'取消失败: {str(e)}'}) @admin_bp.route('/logs') @admin_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) # 计算今日操作数 today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) today_logs_count = OperationLog.query.filter( OperationLog.created_at >= today_start ).count() return render_template('admin/logs.html', logs=logs, user_type=user_type, action=action, today_logs_count=today_logs_count) @admin_bp.route('/logs//detail') @admin_required def log_detail(log_id): """获取日志详情""" try: log = OperationLog.query.get_or_404(log_id) return jsonify({ 'success': True, 'log': log.to_dict() }) except Exception as e: return jsonify({ 'success': False, 'message': str(e) }) @admin_bp.route('/logs/clear', methods=['POST']) @admin_required @log_operation('清理操作日志') def clear_logs(): """清理操作日志""" try: data = request.get_json() days_to_keep = data.get('days_to_keep', 30) # 计算删除日期 delete_before = datetime.now() - timedelta(days=days_to_keep) # 删除旧日志 deleted_count = OperationLog.query.filter( OperationLog.created_at < delete_before ).delete() db.session.commit() return jsonify({ 'success': True, 'message': f'已清理 {deleted_count} 条历史日志' }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': str(e) }) 🔸============================================================================== 📄 文件: 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/favorite.py 📊 大小: 7467 bytes (7.29 KB) 🕒 修改时间: 2025-07-09 02:26:14 🔸============================================================================== """ 收藏管理视图 """ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session from app.models.favorite import UserFavorite from app.models.product import Product from app.utils.decorators import login_required from config.database import db favorite_bp = Blueprint('favorite', __name__, url_prefix='/favorite') @favorite_bp.route('/') @login_required def index(): """收藏夹首页""" page = request.args.get('page', 1, type=int) per_page = 20 # 获取用户收藏列表 favorites = UserFavorite.get_user_favorites(session['user_id'], page, per_page) # 获取收藏总数 total_count = UserFavorite.get_user_favorites_count(session['user_id']) return render_template('user/favorites.html', favorites=favorites, total_count=total_count) @favorite_bp.route('/add', methods=['POST']) @login_required def add(): """添加收藏""" try: data = request.get_json() product_id = data.get('product_id') # 调试信息 print(f"收到添加收藏请求,用户ID: {session['user_id']}, 商品ID: {product_id}") if not product_id: return jsonify({'success': False, 'message': '商品ID不能为空'}) # 确保product_id是整数 try: product_id = int(product_id) except (ValueError, TypeError): return jsonify({'success': False, 'message': '商品ID格式错误'}) success, message = UserFavorite.add_favorite(session['user_id'], product_id) if success: # 获取更新后的收藏数量 favorite_count = UserFavorite.get_user_favorites_count(session['user_id']) return jsonify({ 'success': True, 'message': message, 'favorite_count': favorite_count }) else: return jsonify({'success': False, 'message': message}) except Exception as e: print(f"添加收藏失败: {str(e)}") return jsonify({'success': False, 'message': f'添加收藏失败: {str(e)}'}) @favorite_bp.route('/remove', methods=['POST']) @login_required def remove(): """取消收藏""" try: data = request.get_json() product_id = data.get('product_id') # 调试信息 print(f"收到取消收藏请求,用户ID: {session['user_id']}, 商品ID: {product_id}") if not product_id: return jsonify({'success': False, 'message': '商品ID不能为空'}) # 确保product_id是整数 try: product_id = int(product_id) except (ValueError, TypeError): return jsonify({'success': False, 'message': '商品ID格式错误'}) success, message = UserFavorite.remove_favorite(session['user_id'], product_id) if success: # 获取更新后的收藏数量 favorite_count = UserFavorite.get_user_favorites_count(session['user_id']) return jsonify({ 'success': True, 'message': message, 'favorite_count': favorite_count }) else: return jsonify({'success': False, 'message': message}) except Exception as e: print(f"取消收藏失败: {str(e)}") return jsonify({'success': False, 'message': f'取消收藏失败: {str(e)}'}) @favorite_bp.route('/toggle', methods=['POST']) @login_required def toggle(): """切换收藏状态""" try: data = request.get_json() product_id = data.get('product_id') # 调试信息 print(f"收到切换收藏请求,用户ID: {session['user_id']}, 商品ID: {product_id}, 数据类型: {type(product_id)}") if not product_id: return jsonify({'success': False, 'message': '商品ID不能为空'}) # 确保product_id是整数 try: product_id = int(product_id) except (ValueError, TypeError): return jsonify({'success': False, 'message': '商品ID格式错误'}) # 检查当前是否已收藏 is_favorited = UserFavorite.is_favorited(session['user_id'], product_id) if is_favorited: success, message = UserFavorite.remove_favorite(session['user_id'], product_id) action = 'removed' else: success, message = UserFavorite.add_favorite(session['user_id'], product_id) action = 'added' if success: favorite_count = UserFavorite.get_user_favorites_count(session['user_id']) return jsonify({ 'success': True, 'message': message, 'action': action, 'is_favorited': not is_favorited, 'favorite_count': favorite_count }) else: return jsonify({'success': False, 'message': message}) except Exception as e: print(f"切换收藏状态失败: {str(e)}") return jsonify({'success': False, 'message': f'操作失败: {str(e)}'}) @favorite_bp.route('/check/') @login_required def check(product_id): """检查商品是否已收藏""" try: is_favorited = UserFavorite.is_favorited(session['user_id'], product_id) return jsonify({ 'success': True, 'is_favorited': is_favorited }) except Exception as e: print(f"检查收藏状态失败: {str(e)}") return jsonify({'success': False, 'message': str(e)}) @favorite_bp.route('/batch-remove', methods=['POST']) @login_required def batch_remove(): """批量取消收藏""" try: data = request.get_json() product_ids = data.get('product_ids', []) if not product_ids: return jsonify({'success': False, 'message': '请选择要取消的商品'}) success_count = 0 fail_count = 0 for product_id in product_ids: try: product_id = int(product_id) success, _ = UserFavorite.remove_favorite(session['user_id'], product_id) if success: success_count += 1 else: fail_count += 1 except (ValueError, TypeError): fail_count += 1 message = f'成功取消收藏 {success_count} 个商品' if fail_count > 0: message += f',失败 {fail_count} 个' # 获取更新后的收藏数量 favorite_count = UserFavorite.get_user_favorites_count(session['user_id']) return jsonify({ 'success': True, 'message': message, 'favorite_count': favorite_count }) except Exception as e: print(f"批量操作失败: {str(e)}") return jsonify({'success': False, 'message': f'批量操作失败: {str(e)}'}) @favorite_bp.route('/count') @login_required def count(): """获取收藏数量""" try: favorite_count = UserFavorite.get_user_favorites_count(session['user_id']) return jsonify({ 'success': True, 'favorite_count': favorite_count }) except Exception as e: print(f"获取收藏数量失败: {str(e)}") return jsonify({'success': False, 'message': str(e)}) 🔸============================================================================== 📄 文件: app/views/history.py 📊 大小: 4841 bytes (4.73 KB) 🕒 修改时间: 2025-07-09 02:19:53 🔸============================================================================== """ 浏览历史管理视图 """ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session from app.models.browse_history import BrowseHistory from app.models.product import Product from app.utils.decorators import login_required from config.database import db history_bp = Blueprint('history', __name__, url_prefix='/history') @history_bp.route('/') @login_required def index(): """浏览历史页面""" page = request.args.get('page', 1, type=int) per_page = 20 # 获取用户浏览历史 history = BrowseHistory.get_user_history(session['user_id'], page, per_page) # 获取浏览历史总数 total_count = BrowseHistory.get_user_history_count(session['user_id']) return render_template('user/history.html', history=history, total_count=total_count) @history_bp.route('/add', methods=['POST']) @login_required def add(): """添加浏览记录""" try: data = request.get_json() product_id = data.get('product_id') if not product_id: return jsonify({'success': False, 'message': '商品ID不能为空'}) success, message = BrowseHistory.add_history(session['user_id'], product_id) if success: # 获取更新后的浏览历史数量 history_count = BrowseHistory.get_user_history_count(session['user_id']) return jsonify({ 'success': True, 'message': message, 'history_count': history_count }) else: return jsonify({'success': False, 'message': message}) except Exception as e: return jsonify({'success': False, 'message': f'添加浏览记录失败: {str(e)}'}) @history_bp.route('/remove', methods=['POST']) @login_required def remove(): """删除单个浏览记录""" try: data = request.get_json() product_id = data.get('product_id') if not product_id: return jsonify({'success': False, 'message': '商品ID不能为空'}) success, message = BrowseHistory.remove_history_item(session['user_id'], product_id) if success: # 获取更新后的浏览历史数量 history_count = BrowseHistory.get_user_history_count(session['user_id']) return jsonify({ 'success': True, 'message': message, 'history_count': history_count }) else: return jsonify({'success': False, 'message': message}) except Exception as e: return jsonify({'success': False, 'message': f'删除浏览记录失败: {str(e)}'}) @history_bp.route('/clear', methods=['POST']) @login_required def clear(): """清空浏览历史""" try: success, message = BrowseHistory.clear_user_history(session['user_id']) return jsonify({ 'success': success, 'message': message, 'history_count': 0 if success else BrowseHistory.get_user_history_count(session['user_id']) }) except Exception as e: return jsonify({'success': False, 'message': f'清空浏览历史失败: {str(e)}'}) @history_bp.route('/batch-remove', methods=['POST']) @login_required def batch_remove(): """批量删除浏览记录""" try: data = request.get_json() product_ids = data.get('product_ids', []) if not product_ids: return jsonify({'success': False, 'message': '请选择要删除的商品'}) success_count = 0 fail_count = 0 for product_id in product_ids: success, _ = BrowseHistory.remove_history_item(session['user_id'], product_id) if success: success_count += 1 else: fail_count += 1 message = f'成功删除 {success_count} 个浏览记录' if fail_count > 0: message += f',失败 {fail_count} 个' # 获取更新后的浏览历史数量 history_count = BrowseHistory.get_user_history_count(session['user_id']) return jsonify({ 'success': True, 'message': message, 'history_count': history_count }) except Exception as e: return jsonify({'success': False, 'message': f'批量操作失败: {str(e)}'}) @history_bp.route('/count') @login_required def count(): """获取浏览历史数量""" try: history_count = BrowseHistory.get_user_history_count(session['user_id']) return jsonify({ 'success': True, 'history_count': history_count }) except Exception as e: return jsonify({'success': False, 'message': str(e)}) 🔸============================================================================== 📄 文件: 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/') 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/') 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/') @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/', 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/', 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/') @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 📊 大小: 8188 bytes (8.00 KB) 🕒 修改时间: 2025-07-08 17:11:04 🔸============================================================================== """ 支付视图 """ 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 import time import random 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) elif payment_method == 'simulate': # 模拟支付 result = process_simulate_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': '正在跳转到网银...' } def process_simulate_pay(payment): """处理模拟支付""" return { 'success': True, 'payment_type': 'simulate', 'payment_sn': payment.payment_sn, 'amount': float(payment.amount), 'message': '模拟支付模式,可直接完成支付' } @payment_bp.route('/callback/wechat', methods=['POST']) def wechat_callback(): """微信支付回调""" 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/') @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/', 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_{int(time.time())}_{random.randint(1000, 9999)}') 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)}'}) @payment_bp.route('/simulate_fail/', methods=['POST']) @login_required def simulate_fail(payment_sn): """模拟支付失败(开发测试用)""" try: user_id = session['user_id'] payment = Payment.query.filter_by(payment_sn=payment_sn).first() if not payment or payment.order.user_id != user_id: return jsonify({'success': False, 'message': '支付记录不存在'}) if payment.status == Payment.STATUS_SUCCESS: return jsonify({'success': False, 'message': '订单已支付,无法模拟失败'}) # 模拟支付失败 payment.status = Payment.STATUS_FAILED payment.third_party_sn = f'SIMULATE_FAIL_{int(time.time())}_{random.randint(1000, 9999)}' db.session.commit() return jsonify({'success': True, 'message': '模拟支付失败'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'模拟支付失败操作失败: {str(e)}'}) 🔸============================================================================== 📄 文件: 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/') @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/', 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/', 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/', 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/', 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/', 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/', 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/', 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/') @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/') @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/review.py 📊 大小: 9163 bytes (8.95 KB) 🕒 修改时间: 2025-07-09 04:46:37 🔸============================================================================== """ 评价管理视图 """ from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, flash, g from sqlalchemy import func, desc from config.database import db from app.models.review import Review from app.models.order import Order, OrderItem from app.models.product import Product from app.models.user import User from app.utils.decorators import login_required, log_operation from app.utils.file_upload import file_upload_handler import json review_bp = Blueprint('review', __name__, url_prefix='/review') @review_bp.route('/product/') def product_reviews(product_id): """商品评价列表(AJAX接口)""" try: page = request.args.get('page', 1, type=int) rating_filter = request.args.get('rating', type=int) # 基础查询 query = Review.query.filter_by(product_id=product_id, status=1) # 评分筛选 if rating_filter: query = query.filter_by(rating=rating_filter) # 分页查询 reviews = query.order_by(desc(Review.created_at)).paginate( page=page, per_page=10, error_out=False ) # 评价统计 stats = db.session.query( Review.rating, func.count(Review.id).label('count') ).filter_by(product_id=product_id, status=1).group_by(Review.rating).all() rating_stats = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0} total_reviews = 0 for stat in stats: rating_stats[stat.rating] = stat.count total_reviews += stat.count # 好评率计算 good_rate = 0 if total_reviews > 0: good_reviews = rating_stats[4] + rating_stats[5] good_rate = round(good_reviews / total_reviews * 100, 1) # 转换为字典 reviews_data = [] for review in reviews.items: review_dict = review.to_dict() # 添加用户头像 if review.user: review_dict['user_avatar'] = review.user.avatar_url reviews_data.append(review_dict) return jsonify({ 'success': True, 'reviews': reviews_data, 'pagination': { 'page': reviews.page, 'pages': reviews.pages, 'per_page': reviews.per_page, 'total': reviews.total, 'has_next': reviews.has_next, 'has_prev': reviews.has_prev }, 'stats': { 'total_reviews': total_reviews, 'good_rate': good_rate, 'rating_stats': rating_stats } }) except Exception as e: return jsonify({'success': False, 'message': str(e)}) @review_bp.route('/write//') @login_required def write_review(order_id, product_id): """写评价页面""" # 验证订单和商品 order = Order.query.filter_by(id=order_id, user_id=session["user_id"]).first() if not order: flash('订单不存在', 'error') return redirect(url_for('order.list')) # 检查订单状态 if order.status not in [4, 5]: # 待评价或已完成 flash('该订单暂时无法评价', 'error') return redirect(url_for('order.detail', order_id=order_id)) # 检查商品是否在订单中 order_item = OrderItem.query.filter_by(order_id=order_id, product_id=product_id).first() if not order_item: flash('商品不在此订单中', 'error') return redirect(url_for('order.detail', order_id=order_id)) # 检查是否已经评价过 existing_review = Review.query.filter_by( user_id=session["user_id"], product_id=product_id, order_id=order_id ).first() if existing_review: flash('您已经评价过该商品', 'info') return redirect(url_for('order.detail', order_id=order_id)) return render_template('review/write.html', order=order, order_item=order_item, product=order_item.product) @review_bp.route('/submit', methods=['POST']) @login_required @log_operation('提交商品评价') def submit_review(): """提交评价""" try: data = request.get_json() order_id = data.get('order_id') product_id = data.get('product_id') rating = data.get('rating') content = data.get('content', '').strip() is_anonymous = data.get('is_anonymous', False) images = data.get('images', []) # 参数验证 if not all([order_id, product_id, rating]): return jsonify({'success': False, 'message': '参数不完整'}) if not (1 <= rating <= 5): return jsonify({'success': False, 'message': '评分必须在1-5星之间'}) # 验证订单 order = Order.query.filter_by(id=order_id, user_id=session["user_id"]).first() if not order: return jsonify({'success': False, 'message': '订单不存在'}) if order.status not in [4, 5]: return jsonify({'success': False, 'message': '该订单暂时无法评价'}) # 验证商品在订单中 order_item = OrderItem.query.filter_by(order_id=order_id, product_id=product_id).first() if not order_item: return jsonify({'success': False, 'message': '商品不在此订单中'}) # 检查是否已评价 existing_review = Review.query.filter_by( user_id=session["user_id"], product_id=product_id, order_id=order_id ).first() if existing_review: return jsonify({'success': False, 'message': '您已经评价过该商品'}) # 创建评价 review = Review( user_id=session["user_id"], product_id=product_id, order_id=order_id, rating=rating, content=content if content else None, is_anonymous=1 if is_anonymous else 0 ) # 设置图片 if images: review.set_images(images) db.session.add(review) # 检查订单中所有商品是否都已评价 total_items = OrderItem.query.filter_by(order_id=order_id).count() reviewed_items = Review.query.filter_by(order_id=order_id).count() + 1 # +1 是当前这个评价 # 如果所有商品都已评价,更新订单状态为已完成 if reviewed_items >= total_items and order.status == 4: order.status = 5 # 已完成 db.session.commit() return jsonify({ 'success': True, 'message': '评价提交成功', 'review_id': review.id }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'提交失败: {str(e)}'}) @review_bp.route('/upload_image', methods=['POST']) @login_required def upload_review_image(): """上传评价图片""" try: if 'file' not in request.files: return jsonify({'success': False, 'message': '没有选择文件'}) file = request.files['file'] if file.filename == '': return jsonify({'success': False, 'message': '没有选择文件'}) # 使用现有的文件上传处理器 result = file_upload_handler.upload_image(file, 'reviews', process_image=True) if result['success']: return jsonify({ 'success': True, 'message': '图片上传成功', 'url': result['url'] }) else: return jsonify({'success': False, 'message': result['error']}) except Exception as e: return jsonify({'success': False, 'message': f'上传失败: {str(e)}'}) @review_bp.route('/my_reviews') @login_required def my_reviews(): """我的评价列表""" page = request.args.get('page', 1, type=int) reviews = Review.query.filter_by(user_id=session["user_id"]).order_by( desc(Review.created_at) ).paginate(page=page, per_page=10, error_out=False) return render_template('review/my_reviews.html', reviews=reviews) @review_bp.route('/delete/', methods=['POST']) @login_required @log_operation('删除商品评价') def delete_review(review_id): """删除评价(仅限自己的评价)""" try: review = Review.query.filter_by(id=review_id, user_id=session["user_id"]).first() if not review: return jsonify({'success': False, 'message': '评价不存在'}) db.session.delete(review) db.session.commit() return jsonify({'success': True, 'message': '评价删除成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) 🔸============================================================================== 📄 文件: 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 ''' 测试上传

测试文件上传

''' # 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'''

上传成功!

文件路径: {result['file_key']}

访问URL: {result['url']}

''' else: return f'上传失败: {result["error"]}' 🔸============================================================================== 📄 文件: app/views/user.py 📊 大小: 1012 bytes (0.99 KB) 🕒 修改时间: 2025-07-04 03:43:23 🔸============================================================================== 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(): """用户订单 - 重定向到订单列表""" return redirect(url_for('order.list')) 🔸============================================================================== 📄 文件: 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_product_with_specs.py 📊 大小: 4357 bytes (4.25 KB) 🕒 修改时间: 2025-07-09 05:23:14 🔸============================================================================== """ 创建带规格的测试商品 """ from app.models.product import Product, Category, ProductInventory, SpecName, SpecValue from config.database import db import json def create_test_product(): """创建测试商品""" try: # 创建或获取测试分类 category = Category.query.filter_by(name='测试分类').first() if not category: category = Category( name='测试分类', parent_id=0, level=1, sort_order=1, is_active=1 ) db.session.add(category) db.session.flush() # 创建测试商品 product = Product( name='测试商品 - 多规格T恤', category_id=category.id, brand='测试品牌', price=99.00, original_price=129.00, description='这是一件多规格测试商品,支持颜色和尺寸选择', has_specs=1, status=1, weight=0.3 ) db.session.add(product) db.session.flush() # 创建规格名称(如果不存在) color_spec = SpecName.query.filter_by(name='颜色').first() if not color_spec: color_spec = SpecName(name='颜色', sort_order=1) db.session.add(color_spec) db.session.flush() size_spec = SpecName.query.filter_by(name='尺寸').first() if not size_spec: size_spec = SpecName(name='尺寸', sort_order=2) db.session.add(size_spec) db.session.flush() # 创建规格值(如果不存在) colors = ['红色', '蓝色', '黑色'] sizes = ['S', 'M', 'L', 'XL'] for color in colors: color_value = SpecValue.query.filter_by(spec_name_id=color_spec.id, value=color).first() if not color_value: color_value = SpecValue(spec_name_id=color_spec.id, value=color) db.session.add(color_value) for size in sizes: size_value = SpecValue.query.filter_by(spec_name_id=size_spec.id, value=size).first() if not size_value: size_value = SpecValue(spec_name_id=size_spec.id, value=size) db.session.add(size_value) db.session.flush() # 创建商品库存(多个SKU) sku_data = [ {'color': '红色', 'size': 'S', 'stock': 10, 'price_adj': 0, 'is_default': 1}, {'color': '红色', 'size': 'M', 'stock': 15, 'price_adj': 0, 'is_default': 0}, {'color': '红色', 'size': 'L', 'stock': 12, 'price_adj': 0, 'is_default': 0}, {'color': '蓝色', 'size': 'S', 'stock': 8, 'price_adj': 5, 'is_default': 0}, {'color': '蓝色', 'size': 'M', 'stock': 20, 'price_adj': 5, 'is_default': 0}, {'color': '蓝色', 'size': 'L', 'stock': 18, 'price_adj': 5, 'is_default': 0}, {'color': '黑色', 'size': 'M', 'stock': 25, 'price_adj': -5, 'is_default': 0}, {'color': '黑色', 'size': 'L', 'stock': 22, 'price_adj': -5, 'is_default': 0}, {'color': '黑色', 'size': 'XL', 'stock': 15, 'price_adj': 10, 'is_default': 0}, ] for i, sku_info in enumerate(sku_data): spec_combination = {'颜色': sku_info['color'], '尺寸': sku_info['size']} sku_code = f"TST-{sku_info['color'][:1]}{sku_info['size']}-{product.id:03d}" inventory = ProductInventory( product_id=product.id, sku_code=sku_code, spec_combination=spec_combination, price_adjustment=sku_info['price_adj'], stock=sku_info['stock'], warning_stock=5, is_default=sku_info['is_default'], status=1 ) db.session.add(inventory) db.session.commit() print(f"成功创建测试商品: {product.name} (ID: {product.id})") print(f"创建了 {len(sku_data)} 个SKU") return product.id except Exception as e: db.session.rollback() print(f"创建失败: {str(e)}") return None if __name__ == '__main__': from app import create_app app = create_app() with app.app_context(): create_test_product() 🔸============================================================================== 📄 文件: 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-09 05:29:12 ================================================================================