taibai_shopping/project_code_export_20250709_052912.txt
2025-07-09 05:33:23 +08:00

25705 lines
912 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

================================================================================
项目代码导出文件
================================================================================
项目名称: 基于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
🔸==============================================================================
<div align="center">
# 🛍️ 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)
</div>
---
## ✨ 项目简介
本项目是一个基于 **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` | 用户认证、表单处理、邮件服务 |
## 🌟 系统核心功能
<details>
<summary><b>🛍️ 用户端功能 (点击展开)</b></summary>
- **👤 用户中心**
- 手机/邮箱注册登录,支持短信/邮箱验证码
- 微信授权登录(可选)
- 个人信息编辑(昵称、头像、性别等)
- 头像上传至腾讯云COS
- 收货地址管理(增删改查、设为默认)
- 我的收藏夹 & 浏览历史
- **🛒 购物流程**
- 多级商品分类导航
- 商品列表(分页、排序、筛选、搜索)
- 商品详情页(轮播图、规格选择、用户评价)
- 购物车(添加、修改数量、删除、结算)
- 未登录用户购物车(`localStorage` 支持)
- **💳 订单与支付**
- 创建订单,填写备注
- **微信支付**PC端扫码、移动端JSAPI
- 订单状态跟踪(待支付、待发货、待收货、待评价...
- 查看订单详情与物流信息
- 取消订单、申请退款、确认收货
- **✍️ 评价系统**
- 对已完成订单的商品进行评价(评分、文字、图片)
- 匿名评价选项
</details>
<details>
<summary><b>🔧 管理后台功能 (点击展开)</b></summary>
- **📊 数据看板 (Dashboard)**
- 销售额、订单量、用户增长等核心指标可视化
- **📦 商品管理**
- 商品分类的增删改查
- 商品信息管理(上架/下架、编辑、库存、价格)
- 商品规格与属性管理
- **📋 订单管理**
- 按状态筛选和搜索订单
- 查看订单详情
- 执行发货操作(填写物流信息)
- 处理用户退款申请
- **👥 用户管理**
- 查看用户列表
- 禁用/启用用户账户
- **⚙️ 系统设置**
- 网站基本信息配置
- 支付接口与云存储配置
</details>
## 📂 项目结构
```
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 # 📖 你正在阅读的文件
```
## 🗄️ 数据库设计
项目数据库设计遵循电商业务逻辑,结构清晰,关系明确。
<details>
<summary><b>查看核心数据表 (点击展开)</b></summary>
| 表名 | 用途 |
| :--- | :--- |
| `users` | 存储用户信息 |
| `user_addresses` | 用户收货地址 |
| `products` | 商品基本信息 (SPU) |
| `product_inventory` | 商品库存单元 (SKU) |
| `categories` | 商品分类 |
| `cart` | 购物车 |
| `orders` | 订单主表 |
| `order_items` | 订单详情表 |
| `payments` | 支付记录 |
| `reviews` | 商品评价 |
| `admin_users` | 后台管理员 |
</details>
> 完整的 `CREATE TABLE` SQL语句已在项目文件中提供包含了详细的字段、索引和外键设计。
## 🛠️ 本地运行与部署
### 1. 环境准备
- 克隆项目到本地
```bash
git clone <your-repository-url>
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为高并发和大数据量场景打下基础。
---
<div align="center">
<p><em>本项目为毕业设计作品旨在展示一个完整的Web应用开发流程。</em></p>
<p>作者:林金兴 | 指导老师:[指导老师姓名]</p>
</div>
🔸==============================================================================
📄 文件: 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'<UserAddress {self.receiver_name}-{self.get_full_address()}>'
🔸==============================================================================
📄 文件: app/models/admin.py
📊 大小: 1814 bytes (1.77 KB)
🕒 修改时间: 2025-07-03 05:49:59
🔸==============================================================================
"""
管理员模型
"""
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from config.database import db
class AdminUser(db.Model):
__tablename__ = 'admin_users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
real_name = db.Column(db.String(50))
email = db.Column(db.String(100))
phone = db.Column(db.String(20))
role = db.Column(db.String(20), default='admin')
status = db.Column(db.Integer, default=1) # 0-禁用 1-正常
last_login_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def set_password(self, password):
"""设置密码"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""验证密码"""
return check_password_hash(self.password_hash, password)
def update_last_login(self):
"""更新最后登录时间"""
self.last_login_at = datetime.utcnow()
db.session.commit()
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'username': self.username,
'real_name': self.real_name,
'email': self.email,
'phone': self.phone,
'role': self.role,
'status': self.status,
'last_login_at': self.last_login_at.isoformat() if self.last_login_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<AdminUser {self.username}>'
🔸==============================================================================
📄 文件: app/models/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'<BrowseHistory {self.user_id}-{self.product_id}>'
🔸==============================================================================
📄 文件: app/models/cart.py
📊 大小: 4608 bytes (4.50 KB)
🕒 修改时间: 2025-07-03 15:24:11
🔸==============================================================================
"""
购物车模型
"""
from datetime import datetime
from config.database import db
from app.models.product import Product, ProductInventory
class Cart(db.Model):
"""购物车模型"""
__tablename__ = 'cart'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
sku_code = db.Column(db.String(100))
spec_combination = db.Column(db.String(255))
quantity = db.Column(db.Integer, nullable=False, default=1)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
user = db.relationship('User', backref='cart_items')
product = db.relationship('Product', backref='cart_items')
def get_sku_info(self):
"""获取SKU信息"""
if self.sku_code:
return ProductInventory.query.filter_by(sku_code=self.sku_code).first()
else:
# 如果没有SKU返回默认库存信息
return ProductInventory.query.filter_by(
product_id=self.product_id,
is_default=1
).first()
def get_price(self):
"""获取商品价格"""
sku_info = self.get_sku_info()
if sku_info:
return sku_info.get_final_price()
return float(self.product.price) if self.product and self.product.price else 0
def get_total_price(self):
"""获取小计金额"""
return self.get_price() * self.quantity
def get_stock(self):
"""获取库存数量"""
sku_info = self.get_sku_info()
return sku_info.stock if sku_info else 0
def is_available(self):
"""检查商品是否可用"""
# 检查商品是否上架
if not self.product or self.product.status != 1:
return False
# 检查库存
if self.get_stock() < self.quantity:
return False
return True
def to_dict(self):
"""转换为字典"""
sku_info = self.get_sku_info()
return {
'id': self.id,
'user_id': self.user_id,
'product_id': self.product_id,
'product_name': self.product.name if self.product else '',
'product_image': self.product.main_image if self.product else '',
'brand': self.product.brand if self.product else '',
'sku_code': self.sku_code,
'spec_combination': self.spec_combination,
'quantity': self.quantity,
'price': self.get_price(),
'total_price': self.get_total_price(),
'stock': self.get_stock(),
'is_available': self.is_available(),
'created_at': self.created_at.isoformat() if self.created_at else None
}
@classmethod
def add_to_cart(cls, user_id, product_id, sku_code=None, spec_combination=None, quantity=1):
"""添加商品到购物车"""
# 检查是否已存在相同商品
existing_item = cls.query.filter_by(
user_id=user_id,
product_id=product_id,
sku_code=sku_code
).first()
if existing_item:
# 更新数量
existing_item.quantity += quantity
existing_item.updated_at = datetime.utcnow()
db.session.commit()
return existing_item
else:
# 创建新记录
cart_item = cls(
user_id=user_id,
product_id=product_id,
sku_code=sku_code,
spec_combination=spec_combination,
quantity=quantity
)
db.session.add(cart_item)
db.session.commit()
return cart_item
@classmethod
def get_user_cart(cls, user_id):
"""获取用户购物车"""
return cls.query.filter_by(user_id=user_id)\
.order_by(cls.created_at.desc()).all()
@classmethod
def get_cart_count(cls, user_id):
"""获取购物车商品数量"""
return cls.query.filter_by(user_id=user_id).count()
@classmethod
def get_cart_total(cls, user_id):
"""获取购物车总金额"""
cart_items = cls.get_user_cart(user_id)
total = 0
for item in cart_items:
if item.is_available():
total += item.get_total_price()
return total
def __repr__(self):
return f'<Cart {self.user_id}-{self.product_id}>'
🔸==============================================================================
📄 文件: app/models/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'<UserFavorite {self.user_id}-{self.product_id}>'
🔸==============================================================================
📄 文件: app/models/operation_log.py
📊 大小: 1850 bytes (1.81 KB)
🕒 修改时间: 2025-07-03 05:50:29
🔸==============================================================================
"""
操作日志模型
"""
from datetime import datetime
from config.database import db
import json
class OperationLog(db.Model):
__tablename__ = 'operation_logs'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer)
user_type = db.Column(db.Integer) # 1-普通用户 2-管理员
action = db.Column(db.String(100), nullable=False)
resource_type = db.Column(db.String(50))
resource_id = db.Column(db.Integer)
ip_address = db.Column(db.String(45))
user_agent = db.Column(db.Text)
request_data = db.Column(db.JSON)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@classmethod
def create_log(cls, user_id=None, user_type=1, action='', resource_type=None,
resource_id=None, ip_address=None, user_agent=None, request_data=None):
"""创建操作日志"""
log = cls(
user_id=user_id,
user_type=user_type,
action=action,
resource_type=resource_type,
resource_id=resource_id,
ip_address=ip_address,
user_agent=user_agent,
request_data=request_data
)
db.session.add(log)
db.session.commit()
return log
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'user_id': self.user_id,
'user_type': self.user_type,
'action': self.action,
'resource_type': self.resource_type,
'resource_id': self.resource_id,
'ip_address': self.ip_address,
'user_agent': self.user_agent,
'request_data': self.request_data,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<OperationLog {self.action}>'
🔸==============================================================================
📄 文件: app/models/order.py
📊 大小: 6846 bytes (6.69 KB)
🕒 修改时间: 2025-07-04 02:50:35
🔸==============================================================================
"""
订单模型
"""
from datetime import datetime, timedelta
import json
from config.database import db
from app.models.user import User
from app.models.product import Product
class Order(db.Model):
"""订单模型"""
__tablename__ = 'orders'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
order_sn = db.Column(db.String(50), unique=True, nullable=False)
total_amount = db.Column(db.Numeric(10, 2), nullable=False)
actual_amount = db.Column(db.Numeric(10, 2), nullable=False)
shipping_fee = db.Column(db.Numeric(10, 2), default=0)
status = db.Column(db.Integer, default=1) # 1-待支付 2-待发货 3-待收货 4-待评价 5-已完成 6-已取消 7-退款中
payment_method = db.Column(db.String(20))
shipping_method = db.Column(db.String(50))
receiver_info = db.Column(db.Text) # JSON格式存储收货人信息
remark = db.Column(db.Text)
shipped_at = db.Column(db.DateTime)
received_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
user = db.relationship('User', backref='orders')
order_items = db.relationship('OrderItem', backref='order', cascade='all, delete-orphan')
# 状态常量
STATUS_PENDING_PAYMENT = 1 # 待支付
STATUS_PENDING_SHIPMENT = 2 # 待发货
STATUS_SHIPPED = 3 # 待收货
STATUS_PENDING_REVIEW = 4 # 待评价
STATUS_COMPLETED = 5 # 已完成
STATUS_CANCELLED = 6 # 已取消
STATUS_REFUNDING = 7 # 退款中
STATUS_CHOICES = {
STATUS_PENDING_PAYMENT: '待支付',
STATUS_PENDING_SHIPMENT: '待发货',
STATUS_SHIPPED: '待收货',
STATUS_PENDING_REVIEW: '待评价',
STATUS_COMPLETED: '已完成',
STATUS_CANCELLED: '已取消',
STATUS_REFUNDING: '退款中'
}
def get_status_text(self):
"""获取状态文本"""
return self.STATUS_CHOICES.get(self.status, '未知状态')
def get_receiver_info(self):
"""获取收货人信息"""
if self.receiver_info:
try:
return json.loads(self.receiver_info)
except:
return {}
return {}
def set_receiver_info(self, info):
"""设置收货人信息"""
if isinstance(info, dict):
self.receiver_info = json.dumps(info, ensure_ascii=False)
def is_expired(self):
"""检查订单是否已过期15分钟未支付"""
if self.status == self.STATUS_PENDING_PAYMENT:
expire_time = self.created_at + timedelta(minutes=15)
return datetime.utcnow() > expire_time
return False
def can_cancel(self):
"""检查是否可以取消"""
return self.status in [self.STATUS_PENDING_PAYMENT, self.STATUS_PENDING_SHIPMENT]
def can_pay(self):
"""检查是否可以支付"""
return self.status == self.STATUS_PENDING_PAYMENT and not self.is_expired()
def can_confirm_receipt(self):
"""检查是否可以确认收货"""
return self.status == self.STATUS_SHIPPED
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'order_sn': self.order_sn,
'total_amount': float(self.total_amount),
'actual_amount': float(self.actual_amount),
'shipping_fee': float(self.shipping_fee),
'status': self.status,
'status_text': self.get_status_text(),
'payment_method': self.payment_method,
'shipping_method': self.shipping_method,
'receiver_info': self.get_receiver_info(),
'remark': self.remark,
'can_cancel': self.can_cancel(),
'can_pay': self.can_pay(),
'can_confirm_receipt': self.can_confirm_receipt(),
'is_expired': self.is_expired(),
'created_at': self.created_at.isoformat() if self.created_at else None,
'shipped_at': self.shipped_at.isoformat() if self.shipped_at else None,
'received_at': self.received_at.isoformat() if self.received_at else None
}
@classmethod
def generate_order_sn(cls):
"""生成订单号"""
import time
import random
timestamp = str(int(time.time()))
random_str = str(random.randint(100000, 999999))
return f"TB{timestamp}{random_str}"
def __repr__(self):
return f'<Order {self.order_sn}>'
class OrderItem(db.Model):
"""订单商品明细模型"""
__tablename__ = 'order_items'
id = db.Column(db.Integer, primary_key=True)
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
sku_code = db.Column(db.String(100))
product_name = db.Column(db.String(200), nullable=False)
product_image = db.Column(db.String(255))
spec_combination = db.Column(db.String(255))
price = db.Column(db.Numeric(10, 2), nullable=False)
quantity = db.Column(db.Integer, nullable=False)
total_price = db.Column(db.Numeric(10, 2), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关联关系
product = db.relationship('Product', backref='order_items')
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'product_id': self.product_id,
'product_name': self.product_name,
'product_image': self.product_image,
'spec_combination': self.spec_combination,
'price': float(self.price),
'quantity': self.quantity,
'total_price': float(self.total_price)
}
def __repr__(self):
return f'<OrderItem {self.product_name}>'
class ShippingInfo(db.Model):
"""物流信息模型"""
__tablename__ = 'shipping_info'
id = db.Column(db.Integer, primary_key=True)
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
shipping_company = db.Column(db.String(50))
tracking_number = db.Column(db.String(100))
shipping_status = db.Column(db.Integer, default=1) # 1-已发货 2-运输中 3-已送达
shipping_address = db.Column(db.Text)
estimated_delivery = db.Column(db.DateTime)
actual_delivery = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
order = db.relationship('Order', backref='shipping_info')
def __repr__(self):
return f'<ShippingInfo {self.tracking_number}>'
🔸==============================================================================
📄 文件: app/models/payment.py
📊 大小: 2370 bytes (2.31 KB)
🕒 修改时间: 2025-07-04 01:56:46
🔸==============================================================================
"""
支付模型
"""
from datetime import datetime
from config.database import db
class Payment(db.Model):
"""支付记录模型"""
__tablename__ = 'payments'
id = db.Column(db.Integer, primary_key=True)
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
payment_sn = db.Column(db.String(64), unique=True, nullable=False)
payment_method = db.Column(db.String(20), nullable=False)
amount = db.Column(db.Numeric(10, 2), nullable=False)
status = db.Column(db.Integer, default=1) # 1-待支付 2-支付成功 3-支付失败 4-已退款
third_party_sn = db.Column(db.String(100)) # 第三方支付流水号
callback_data = db.Column(db.Text) # 支付回调数据
paid_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
order = db.relationship('Order', backref='payments')
# 状态常量
STATUS_PENDING = 1 # 待支付
STATUS_SUCCESS = 2 # 支付成功
STATUS_FAILED = 3 # 支付失败
STATUS_REFUNDED = 4 # 已退款
STATUS_CHOICES = {
STATUS_PENDING: '待支付',
STATUS_SUCCESS: '支付成功',
STATUS_FAILED: '支付失败',
STATUS_REFUNDED: '已退款'
}
def get_status_text(self):
"""获取状态文本"""
return self.STATUS_CHOICES.get(self.status, '未知状态')
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'payment_sn': self.payment_sn,
'payment_method': self.payment_method,
'amount': float(self.amount),
'status': self.status,
'status_text': self.get_status_text(),
'third_party_sn': self.third_party_sn,
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None
}
@classmethod
def generate_payment_sn(cls):
"""生成支付流水号"""
import time
import random
timestamp = str(int(time.time()))
random_str = str(random.randint(100000, 999999))
return f"PAY{timestamp}{random_str}"
def __repr__(self):
return f'<Payment {self.payment_sn}>'
🔸==============================================================================
📄 文件: app/models/product.py
📊 大小: 9669 bytes (9.44 KB)
🕒 修改时间: 2025-07-03 07:03:27
🔸==============================================================================
"""
商品相关模型
"""
from datetime import datetime
from config.database import db
import json
class Category(db.Model):
"""商品分类模型"""
__tablename__ = 'categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
parent_id = db.Column(db.Integer, default=0)
level = db.Column(db.Integer, default=1)
sort_order = db.Column(db.Integer, default=0)
icon_url = db.Column(db.String(255))
is_active = db.Column(db.Integer, default=1)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'parent_id': self.parent_id,
'level': self.level,
'sort_order': self.sort_order,
'icon_url': self.icon_url,
'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<Category {self.name}>'
class SpecName(db.Model):
"""规格名称模型"""
__tablename__ = 'spec_names'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
sort_order = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'sort_order': self.sort_order
}
def __repr__(self):
return f'<SpecName {self.name}>'
class SpecValue(db.Model):
"""规格值模型"""
__tablename__ = 'spec_values'
id = db.Column(db.Integer, primary_key=True)
spec_name_id = db.Column(db.Integer, db.ForeignKey('spec_names.id'), nullable=False)
value = db.Column(db.String(100), nullable=False)
sort_order = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
spec_name = db.relationship('SpecName', backref='values')
def to_dict(self):
return {
'id': self.id,
'spec_name_id': self.spec_name_id,
'value': self.value,
'sort_order': self.sort_order
}
def __repr__(self):
return f'<SpecValue {self.value}>'
class Product(db.Model):
"""商品模型"""
__tablename__ = 'products'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
brand = db.Column(db.String(100))
price = db.Column(db.Numeric(10, 2), nullable=False)
original_price = db.Column(db.Numeric(10, 2))
description = db.Column(db.Text)
main_image = db.Column(db.String(255))
status = db.Column(db.Integer, default=1) # 0-下架 1-上架
has_specs = db.Column(db.Integer, default=0) # 0-无规格 1-有规格
sales_count = db.Column(db.Integer, default=0)
view_count = db.Column(db.Integer, default=0)
weight = db.Column(db.Numeric(8, 2))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
category = db.relationship('Category', backref='products')
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'category_id': self.category_id,
'category_name': self.category.name if self.category else '',
'brand': self.brand,
'price': float(self.price) if self.price else 0,
'original_price': float(self.original_price) if self.original_price else None,
'description': self.description,
'main_image': self.main_image,
'status': self.status,
'has_specs': self.has_specs,
'sales_count': self.sales_count,
'view_count': self.view_count,
'weight': float(self.weight) if self.weight else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return f'<Product {self.name}>'
class ProductImage(db.Model):
"""商品图片模型"""
__tablename__ = 'product_images'
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
image_url = db.Column(db.String(255), nullable=False)
sort_order = db.Column(db.Integer, default=0)
is_main = db.Column(db.Integer, default=0) # 0-否 1-是
created_at = db.Column(db.DateTime, default=datetime.utcnow)
product = db.relationship('Product', backref='images')
def to_dict(self):
return {
'id': self.id,
'product_id': self.product_id,
'image_url': self.image_url,
'sort_order': self.sort_order,
'is_main': self.is_main,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<ProductImage {self.id}>'
class ProductSpecRelation(db.Model):
"""商品规格关联模型"""
__tablename__ = 'product_spec_relations'
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
spec_name_id = db.Column(db.Integer, db.ForeignKey('spec_names.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
product = db.relationship('Product', backref='spec_relations')
spec_name = db.relationship('SpecName')
def __repr__(self):
return f'<ProductSpecRelation {self.product_id}-{self.spec_name_id}>'
class ProductInventory(db.Model):
"""商品库存模型(SKU)"""
__tablename__ = 'product_inventory'
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
sku_code = db.Column(db.String(100), unique=True, nullable=False)
spec_combination = db.Column(db.JSON)
price_adjustment = db.Column(db.Numeric(10, 2), default=0)
stock = db.Column(db.Integer, nullable=False, default=0)
warning_stock = db.Column(db.Integer, default=10)
is_default = db.Column(db.Integer, default=0)
status = db.Column(db.Integer, default=1)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
product = db.relationship('Product', backref='inventory')
def get_final_price(self):
"""获取最终价格"""
base_price = float(self.product.price) if self.product and self.product.price else 0
adjustment = float(self.price_adjustment) if self.price_adjustment else 0
return base_price + adjustment
def to_dict(self):
return {
'id': self.id,
'product_id': self.product_id,
'sku_code': self.sku_code,
'spec_combination': self.spec_combination,
'price_adjustment': float(self.price_adjustment) if self.price_adjustment else 0,
'final_price': self.get_final_price(),
'stock': self.stock,
'warning_stock': self.warning_stock,
'is_default': self.is_default,
'status': self.status,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<ProductInventory {self.sku_code}>'
class InventoryLog(db.Model):
"""库存变更日志模型"""
__tablename__ = 'inventory_logs'
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
sku_code = db.Column(db.String(100), nullable=False)
change_type = db.Column(db.Integer, nullable=False) # 1-入库 2-出库 3-调整
change_quantity = db.Column(db.Integer, nullable=False)
before_stock = db.Column(db.Integer, nullable=False)
after_stock = db.Column(db.Integer, nullable=False)
related_order_id = db.Column(db.Integer)
remark = db.Column(db.String(255))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
product = db.relationship('Product')
@classmethod
def create_log(cls, product_id, sku_code, change_type, change_quantity,
before_stock, after_stock, related_order_id=None, remark=None):
"""创建库存变更日志"""
log = cls(
product_id=product_id,
sku_code=sku_code,
change_type=change_type,
change_quantity=change_quantity,
before_stock=before_stock,
after_stock=after_stock,
related_order_id=related_order_id,
remark=remark
)
db.session.add(log)
db.session.commit()
return log
def to_dict(self):
return {
'id': self.id,
'product_id': self.product_id,
'sku_code': self.sku_code,
'change_type': self.change_type,
'change_quantity': self.change_quantity,
'before_stock': self.before_stock,
'after_stock': self.after_stock,
'related_order_id': self.related_order_id,
'remark': self.remark,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<InventoryLog {self.sku_code}>'
🔸==============================================================================
📄 文件: app/models/review.py
📊 大小: 2204 bytes (2.15 KB)
🕒 修改时间: 2025-07-04 02:45:14
🔸==============================================================================
"""
评价模型
"""
from datetime import datetime
import json
from config.database import db
class Review(db.Model):
"""商品评价模型"""
__tablename__ = 'reviews'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
rating = db.Column(db.Integer, nullable=False) # 1-5星
content = db.Column(db.Text)
images = db.Column(db.Text) # JSON格式存储图片URLs
is_anonymous = db.Column(db.Integer, default=0)
status = db.Column(db.Integer, default=1) # 0-隐藏 1-显示
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关联关系
user = db.relationship('User', backref='reviews')
product = db.relationship('Product', backref='reviews')
order = db.relationship('Order', backref='reviews')
def get_images(self):
"""获取评价图片列表"""
if self.images:
try:
return json.loads(self.images)
except:
return []
return []
def set_images(self, image_list):
"""设置评价图片"""
if isinstance(image_list, list):
self.images = json.dumps(image_list)
def get_rating_stars(self):
"""获取星级显示"""
return '★' * self.rating + '☆' * (5 - self.rating)
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'user_id': self.user_id,
'username': self.user.username if not self.is_anonymous else '匿名用户',
'product_id': self.product_id,
'order_id': self.order_id,
'rating': self.rating,
'rating_stars': self.get_rating_stars(),
'content': self.content,
'images': self.get_images(),
'is_anonymous': self.is_anonymous,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<Review {self.id}-{self.rating}星>'
🔸==============================================================================
📄 文件: app/models/user.py
📊 大小: 1785 bytes (1.74 KB)
🕒 修改时间: 2025-07-03 04:43:31
🔸==============================================================================
"""
用户模型
"""
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from config.database import db # 确保从正确位置导入
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
phone = db.Column(db.String(20), unique=True)
email = db.Column(db.String(100), unique=True)
password_hash = db.Column(db.String(255), nullable=False)
nickname = db.Column(db.String(50))
avatar_url = db.Column(db.String(255))
gender = db.Column(db.Integer, default=0)
birthday = db.Column(db.Date)
status = db.Column(db.Integer, default=1)
wechat_openid = db.Column(db.String(100))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def set_password(self, password):
"""设置密码"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""验证密码"""
return check_password_hash(self.password_hash, password)
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'username': self.username,
'phone': self.phone,
'email': self.email,
'nickname': self.nickname,
'avatar_url': self.avatar_url,
'gender': self.gender,
'birthday': self.birthday.isoformat() if self.birthday else None,
'status': self.status,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def __repr__(self):
return f'<User {self.username}>'
🔸==============================================================================
📄 文件: app/models/verification.py
📊 大小: 1832 bytes (1.79 KB)
🕒 修改时间: 2025-07-03 03:40:15
🔸==============================================================================
from datetime import datetime, timedelta
from config.database import db
import random
import string
class EmailVerification(db.Model):
__tablename__ = 'email_verifications'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(100), nullable=False, index=True)
code = db.Column(db.String(6), nullable=False)
type = db.Column(db.SmallInteger, nullable=False) # 1-注册 2-登录 3-找回密码
is_used = db.Column(db.SmallInteger, default=0) # 0-未使用 1-已使用
expired_at = db.Column(db.DateTime, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@staticmethod
def generate_code():
"""生成6位数字验证码"""
return ''.join(random.choices(string.digits, k=6))
@classmethod
def create_verification(cls, email, code_type, expire_minutes=10):
"""创建验证码记录"""
code = cls.generate_code()
expired_at = datetime.utcnow() + timedelta(minutes=expire_minutes)
verification = cls(
email=email,
code=code,
type=code_type,
expired_at=expired_at
)
db.session.add(verification)
db.session.commit()
return verification
@classmethod
def verify_code(cls, email, code, code_type):
"""验证验证码"""
verification = cls.query.filter_by(
email=email,
code=code,
type=code_type,
is_used=0
).filter(
cls.expired_at > datetime.utcnow()
).first()
if verification:
verification.is_used = 1
db.session.commit()
return True
return False
def is_expired(self):
"""检查是否过期"""
return datetime.utcnow() > self.expired_at
🔸==============================================================================
📄 文件: app/static/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 = '<span class="text-danger">❌ 地址数据加载失败</span>';
showAlert('地址数据加载失败,请刷新页面重试', 'error');
return;
}
console.log('✅ cityData 已加载,省份数量:', Object.keys(cityData).length);
document.getElementById('debugInfo').innerHTML = '<span class="text-success">✅ 地址数据已加载,省份数量: ' + Object.keys(cityData).length + '</span>';
// 避免重复初始化
if (addressFormInitialized) {
console.log('地址表单已初始化,跳过');
return;
}
addressFormInitialized = true;
// 初始化省份列表
initializeProvinces();
// 设置事件监听器
setupEventListeners();
// 如果是编辑模式,设置初始值
const savedProvince = document.getElementById('provinceValue').value;
const savedCity = document.getElementById('cityValue').value;
const savedDistrict = document.getElementById('districtValue').value;
console.log('初始值:', {savedProvince, savedCity, savedDistrict});
if (savedProvince) {
setTimeout(() => {
setInitialValues(savedProvince, savedCity, savedDistrict);
}, 500); // 增加延迟确保DOM完全准备好
}
console.log('=== 地址表单初始化完成 ===');
});
// 初始化省份列表
function initializeProvinces() {
const provinceSelect = document.getElementById('province');
if (!provinceSelect) {
console.error('省份选择框未找到');
return;
}
console.log('开始初始化省份列表...');
// 清空并添加默认选项
provinceSelect.innerHTML = '<option value="">请选择省份</option>';
// 添加所有省份
const provinces = Object.keys(cityData);
console.log('可用省份:', provinces);
provinces.forEach(province => {
const option = document.createElement('option');
option.value = province;
option.textContent = province;
provinceSelect.appendChild(option);
console.log('添加省份:', province);
});
console.log('省份列表初始化完成,总计:', provinces.length, '个');
}
// 设置事件监听器
function setupEventListeners() {
console.log('设置事件监听器...');
// 省份变化事件
const provinceSelect = document.getElementById('province');
if (provinceSelect) {
provinceSelect.addEventListener('change', function() {
const province = this.value;
console.log('选择省份:', province);
updateCities(province);
clearDistricts();
});
}
// 城市变化事件
const citySelect = document.getElementById('city');
if (citySelect) {
citySelect.addEventListener('change', function() {
const province = document.getElementById('province').value;
const city = this.value;
console.log('选择城市:', city, '省份:', province);
updateDistricts(province, city);
});
}
// 表单提交验证
document.getElementById('addressForm').addEventListener('submit', function(e) {
const province = document.getElementById('province').value;
const city = document.getElementById('city').value;
const district = document.getElementById('district').value;
console.log('表单验证:', {province, city, district});
if (!province) {
e.preventDefault();
showAlert('请选择省份', 'warning');
return false;
}
if (!city) {
e.preventDefault();
showAlert('请选择城市', 'warning');
return false;
}
if (!district) {
e.preventDefault();
showAlert('请选择区县', 'warning');
return false;
}
return true;
});
console.log('事件监听器设置完成');
}
// 更新城市列表
function updateCities(province) {
const citySelect = document.getElementById('city');
citySelect.innerHTML = '<option value="">请选择城市</option>';
console.log('更新城市列表,省份:', province);
if (!province || !cityData[province]) {
console.log('省份为空或数据不存在');
return;
}
const cities = Object.keys(cityData[province]);
console.log('可用城市:', cities);
cities.forEach(city => {
const option = document.createElement('option');
option.value = city;
option.textContent = city;
citySelect.appendChild(option);
});
console.log('城市列表更新完成,总计:', cities.length, '个');
}
// 更新区县列表
function updateDistricts(province, city) {
const districtSelect = document.getElementById('district');
districtSelect.innerHTML = '<option value="">请选择区县</option>';
console.log('更新区县列表,省份:', province, '城市:', city);
if (!province || !city || !cityData[province] || !cityData[province][city]) {
console.log('省份或城市为空或数据不存在');
return;
}
const districts = cityData[province][city];
console.log('可用区县:', districts);
districts.forEach(district => {
const option = document.createElement('option');
option.value = district;
option.textContent = district;
districtSelect.appendChild(option);
});
console.log('区县列表更新完成,总计:', districts.length, '个');
}
// 清空区县列表
function clearDistricts() {
const districtSelect = document.getElementById('district');
districtSelect.innerHTML = '<option value="">请选择区县</option>';
console.log('区县列表已清空');
}
// 设置初始值(编辑模式)
function setInitialValues(province, city, district) {
console.log('设置初始值:', {province, city, district});
const provinceSelect = document.getElementById('province');
const citySelect = document.getElementById('city');
const districtSelect = document.getElementById('district');
// 设置省份
if (province && provinceSelect) {
provinceSelect.value = province;
console.log('省份设置为:', province);
updateCities(province);
// 延迟设置城市
setTimeout(() => {
if (city && citySelect) {
citySelect.value = city;
console.log('城市设置为:', city);
updateDistricts(province, city);
// 延迟设置区县
setTimeout(() => {
if (district && districtSelect) {
districtSelect.value = district;
console.log('区县设置为:', district);
}
}, 200);
}
}, 200);
}
}
🔸==============================================================================
📄 文件: 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 = '<i class="bi bi-hourglass-split"></i> 处理中...';
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}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
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}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
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 = `
<div class="modal fade" id="logDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">操作日志详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="log-detail">
<div class="row">
<div class="col-md-6">
<div class="log-info-item">
<span class="log-info-label">日志ID:</span>
<span class="log-info-value">#${log.id}</span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作者:</span>
<span class="log-info-value">
<span class="badge bg-${log.user_type === 2 ? 'warning' : 'info'}">
${log.user_type === 2 ? '管理员' : '用户'}
</span>
#${log.user_id || '未知'}
</span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作类型:</span>
<span class="log-info-value operation-action">${log.action}</span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作对象:</span>
<span class="log-info-value">
${log.resource_type || '无'}
${log.resource_id ? `#${log.resource_id}` : ''}
</span>
</div>
</div>
<div class="col-md-6">
<div class="log-info-item">
<span class="log-info-label">IP地址:</span>
<span class="log-info-value"><code>${log.ip_address || '未知'}</code></span>
</div>
<div class="log-info-item">
<span class="log-info-label">操作时间:</span>
<span class="log-info-value">${formatDateTime(log.created_at)}</span>
</div>
</div>
</div>
${log.user_agent ? `
<div class="mt-3">
<h6>用户代理信息:</h6>
<div class="user-agent-detail">
<code>${log.user_agent}</code>
</div>
</div>
` : ''}
${log.request_data ? `
<div class="mt-3">
<h6>请求数据:</h6>
<pre class="request-data"><code>${JSON.stringify(log.request_data, null, 2)}</code></pre>
</div>
` : ''}
</div>
</div>
</div>
</div>
</div>
`;
// 移除现有的模态框
const existingModal = document.getElementById('logDetailModal');
if (existingModal) {
existingModal.remove();
}
// 添加新的模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
modal.show();
}
// 导出日志
function exportLogs() {
// 获取当前筛选条件
const userType = document.getElementById('user_type').value;
const action = document.getElementById('action').value;
// 构建导出URL
const params = new URLSearchParams();
if (userType) params.append('user_type', userType);
if (action) params.append('action', action);
const exportUrl = `/admin/logs/export?${params.toString()}`;
// 下载文件
window.location.href = exportUrl;
}
// 清理日志
function clearLogs() {
if (!confirm('确定要清理历史日志吗?此操作不可逆!')) {
return;
}
const daysToKeep = prompt('请输入要保留的天数例如30:', '30');
if (!daysToKeep || isNaN(daysToKeep) || daysToKeep <= 0) {
showError('请输入有效的天数');
return;
}
// 显示加载状态
showLoading();
// 发送AJAX请求
fetch('/admin/logs/clear', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
days_to_keep: parseInt(daysToKeep)
})
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showSuccess(data.message);
// 刷新页面
setTimeout(() => {
location.reload();
}, 1000);
} else {
showError(data.message);
}
})
.catch(error => {
hideLoading();
console.error('Error:', error);
showError('网络错误,请重试');
});
}
// 搜索日志
function searchLogs() {
const searchForm = document.querySelector('form[method="GET"]');
if (searchForm) {
searchForm.submit();
}
}
// 重置搜索
function resetSearch() {
window.location.href = '/admin/logs';
}
// 格式化日期时间
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '未知';
const date = new Date(dateTimeString);
return date.toLocaleString('zh-CN');
}
// 显示成功消息
function showSuccess(message) {
showAlert(message, 'success');
}
// 显示错误消息
function showError(message) {
showAlert(message, 'danger');
}
// 显示提示消息
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 插入到页面顶部
const container = document.querySelector('.admin-content');
container.insertBefore(alertDiv, container.firstChild);
// 3秒后自动关闭
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
// 显示加载状态
function showLoading() {
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loading-overlay';
loadingDiv.innerHTML = `
<div class="loading-spinner">
<i class="bi bi-hourglass-split"></i>
<div>处理中...</div>
</div>
`;
loadingDiv.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
color: white;
`;
document.body.appendChild(loadingDiv);
}
// 隐藏加载状态
function hideLoading() {
const loadingDiv = document.getElementById('loading-overlay');
if (loadingDiv) {
loadingDiv.remove();
}
}
// 表格排序功能
function sortTable(column) {
// 实现表格排序功能
console.log('Sort by:', column);
}
// 批量操作功能(可选)
function bulkOperation() {
// 实现批量操作功能
const selectedLogs = document.querySelectorAll('input[type="checkbox"]:checked');
if (selectedLogs.length === 0) {
showError('请选择要操作的日志');
return;
}
// 实现批量操作逻辑
}
🔸==============================================================================
📄 文件: 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 = '<i class="bi bi-arrow-clockwise spin"></i> 处理中...';
submitBtn.disabled = true;
fetch(`/admin/orders/${currentOrderId}/ship`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', '发货成功!', 'success');
shipModal.hide();
setTimeout(() => {
location.reload();
}, 1000);
} else {
showAlert('错误', data.message || '发货失败', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('错误', '网络请求失败', 'danger');
})
.finally(() => {
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
// 处理退款表单提交
document.getElementById('refundForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!currentOrderId) {
showAlert('错误', '订单ID不能为空', 'danger');
return;
}
const formData = new FormData(this);
const submitBtn = this.querySelector('button[type="submit"]');
// 显示加载状态
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> 处理中...';
submitBtn.disabled = true;
fetch(`/admin/orders/${currentOrderId}/refund`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', '退款成功!', 'success');
refundModal.hide();
setTimeout(() => {
location.reload();
}, 1000);
} else {
showAlert('错误', data.message || '退款失败', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('错误', '网络请求失败', 'danger');
})
.finally(() => {
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
// 处理取消表单提交
document.getElementById('cancelForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!currentOrderId) {
showAlert('错误', '订单ID不能为空', 'danger');
return;
}
const formData = new FormData(this);
const submitBtn = this.querySelector('button[type="submit"]');
// 显示加载状态
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-arrow-clockwise spin"></i> 处理中...';
submitBtn.disabled = true;
fetch(`/admin/orders/${currentOrderId}/cancel`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', '订单取消成功!', 'success');
cancelModal.hide();
setTimeout(() => {
location.reload();
}, 1000);
} else {
showAlert('错误', data.message || '取消失败', 'danger');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('错误', '网络请求失败', 'danger');
})
.finally(() => {
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
// 通用提示函数
function showAlert(title, message, type) {
// 创建提示框
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
<strong>${title}</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动关闭
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 3000);
}
});
// 旋转动画CSS如果需要
if (!document.querySelector('#admin-orders-style')) {
const style = document.createElement('style');
style.id = 'admin-orders-style';
style.textContent = `
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
🔸==============================================================================
📄 文件: 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 = `
<div class="loading">
<i class="bi bi-hourglass-split"></i>
<div>加载中...</div>
</div>
`;
modal.show();
// 发送AJAX请求获取用户详情
fetch(`/admin/users/${userId}/detail`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderUserDetail(data.user);
// 立即强制设置头像样式
setTimeout(() => {
forceModalAvatarStyles();
}, 50);
// 再次确保样式正确应用
setTimeout(() => {
forceModalAvatarStyles();
}, 200);
} else {
showError('获取用户详情失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
showError('网络错误,请重试');
});
}
// 渲染用户详情
function renderUserDetail(user) {
const content = document.getElementById('userDetailContent');
content.innerHTML = `
<div class="user-detail">
<div class="row">
<div class="col-md-3 text-center">
<div class="user-avatar-large-wrapper">
${user.avatar_url ?
`<img src="${user.avatar_url}" alt="用户头像" class="avatar-large" id="modalAvatar">` :
`<div class="avatar-placeholder-large"><i class="bi bi-person"></i></div>`
}
</div>
<h5 class="mt-3">${user.nickname || user.username}</h5>
<span class="badge bg-${user.status === 1 ? 'success' : 'danger'}">
${user.status === 1 ? '正常' : '禁用'}
</span>
</div>
<div class="col-md-9">
<div class="user-info-list">
<div class="user-info-item">
<span class="user-info-label">用户ID:</span>
<span class="user-info-value">#${user.id}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">用户名:</span>
<span class="user-info-value">${user.username}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">昵称:</span>
<span class="user-info-value">${user.nickname || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">邮箱:</span>
<span class="user-info-value">${user.email || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">手机号:</span>
<span class="user-info-value">${user.phone || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">性别:</span>
<span class="user-info-value">${getGenderText(user.gender)}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">生日:</span>
<span class="user-info-value">${user.birthday || '未设置'}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">注册时间:</span>
<span class="user-info-value">${formatDateTime(user.created_at)}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">最后更新:</span>
<span class="user-info-value">${formatDateTime(user.updated_at)}</span>
</div>
</div>
</div>
</div>
</div>
`;
}
// 强制设置模态框中的头像样式
function forceModalAvatarStyles() {
const modalAvatar = document.getElementById('modalAvatar');
if (modalAvatar) {
setAvatarStyles(modalAvatar, '80px');
// 设置容器样式
const wrapper = document.querySelector('.user-avatar-large-wrapper');
if (wrapper) {
wrapper.style.setProperty('width', '80px', 'important');
wrapper.style.setProperty('height', '80px', 'important');
wrapper.style.setProperty('margin', '0 auto', 'important');
wrapper.style.setProperty('overflow', 'hidden', 'important');
wrapper.style.setProperty('border-radius', '50%', 'important');
wrapper.style.setProperty('position', 'relative', 'important');
wrapper.style.setProperty('flex-shrink', '0', 'important');
wrapper.style.setProperty('flex-grow', '0', 'important');
}
}
// 通用的模态框头像处理
const modalAvatars = document.querySelectorAll('.modal .avatar-large');
modalAvatars.forEach(avatar => {
setAvatarStyles(avatar, '80px');
});
}
// 切换用户状态
function toggleUserStatus(userId, currentStatus) {
const action = currentStatus === 1 ? '禁用' : '启用';
const newStatus = currentStatus === 1 ? 0 : 1;
if (!confirm(`确定要${action}此用户吗?`)) {
return;
}
// 显示加载状态
showLoading();
// 发送AJAX请求
fetch(`/admin/users/${userId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
status: newStatus
})
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
showSuccess(data.message);
// 刷新页面
setTimeout(() => {
location.reload();
}, 1000);
} else {
showError(data.message);
}
})
.catch(error => {
hideLoading();
console.error('Error:', error);
showError('网络错误,请重试');
});
}
// 获取性别文本
function getGenderText(gender) {
switch (gender) {
case 1: return '男';
case 2: return '女';
default: return '未知';
}
}
// 格式化日期时间
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '未知';
const date = new Date(dateTimeString);
return date.toLocaleString('zh-CN');
}
// 显示成功消息
function showSuccess(message) {
showAlert(message, 'success');
}
// 显示错误消息
function showError(message) {
showAlert(message, 'danger');
}
// 显示提示消息
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 插入到页面顶部
const container = document.querySelector('.admin-content');
container.insertBefore(alertDiv, container.firstChild);
// 3秒后自动关闭
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
// 显示加载状态
function showLoading() {
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loading-overlay';
loadingDiv.innerHTML = `
<div class="loading-spinner">
<i class="bi bi-hourglass-split"></i>
<div>处理中...</div>
</div>
`;
loadingDiv.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
color: white;
`;
document.body.appendChild(loadingDiv);
}
// 隐藏加载状态
function hideLoading() {
const loadingDiv = document.getElementById('loading-overlay');
if (loadingDiv) {
loadingDiv.remove();
}
}
// 页面加载完成后强制设置头像样式
window.addEventListener('load', function() {
forceAvatarStyles();
});
// 定时检查并修复头像样式
setInterval(function() {
// 检查并修复表格头像
const tableAvatars = document.querySelectorAll('.table .user-avatar');
tableAvatars.forEach(avatar => {
if (avatar.offsetWidth > 60 || avatar.offsetHeight > 60) {
setAvatarStyles(avatar, '48px');
}
});
// 检查并修复模态框头像
const modalAvatars = document.querySelectorAll('.modal .avatar-large');
modalAvatars.forEach(avatar => {
if (avatar.offsetWidth > 100 || avatar.offsetHeight > 100) {
setAvatarStyles(avatar, '80px');
}
});
}, 500);
🔸==============================================================================
📄 文件: 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 = `
<div class="d-flex align-items-center">
<i class="bi ${icon} me-2"></i>
<div class="flex-grow-1">${message}</div>
<button type="button" class="btn-close" onclick="this.parentElement.parentElement.remove()"></button>
</div>
`;
document.body.appendChild(alertDiv);
// 自动消失
if (duration > 0) {
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.classList.remove('show');
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 150);
}
}, duration);
}
return alertDiv;
}
// 获取Bootstrap警告类型
function getBootstrapAlertType(type) {
const typeMap = {
'success': 'success',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
return typeMap[type] || 'info';
}
// 获取警告图标
function getAlertIcon(type) {
const iconMap = {
'success': 'bi-check-circle-fill',
'error': 'bi-exclamation-triangle-fill',
'warning': 'bi-exclamation-triangle-fill',
'info': 'bi-info-circle-fill'
};
return iconMap[type] || 'bi-info-circle-fill';
}
// 确认对话框
function showConfirm(message, callback) {
if (confirm(message)) {
if (typeof callback === 'function') {
callback();
}
return true;
}
return false;
}
// 页面加载完成后的初始化
document.addEventListener('DOMContentLoaded', function() {
// 当前页面高亮
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 = '<i class="bi bi-hourglass-split"></i> 提交中...';
submitBtn.disabled = true;
// 提交订单
fetch('/order/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(orderData)
})
.then(response => {
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 ?
'<i class="bi bi-square"></i> 取消全选' :
'<i class="bi bi-check-square"></i> 全选';
}
}
function removeFavorite(productId) {
if (confirm('确定要取消收藏这个商品吗?')) {
fetch('/favorite/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除商品卡片
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
// 更新收藏数量
updateFavoriteCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('操作失败,请稍后再试');
});
}
}
function batchRemove() {
if (selectedItems.length === 0) {
showErrorMessage('请选择要删除的商品');
return;
}
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
document.getElementById('confirmMessage').textContent =
`确定要取消收藏这 ${selectedItems.length} 个商品吗?`;
document.getElementById('confirmBtn').onclick = function() {
performBatchRemove();
modal.hide();
};
modal.show();
}
function performBatchRemove() {
fetch('/favorite/batch-remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_ids: selectedItems
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除选中的商品卡片
selectedItems.forEach(productId => {
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
});
// 重置选择状态
selectedItems = [];
isSelectAll = false;
updateSelectedItems();
// 更新收藏数量
updateFavoriteCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('批量删除失败,请稍后再试');
});
}
function addToCart(productId) {
// 调用购物车添加功能
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
quantity: 1
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 更新购物车数量
if (typeof updateCartBadge === 'function') {
updateCartBadge(data.cart_count);
}
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('加入购物车失败,请稍后再试');
});
}
function updateFavoriteCount() {
fetch('/favorite/count')
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新收藏数量显示
const badge = document.querySelector('.badge.bg-secondary');
if (badge) {
badge.textContent = `共 ${data.favorite_count} 件商品`;
}
}
})
.catch(error => {
console.error('Error:', error);
});
}
function checkEmptyState() {
const itemsContainer = document.querySelector('.row');
const items = itemsContainer.querySelectorAll('.favorite-item');
if (items.length === 0) {
// 显示空状态
itemsContainer.innerHTML = `
<div class="col-12">
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-heart display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有收藏任何商品</h4>
<p class="text-muted">去逛逛,收藏心仪的商品吧~</p>
<a href="/" class="btn btn-primary">
<i class="bi bi-house"></i> 去首页逛逛
</a>
</div>
</div>
</div>
`;
}
}
function showSuccessMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'success');
} else {
alert(message);
}
}
function showErrorMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'error');
} else {
alert(message);
}
}
🔸==============================================================================
📄 文件: 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 ?
'<i class="bi bi-square"></i> 取消全选' :
'<i class="bi bi-check-square"></i> 全选';
}
}
function removeHistory(productId) {
if (confirm('确定要删除这个浏览记录吗?')) {
fetch('/history/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除商品卡片
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
// 更新历史数量
updateHistoryCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('操作失败,请稍后再试');
});
}
}
function batchRemove() {
if (selectedItems.length === 0) {
showErrorMessage('请选择要删除的商品');
return;
}
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
document.getElementById('confirmMessage').textContent =
`确定要删除这 ${selectedItems.length} 个浏览记录吗?`;
document.getElementById('confirmBtn').onclick = function() {
performBatchRemove();
modal.hide();
};
modal.show();
}
function performBatchRemove() {
fetch('/history/batch-remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_ids: selectedItems
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 移除选中的商品卡片
selectedItems.forEach(productId => {
const itemElement = document.querySelector(`[data-product-id="${productId}"]`);
if (itemElement) {
itemElement.remove();
}
});
// 重置选择状态
selectedItems = [];
isSelectAll = false;
updateSelectedItems();
// 更新历史数量
updateHistoryCount();
// 检查是否为空
checkEmptyState();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('批量删除失败,请稍后再试');
});
}
function clearHistory() {
if (confirm('确定要清空所有浏览历史吗?此操作不可恢复。')) {
fetch('/history/clear', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 重新加载页面或显示空状态
location.reload();
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('清空历史失败,请稍后再试');
});
}
}
function addToCart(productId) {
// 调用购物车添加功能
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
quantity: 1
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
// 更新购物车数量
if (typeof updateCartBadge === 'function') {
updateCartBadge(data.cart_count);
}
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('加入购物车失败,请稍后再试');
});
}
function addToFavorites(productId) {
fetch('/favorite/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
} else {
showErrorMessage(data.message);
}
})
.catch(error => {
console.error('Error:', error);
showErrorMessage('收藏失败,请稍后再试');
});
}
function updateHistoryCount() {
fetch('/history/count')
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新历史数量显示
const badge = document.querySelector('.badge.bg-secondary');
if (badge) {
badge.textContent = `共 ${data.history_count} 件商品`;
}
}
})
.catch(error => {
console.error('Error:', error);
});
}
function checkEmptyState() {
const itemsContainer = document.querySelector('.row');
const items = itemsContainer.querySelectorAll('.history-item');
if (items.length === 0) {
// 显示空状态
itemsContainer.innerHTML = `
<div class="col-12">
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-clock-history display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有浏览任何商品</h4>
<p class="text-muted">去逛逛,看看有什么好商品~</p>
<a href="/" class="btn btn-primary">
<i class="bi bi-house"></i> 去首页逛逛
</a>
</div>
</div>
</div>
`;
}
}
function showSuccessMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'success');
} else {
alert(message);
}
}
function showErrorMessage(message) {
// 使用现有的消息提示函数
if (typeof showMessage === 'function') {
showMessage(message, 'error');
} else {
alert(message);
}
}
🔸==============================================================================
📄 文件: 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 = `
<div style="width: 200px; height: 200px; margin: 0 auto; background: #f0f0f0;
display: flex; align-items: center; justify-content: center; border: 1px solid #ddd;">
<div style="text-align: center;">
<i class="bi bi-qr-code display-4"></i><br>
<small>微信支付二维码</small>
</div>
</div>
`;
qrArea.style.display = 'block';
}
// 开始检查支付状态
function startStatusCheck() {
statusCheckTimer = setInterval(() => {
checkPaymentStatus();
}, 3000); // 每3秒检查一次
}
// 检查支付状态
function checkPaymentStatus() {
const paymentSn = 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 = '<i class="bi bi-hourglass-split"></i> 处理中...';
button.disabled = true;
fetch(`/payment/simulate_success/${paymentSn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
showAlert('模拟支付成功!', 'success');
setTimeout(() => {
showPaymentSuccess();
}, 1000);
} else {
showAlert(data.message || '模拟支付失败', 'error');
button.innerHTML = originalText;
button.disabled = false;
}
})
.catch(error => {
console.error('模拟支付错误:', error);
showAlert('模拟支付失败', 'error');
button.innerHTML = originalText;
button.disabled = false;
});
}
// 模拟支付失败
function simulatePaymentFail() {
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
if (!paymentSn) {
showAlert('支付信息获取失败', 'error');
return;
}
showConfirm('确定要模拟支付失败吗?这将导致订单支付失败。', () => {
// 显示处理中状态
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="bi bi-hourglass-split"></i> 处理中...';
button.disabled = true;
fetch(`/payment/simulate_fail/${paymentSn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
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 = '<i class="bi bi-heart-fill text-danger"></i> 已收藏';
favoriteBtn.className = 'btn btn-outline-danger';
} else {
favoriteBtn.innerHTML = '<i class="bi bi-heart"></i> 收藏商品';
favoriteBtn.className = 'btn btn-outline-secondary';
}
}
}
// 规格选择
function selectSpec(button) {
const specName = button.getAttribute('data-spec-name');
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 = '<i class="bi bi-hourglass-split"></i> 添加中...';
// 准备规格组合数据
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 = '<i class="bi bi-cart-plus"></i> 加入购物车';
});
}
// 立即购买
function buyNow() {
if (!currentSku) {
alert('请选择商品规格');
return;
}
const quantity = parseInt(document.getElementById('quantity').value);
if (quantity <= 0 || quantity > currentSku.stock) {
alert('请选择正确的购买数量');
return;
}
// 禁用按钮,防止重复点击
const buyNowBtn = document.getElementById('buyNowBtn');
buyNowBtn.disabled = true;
buyNowBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 处理中...';
// 准备规格组合数据
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 = '<i class="bi bi-lightning-fill"></i> 立即购买';
});
}
// 收藏商品
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 = '<i class="bi bi-hourglass-split"></i> 处理中...';
}
fetch('/favorite/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: parseInt(productId)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
updateFavoriteButton(data.is_favorited);
} else {
alert(data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('操作失败,请稍后再试');
})
.finally(() => {
// 恢复按钮状态
if (favoriteBtn) {
favoriteBtn.disabled = false;
}
});
}
// 显示成功消息
function showSuccessMessage(message) {
// 这里可以使用Toast或其他方式显示消息
if (typeof showToast === 'function') {
showToast(message, 'success');
} else {
// 简单的成功提示
alert(message);
}
}
🔸==============================================================================
📄 文件: 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 = `
<i class="bi bi-${icon} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动消失
setTimeout(() => {
if (alertDiv.parentElement) {
alertDiv.classList.remove('show');
setTimeout(() => {
if (alertDiv.parentElement) {
alertDiv.remove();
}
}, 150);
}
}, 3000);
}
// 初始化拖拽上传功能
function initDragAndDrop() {
const avatarUpload = document.querySelector('.avatar-upload');
if (avatarUpload) {
// 防止默认拖拽行为
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
avatarUpload.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// 拖拽进入和离开的视觉反馈
['dragenter', 'dragover'].forEach(eventName => {
avatarUpload.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
avatarUpload.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
avatarUpload.style.transform = 'scale(1.05)';
avatarUpload.style.boxShadow = '0 0 20px rgba(0,123,255,0.5)';
}
function unhighlight(e) {
avatarUpload.style.transform = '';
avatarUpload.style.boxShadow = '';
}
// 处理文件拖拽
avatarUpload.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
const file = files[0];
// 模拟文件输入事件
const event = { target: { files: [file] } };
handleFileSelect(event);
}
}
}
}
// 额外的保险措施:定期检查并修正头像样式
setInterval(function() {
const avatar = document.getElementById('avatarPreview');
if (avatar) {
// 检查头像是否超出预期尺寸
const rect = avatar.getBoundingClientRect();
if (rect.width > 125 || rect.height > 125) {
forceAvatarStyle(avatar);
}
}
}, 1000); // 每秒检查一次
🔸==============================================================================
📄 文件: 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 = '<i class="bi bi-hourglass-split"></i> 提交中...';
const data = {
order_id: parseInt(orderId),
product_id: parseInt(productId),
rating: parseInt(rating),
content: content.trim(),
is_anonymous: isAnonymous,
images: window.getUploadedImages ? window.getUploadedImages() : []
};
fetch('/review/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => {
window.location.href = `/order/detail/${orderId}`;
}, 1500);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('提交失败,请重试', 'error');
})
.finally(() => {
// 恢复提交按钮
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle"></i> 提交评价';
});
}
// 加载商品评价列表(用于商品详情页)
function loadProductReviews(productId, page = 1, rating = null) {
const reviewsContainer = document.getElementById('reviewsContainer');
if (!reviewsContainer) return;
const params = new URLSearchParams({
page: page
});
if (rating) {
params.append('rating', rating);
}
reviewsContainer.innerHTML = '<div class="text-center p-4"><i class="bi bi-hourglass-split"></i> 加载中...</div>';
fetch(`/review/product/${productId}?${params}`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderReviews(data);
} else {
reviewsContainer.innerHTML = '<div class="text-center p-4 text-muted">加载失败</div>';
}
})
.catch(error => {
reviewsContainer.innerHTML = '<div class="text-center p-4 text-muted">加载失败</div>';
});
}
// 渲染评价列表
function renderReviews(data) {
const reviewsContainer = document.getElementById('reviewsContainer');
if (!reviewsContainer) return;
let html = '';
// 评价统计
if (data.stats) {
html += renderReviewStats(data.stats);
}
// 评价筛选
html += renderReviewFilter();
// 评价列表
if (data.reviews && data.reviews.length > 0) {
data.reviews.forEach(review => {
html += renderReviewItem(review);
});
// 分页
if (data.pagination && data.pagination.pages > 1) {
html += renderPagination(data.pagination);
}
} else {
html += '<div class="text-center p-4 text-muted">暂无评价</div>';
}
reviewsContainer.innerHTML = html;
}
// 渲染评价统计
function renderReviewStats(stats) {
const goodRate = stats.good_rate || 0;
const totalReviews = stats.total_reviews || 0;
let html = `
<div class="reviews-stats">
<div class="rating-summary">
<div class="overall-rating">
<div class="score">${goodRate}%</div>
<div class="total">好评率 (${totalReviews}条评价)</div>
</div>
<div class="rating-breakdown">
`;
for (let i = 5; i >= 1; i--) {
const count = stats.rating_stats[i] || 0;
const percentage = totalReviews > 0 ? (count / totalReviews * 100) : 0;
html += `
<div class="rating-bar">
<span class="label">${i}星</span>
<div class="progress">
<div class="progress-bar" style="width: ${percentage}%"></div>
</div>
<span class="count">${count}</span>
</div>
`;
}
html += `
</div>
</div>
</div>
`;
return html;
}
// 渲染评价筛选
function renderReviewFilter() {
return `
<div class="reviews-filter">
<button class="btn btn-outline-primary btn-sm active" onclick="filterReviews(null)">
全部评价
</button>
<button class="btn btn-outline-primary btn-sm" onclick="filterReviews(5)">
好评 (5星)
</button>
<button class="btn btn-outline-primary btn-sm" onclick="filterReviews(3)">
中评 (3星)
</button>
<button class="btn btn-outline-primary btn-sm" onclick="filterReviews(1)">
差评 (1星)
</button>
</div>
`;
}
// 渲染单个评价 - 修复图片和头像问题
function renderReviewItem(review) {
let html = `
<div class="review-list-item">
<div class="reviewer-info">
`;
if (review.user_avatar) {
// 用户头像 - 添加内联样式强制约束尺寸
html += `<img src="${review.user_avatar}"
class="reviewer-avatar"
alt="用户头像"
style="width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; border-radius: 50% !important; object-fit: cover !important; border: 2px solid #e9ecef !important; display: block !important; flex-shrink: 0 !important;">`;
} else {
html += `<div class="reviewer-avatar bg-secondary d-flex align-items-center justify-content-center text-white"
style="width: 40px !important; height: 40px !important; max-width: 40px !important; max-height: 40px !important; min-width: 40px !important; min-height: 40px !important; border-radius: 50% !important; flex-shrink: 0 !important;">
<i class="bi bi-person"></i>
</div>`;
}
html += `
<div>
<div class="reviewer-name">${review.username}</div>
<div class="review-time">${new Date(review.created_at).toLocaleDateString()}</div>
</div>
</div>
<div class="rating-display mb-2">
<span class="stars">${review.rating_stars}</span>
<span class="text-muted">${review.rating}分</span>
</div>
`;
if (review.content) {
html += `<p class="review-content">${review.content}</p>`;
}
if (review.images && review.images.length > 0) {
html += '<div class="product-review-images mb-2">';
review.images.forEach(imageUrl => {
// 评价图片 - 使用特殊的类名和内联样式确保图片尺寸正确
html += `<img src="${imageUrl}"
class="product-review-image"
alt="评价图片"
style="width: 80px !important; height: 80px !important; max-width: 80px !important; max-height: 80px !important; min-width: 80px !important; min-height: 80px !important; object-fit: cover !important; border-radius: 6px !important; border: 1px solid #dee2e6 !important; cursor: pointer !important; margin-right: 8px !important; margin-bottom: 8px !important; display: inline-block !important; vertical-align: top !important;"
onclick="showImageModal('${imageUrl}')">`;
});
html += '</div>';
}
html += '</div>';
return html;
}
// 渲染分页
function renderPagination(pagination) {
if (pagination.pages <= 1) return '';
let html = '<nav aria-label="评价分页"><ul class="pagination justify-content-center">';
// 上一页
if (pagination.has_prev) {
html += `<li class="page-item">
<a class="page-link" href="#" onclick="loadProductReviews(window.currentProductId, ${pagination.page - 1}); return false;">
上一页
</a>
</li>`;
}
// 页码
const startPage = Math.max(1, pagination.page - 2);
const endPage = Math.min(pagination.pages, pagination.page + 2);
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === pagination.page ? 'active' : '';
html += `<li class="page-item ${activeClass}">
<a class="page-link" href="#" onclick="loadProductReviews(window.currentProductId, ${i}); return false;">
${i}
</a>
</li>`;
}
// 下一页
if (pagination.has_next) {
html += `<li class="page-item">
<a class="page-link" href="#" onclick="loadProductReviews(window.currentProductId, ${pagination.page + 1}); return false;">
下一页
</a>
</li>`;
}
html += '</ul></nav>';
return html;
}
// 筛选评价
function filterReviews(rating) {
// 更新筛选按钮状态
const filterButtons = document.querySelectorAll('.reviews-filter .btn');
filterButtons.forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// 重新加载评价
loadProductReviews(window.currentProductId, 1, rating);
}
// 显示图片模态框
function showImageModal(imageUrl) {
const modal = document.getElementById('imageModal');
const modalImage = document.getElementById('modalImage');
if (modal && modalImage) {
modalImage.src = imageUrl;
new bootstrap.Modal(modal).show();
}
}
// 显示提示信息
function showAlert(message, type = 'info') {
// 创建警告框
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 添加到页面
document.body.appendChild(alertDiv);
// 自动消失
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// 全局变量用于存储当前商品ID
window.currentProductId = null;
🔸==============================================================================
📄 文件: app/templates/admin/base.html
📊 大小: 5154 bytes (5.03 KB)
🕒 修改时间: 2025-07-08 17:54:21
🔸==============================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}太白购物商城 - 管理后台{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
<!-- Admin Base CSS -->
<link href="{{ url_for('static', filename='css/admin_base.css') }}" rel="stylesheet">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 侧边栏 -->
<div class="admin-sidebar">
<div class="sidebar-brand">
<i class="bi bi-shop"></i>
太白购物商城
</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
href="{{ url_for('admin.dashboard') }}">
<i class="bi bi-speedometer2"></i>
仪表板
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.users' %}active{% endif %}"
href="{{ url_for('admin.users') }}">
<i class="bi bi-people"></i>
用户管理
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint.startswith('product.') %}active{% endif %}"
href="{{ url_for('product.index') }}">
<i class="bi bi-box"></i>
商品管理
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint.startswith('admin.order') %}active{% endif %}"
href="{{ url_for('admin.orders') }}">
<i class="bi bi-receipt"></i>
订单管理
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.logs' %}active{% endif %}"
href="{{ url_for('admin.logs') }}">
<i class="bi bi-journal-text"></i>
操作日志
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.profile' %}active{% endif %}"
href="{{ url_for('admin.profile') }}">
<i class="bi bi-person-gear"></i>
个人资料
</a>
</li>
</ul>
</div>
<!-- 主要内容区域 -->
<div class="admin-main">
<!-- 顶部导航 -->
<div class="admin-header d-flex justify-content-between align-items-center">
<div>
<h4 class="mb-0">{% block page_title %}管理后台{% endblock %}</h4>
<small class="text-muted">{% block page_description %}{% endblock %}</small>
</div>
<div class="d-flex align-items-center">
<div class="dropdown">
<a class="btn btn-link text-decoration-none dropdown-toggle" href="#" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i>
{{ session.admin_username }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('admin.profile') }}">
<i class="bi bi-person-gear"></i> 个人资料
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('admin.logout') }}">
<i class="bi bi-box-arrow-right"></i> 退出登录
</a></li>
</ul>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="admin-content">
<!-- 消息提示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
🔸==============================================================================
📄 文件: app/templates/admin/categories.html
📊 大小: 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin_categories.css') }}">
{% endblock %}
{% block content %}
<!-- 添加分类表单 -->
<div class="add-category-form">
<h5 class="mb-3">
<i class="bi bi-plus-circle"></i> 添加新分类
</h5>
<form id="addCategoryForm" method="POST" action="{{ url_for('product.save_category') }}" enctype="multipart/form-data">
<div class="row">
<div class="col-md-2">
<label class="form-label">分类图标</label>
<div class="icon-upload-area" id="iconUploadArea">
<i class="bi bi-image fs-3 text-muted"></i>
<img id="iconPreview" style="display: none;">
</div>
<input type="file" id="iconInput" name="icon" accept="image/*" style="display: none;">
<small class="text-muted">点击上传图标</small>
</div>
<div class="col-md-3">
<label for="name" class="form-label">分类名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="col-md-3">
<label for="parent_id" class="form-label">父分类</label>
<select name="parent_id" id="parent_id" class="form-select">
<option value="0">顶级分类</option>
{% for category in categories %}
{% if category.level <= 2 %}
<option value="{{ category.id }}">
{{ '└─' * (category.level - 1) }}{{ category.name }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="sort_order" class="form-label">排序</label>
<input type="number" class="form-control" id="sort_order" name="sort_order" value="0">
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-plus-lg"></i> 添加
</button>
</div>
</div>
</form>
</div>
<!-- 分类列表 -->
<div class="category-tree">
{% if categories %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
<i class="bi bi-diagram-3"></i> 分类结构
<span class="badge bg-secondary">{{ categories|length }}</span>
</h5>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="expandAll()">
<i class="bi bi-arrows-expand"></i> 展开全部
</button>
<button class="btn btn-outline-secondary" onclick="collapseAll()">
<i class="bi bi-arrows-collapse"></i> 收起全部
</button>
</div>
</div>
<div id="categoryList">
<!-- 渲染顶级分类 -->
{% set top_categories = categories | selectattr('parent_id', 'equalto', 0) | sort(attribute='sort_order') %}
{% for category in top_categories %}
<div class="category-item category-level-{{ category.level }}" data-id="{{ category.id }}">
<div class="category-header">
<div class="category-info">
<i class="bi bi-grip-vertical sort-handle"></i>
{% if category.icon_url %}
<img src="{{ category.icon_url }}" alt="{{ category.name }}" class="category-icon">
{% else %}
<div class="default-icon">
<i class="bi bi-folder"></i>
</div>
{% endif %}
<div class="category-details">
<h6>{{ category.name }}</h6>
<div class="category-meta">
ID: {{ category.id }} |
层级: {{ category.level }} |
排序: {{ category.sort_order }} |
{% if category.is_active %}
<span class="text-success">启用</span>
{% else %}
<span class="text-danger">禁用</span>
{% endif %}
</div>
</div>
</div>
<div class="category-actions">
<button class="btn btn-light btn-icon" onclick="editCategory({{ category.id }})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-light btn-icon" onclick="addSubCategory({{ category.id }})" title="添加子分类">
<i class="bi bi-plus"></i>
</button>
{% if category.level < 3 %}
<button class="btn btn-light btn-icon" onclick="toggleCategory({{ category.id }})" title="展开/收起">
<i class="bi bi-chevron-down"></i>
</button>
{% endif %}
<button class="btn btn-light btn-icon text-danger" onclick="deleteCategory({{ category.id }})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<!-- 二级分类 -->
{% set level2_categories = categories | selectattr('parent_id', 'equalto', category.id) | sort(attribute='sort_order') %}
{% if level2_categories %}
<div class="children-categories">
{% for child in level2_categories %}
<div class="category-item category-level-{{ child.level }}" data-id="{{ child.id }}">
<div class="category-header">
<div class="category-info">
<i class="bi bi-grip-vertical sort-handle"></i>
{% if child.icon_url %}
<img src="{{ child.icon_url }}" alt="{{ child.name }}" class="category-icon">
{% else %}
<div class="default-icon">
<i class="bi bi-folder"></i>
</div>
{% endif %}
<div class="category-details">
<h6>{{ child.name }}</h6>
<div class="category-meta">
ID: {{ child.id }} |
层级: {{ child.level }} |
排序: {{ child.sort_order }} |
{% if child.is_active %}
<span class="text-success">启用</span>
{% else %}
<span class="text-danger">禁用</span>
{% endif %}
</div>
</div>
</div>
<div class="category-actions">
<button class="btn btn-light btn-icon" onclick="editCategory({{ child.id }})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
{% if child.level < 3 %}
<button class="btn btn-light btn-icon" onclick="addSubCategory({{ child.id }})" title="添加子分类">
<i class="bi bi-plus"></i>
</button>
{% endif %}
<button class="btn btn-light btn-icon text-danger" onclick="deleteCategory({{ child.id }})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<!-- 三级分类 -->
{% set level3_categories = categories | selectattr('parent_id', 'equalto', child.id) | sort(attribute='sort_order') %}
{% if level3_categories %}
<div class="children-categories">
{% for grandchild in level3_categories %}
<div class="category-item category-level-{{ grandchild.level }}" data-id="{{ grandchild.id }}">
<div class="category-header">
<div class="category-info">
<i class="bi bi-grip-vertical sort-handle"></i>
{% if grandchild.icon_url %}
<img src="{{ grandchild.icon_url }}" alt="{{ grandchild.name }}" class="category-icon">
{% else %}
<div class="default-icon">
<i class="bi bi-folder"></i>
</div>
{% endif %}
<div class="category-details">
<h6>{{ grandchild.name }}</h6>
<div class="category-meta">
ID: {{ grandchild.id }} |
层级: {{ grandchild.level }} |
排序: {{ grandchild.sort_order }} |
{% if grandchild.is_active %}
<span class="text-success">启用</span>
{% else %}
<span class="text-danger">禁用</span>
{% endif %}
</div>
</div>
</div>
<div class="category-actions">
<button class="btn btn-light btn-icon" onclick="editCategory({{ grandchild.id }})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-light btn-icon text-danger" onclick="deleteCategory({{ grandchild.id }})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<i class="bi bi-folder"></i>
<h5>还没有创建任何分类</h5>
<p class="text-muted">点击上方的"添加新分类"来创建第一个商品分类</p>
</div>
{% endif %}
</div>
<!-- 编辑分类模态框 -->
<div class="modal fade" id="editCategoryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-pencil"></i> 编辑分类
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editCategoryForm" method="POST" action="{{ url_for('product.save_category') }}" enctype="multipart/form-data">
<div class="modal-body">
<input type="hidden" id="edit_category_id" name="category_id">
<div class="row">
<div class="col-md-4">
<label class="form-label">分类图标</label>
<div class="icon-upload-area" id="editIconUploadArea">
<i class="bi bi-image fs-3 text-muted"></i>
<img id="editIconPreview" style="display: none;">
</div>
<input type="file" id="editIconInput" name="icon" accept="image/*" style="display: none;">
<small class="text-muted">点击更换图标</small>
</div>
<div class="col-md-8">
<div class="mb-3">
<label for="edit_name" class="form-label">分类名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="edit_name" name="name" required>
</div>
<div class="mb-3">
<label for="edit_parent_id" class="form-label">父分类</label>
<select name="parent_id" id="edit_parent_id" class="form-select">
<option value="0">顶级分类</option>
{% for category in categories %}
{% if category.level <= 2 %}
<option value="{{ category.id }}">
{{ '└─' * (category.level - 1) }}{{ category.name }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="row">
<div class="col-6">
<label for="edit_sort_order" class="form-label">排序</label>
<input type="number" class="form-control" id="edit_sort_order" name="sort_order">
</div>
<div class="col-6">
<label for="edit_is_active" class="form-label">状态</label>
<select name="is_active" id="edit_is_active" class="form-select">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> 保存修改
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_categories.js') }}"></script>
{% 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 %}
<link href="{{ url_for('static', filename='css/admin_dashboard.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="row dashboard-stats">
<!-- 统计卡片 -->
<div class="col-md-3 mb-4">
<div class="card stats-card">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3 class="mb-1">{{ stats.total_users or 0 }}</h3>
<p class="mb-0">总用户数</p>
</div>
<div class="fs-1">
<i class="bi bi-people"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card stats-card success">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3 class="mb-1">{{ stats.active_users or 0 }}</h3>
<p class="mb-0">活跃用户</p>
</div>
<div class="fs-1">
<i class="bi bi-person-check"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card stats-card warning">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3 class="mb-1">{{ stats.total_admins or 0 }}</h3>
<p class="mb-0">管理员数</p>
</div>
<div class="fs-1">
<i class="bi bi-shield-check"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card stats-card info">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3 class="mb-1">{{ stats.recent_logs_count or 0 }}</h3>
<p class="mb-0">7天操作数</p>
</div>
<div class="fs-1">
<i class="bi bi-activity"></i>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- 用户注册趋势 -->
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-graph-up"></i>
用户注册趋势最近7天
</h5>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="userTrendChart"></canvas>
</div>
</div>
</div>
</div>
<!-- 系统状态 -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-info-circle"></i>
系统状态
</h5>
</div>
<div class="card-body">
<div class="system-status-item">
<span>数据库连接</span>
<span class="badge bg-success">正常</span>
</div>
<div class="system-status-item">
<span>文件存储</span>
<span class="badge bg-success">正常</span>
</div>
<div class="system-status-item">
<span>邮件服务</span>
<span class="badge bg-success">正常</span>
</div>
<div class="system-status-item">
<span>系统版本</span>
<span class="badge bg-info">v1.0.0</span>
</div>
</div>
</div>
</div>
</div>
<!-- 最近操作日志 -->
<div class="row log-table-container">
<div class="col-12">
<div class="card admin-table">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-journal-text"></i>
最近操作日志
</h5>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>时间</th>
<th>操作者</th>
<th>操作类型</th>
<th>操作内容</th>
<th>IP地址</th>
</tr>
</thead>
<tbody>
{% if recent_logs %}
{% for log in recent_logs %}
<tr>
<td>{{ log.created_at.strftime('%m-%d %H:%M') if log.created_at else '' }}</td>
<td>
{% if log.user_type == 2 %}
<span class="badge bg-warning">管理员</span>
{% else %}
<span class="badge bg-info">用户</span>
{% endif %}
{{ log.user_id }}
</td>
<td>{{ log.action }}</td>
<td>
{% if log.resource_type %}
{{ log.resource_type }}
{% if log.resource_id %}#{{ log.resource_id }}{% endif %}
{% else %}
-
{% endif %}
</td>
<td>{{ log.ip_address or '-' }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="5" class="empty-state">
<i class="bi bi-inbox"></i>
<div>暂无操作日志</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% if recent_logs %}
<div class="card-footer bg-white text-center">
<a href="{{ url_for('admin.logs') }}" class="btn btn-outline-primary btn-sm">
查看全部日志 <i class="bi bi-arrow-right"></i>
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Pass data from template to JavaScript
window.userTrendLabels = [
{% for item in user_trend %}
'{{ item.date }}'{% if not loop.last %},{% endif %}
{% endfor %}
];
window.userTrendData = [
{% for item in user_trend %}
{{ item.count }}{% if not loop.last %},{% endif %}
{% endfor %}
];
</script>
<script src="{{ url_for('static', filename='js/admin_dashboard.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/admin/login.html
📊 大小: 2726 bytes (2.66 KB)
🕒 修改时间: 2025-07-04 14:51:53
🔸==============================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员登录 - 太白购物商城</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
<!-- Admin Login CSS -->
<link href="{{ url_for('static', filename='css/admin_login.css') }}" rel="stylesheet">
</head>
<body>
<div class="login-card">
<div class="login-header">
<h2><i class="bi bi-shield-lock"></i> 管理员登录</h2>
<p>太白购物商城管理后台</p>
</div>
<!-- 消息提示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="mb-3">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-person"></i>
</span>
<input type="text" class="form-control" name="username" placeholder="管理员用户名" required>
</div>
</div>
<div class="mb-4">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-lock"></i>
</span>
<input type="password" class="form-control" name="password" placeholder="登录密码" required>
</div>
</div>
<button type="submit" class="btn btn-primary btn-login w-100">
<i class="bi bi-box-arrow-in-right"></i>
登录管理后台
</button>
</form>
<div class="text-center mt-3">
<a href="{{ url_for('main.index') }}" class="back-link">
<i class="bi bi-arrow-left"></i> 返回前台
</a>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
🔸==============================================================================
📄 文件: app/templates/admin/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 %}
<link href="{{ url_for('static', filename='css/admin_logs.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-logs">
<!-- 筛选条件 -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label for="user_type" class="form-label">用户类型</label>
<select class="form-select" id="user_type" name="user_type">
<option value="">全部类型</option>
<option value="1" {% if user_type == '1' %}selected{% endif %}>普通用户</option>
<option value="2" {% if user_type == '2' %}selected{% endif %}>管理员</option>
</select>
</div>
<div class="col-md-4">
<label for="action" class="form-label">操作类型</label>
<input type="text" class="form-control" id="action" name="action"
value="{{ action }}" placeholder="搜索操作类型">
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 搜索
</button>
<a href="{{ url_for('admin.logs') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> 重置
</a>
</div>
</div>
</form>
</div>
</div>
<!-- 日志统计 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ logs.total }}</h5>
<p class="card-text">总日志数</p>
</div>
<div class="icon-wrapper primary">
<i class="bi bi-journal-text"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ logs.items | selectattr('user_type', 'equalto', 1) | list | length }}</h5>
<p class="card-text">用户操作</p>
</div>
<div class="icon-wrapper info">
<i class="bi bi-person"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ logs.items | selectattr('user_type', 'equalto', 2) | list | length }}</h5>
<p class="card-text">管理员操作</p>
</div>
<div class="icon-wrapper warning">
<i class="bi bi-shield-check"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ today_logs_count }}</h5>
<p class="card-text">今日操作</p>
</div>
<div class="icon-wrapper success">
<i class="bi bi-clock"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 日志列表 -->
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-journal-text"></i>
操作日志
<small class="text-muted ms-2">共 {{ logs.total }} 条记录</small>
</h5>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="15%">时间</th>
<th width="10%">操作者</th>
<th width="15%">操作类型</th>
<th width="20%">操作内容</th>
<th width="15%">IP地址</th>
<th width="25%">用户代理</th>
</tr>
</thead>
<tbody>
{% if logs.items %}
{% for log in logs.items %}
<tr>
<td>
<div>{{ log.created_at.strftime('%Y-%m-%d') if log.created_at else '-' }}</div>
<small class="text-muted">{{ log.created_at.strftime('%H:%M:%S') if log.created_at else '' }}</small>
</td>
<td>
<div class="d-flex align-items-center">
<span class="badge bg-{{ 'warning' if log.user_type == 2 else 'info' }} me-2">
{{ '管理员' if log.user_type == 2 else '用户' }}
</span>
<span class="fw-bold">#{{ log.user_id or '-' }}</span>
</div>
</td>
<td>
<span class="operation-action">{{ log.action }}</span>
</td>
<td>
<div>
{% if log.resource_type %}
<span class="resource-type">{{ log.resource_type }}</span>
{% if log.resource_id %}
<span class="resource-id">#{{ log.resource_id }}</span>
{% endif %}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</div>
</td>
<td>
<code>{{ log.ip_address or '-' }}</code>
</td>
<td>
<div class="user-agent-wrapper">
{% if log.user_agent %}
<span class="user-agent" title="{{ log.user_agent }}">
{{ log.user_agent[:50] }}{% if log.user_agent|length > 50 %}...{% endif %}
</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center py-4">
<div class="empty-state">
<i class="bi bi-journal-x"></i>
<div>暂无操作日志</div>
{% if user_type or action %}
<small class="text-muted">尝试调整筛选条件</small>
{% endif %}
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if logs.pages > 1 %}
<div class="card-footer bg-white">
<nav aria-label="操作日志分页">
<ul class="pagination justify-content-center mb-0">
{% if logs.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.logs', page=logs.prev_num, user_type=user_type, action=action) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in logs.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
{% if page_num %}
{% if page_num != logs.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.logs', page=page_num, user_type=user_type, action=action) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if logs.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.logs', page=logs.next_num, user_type=user_type, action=action) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_logs.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: 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 %}
<link href="{{ url_for('static', filename='css/admin_orders.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-order-detail">
<div class="row">
<!-- 左侧:订单信息 -->
<div class="col-md-8">
<!-- 订单基本信息 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">订单基本信息</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">订单号:</th>
<td>{{ order.order_sn }}</td>
</tr>
<tr>
<th>用户信息:</th>
<td>
<div>{{ order.user.username }}</div>
{% if order.user.phone %}
<small class="text-muted">{{ order.user.phone }}</small>
{% endif %}
</td>
</tr>
<tr>
<th>订单状态:</th>
<td>
<span class="badge order-status-{{ order.status }}">{{ order.get_status_text() }}</span>
</td>
</tr>
<tr>
<th>支付方式:</th>
<td>
{% if order.payment_method %}
<span class="badge bg-info">{{ order.payment_method }}</span>
{% else %}
<span class="text-muted">未设置</span>
{% endif %}
</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">创建时间:</th>
<td>{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% if order.shipped_at %}
<tr>
<th>发货时间:</th>
<td>{{ order.shipped_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endif %}
{% if order.received_at %}
<tr>
<th>收货时间:</th>
<td>{{ order.received_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endif %}
<tr>
<th>配送方式:</th>
<td>{{ order.shipping_method or '标准配送' }}</td>
</tr>
</table>
</div>
</div>
{% if order.remark %}
<div class="mt-3">
<strong>订单备注:</strong>
<p class="text-muted mb-0">{{ order.remark }}</p>
</div>
{% endif %}
</div>
</div>
<!-- 收货信息 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">收货信息</h5>
</div>
<div class="card-body">
{% set receiver = order.get_receiver_info() %}
{% if receiver %}
<div class="row">
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">收货人:</th>
<td>{{ receiver.receiver_name }}</td>
</tr>
<tr>
<th>联系电话:</th>
<td>{{ receiver.receiver_phone }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-sm">
<tr>
<th width="120">收货地址:</th>
<td>{{ receiver.full_address }}</td>
</tr>
{% if receiver.postal_code %}
<tr>
<th>邮政编码:</th>
<td>{{ receiver.postal_code }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% else %}
<p class="text-muted">暂无收货信息</p>
{% endif %}
</div>
</div>
<!-- 订单商品 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">订单商品</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>商品信息</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
</thead>
<tbody>
{% for item in order.order_items %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if item.product_image %}
<img src="{{ item.product_image }}" alt="{{ item.product_name }}"
class="product-thumb me-3" style="width: 60px; height: 60px; object-fit: cover;">
{% endif %}
<div>
<div class="fw-bold">{{ item.product_name }}</div>
{% if item.spec_combination %}
<small class="text-muted">{{ item.spec_combination }}</small>
{% endif %}
{% if item.sku_code %}
<small class="text-muted d-block">SKU: {{ item.sku_code }}</small>
{% endif %}
</div>
</div>
</td>
<td>¥{{ "%.2f"|format(item.price) }}</td>
<td>{{ item.quantity }}</td>
<td>¥{{ "%.2f"|format(item.total_price) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 右侧:金额信息和操作 -->
<div class="col-md-4">
<!-- 金额信息 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">金额信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>商品总额:</th>
<td class="text-end">¥{{ "%.2f"|format(order.total_amount) }}</td>
</tr>
<tr>
<th>运费:</th>
<td class="text-end">¥{{ "%.2f"|format(order.shipping_fee) }}</td>
</tr>
<tr class="table-active">
<th>实付金额:</th>
<td class="text-end fw-bold text-primary">¥{{ "%.2f"|format(order.actual_amount) }}</td>
</tr>
</table>
</div>
</div>
<!-- 支付信息 -->
{% if payment %}
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">支付信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>支付流水号:</th>
<td>{{ payment.payment_sn }}</td>
</tr>
<tr>
<th>支付状态:</th>
<td>
<span class="badge {% if payment.status == 2 %}bg-success{% elif payment.status == 1 %}bg-warning{% else %}bg-danger{% endif %}">
{{ payment.get_status_text() }}
</span>
</td>
</tr>
{% if payment.paid_at %}
<tr>
<th>支付时间:</th>
<td>{{ payment.paid_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endif %}
{% if payment.third_party_sn %}
<tr>
<th>第三方流水号:</th>
<td>{{ payment.third_party_sn }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% endif %}
<!-- 物流信息 -->
{% if shipping_info %}
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">物流信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>物流公司:</th>
<td>{{ shipping_info.shipping_company }}</td>
</tr>
<tr>
<th>快递单号:</th>
<td>{{ shipping_info.tracking_number }}</td>
</tr>
<tr>
<th>物流状态:</th>
<td>
<span class="badge {% if shipping_info.shipping_status == 3 %}bg-success{% elif shipping_info.shipping_status == 2 %}bg-warning{% else %}bg-info{% endif %}">
{% if shipping_info.shipping_status == 1 %}已发货
{% elif shipping_info.shipping_status == 2 %}运输中
{% elif shipping_info.shipping_status == 3 %}已送达
{% endif %}
</span>
</td>
</tr>
</table>
</div>
</div>
{% endif %}
<!-- 操作按钮 -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">订单操作</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
{% if order.status == 2 %}
<button class="btn btn-success" onclick="showShipModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-truck"></i> 发货
</button>
{% endif %}
{% if order.status in [2, 3] %}
<button class="btn btn-warning" onclick="showRefundModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-arrow-return-left"></i> 退款
</button>
{% endif %}
{% if order.can_cancel() %}
<button class="btn btn-danger" onclick="showCancelModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-x-circle"></i> 取消订单
</button>
{% endif %}
<a href="{{ url_for('admin.orders') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> 返回列表
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 发货模态框 -->
<div class="modal fade" id="shipModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">订单发货</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="shipForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="shipOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">物流公司 <span class="text-danger">*</span></label>
<select class="form-select" name="shipping_company" required>
<option value="">请选择物流公司</option>
<option value="顺丰速运">顺丰速运</option>
<option value="圆通速递">圆通速递</option>
<option value="中通快递">中通快递</option>
<option value="申通快递">申通快递</option>
<option value="韵达速递">韵达速递</option>
<option value="百世快递">百世快递</option>
<option value="德邦快递">德邦快递</option>
<option value="京东物流">京东物流</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">快递单号 <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="tracking_number" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-truck"></i> 确认发货
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 退款模态框 -->
<div class="modal fade" id="refundModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">订单退款</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="refundForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="refundOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">退款原因 <span class="text-danger">*</span></label>
<textarea class="form-control" name="refund_reason" rows="3" required placeholder="请输入退款原因"></textarea>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
退款后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-warning">
<i class="bi bi-arrow-return-left"></i> 确认退款
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 取消模态框 -->
<div class="modal fade" id="cancelModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">取消订单</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="cancelForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="cancelOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">取消原因</label>
<textarea class="form-control" name="cancel_reason" rows="3" placeholder="请输入取消原因(可选)"></textarea>
</div>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i>
取消订单后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-danger">
<i class="bi bi-x-circle"></i> 确认取消
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_orders.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: 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 %}
<link href="{{ url_for('static', filename='css/admin_orders.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-orders">
<!-- 统计卡片 -->
<div class="row mb-4">
<div class="col-md-2">
<div class="card stats-card">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(0, {}).get('count', 0) + order_stats.get(1, {}).get('count', 0) + order_stats.get(2, {}).get('count', 0) + order_stats.get(3, {}).get('count', 0) + order_stats.get(4, {}).get('count', 0) + order_stats.get(5, {}).get('count', 0) + order_stats.get(6, {}).get('count', 0) + order_stats.get(7, {}).get('count', 0) }}</div>
<div class="stats-label">总订单</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card pending-payment">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(1, {}).get('count', 0) }}</div>
<div class="stats-label">待支付</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card pending-shipment">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(2, {}).get('count', 0) }}</div>
<div class="stats-label">待发货</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card shipped">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(3, {}).get('count', 0) }}</div>
<div class="stats-label">待收货</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card completed">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(5, {}).get('count', 0) }}</div>
<div class="stats-label">已完成</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stats-card cancelled">
<div class="card-body text-center">
<div class="stats-number">{{ order_stats.get(6, {}).get('count', 0) }}</div>
<div class="stats-label">已取消</div>
</div>
</div>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label for="search" class="form-label">搜索</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="订单号/用户名/手机号">
</div>
<div class="col-md-2">
<label for="status" class="form-label">订单状态</label>
<select class="form-select" id="status" name="status">
<option value="">全部状态</option>
{% for status_code, status_name in ORDER_STATUS.items() %}
<option value="{{ status_code }}" {% if status == status_code|string %}selected{% endif %}>
{{ status_name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="start_date" class="form-label">开始日期</label>
<input type="date" class="form-control" id="start_date" name="start_date" value="{{ start_date }}">
</div>
<div class="col-md-2">
<label for="end_date" class="form-label">结束日期</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="{{ end_date }}">
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 搜索
</button>
<a href="{{ url_for('admin.orders') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> 重置
</a>
</div>
</div>
</form>
</div>
</div>
<!-- 订单列表 -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">订单列表</h5>
</div>
<div class="card-body">
{% if orders.items %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>订单号</th>
<th>用户信息</th>
<th>订单金额</th>
<th>订单状态</th>
<th>支付方式</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for order in orders.items %}
<tr>
<td>
<div class="fw-bold">{{ order.order_sn }}</div>
<small class="text-muted">ID: {{ order.id }}</small>
</td>
<td>
<div class="fw-bold">{{ order.user.username }}</div>
{% if order.user.phone %}
<small class="text-muted">{{ order.user.phone }}</small>
{% endif %}
</td>
<td>
<div class="fw-bold text-primary">¥{{ "%.2f"|format(order.actual_amount) }}</div>
{% if order.shipping_fee > 0 %}
<small class="text-muted">含运费: ¥{{ "%.2f"|format(order.shipping_fee) }}</small>
{% endif %}
</td>
<td>
<span class="badge order-status-{{ order.status }}">{{ order.get_status_text() }}</span>
</td>
<td>
{% if order.payment_method %}
<span class="badge bg-info">{{ order.payment_method }}</span>
{% else %}
<span class="text-muted">未设置</span>
{% endif %}
</td>
<td>
<div>{{ order.created_at.strftime('%Y-%m-%d') }}</div>
<small class="text-muted">{{ order.created_at.strftime('%H:%M:%S') }}</small>
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('admin.order_detail', order_id=order.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> 详情
</a>
{% if order.status == 2 %}
<button class="btn btn-sm btn-outline-success"
onclick="showShipModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-truck"></i> 发货
</button>
{% endif %}
{% if order.status in [2, 3] %}
<button class="btn btn-sm btn-outline-warning"
onclick="showRefundModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-arrow-return-left"></i> 退款
</button>
{% endif %}
{% if order.can_cancel() %}
<button class="btn btn-sm btn-outline-danger"
onclick="showCancelModal({{ order.id }}, '{{ order.order_sn }}')">
<i class="bi bi-x-circle"></i> 取消
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if orders.pages > 1 %}
<nav aria-label="订单分页">
<ul class="pagination justify-content-center">
{% if orders.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.orders', page=orders.prev_num, search=search, status=status, start_date=start_date, end_date=end_date) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in orders.iter_pages() %}
{% if page_num %}
{% if page_num != orders.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.orders', page=page_num, search=search, status=status, start_date=start_date, end_date=end_date) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if orders.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.orders', page=orders.next_num, search=search, status=status, start_date=start_date, end_date=end_date) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-4">
<i class="bi bi-inbox display-1 text-muted"></i>
<p class="text-muted mt-2">暂无订单数据</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 发货模态框 -->
<div class="modal fade" id="shipModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">订单发货</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="shipForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="shipOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">物流公司 <span class="text-danger">*</span></label>
<select class="form-select" name="shipping_company" required>
<option value="">请选择物流公司</option>
<option value="顺丰速运">顺丰速运</option>
<option value="圆通速递">圆通速递</option>
<option value="中通快递">中通快递</option>
<option value="申通快递">申通快递</option>
<option value="韵达速递">韵达速递</option>
<option value="百世快递">百世快递</option>
<option value="德邦快递">德邦快递</option>
<option value="京东物流">京东物流</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">快递单号 <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="tracking_number" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-truck"></i> 确认发货
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 退款模态框 -->
<div class="modal fade" id="refundModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">订单退款</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="refundForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="refundOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">退款原因 <span class="text-danger">*</span></label>
<textarea class="form-control" name="refund_reason" rows="3" required placeholder="请输入退款原因"></textarea>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
退款后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-warning">
<i class="bi bi-arrow-return-left"></i> 确认退款
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 取消模态框 -->
<div class="modal fade" id="cancelModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">取消订单</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="cancelForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">订单号</label>
<input type="text" class="form-control" id="cancelOrderSn" readonly>
</div>
<div class="mb-3">
<label class="form-label">取消原因</label>
<textarea class="form-control" name="cancel_reason" rows="3" placeholder="请输入取消原因(可选)"></textarea>
</div>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i>
取消订单后将自动恢复库存,减少销量统计
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-danger">
<i class="bi bi-x-circle"></i> 确认取消
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_orders.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/admin/product_form.html
📊 大小: 33693 bytes (32.90 KB)
🕒 修改时间: 2025-07-03 15:08:21
🔸==============================================================================
{% extends "admin/base.html" %}
{% block title %}
{% if product %}编辑商品{% else %}添加商品{% endif %} - 太白购物商城管理后台
{% endblock %}
{% block page_title %}
{% if product %}编辑商品{% else %}添加商品{% endif %}
{% endblock %}
{% block page_description %}
商品信息管理
{% endblock %}
{% block extra_css %}
<style>
.image-upload-area {
border: 2px dashed #dee2e6;
border-radius: 10px;
padding: 30px;
text-align: center;
background-color: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
}
.image-upload-area:hover {
border-color: #0d6efd;
background-color: #e3f2fd;
}
.image-upload-area.dragover {
border-color: #0d6efd;
background-color: #e3f2fd;
}
.image-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.image-item {
position: relative;
border-radius: 8px;
overflow: hidden;
border: 2px solid #dee2e6;
transition: all 0.3s ease;
}
.image-item:hover {
border-color: #0d6efd;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.image-item.main-image {
border-color: #198754;
}
.image-item img {
width: 100%;
height: 120px;
object-fit: cover;
}
.image-controls {
position: absolute;
top: 5px;
right: 5px;
display: flex;
gap: 5px;
}
.image-controls .btn {
padding: 2px 6px;
font-size: 12px;
}
.main-badge {
position: absolute;
bottom: 5px;
left: 5px;
background-color: rgba(25, 135, 84, 0.9);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
}
.upload-progress {
display: none;
margin-top: 10px;
}
.inventory-table {
font-size: 0.9rem;
}
.inventory-table th {
background-color: #f8f9fa;
font-weight: 600;
}
.spec-selector {
max-height: 200px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 10px;
}
.spec-group {
margin-bottom: 15px;
}
.spec-option {
margin: 2px;
}
.inventory-row {
background-color: #f8f9fa;
}
.inventory-row:nth-child(even) {
background-color: white;
}
</style>
{% endblock %}
{% block content %}
<form method="POST" action="{{ url_for('product.save') }}" id="productForm">
{% if product %}
<input type="hidden" name="product_id" value="{{ product.id }}">
{% endif %}
<div class="row">
<!-- 基本信息 -->
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-info-circle"></i> 基本信息
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="name" class="form-label">商品名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" name="name"
value="{{ product.name if product else '' }}" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="category_id" class="form-label">商品分类 <span class="text-danger">*</span></label>
<select name="category_id" id="category_id" class="form-select" required>
<option value="">选择分类</option>
{% for category in categories %}
<option value="{{ category.id }}"
{% if product and product.category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="price" class="form-label">销售价格 <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text">¥</span>
<input type="number" class="form-control" id="price" name="price"
step="0.01" min="0" value="{{ product.price if product else '' }}" required>
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="original_price" class="form-label">原价</label>
<div class="input-group">
<span class="input-group-text">¥</span>
<input type="number" class="form-control" id="original_price" name="original_price"
step="0.01" min="0" value="{{ product.original_price if product else '' }}">
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="brand" class="form-label">品牌</label>
<input type="text" class="form-control" id="brand" name="brand"
value="{{ product.brand if product else '' }}">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="weight" class="form-label">重量 (kg)</label>
<input type="number" class="form-control" id="weight" name="weight"
step="0.01" min="0" value="{{ product.weight if product else '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="status" class="form-label">状态</label>
<select name="status" id="status" class="form-select">
<option value="1" {% if not product or product.status == 1 %}selected{% endif %}>上架</option>
<option value="0" {% if product and product.status == 0 %}selected{% endif %}>下架</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">商品描述</label>
<textarea class="form-control" id="description" name="description" rows="5">{{ product.description if product else '' }}</textarea>
</div>
</div>
</div>
<!-- 库存管理 -->
<div class="card mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-boxes"></i> 库存管理
</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="inventory_type"
id="single_sku" value="single" checked onchange="toggleInventoryType()">
<label class="form-check-label" for="single_sku">
单一规格(无变体)
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="inventory_type"
id="multi_sku" value="multi" onchange="toggleInventoryType()">
<label class="form-check-label" for="multi_sku">
多规格(有变体)
</label>
</div>
</div>
</div>
<!-- 单一规格库存 -->
<div id="singleInventorySection">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="single_stock" class="form-label">库存数量</label>
<input type="number" class="form-control" id="single_stock"
name="single_stock" min="0" value="0">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="warning_stock" class="form-label">预警库存</label>
<input type="number" class="form-control" id="warning_stock"
name="warning_stock" min="0" value="10">
</div>
</div>
</div>
</div>
<!-- 多规格管理 -->
<div id="multiInventorySection" style="display: none;">
<!-- 规格选择 -->
<div class="mb-3">
<label class="form-label">选择规格类型:</label>
<div class="spec-selector">
{% for spec_name in spec_names %}
<div class="spec-group">
<div class="form-check">
<input class="form-check-input spec-checkbox" type="checkbox"
id="spec_{{ spec_name.id }}" value="{{ spec_name.id }}"
data-spec-name="{{ spec_name.name }}" onchange="updateSpecValues()">
<label class="form-check-label fw-bold" for="spec_{{ spec_name.id }}">
{{ spec_name.name }}
</label>
</div>
<div class="spec-values ms-3" id="values_{{ spec_name.id }}" style="display: none;">
{% for value in spec_name.values %}
<div class="form-check form-check-inline">
<input class="form-check-input spec-value-checkbox" type="checkbox"
id="value_{{ value.id }}" value="{{ value.value }}"
data-spec-id="{{ spec_name.id }}" data-spec-name="{{ spec_name.name }}">
<label class="form-check-label" for="value_{{ value.id }}">
{{ value.value }}
</label>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="mb-3">
<button type="button" class="btn btn-success" onclick="generateSkuTable()">
<i class="bi bi-gear"></i> 生成SKU表格
</button>
</div>
<!-- SKU表格 -->
<div id="skuTableContainer" style="display: none;">
<div class="table-responsive">
<table class="table table-bordered inventory-table" id="skuTable">
<thead>
<tr>
<th>规格组合</th>
<th>SKU编码</th>
<th>价格调整</th>
<th>库存数量</th>
<th>预警库存</th>
<th>默认规格</th>
</tr>
</thead>
<tbody id="skuTableBody">
</tbody>
</table>
</div>
</div>
</div>
<!-- 现有库存信息展示(编辑时) -->
{% if product and product.inventory %}
<div class="mt-4">
<h6>当前库存信息:</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>SKU编码</th>
<th>规格组合</th>
<th>库存</th>
<th>价格</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for inventory in product.inventory %}
<tr>
<td>{{ inventory.sku_code }}</td>
<td>
{% if inventory.spec_combination %}
{% for key, value in inventory.spec_combination.items() %}
{{ key }}:{{ value }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
默认规格
{% endif %}
</td>
<td>{{ inventory.stock }}</td>
<td>¥{{ "%.2f"|format(inventory.get_final_price()) }}</td>
<td>
{% if inventory.status %}
<span class="badge bg-success">启用</span>
{% else %}
<span class="badge bg-secondary">禁用</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
</div>
<!-- 商品图片 -->
{% if product %}
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-images"></i> 商品图片
</h5>
</div>
<div class="card-body">
<!-- 图片上传区域 -->
<div class="image-upload-area" id="imageUploadArea">
<i class="bi bi-cloud-upload fs-1 text-muted"></i>
<h5 class="mt-2">拖拽图片到这里或点击选择</h5>
<p class="text-muted mb-0">支持 JPG、PNG、GIF 格式,单张图片不超过 5MB</p>
<input type="file" id="imageInput" multiple accept="image/*" style="display: none;">
</div>
<!-- 上传进度 -->
<div class="upload-progress">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted">上传中...</small>
</div>
<!-- 图片画廊 -->
<div class="image-gallery" id="imageGallery">
{% if product.images %}
{% for image in product.images|sort(attribute='sort_order') %}
<div class="image-item {% if image.is_main %}main-image{% endif %}" data-id="{{ image.id }}">
<img src="{{ image.image_url }}" alt="商品图片">
<div class="image-controls">
{% if not image.is_main %}
<button type="button" class="btn btn-success btn-sm"
onclick="setMainImage({{ image.id }})" title="设为主图">
<i class="bi bi-star"></i>
</button>
{% endif %}
<button type="button" class="btn btn-danger btn-sm"
onclick="deleteImage({{ image.id }})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
{% if image.is_main %}
<div class="main-badge">主图</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- 侧边栏 -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-gear"></i> 操作
</h5>
</div>
<div class="card-body">
<button type="submit" class="btn btn-primary w-100 mb-2">
<i class="bi bi-check-lg"></i> 保存商品
</button>
{% if product %}
<a href="{{ url_for('product.index') }}" class="btn btn-outline-secondary w-100 mb-2">
<i class="bi bi-arrow-left"></i> 返回列表
</a>
<button type="button" class="btn btn-outline-danger w-100"
onclick="deleteProduct({{ product.id }})">
<i class="bi bi-trash"></i> 删除商品
</button>
{% else %}
<a href="{{ url_for('product.index') }}" class="btn btn-outline-secondary w-100">
<i class="bi bi-arrow-left"></i> 返回列表
</a>
{% endif %}
</div>
</div>
{% if product %}
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-info-circle"></i> 商品信息
</h5>
</div>
<div class="card-body">
<div class="mb-2">
<strong>商品ID</strong>{{ product.id }}
</div>
<div class="mb-2">
<strong>销量:</strong>{{ product.sales_count }}
</div>
<div class="mb-2">
<strong>浏览量:</strong>{{ product.view_count }}
</div>
<div class="mb-2">
<strong>创建时间:</strong><br>
{{ product.created_at.strftime('%Y-%m-%d %H:%M:%S') if product.created_at else '' }}
</div>
<div class="mb-2">
<strong>更新时间:</strong><br>
{{ product.updated_at.strftime('%Y-%m-%d %H:%M:%S') if product.updated_at else '' }}
</div>
</div>
</div>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block extra_js %}
<script>
let skuCounter = 0;
// 切换库存类型
function toggleInventoryType() {
const singleSection = document.getElementById('singleInventorySection');
const multiSection = document.getElementById('multiInventorySection');
const singleRadio = document.getElementById('single_sku');
if (singleRadio.checked) {
singleSection.style.display = 'block';
multiSection.style.display = 'none';
} else {
singleSection.style.display = 'none';
multiSection.style.display = 'block';
}
}
// 更新规格值显示
function updateSpecValues() {
document.querySelectorAll('.spec-checkbox').forEach(checkbox => {
const valuesDiv = document.getElementById('values_' + checkbox.value);
if (checkbox.checked) {
valuesDiv.style.display = 'block';
} else {
valuesDiv.style.display = 'none';
// 取消选中该规格下的所有值
valuesDiv.querySelectorAll('.spec-value-checkbox').forEach(valueCheckbox => {
valueCheckbox.checked = false;
});
}
});
}
// 生成SKU表格
function generateSkuTable() {
const selectedSpecs = {};
// 收集选中的规格和值
document.querySelectorAll('.spec-value-checkbox:checked').forEach(checkbox => {
const specId = checkbox.getAttribute('data-spec-id');
const specName = checkbox.getAttribute('data-spec-name');
const value = checkbox.value;
if (!selectedSpecs[specName]) {
selectedSpecs[specName] = [];
}
selectedSpecs[specName].push(value);
});
if (Object.keys(selectedSpecs).length === 0) {
alert('请先选择规格');
return;
}
// 生成SKU组合
const combinations = generateCombinations(selectedSpecs);
// 创建表格
const tableBody = document.getElementById('skuTableBody');
tableBody.innerHTML = '';
combinations.forEach((combination, index) => {
const row = document.createElement('tr');
row.className = 'inventory-row';
// 规格组合显示
const specText = Object.entries(combination).map(([key, value]) => `${key}:${value}`).join(', ');
// 生成SKU编码
const skuCode = generateSkuCode(combination);
row.innerHTML = `
<td>${specText}</td>
<td>
<input type="text" class="form-control form-control-sm"
name="sku_codes[]" value="${skuCode}" required>
<input type="hidden" name="spec_combinations[]" value='${JSON.stringify(combination)}'>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-text">¥</span>
<input type="number" class="form-control" name="price_adjustments[]"
value="0" step="0.01">
</div>
</td>
<td>
<input type="number" class="form-control form-control-sm"
name="stocks[]" value="0" min="0" required>
</td>
<td>
<input type="number" class="form-control form-control-sm"
name="warning_stocks[]" value="10" min="0">
</td>
<td>
<div class="form-check">
<input class="form-check-input" type="radio" name="default_sku"
value="${index}" ${index === 0 ? 'checked' : ''}>
</div>
</td>
`;
tableBody.appendChild(row);
});
document.getElementById('skuTableContainer').style.display = 'block';
}
// 生成组合
function generateCombinations(specs) {
const keys = Object.keys(specs);
const values = Object.values(specs);
function cartesian(arrays) {
return arrays.reduce((a, b) =>
a.flatMap(x => b.map(y => [...x, y])), [[]]
);
}
const combinations = cartesian(values);
return combinations.map(combination => {
const result = {};
keys.forEach((key, index) => {
result[key] = combination[index];
});
return result;
});
}
// 生成SKU编码
function generateSkuCode(combination) {
const productName = document.getElementById('name').value || 'PRODUCT';
const shortName = productName.substring(0, 3).toUpperCase();
const specCode = Object.values(combination).map(v => v.substring(0, 2)).join('');
const timestamp = Date.now().toString().slice(-4);
return `${shortName}-${specCode}-${timestamp}`;
}
// 表单提交前的验证
document.getElementById('productForm').addEventListener('submit', function(e) {
const inventoryType = document.querySelector('input[name="inventory_type"]:checked').value;
if (inventoryType === 'single') {
// 单规格验证
const stock = document.getElementById('single_stock').value;
if (!stock || stock < 0) {
e.preventDefault();
alert('请输入正确的库存数量');
return;
}
} else {
// 多规格验证
const skuTable = document.getElementById('skuTableContainer');
if (skuTable.style.display === 'none') {
e.preventDefault();
alert('请生成SKU表格并设置库存信息');
return;
}
const stockInputs = document.querySelectorAll('input[name="stocks[]"]');
if (stockInputs.length === 0) {
e.preventDefault();
alert('请添加至少一个SKU');
return;
}
}
});
{% if product %}
// 图片上传功能
document.addEventListener('DOMContentLoaded', function() {
const uploadArea = document.getElementById('imageUploadArea');
const imageInput = document.getElementById('imageInput');
const imageGallery = document.getElementById('imageGallery');
const progressDiv = document.querySelector('.upload-progress');
const progressBar = document.querySelector('.progress-bar');
// 点击上传区域
uploadArea.addEventListener('click', function() {
imageInput.click();
});
// 文件选择
imageInput.addEventListener('change', function(e) {
uploadImages(e.target.files);
});
// 拖拽上传
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
uploadImages(e.dataTransfer.files);
});
// 上传图片
function uploadImages(files) {
if (files.length === 0) return;
const formData = new FormData();
for (let file of files) {
formData.append('files', file);
}
// 显示进度条
progressDiv.style.display = 'block';
progressBar.style.width = '0%';
fetch(`/admin/products/upload-images/{{ product.id }}`, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
progressDiv.style.display = 'none';
if (data.success) {
// 添加新图片到画廊
data.images.forEach(image => {
addImageToGallery(image);
});
alert(`成功上传 ${data.images.length} 张图片`);
} else {
alert('上传失败: ' + data.message);
}
})
.catch(error => {
progressDiv.style.display = 'none';
alert('上传失败: ' + error);
});
}
// 添加图片到画廊
function addImageToGallery(image) {
const imageItem = document.createElement('div');
imageItem.className = 'image-item';
imageItem.setAttribute('data-id', image.id);
imageItem.innerHTML = `
<img src="${image.url}" alt="商品图片">
<div class="image-controls">
<button type="button" class="btn btn-success btn-sm"
onclick="setMainImage(${image.id})" title="设为主图">
<i class="bi bi-star"></i>
</button>
<button type="button" class="btn btn-danger btn-sm"
onclick="deleteImage(${image.id})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
`;
imageGallery.appendChild(imageItem);
}
});
// 设置主图
function setMainImage(imageId) {
fetch(`/admin/products/set-main-image/${imageId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新UI
document.querySelectorAll('.image-item').forEach(item => {
item.classList.remove('main-image');
const badge = item.querySelector('.main-badge');
if (badge) badge.remove();
const starBtn = item.querySelector('.btn-success');
if (starBtn) starBtn.style.display = 'block';
});
const currentItem = document.querySelector(`[data-id="${imageId}"]`);
currentItem.classList.add('main-image');
currentItem.querySelector('.btn-success').style.display = 'none';
const badge = document.createElement('div');
badge.className = 'main-badge';
badge.textContent = '主图';
currentItem.appendChild(badge);
alert('主图设置成功');
} else {
alert('设置失败: ' + data.message);
}
})
.catch(error => {
alert('设置失败: ' + error);
});
}
// 删除图片
function deleteImage(imageId) {
if (confirm('确定要删除这张图片吗?')) {
fetch(`/admin/products/delete-image/${imageId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.querySelector(`[data-id="${imageId}"]`).remove();
alert('图片删除成功');
} else {
alert('删除失败: ' + data.message);
}
})
.catch(error => {
alert('删除失败: ' + error);
});
}
}
// 删除商品
function deleteProduct(productId) {
if (confirm('确定要删除这个商品吗?此操作不可恢复!')) {
fetch(`/admin/products/delete/${productId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('商品删除成功');
window.location.href = '{{ url_for("product.index") }}';
} else {
alert('删除失败: ' + data.message);
}
})
.catch(error => {
alert('删除失败: ' + error);
});
}
}
{% endif %}
</script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/admin/products.html
📊 大小: 17989 bytes (17.57 KB)
🕒 修改时间: 2025-07-03 15:15:27
🔸==============================================================================
{% extends "admin/base.html" %}
{% block title %}商品管理 - 太白购物商城管理后台{% endblock %}
{% block page_title %}商品管理{% endblock %}
{% block page_description %}商品信息管理{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-md-8">
<!-- 搜索和筛选 -->
<form method="GET" class="d-flex">
<input type="text" class="form-control me-2" name="search"
placeholder="搜索商品名称..." value="{{ search }}">
<select name="category_id" class="form-select me-2">
<option value="">全部分类</option>
{% for category in categories %}
<option value="{{ category.id }}"
{% if category_id == category.id|string %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
<select name="status" class="form-select me-2">
<option value="">全部状态</option>
<option value="1" {% if status == '1' %}selected{% endif %}>上架</option>
<option value="0" {% if status == '0' %}selected{% endif %}>下架</option>
</select>
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> 搜索
</button>
</form>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('product.add') }}" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> 添加商品
</a>
<a href="{{ url_for('product.categories') }}" class="btn btn-outline-secondary">
<i class="bi bi-tags"></i> 分类管理
</a>
</div>
</div>
<!-- 商品列表 -->
<div class="card admin-table">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th width="80">ID</th>
<th width="100">商品图片</th>
<th>商品名称</th>
<th width="120">分类</th>
<th width="100">价格</th>
<th width="100">库存</th>
<th width="80">状态</th>
<th width="100">销量</th>
<th width="120">创建时间</th>
<th width="150">操作</th>
</tr>
</thead>
<tbody>
{% if products.items %}
{% for product in products.items %}
<tr>
<td>{{ product.id }}</td>
<td>
{% if product.main_image %}
<img src="{{ product.main_image }}" class="img-thumbnail"
style="width: 60px; height: 60px; object-fit: cover;">
{% else %}
<div class="bg-light text-center d-flex align-items-center justify-content-center"
style="width: 60px; height: 60px;">
<i class="bi bi-image text-muted"></i>
</div>
{% endif %}
</td>
<td>
<div>
<strong>{{ product.name[:40] }}{% if product.name|length > 40 %}...{% endif %}</strong>
{% if product.brand %}
<br><small class="text-muted"><i class="bi bi-award"></i> {{ product.brand }}</small>
{% endif %}
{% if product.has_specs %}
<br><span class="badge bg-info badge-sm">多规格</span>
{% endif %}
</div>
</td>
<td>
<span class="badge bg-secondary">{{ product.category.name if product.category else '未分类' }}</span>
</td>
<td>
<strong class="text-danger">¥{{ "%.2f"|format(product.price) }}</strong>
{% if product.original_price and product.original_price > product.price %}
<br><small class="text-muted text-decoration-line-through">
¥{{ "%.2f"|format(product.original_price) }}
</small>
{% endif %}
</td>
<td>
{% set total_stock = product.inventory|sum(attribute='stock') if product.inventory else 0 %}
{% set sku_count = product.inventory|length if product.inventory else 0 %}
<div class="text-center">
<span class="fw-bold {% if total_stock <= 0 %}text-danger{% elif total_stock <= 10 %}text-warning{% else %}text-success{% endif %}">
{{ total_stock }}
</span>
{% if sku_count > 1 %}
<br><small class="text-muted">{{ sku_count }}个SKU</small>
{% endif %}
{% if total_stock <= 0 %}
<br><small class="text-danger">缺货</small>
{% elif total_stock <= 10 %}
<br><small class="text-warning">库存不足</small>
{% endif %}
</div>
</td>
<td>
{% if product.status == 1 %}
<span class="badge bg-success">上架</span>
{% else %}
<span class="badge bg-secondary">下架</span>
{% endif %}
</td>
<td class="text-center">
<div>{{ product.sales_count }}</div>
<small class="text-muted">浏览:{{ product.view_count }}</small>
</td>
<td>
<div class="text-center">
{{ product.created_at.strftime('%m-%d') if product.created_at else '' }}
<br><small class="text-muted">{{ product.created_at.strftime('%H:%M') if product.created_at else '' }}</small>
</div>
</td>
<td>
<div class="btn-group-vertical btn-group-sm" role="group">
<a href="{{ url_for('product.edit', product_id=product.id) }}"
class="btn btn-outline-primary btn-sm" title="编辑">
<i class="bi bi-pencil"></i> 编辑
</a>
{% if product.inventory %}
<button class="btn btn-outline-info btn-sm"
onclick="showInventory({{ product.id }})" title="库存详情">
<i class="bi bi-boxes"></i> 库存
</button>
{% endif %}
<button class="btn btn-outline-danger btn-sm"
onclick="deleteProduct({{ product.id }})" title="删除">
<i class="bi bi-trash"></i> 删除
</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="10" class="text-center py-4">
<div class="text-muted">
<i class="bi bi-inbox display-4"></i>
<p class="mt-2">暂无商品数据</p>
{% if search or category_id or status %}
<a href="{{ url_for('product.index') }}" class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-clockwise"></i> 清除筛选
</a>
{% endif %}
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if products.pages > 1 %}
<div class="card-footer bg-white">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
显示第 {{ (products.page - 1) * products.per_page + 1 }} -
{{ products.page * products.per_page if products.page * products.per_page < products.total else products.total }}
条,共 {{ products.total }} 条
</small>
<nav aria-label="商品分页">
<ul class="pagination pagination-sm mb-0">
{% if products.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('product.index', page=products.prev_num, search=search, category_id=category_id, status=status) }}">
<i class="bi bi-chevron-left"></i> 上一页
</a>
</li>
{% endif %}
{% for page_num in products.iter_pages() %}
{% if page_num %}
{% if page_num != products.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('product.index', page=page_num, search=search, category_id=category_id, status=status) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if products.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('product.index', page=products.next_num, search=search, category_id=category_id, status=status) }}">
下一页 <i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% endif %}
</div>
<!-- 库存详情模态框 -->
<div class="modal fade" id="inventoryModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-boxes"></i> 库存详情
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="inventoryModalBody">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">加载中...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function deleteProduct(productId) {
if (confirm('确定要删除这个商品吗?此操作不可恢复!\n删除商品将同时删除所有相关的库存信息。')) {
fetch(`/admin/products/delete/${productId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage('商品删除成功');
setTimeout(() => {
location.reload();
}, 1000);
} else {
alert('删除失败: ' + data.message);
}
})
.catch(error => {
alert('删除失败: ' + error);
});
}
}
function showInventory(productId) {
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
const modalBody = document.getElementById('inventoryModalBody');
// 显示加载状态
modalBody.innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">正在加载库存信息...</p>
</div>
`;
modal.show();
// 获取库存信息
fetch(`/admin/products/inventory/${productId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
displayInventoryInfo(data.inventory);
} else {
modalBody.innerHTML = `<div class="alert alert-danger">加载失败: ${data.message}</div>`;
}
})
.catch(error => {
modalBody.innerHTML = `<div class="alert alert-danger">加载失败: ${error}</div>`;
});
}
function displayInventoryInfo(inventoryList) {
const modalBody = document.getElementById('inventoryModalBody');
if (!inventoryList || inventoryList.length === 0) {
modalBody.innerHTML = '<div class="alert alert-info">暂无库存信息</div>';
return;
}
let html = `
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>SKU编码</th>
<th>规格组合</th>
<th>库存数量</th>
<th>预警库存</th>
<th>价格调整</th>
<th>状态</th>
</tr>
</thead>
<tbody>
`;
inventoryList.forEach(item => {
const specText = item.spec_combination ?
Object.entries(item.spec_combination).map(([k, v]) => `${k}:${v}`).join(', ') :
'默认规格';
const stockClass = item.stock <= 0 ? 'text-danger' :
item.stock <= item.warning_stock ? 'text-warning' : 'text-success';
html += `
<tr>
<td>
<code>${item.sku_code}</code>
${item.is_default ? '<span class="badge bg-primary badge-sm ms-1">默认</span>' : ''}
</td>
<td>${specText}</td>
<td class="${stockClass} fw-bold">${item.stock}</td>
<td>${item.warning_stock}</td>
<td>
${item.price_adjustment > 0 ? '+' : ''}¥${item.price_adjustment.toFixed(2)}
</td>
<td>
${item.status ? '<span class="badge bg-success">启用</span>' : '<span class="badge bg-secondary">禁用</span>'}
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
modalBody.innerHTML = html;
}
function showSuccessMessage(message) {
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed';
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 3000);
}
// 页面加载完成后的初始化
document.addEventListener('DOMContentLoaded', function() {
// 添加表格行悬停效果
const tableRows = document.querySelectorAll('.table tbody tr');
tableRows.forEach(row => {
row.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#f8f9fa';
});
row.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
});
</script>
<style>
.badge-sm {
font-size: 0.7em;
}
.btn-group-vertical .btn {
margin-bottom: 2px;
}
.table td {
vertical-align: middle;
}
.img-thumbnail {
border-radius: 8px;
}
.admin-table .table thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
}
@media (max-width: 768px) {
.btn-group-vertical {
width: 100%;
}
.btn-group-vertical .btn {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
}
}
</style>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/admin/profile.html
📊 大小: 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin_profile.css') }}">
{% endblock %}
{% block content %}
<div class="profile-container">
<div class="row">
<div class="col-md-8">
<div class="card profile-card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-person-gear"></i>
基本信息
</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('admin.edit_profile') }}">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username"
value="{{ admin.username }}" readonly>
<div class="form-text">用户名不可修改</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="real_name" class="form-label">真实姓名</label>
<input type="text" class="form-control" id="real_name" name="real_name"
value="{{ admin.real_name or '' }}">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="email" class="form-label">邮箱地址</label>
<input type="email" class="form-control" id="email" name="email"
value="{{ admin.email or '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="phone" class="form-label">手机号</label>
<input type="tel" class="form-control" id="phone" name="phone"
value="{{ admin.phone or '' }}">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> 保存修改
</button>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card info-card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-info-circle"></i>
账号信息
</h5>
</div>
<div class="card-body">
<div class="info-item">
<strong>角色:</strong>
<span class="badge bg-success">{{ admin.role }}</span>
</div>
<div class="info-item">
<strong>状态:</strong>
{% if admin.status == 1 %}
<span class="badge bg-success">正常</span>
{% else %}
<span class="badge bg-danger">禁用</span>
{% endif %}
</div>
<div class="info-item">
<strong>创建时间:</strong><br>
{{ admin.created_at.strftime('%Y-%m-%d %H:%M:%S') if admin.created_at else '' }}
</div>
<div class="info-item">
<strong>最后登录:</strong><br>
{{ admin.last_login_at.strftime('%Y-%m-%d %H:%M:%S') if admin.last_login_at else '从未登录' }}
</div>
</div>
</div>
<!-- 修改密码 -->
<div class="card password-card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-key"></i>
修改密码
</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('admin.change_password') }}">
<div class="mb-3">
<label for="current_password" class="form-label">当前密码</label>
<input type="password" class="form-control" id="current_password"
name="current_password" required>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">新密码</label>
<input type="password" class="form-control" id="new_password"
name="new_password" required>
<div class="form-text">密码长度至少6位建议包含字母和数字</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirm_password"
name="confirm_password" required>
</div>
<button type="submit" class="btn btn-warning">
<i class="bi bi-key"></i> 修改密码
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% 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 %}
<link href="{{ url_for('static', filename='css/admin_users.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="admin-users">
<!-- 搜索和筛选 -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label for="search" class="form-label">搜索用户</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="用户名、邮箱、手机号、昵称">
</div>
<div class="col-md-3">
<label for="status" class="form-label">状态筛选</label>
<select class="form-select" id="status" name="status">
<option value="">全部状态</option>
<option value="1" {% if status == '1' %}selected{% endif %}>正常</option>
<option value="0" {% if status == '0' %}selected{% endif %}>禁用</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 搜索
</button>
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> 重置
</a>
</div>
</div>
</form>
</div>
</div>
<!-- 用户统计 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ users.total }}</h5>
<p class="card-text">总用户数</p>
</div>
<div class="icon-wrapper primary">
<i class="bi bi-people"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ users.items | selectattr('status', 'equalto', 1) | list | length }}</h5>
<p class="card-text">正常用户</p>
</div>
<div class="icon-wrapper success">
<i class="bi bi-person-check"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ users.items | selectattr('status', 'equalto', 0) | list | length }}</h5>
<p class="card-text">禁用用户</p>
</div>
<div class="icon-wrapper danger">
<i class="bi bi-person-x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title">{{ week_new_users }}</h5>
<p class="card-text">本周新增</p>
</div>
<div class="icon-wrapper info">
<i class="bi bi-person-plus"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 用户列表 -->
<div class="card">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="bi bi-people"></i>
用户列表
<small class="text-muted ms-2">共 {{ users.total }} 条记录</small>
</h5>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>用户ID</th>
<th>用户信息</th>
<th>联系方式</th>
<th>注册时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% if users.items %}
{% for user in users.items %}
<tr>
<td>
<strong>#{{ user.id }}</strong>
</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-wrapper me-3">
{% if user.avatar_url %}
<img src="{{ user.avatar_url }}" alt="头像" class="user-avatar">
{% else %}
<div class="user-avatar-placeholder">
<i class="bi bi-person"></i>
</div>
{% endif %}
</div>
<div>
<div class="fw-bold">{{ user.username }}</div>
{% if user.nickname %}
<small class="text-muted">{{ user.nickname }}</small>
{% endif %}
</div>
</div>
</td>
<td>
<div>
{% if user.email %}
<div><i class="bi bi-envelope"></i> {{ user.email }}</div>
{% endif %}
{% if user.phone %}
<div><i class="bi bi-phone"></i> {{ user.phone }}</div>
{% endif %}
{% if not user.email and not user.phone %}
<span class="text-muted">未设置</span>
{% endif %}
</div>
</td>
<td>
<div>{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else '-' }}</div>
<small class="text-muted">{{ user.created_at.strftime('%H:%M:%S') if user.created_at else '' }}</small>
</td>
<td>
<span class="badge bg-{{ 'success' if user.status == 1 else 'danger' }}">
{{ '正常' if user.status == 1 else '禁用' }}
</span>
</td>
<td>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="viewUser({{ user.id }})">
<i class="bi bi-eye"></i> 查看
</button>
<button type="button" class="btn btn-sm btn-outline-{{ 'warning' if user.status == 1 else 'success' }}"
onclick="toggleUserStatus({{ user.id }}, {{ user.status }})">
<i class="bi bi-{{ 'person-x' if user.status == 1 else 'person-check' }}"></i>
{{ '禁用' if user.status == 1 else '启用' }}
</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center py-4">
<div class="empty-state">
<i class="bi bi-person-slash"></i>
<div>暂无用户数据</div>
{% if search or status %}
<small class="text-muted">尝试调整搜索条件</small>
{% endif %}
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if users.pages > 1 %}
<div class="card-footer bg-white">
<nav aria-label="用户列表分页">
<ul class="pagination justify-content-center mb-0">
{% if users.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.prev_num, search=search, status=status) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in users.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
{% if page_num %}
{% if page_num != users.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=page_num, search=search, status=status) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if users.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.next_num, search=search, status=status) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
<!-- 用户详情模态框 -->
<div class="modal fade" id="userDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">用户详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="userDetailContent">
<!-- 用户详情内容将通过AJAX加载 -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/admin_users.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/base.html
📊 大小: 5960 bytes (5.82 KB)
🕒 修改时间: 2025-07-09 04:36:54
🔸==============================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}太白购物商城{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
<!-- 自定义CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}">
<i class="bi bi-shop"></i> 太白购物商城
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.product_list') }}">全部商品</a>
</li>
</ul>
<!-- 搜索框 -->
<form class="d-flex me-3" method="GET" action="{{ url_for('main.product_list') }}">
<input class="form-control me-2" type="search" name="search" placeholder="搜索商品..."
value="{{ request.args.get('search', '') }}">
<button class="btn btn-outline-primary" type="submit">
<i class="bi bi-search"></i>
</button>
</form>
<ul class="navbar-nav">
{% if session.user_id %}
<li class="nav-item">
<a class="nav-link position-relative" href="{{ url_for('cart.index') }}">
<i class="bi bi-cart"></i> 购物车
<span class="badge bg-danger cart-badge" id="cartBadge" style="display: none;">0</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> {{ session.nickname or session.username }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('user.profile') }}">个人中心</a></li>
<li><a class="dropdown-item" href="{{ url_for('user.orders') }}">我的订单</a></li>
<li><a class="dropdown-item" href="{{ url_for('favorite.index') }}">我的收藏</a></li>
<li><a class="dropdown-item" href="{{ url_for('history.index') }}">浏览历史</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">退出登录</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">注册</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- 主要内容 -->
<main class="container mt-4">
<!-- Flash消息提示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<!-- 页脚 -->
<footer class="bg-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>太白购物商城</h5>
<p class="text-muted">您身边的购物专家</p>
</div>
<div class="col-md-6">
<h6>联系我们</h6>
<p class="text-muted">
<i class="bi bi-envelope"></i> service@taibai-mall.com<br>
<i class="bi bi-telephone"></i> 400-888-8888
</p>
</div>
</div>
<hr>
<div class="text-center text-muted">
<small>&copy; 2025 太白购物商城. 保留所有权利.</small>
</div>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- 自定义JavaScript -->
<script src="{{ url_for('static', filename='js/base.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
🔸==============================================================================
📄 文件: app/templates/cart/index.html
📊 大小: 10126 bytes (9.89 KB)
🕒 修改时间: 2025-07-04 14:42:12
🔸==============================================================================
{% extends "base.html" %}
{% block title %}购物车 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/cart.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
<li class="breadcrumb-item active">购物车</li>
</ol>
</nav>
<div class="row">
<div class="col-12">
<h3><i class="bi bi-cart"></i> 我的购物车</h3>
<hr>
</div>
</div>
{% if cart_items %}
<div class="row">
<!-- 购物车商品列表 -->
<div class="col-md-8">
<div class="card">
<div class="card-header">
<div class="row align-items-center">
<div class="col-1">
<input type="checkbox" class="form-check-input" id="selectAll">
</div>
<div class="col-5">商品信息</div>
<div class="col-2 text-center">单价</div>
<div class="col-2 text-center">数量</div>
<div class="col-2 text-center">操作</div>
</div>
</div>
<div class="card-body p-0">
{% for item in cart_items %}
<div class="cart-item border-bottom p-3" data-cart-id="{{ item.id }}">
<div class="row align-items-center">
<!-- 选择框 -->
<div class="col-1">
<input type="checkbox" class="form-check-input item-checkbox"
value="{{ item.id }}" {% if item.is_available() %}{% else %}disabled{% endif %}>
</div>
<!-- 商品信息 -->
<div class="col-5">
<div class="d-flex">
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}"
class="text-decoration-none">
{% if item.product.main_image %}
<img src="{{ item.product.main_image }}" alt="{{ item.product.name }}"
class="me-3" style="width: 80px; height: 80px; object-fit: cover;">
{% else %}
<div class="bg-light me-3 d-flex align-items-center justify-content-center"
style="width: 80px; height: 80px;">
<i class="bi bi-image text-muted"></i>
</div>
{% endif %}
</a>
<div class="flex-grow-1">
<h6 class="mb-1">
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}"
class="text-decoration-none text-dark">
{{ item.product.name }}
</a>
</h6>
{% if item.product.brand %}
<small class="text-muted">品牌:{{ item.product.brand }}</small><br>
{% endif %}
{% if item.spec_combination %}
<small class="text-muted">规格:{{ item.spec_combination }}</small><br>
{% endif %}
<small class="text-muted">库存:{{ item.get_stock() }}件</small>
{% if not item.is_available() %}
<div class="mt-1">
{% if item.product.status != 1 %}
<span class="badge bg-danger">商品已下架</span>
{% elif item.get_stock() < item.quantity %}
<span class="badge bg-warning">库存不足</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- 单价 -->
<div class="col-2 text-center">
<span class="fw-bold text-danger">¥{{ "%.2f"|format(item.get_price()) }}</span>
</div>
<!-- 数量 -->
<div class="col-2 text-center">
<div class="input-group" style="width: 100px; margin: 0 auto;">
<button class="btn btn-outline-secondary btn-sm" type="button"
onclick="changeQuantity({{ item.id }}, -1)"
{% if not item.is_available() or item.quantity <= 1 %}disabled{% endif %}>-</button>
<input type="number" class="form-control form-control-sm text-center quantity-input"
value="{{ item.quantity }}" min="1" max="{{ item.get_stock() }}"
data-cart-id="{{ item.id }}" onchange="updateQuantity({{ item.id }}, this.value)"
{% if not item.is_available() %}disabled{% endif %}>
<button class="btn btn-outline-secondary btn-sm" type="button"
onclick="changeQuantity({{ item.id }}, 1)"
{% if not item.is_available() or item.quantity >= item.get_stock() %}disabled{% endif %}>+</button>
</div>
<div class="mt-1">
<small class="text-muted">小计:¥<span class="item-total">{{ "%.2f"|format(item.get_total_price()) }}</span></small>
</div>
</div>
<!-- 操作 -->
<div class="col-2 text-center">
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="removeItem({{ item.id }})">
<i class="bi bi-trash"></i> 删除
</button>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- 购物车操作栏 -->
<div class="col-md-4">
<div class="card position-sticky" style="top: 20px;">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-calculator"></i> 结算信息</h6>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span>已选商品:</span>
<span id="selectedCount">0</span>件
</div>
<div class="d-flex justify-content-between mb-3">
<span>商品总价:</span>
<span class="text-danger fw-bold h5">¥<span id="selectedTotal">0.00</span></span>
</div>
<hr>
<div class="d-flex justify-content-between mb-3">
<span class="fw-bold">应付总额:</span>
<span class="text-danger fw-bold h4">¥<span id="finalTotal">0.00</span></span>
</div>
<div class="d-grid gap-2">
<button type="button" class="btn btn-danger btn-lg" id="checkoutBtn"
onclick="checkout()" disabled>
<i class="bi bi-credit-card"></i> 去结算
</button>
<button type="button" class="btn btn-outline-secondary" onclick="clearCart()">
<i class="bi bi-trash"></i> 清空购物车
</button>
</div>
<div class="mt-3">
<small class="text-muted">
<i class="bi bi-shield-check"></i> 7天无理由退换<br>
<i class="bi bi-truck"></i> 全国包邮<br>
<i class="bi bi-award"></i> 正品保证
</small>
</div>
</div>
</div>
</div>
</div>
{% else %}
<!-- 空购物车 -->
<div class="text-center py-5">
<i class="bi bi-cart-x text-muted" style="font-size: 5rem;"></i>
<h4 class="text-muted mt-3">购物车是空的</h4>
<p class="text-muted">快去选购您喜欢的商品吧!</p>
<a href="{{ url_for('main.product_list') }}" class="btn btn-primary btn-lg">
<i class="bi bi-shop"></i> 去购物
</a>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/cart.js') }}"></script>
{% 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 %}
<link href="{{ url_for('static', filename='css/index.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<!-- 欢迎横幅 -->
<div class="jumbotron text-white rounded p-5 mb-4">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">欢迎来到太白购物商城</h1>
{% if user %}
<p class="col-md-8 fs-4">你好,{{ user.nickname or user.username }}!开始您的购物之旅吧!</p>
{% else %}
<p class="col-md-8 fs-4">发现优质商品,享受便捷购物体验</p>
<a class="btn btn-light btn-lg" href="{{ url_for('auth.register') }}" role="button">立即注册</a>
{% endif %}
</div>
</div>
<!-- 商品分类导航 -->
{% if top_categories %}
<div class="row mb-4">
<div class="col-12">
<h4><i class="bi bi-grid"></i> 商品分类</h4>
<hr>
</div>
{% for category in top_categories %}
<div class="col-md-2 col-6 mb-3">
<a href="{{ url_for('main.product_list', category_id=category.id) }}" class="text-decoration-none">
<div class="card text-center h-100 category-card">
<div class="card-body">
{% if category.icon_url %}
<img src="{{ category.icon_url }}" alt="{{ category.name }}" class="mb-2" style="width: 48px; height: 48px; object-fit: cover;">
{% else %}
<i class="bi bi-tag display-4 text-primary mb-2"></i>
{% endif %}
<h6 class="card-title">{{ category.name }}</h6>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% endif %}
<!-- 热门商品 -->
{% if hot_products %}
<div class="row mb-4">
<div class="col-12 d-flex justify-content-between align-items-center">
<h4><i class="bi bi-fire"></i> 热门商品</h4>
<a href="{{ url_for('main.product_list', sort='sales') }}" class="btn btn-outline-primary btn-sm">
查看更多 <i class="bi bi-arrow-right"></i>
</a>
</div>
<hr>
{% for product in hot_products %}
<div class="col-lg-3 col-md-4 col-sm-6 mb-4">
<div class="card h-100 product-card">
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none">
{% if product.main_image %}
<img src="{{ product.main_image }}" class="card-img-top product-image" alt="{{ product.name }}">
{% else %}
<div class="card-img-top product-image-placeholder">
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
</div>
{% endif %}
</a>
<div class="card-body">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none text-dark">
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
</a>
</h6>
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="price-current">¥{{ "%.2f"|format(product.price) }}</span>
{% if product.original_price and product.original_price > product.price %}
<small class="price-original">¥{{ "%.2f"|format(product.original_price) }}</small>
{% endif %}
</div>
<small class="text-muted">销量{{ product.sales_count }}</small>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- 最新商品 -->
{% if new_products %}
<div class="row mb-4">
<div class="col-12 d-flex justify-content-between align-items-center">
<h4><i class="bi bi-stars"></i> 最新商品</h4>
<a href="{{ url_for('main.product_list', sort='newest') }}" class="btn btn-outline-primary btn-sm">
查看更多 <i class="bi bi-arrow-right"></i>
</a>
</div>
<hr>
{% for product in new_products %}
<div class="col-lg-3 col-md-4 col-sm-6 mb-4">
<div class="card h-100 product-card">
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none">
{% if product.main_image %}
<img src="{{ product.main_image }}" class="card-img-top product-image" alt="{{ product.name }}">
{% else %}
<div class="card-img-top product-image-placeholder">
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
</div>
{% endif %}
</a>
<div class="card-body">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none text-dark">
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
</a>
</h6>
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="price-current">¥{{ "%.2f"|format(product.price) }}</span>
{% if product.original_price and product.original_price > product.price %}
<small class="price-original">¥{{ "%.2f"|format(product.original_price) }}</small>
{% endif %}
</div>
<small class="text-muted">销量{{ product.sales_count }}</small>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if user %}
<!-- 用户专区 -->
<div class="row mt-5">
<div class="col-12">
<h4><i class="bi bi-person-circle"></i> 我的专区</h4>
<hr>
</div>
<div class="col-md-3 mb-3">
<div class="card text-center user-zone-card">
<div class="card-body">
<i class="bi bi-person display-4 text-info mb-2"></i>
<h6 class="card-title">个人中心</h6>
<a href="{{ url_for('user.profile') }}" class="btn btn-sm btn-outline-info">进入</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-center user-zone-card">
<div class="card-body">
<i class="bi bi-bag display-4 text-primary mb-2"></i>
<h6 class="card-title">我的订单</h6>
<a href="{{ url_for('user.orders') }}" class="btn btn-sm btn-outline-primary">查看</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-center user-zone-card">
<div class="card-body">
<i class="bi bi-cart display-4 text-success mb-2"></i>
<h6 class="card-title">购物车</h6>
<a href="{{ url_for('cart.index') }}" class="btn btn-sm btn-outline-success">查看</a>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-center user-zone-card">
<div class="card-body">
<i class="bi bi-heart display-4 text-danger mb-2"></i>
<h6 class="card-title">我的收藏</h6>
<a href="#" class="btn btn-sm btn-outline-danger">查看</a>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 功能特色 -->
<div class="row mt-5">
<div class="col-12">
<h4><i class="bi bi-star"></i> 服务特色</h4>
<hr>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 feature-card">
<div class="card-body text-center">
<i class="bi bi-tags display-4 text-primary mb-3"></i>
<h5 class="card-title">精选商品</h5>
<p class="card-text">汇聚全球优质商品,品质保证,价格实惠</p>
<a href="{{ url_for('main.product_list') }}" class="btn btn-outline-primary">浏览商品</a>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 feature-card">
<div class="card-body text-center">
<i class="bi bi-truck display-4 text-success mb-3"></i>
<h5 class="card-title">快速配送</h5>
<p class="card-text">全国包邮,快速配送,让您尽快收到心仪商品</p>
<a href="#" class="btn btn-outline-success">了解更多</a>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 feature-card">
<div class="card-body text-center">
<i class="bi bi-shield-check display-4 text-warning mb-3"></i>
<h5 class="card-title">安全保障</h5>
<p class="card-text">正品保证,售后无忧,让您购物更放心</p>
<a href="#" class="btn btn-outline-warning">服务保障</a>
</div>
</div>
</div>
</div>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/order/checkout.html
📊 大小: 13242 bytes (12.93 KB)
🕒 修改时间: 2025-07-08 17:14:27
🔸==============================================================================
{% extends "base.html" %}
{% block title %}订单结算 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/checkout.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('cart.index') }}">购物车</a></li>
<li class="breadcrumb-item active">订单结算</li>
</ol>
</nav>
<div class="row">
<div class="col-lg-8">
<!-- 收货地址 -->
<div class="card checkout-section">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-geo-alt"></i> 收货地址</h5>
<a href="{{ url_for('address.add') }}" class="btn btn-outline-primary btn-sm">
<i class="bi bi-plus"></i> 新增地址
</a>
</div>
<div class="card-body">
<div class="row" id="addressList">
{% for address in addresses %}
<div class="col-md-6 mb-3">
<div class="card address-card {% if address.is_default %}selected{% endif %}"
data-address-id="{{ address.id }}" onclick="selectAddress({{ address.id }})">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">{{ address.receiver_name }}</h6>
<p class="text-muted mb-1">{{ address.receiver_phone }}</p>
<p class="mb-0">{{ address.get_full_address() }}</p>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="address_id"
value="{{ address.id }}" {% if address.is_default %}checked{% endif %}>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- 商品信息 -->
<div class="card checkout-section">
<div class="card-header">
<h5><i class="bi bi-box"></i> 商品信息</h5>
</div>
<div class="card-body">
{% for item in cart_items %}
<div class="product-item">
<div class="row align-items-center">
<div class="col-md-2">
<img src="{{ item.product.main_image or '/static/images/default-product.jpg' }}"
class="img-fluid rounded" alt="{{ item.product.name }}">
</div>
<div class="col-md-6">
<h6>{{ item.product.name }}</h6>
{% if item.spec_combination %}
<p class="text-muted mb-0">{{ item.spec_combination }}</p>
{% endif %}
{% if item.product.brand %}
<small class="text-muted">{{ item.product.brand }}</small>
{% endif %}
</div>
<div class="col-md-2 text-center">
<span class="text-muted">× {{ item.quantity }}</span>
</div>
<div class="col-md-2 text-end">
<span class="fw-bold">¥{{ "%.2f"|format(item.get_total_price()) }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 配送方式 -->
<div class="card checkout-section">
<div class="card-header">
<h5><i class="bi bi-truck"></i> 配送方式</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="radio" name="shipping_method"
value="standard" id="shipping_standard" checked onchange="updateShippingFee()">
<label class="form-check-label" for="shipping_standard">
<strong>标准配送</strong><br>
<small class="text-muted">免费 • 3-5个工作日</small>
</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="radio" name="shipping_method"
value="express" id="shipping_express" onchange="updateShippingFee()">
<label class="form-check-label" for="shipping_express">
<strong>次日达</strong><br>
<small class="text-muted">+10元 • 次日送达</small>
</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="radio" name="shipping_method"
value="same_day" id="shipping_same_day" onchange="updateShippingFee()">
<label class="form-check-label" for="shipping_same_day">
<strong>当日达</strong><br>
<small class="text-muted">+20元 • 当日送达</small>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- 支付方式 -->
<div class="card checkout-section">
<div class="card-header">
<h5><i class="bi bi-credit-card"></i> 支付方式</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="wechat" id="payment_wechat" checked>
<label class="form-check-label" for="payment_wechat">
<i class="bi bi-wechat text-success me-2"></i>
<strong>微信支付</strong>
</label>
</div>
</div>
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="alipay" id="payment_alipay">
<label class="form-check-label" for="payment_alipay">
<i class="bi bi-alipay text-primary me-2"></i>
<strong>支付宝</strong>
</label>
</div>
</div>
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="bank" id="payment_bank">
<label class="form-check-label" for="payment_bank">
<i class="bi bi-credit-card text-info me-2"></i>
<strong>银行卡</strong>
</label>
</div>
</div>
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="payment_method"
value="simulate" id="payment_simulate">
<label class="form-check-label" for="payment_simulate">
<i class="bi bi-gear-fill text-warning me-2"></i>
<strong>模拟支付</strong>
<br><small class="text-muted">测试模式</small>
</label>
</div>
</div>
</div>
<!-- 模拟支付说明 -->
<div class="alert alert-warning mt-3" id="simulatePaymentNotice" style="display: none;">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>模拟支付模式</strong><br>
这是开发测试功能,选择此支付方式后可以直接模拟支付成功或失败,无需真实付款。
实际生产环境中,此选项将被移除。
</div>
</div>
</div>
<!-- 备注 -->
<div class="card checkout-section">
<div class="card-header">
<h5><i class="bi bi-chat-text"></i> 订单备注</h5>
</div>
<div class="card-body">
<textarea class="form-control" id="orderRemark" rows="3"
placeholder="如有特殊需求请在此说明(选填)"></textarea>
</div>
</div>
</div>
<!-- 订单摘要 -->
<div class="col-lg-4">
<div class="card position-sticky" style="top: 20px;">
<div class="card-header">
<h5><i class="bi bi-receipt"></i> 订单摘要</h5>
</div>
<div class="card-body">
<div class="order-summary">
<div class="price-row">
<span>商品总价:</span>
<span id="subtotal">¥{{ "%.2f"|format(total_amount) }}</span>
</div>
<div class="price-row">
<span>运费:</span>
<span id="shippingFee">¥{{ "%.2f"|format(shipping_fee) }}</span>
</div>
<hr>
<div class="price-row total-price">
<span>应付总额:</span>
<span id="totalAmount">¥{{ "%.2f"|format(final_amount) }}</span>
</div>
</div>
<button class="btn btn-danger w-100 mt-3 btn-lg" onclick="submitOrder()">
<i class="bi bi-check-circle"></i> 提交订单
</button>
<div class="mt-3 text-center">
<small class="text-muted">
点击"提交订单"表示您同意
<a href="#" class="text-decoration-none">《用户协议》</a>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/checkout.js') }}"></script>
<script>
// 监听支付方式变化,显示/隐藏模拟支付说明
document.addEventListener('DOMContentLoaded', function() {
const paymentMethods = document.querySelectorAll('input[name="payment_method"]');
const simulateNotice = document.getElementById('simulatePaymentNotice');
paymentMethods.forEach(method => {
method.addEventListener('change', function() {
if (this.value === 'simulate') {
simulateNotice.style.display = 'block';
} else {
simulateNotice.style.display = 'none';
}
});
});
});
</script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/order/detail.html
📊 大小: 12875 bytes (12.57 KB)
🕒 修改时间: 2025-07-08 17:35:20
🔸==============================================================================
{% extends "base.html" %}
{% block title %}订单详情 - 太白购物商城{% endblock %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('order.list') }}">我的订单</a></li>
<li class="breadcrumb-item active">订单详情</li>
</ol>
</nav>
<div class="row">
<div class="col-lg-8">
<!-- 订单状态 -->
<div class="card order-detail-card">
<div class="card-header">
<h5><i class="bi bi-clock-history"></i> 订单状态</h5>
</div>
<div class="card-body">
<div class="order-status-timeline">
<div class="timeline-item completed">
<h6>订单已提交</h6>
<p class="text-muted mb-0">{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
</div>
<div class="timeline-item {% if order.status >= 2 %}completed{% elif order.status == 1 %}current{% endif %}">
<h6>等待买家付款</h6>
{% if order.status >= 2 %}
<p class="text-muted mb-0">已完成</p>
{% else %}
<p class="text-muted mb-0">请在15分钟内完成支付</p>
{% endif %}
</div>
<div class="timeline-item {% if order.status >= 3 %}completed{% elif order.status == 2 %}current{% endif %}">
<h6>卖家发货</h6>
{% if order.status >= 3 %}
<p class="text-muted mb-0">{{ order.shipped_at.strftime('%Y-%m-%d %H:%M:%S') if order.shipped_at else '已发货' }}</p>
{% else %}
<p class="text-muted mb-0">等待卖家发货</p>
{% endif %}
</div>
<div class="timeline-item {% if order.status >= 4 %}completed{% elif order.status == 3 %}current{% endif %}">
<h6>确认收货</h6>
{% if order.status >= 4 %}
<p class="text-muted mb-0">{{ order.received_at.strftime('%Y-%m-%d %H:%M:%S') if order.received_at else '已确认收货' }}</p>
{% else %}
<p class="text-muted mb-0">等待买家确认收货</p>
{% endif %}
</div>
<div class="timeline-item {% if order.status == 5 %}completed{% endif %}">
<h6>交易完成</h6>
{% if order.status == 5 %}
<p class="text-muted mb-0">交易成功</p>
{% else %}
<p class="text-muted mb-0">等待交易完成</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 商品信息 -->
<div class="card order-detail-card">
<div class="card-header">
<h5><i class="bi bi-box"></i> 商品信息</h5>
</div>
<div class="card-body">
{% for item in order.order_items %}
<div class="product-item">
<div class="row align-items-center">
<div class="col-md-2">
<img src="{{ item.product_image or '/static/images/default-product.jpg' }}"
class="product-image" alt="{{ item.product_name }}">
</div>
<div class="col-md-6">
<h6 class="mb-1">{{ item.product_name }}</h6>
{% if item.spec_combination %}
<p class="text-muted mb-1">{{ item.spec_combination }}</p>
{% endif %}
<small class="text-muted">单价:¥{{ "%.2f"|format(item.price) }}</small>
</div>
<div class="col-md-2 text-center">
<span class="text-muted">× {{ item.quantity }}</span>
</div>
<div class="col-md-2 text-end">
<span class="fw-bold">¥{{ "%.2f"|format(item.total_price) }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 物流信息 -->
{% if order.shipping_info %}
<div class="card order-detail-card">
<div class="card-header">
<h5><i class="bi bi-truck"></i> 物流信息</h5>
</div>
<div class="card-body">
{% for shipping in order.shipping_info %}
<div class="info-row">
<span>物流公司:</span>
<span>{{ shipping.shipping_company or '待发货' }}</span>
</div>
<div class="info-row">
<span>快递单号:</span>
<span>{{ shipping.tracking_number or '待发货' }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<div class="col-lg-4">
<!-- 订单信息 -->
<div class="card order-detail-card">
<div class="card-header">
<h5><i class="bi bi-receipt"></i> 订单信息</h5>
</div>
<div class="card-body">
<div class="info-row">
<span>订单号:</span>
<span>{{ order.order_sn }}</span>
</div>
<div class="info-row">
<span>下单时间:</span>
<span>{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div>
<div class="info-row">
<span>订单状态:</span>
<span>
{% if order.status == 1 %}
<span class="badge bg-warning">{{ order.get_status_text() }}</span>
{% elif order.status == 2 %}
<span class="badge bg-info">{{ order.get_status_text() }}</span>
{% elif order.status == 3 %}
<span class="badge bg-primary">{{ order.get_status_text() }}</span>
{% elif order.status == 5 %}
<span class="badge bg-success">{{ order.get_status_text() }}</span>
{% elif order.status == 6 %}
<span class="badge bg-secondary">{{ order.get_status_text() }}</span>
{% else %}
<span class="badge bg-dark">{{ order.get_status_text() }}</span>
{% endif %}
</span>
</div>
<div class="info-row">
<span>支付方式:</span>
<span>{{ order.payment_method or '未选择' }}</span>
</div>
<div class="info-row">
<span>配送方式:</span>
<span>{{ order.shipping_method or '标准配送' }}</span>
</div>
{% if order.remark %}
<div class="info-row">
<span>备注:</span>
<span>{{ order.remark }}</span>
</div>
{% endif %}
</div>
</div>
<!-- 收货信息 -->
<div class="card order-detail-card">
<div class="card-header">
<h5><i class="bi bi-geo-alt"></i> 收货信息</h5>
</div>
<div class="card-body">
{% set receiver = order.get_receiver_info() %}
<div class="info-row">
<span>收货人:</span>
<span>{{ receiver.receiver_name or '未知' }}</span>
</div>
<div class="info-row">
<span>联系电话:</span>
<span>{{ receiver.receiver_phone or '未知' }}</span>
</div>
<div class="info-row">
<span>收货地址:</span>
<span>{{ receiver.full_address or '未知' }}</span>
</div>
</div>
</div>
<!-- 费用明细 -->
<div class="card order-detail-card">
<div class="card-header">
<h5><i class="bi bi-calculator"></i> 费用明细</h5>
</div>
<div class="card-body">
<div class="info-row">
<span>商品总价:</span>
<span>¥{{ "%.2f"|format(order.total_amount) }}</span>
</div>
<div class="info-row">
<span>运费:</span>
<span>¥{{ "%.2f"|format(order.shipping_fee) }}</span>
</div>
<hr>
<div class="info-row">
<span><strong>应付总额:</strong></span>
<span class="total-amount">¥{{ "%.2f"|format(order.actual_amount) }}</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="d-grid gap-2">
{% if order.can_pay() %}
<a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}"
class="btn btn-danger">立即支付</a>
{% endif %}
{% if order.can_cancel() %}
<button class="btn btn-outline-secondary" onclick="cancelOrder({{ order.id }})">
取消订单
</button>
{% endif %}
{% if order.can_confirm_receipt() %}
<button class="btn btn-success" onclick="confirmReceipt({{ order.id }})">
确认收货
</button>
{% endif %}
{% if order.status == 4 %}
<!-- 评价商品按钮,根据商品数量展示 -->
{% if order.order_items|length == 1 %}
<a href="{{ url_for('review.write_review', order_id=order.id, product_id=order.order_items[0].product_id) }}"
class="btn btn-outline-warning">评价商品</a>
{% else %}
<div class="dropdown d-grid">
<button class="btn btn-outline-warning dropdown-toggle" type="button" data-bs-toggle="dropdown">
评价商品
</button>
<ul class="dropdown-menu">
{% for item in order.order_items %}
<li>
<a class="dropdown-item"
href="{{ url_for('review.write_review', order_id=order.id, product_id=item.product_id) }}">
{{ item.product_name[:20] }}{% if item.product_name|length > 20 %}...{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
<a href="{{ url_for('order.list') }}" class="btn btn-outline-primary">
返回订单列表
</a>
</div>
</div>
</div>
</div>
<!-- 重要确保order_detail.css在最后加载 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_detail.css') }}">
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/order_detail.js') }}"></script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/pay.css') }}">
{% endblock %}
{% block content %}
<div class="pay-container">
<div class="card">
<div class="card-header text-center">
<h4><i class="bi bi-credit-card"></i> 订单支付</h4>
</div>
<div class="card-body">
<!-- 隐藏数据属性 -->
<div style="display: none;"
data-payment-sn="{{ payment.payment_sn }}"
data-payment-method="{{ order.payment_method }}"
data-order-id="{{ order.id }}"></div>
<!-- 订单信息 -->
<div class="order-info">
<h6 class="mb-3">订单信息</h6>
<div class="row">
<div class="col-6">
<strong>订单号:</strong>{{ order.order_sn }}
</div>
<div class="col-6 text-end">
<strong class="text-danger">¥{{ "%.2f"|format(order.actual_amount) }}</strong>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<strong>支付方式:</strong>
{% if order.payment_method == 'wechat' %}微信支付
{% elif order.payment_method == 'alipay' %}支付宝
{% elif order.payment_method == 'bank' %}银行卡支付
{% elif order.payment_method == 'simulate' %}模拟支付
{% else %}{{ order.payment_method }}
{% endif %}
</div>
<div class="col-6 text-end">
<span class="countdown" id="countdown">14:59</span>
</div>
</div>
</div>
<!-- 支付区域 -->
<div id="paymentArea">
{% if order.payment_method == 'wechat' %}
<div class="payment-method selected">
<div class="d-flex align-items-center">
<i class="bi bi-wechat text-success fs-1 me-3"></i>
<div>
<h6>微信支付</h6>
<p class="text-muted mb-0">请使用微信扫描二维码完成支付</p>
</div>
</div>
</div>
<div class="qr-code" id="qrCodeArea" style="display: none;">
<div id="qrCodeImage">
<i class="bi bi-qr-code display-1 text-muted"></i>
<p class="mt-2">正在生成支付二维码...</p>
</div>
<p class="mt-3 text-muted">请使用微信扫描上方二维码完成支付</p>
</div>
{% endif %}
{% if order.payment_method == 'alipay' %}
<div class="payment-method selected">
<div class="d-flex align-items-center">
<i class="bi bi-alipay text-primary fs-1 me-3"></i>
<div>
<h6>支付宝</h6>
<p class="text-muted mb-0">正在跳转到支付宝...</p>
</div>
</div>
</div>
{% endif %}
{% if order.payment_method == 'bank' %}
<div class="payment-method selected">
<div class="d-flex align-items-center">
<i class="bi bi-credit-card text-info fs-1 me-3"></i>
<div>
<h6>银行卡支付</h6>
<p class="text-muted mb-0">正在跳转到网银...</p>
</div>
</div>
</div>
{% endif %}
{% if order.payment_method == 'simulate' %}
<div class="payment-method selected">
<div class="d-flex align-items-center">
<i class="bi bi-gear-fill text-warning fs-1 me-3"></i>
<div>
<h6>模拟支付</h6>
<p class="text-muted mb-0">开发测试模式,可直接完成支付</p>
</div>
</div>
</div>
<!-- 模拟支付控制面板 -->
<div class="simulate-panel mt-3 p-3 border rounded bg-light">
<h6 class="text-warning"><i class="bi bi-exclamation-triangle"></i> 模拟支付控制面板</h6>
<p class="text-muted small mb-3">这是开发测试功能,您可以模拟不同的支付结果</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-success" onclick="simulatePaymentSuccess()">
<i class="bi bi-check-circle"></i> 模拟支付成功
</button>
<button class="btn btn-danger" onclick="simulatePaymentFail()">
<i class="bi bi-x-circle"></i> 模拟支付失败
</button>
</div>
<div class="mt-2 text-center">
<small class="text-muted">
<i class="bi bi-info-circle"></i>
实际生产环境中,此面板将被真实支付接口替代
</small>
</div>
</div>
{% endif %}
</div>
<!-- 支付状态 -->
<div class="payment-status" id="paymentStatus" style="display: none;">
<div id="successStatus" style="display: none;">
<i class="bi bi-check-circle-fill text-success display-1"></i>
<h5 class="mt-3 text-success">支付成功</h5>
<p class="text-muted">正在跳转到订单详情...</p>
</div>
<div id="failStatus" style="display: none;">
<i class="bi bi-x-circle-fill text-danger display-1"></i>
<h5 class="mt-3 text-danger">支付失败</h5>
<p class="text-muted">请重新选择支付方式或联系客服</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="d-flex gap-2 mt-4" id="actionButtons">
<button class="btn btn-primary flex-fill" onclick="startPayment()">
<i class="bi bi-credit-card"></i> 立即支付
</button>
<button class="btn btn-outline-secondary" onclick="checkPaymentStatus()">
<i class="bi bi-arrow-clockwise"></i> 刷新状态
</button>
<button class="btn btn-outline-danger" onclick="cancelOrder()">
<i class="bi bi-x-circle"></i> 取消订单
</button>
</div>
<!-- 支付说明 -->
<div class="payment-tips mt-4">
<h6 class="text-muted">支付说明:</h6>
<ul class="text-muted small">
<li>订单有效期为15分钟请及时完成支付</li>
<li>支付成功后,订单状态将自动更新</li>
<li>如遇支付问题请联系客服400-123-4567</li>
{% if order.payment_method == 'simulate' %}
<li class="text-warning">当前为模拟支付模式,仅用于开发测试</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/pay.js') }}"></script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/product_detail.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
<li class="breadcrumb-item">
<a href="{{ url_for('main.product_list', category_id=product.category_id) }}">
{{ product.category.name }}
</a>
</li>
<li class="breadcrumb-item active">{{ product.name }}</li>
</ol>
</nav>
<div class="row">
<!-- 左侧:商品图片 -->
<div class="col-md-6">
{% if images %}
<!-- 主图显示区域 -->
<div id="productImageCarousel" class="carousel slide mb-3" data-bs-ride="carousel">
<div class="carousel-inner">
{% for image in images %}
<div class="carousel-item {% if loop.first or image.is_main %}active{% endif %}">
<img src="{{ image.image_url }}" class="d-block w-100" alt="{{ product.name }}"
style="height: 400px; object-fit: cover; border-radius: 8px;">
</div>
{% endfor %}
</div>
{% if images|length > 1 %}
<button class="carousel-control-prev" type="button" data-bs-target="#productImageCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon"></span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#productImageCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon"></span>
</button>
{% endif %}
</div>
<!-- 缩略图 -->
{% if images|length > 1 %}
<div class="row">
{% for image in images %}
<div class="col-3 mb-2">
<img src="{{ image.image_url }}" class="img-thumbnail thumbnail-image"
alt="{{ product.name }}" style="height: 80px; object-fit: cover; cursor: pointer;"
onclick="goToSlide({{ loop.index0 }})">
</div>
{% endfor %}
</div>
{% endif %}
{% else %}
<!-- 无图片占位 -->
<div class="bg-light d-flex align-items-center justify-content-center"
style="height: 400px; border-radius: 8px;">
<i class="bi bi-image text-muted" style="font-size: 5rem;"></i>
</div>
{% endif %}
</div>
<!-- 右侧:商品信息 -->
<div class="col-md-6">
<h2 class="mb-3">{{ product.name }}</h2>
<!-- 品牌 -->
{% if product.brand %}
<p class="text-muted mb-2">
<strong>品牌:</strong>{{ product.brand }}
</p>
{% endif %}
<!-- 价格 -->
<div class="price-section mb-4">
<div class="d-flex align-items-baseline">
<span class="text-danger fw-bold" style="font-size: 2rem;">
¥<span id="currentPrice">{{ "%.2f"|format(product.price) }}</span>
</span>
{% if product.original_price and product.original_price > product.price %}
<span class="text-muted text-decoration-line-through ms-3" style="font-size: 1.2rem;">
¥{{ "%.2f"|format(product.original_price) }}
</span>
<span class="badge bg-danger ms-2">
省{{ "%.0f"|format(((product.original_price - product.price) / product.original_price * 100)) }}%
</span>
{% endif %}
</div>
<div class="mt-2">
<small class="text-muted">销量:{{ product.sales_count }} | 浏览:{{ product.view_count }}</small>
</div>
</div>
<!-- 商品规格选择 -->
{% if inventory_list and inventory_list|length > 1 %}
<div class="specs-section mb-4">
<h6>选择规格:</h6>
<div id="specsContainer">
{% set spec_groups = {} %}
{% for sku in inventory_list %}
{% if sku.spec_combination %}
{% for spec_name, spec_value in sku.spec_combination.items() %}
{% if spec_name not in spec_groups %}
{% set _ = spec_groups.update({spec_name: []}) %}
{% endif %}
{% if spec_value not in spec_groups[spec_name] %}
{% set _ = spec_groups[spec_name].append(spec_value) %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% for spec_name, spec_values in spec_groups.items() %}
<div class="spec-group mb-3">
<label class="form-label">{{ spec_name }}</label>
<div class="spec-options">
{% for spec_value in spec_values %}
<button type="button" class="btn btn-outline-secondary spec-option me-2 mb-2"
data-spec-name="{{ spec_name }}" data-spec-value="{{ spec_value }}">
{{ spec_value }}
</button>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- 库存信息 -->
<div class="stock-section mb-4">
<div class="row">
<div class="col-6">
<strong>库存:</strong>
<span id="stockCount" class="text-success">
{% if inventory_list %}
{% if inventory_list|length == 1 %}
{{ inventory_list[0].stock }}
{% else %}
请选择规格
{% endif %}
{% else %}
暂无库存
{% endif %}
</span>
<span id="stockUnit">件</span>
</div>
{% if product.weight %}
<div class="col-6">
<strong>重量:</strong>{{ product.weight }}kg
</div>
{% endif %}
</div>
</div>
<!-- 购买数量 -->
<div class="quantity-section mb-4">
<label class="form-label"><strong>数量:</strong></label>
<div class="input-group" style="width: 150px;">
<button class="btn btn-outline-secondary" type="button" onclick="changeQuantity(-1)">-</button>
<input type="number" class="form-control text-center" id="quantity" value="1" min="1" max="999">
<button class="btn btn-outline-secondary" type="button" onclick="changeQuantity(1)">+</button>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons mb-4">
<div class="d-grid gap-2 d-md-flex">
<button type="button" class="btn btn-warning btn-lg flex-fill" id="addToCartBtn"
onclick="addToCart()" {% if not inventory_list %}disabled{% endif %}>
<i class="bi bi-cart-plus"></i> 加入购物车
</button>
<button type="button" class="btn btn-danger btn-lg flex-fill" id="buyNowBtn"
onclick="buyNow()" {% if not inventory_list %}disabled{% endif %}>
<i class="bi bi-lightning-fill"></i> 立即购买
</button>
</div>
<div class="mt-2">
<button type="button" class="btn btn-outline-secondary" onclick="addToFavorites()">
<i class="bi bi-heart"></i> 收藏商品
</button>
</div>
</div>
<!-- 服务承诺 -->
<div class="service-promises">
<h6>服务承诺:</h6>
<ul class="list-unstyled">
<li><i class="bi bi-check-circle text-success"></i> 正品保证</li>
<li><i class="bi bi-check-circle text-success"></i> 7天无理由退换</li>
<li><i class="bi bi-check-circle text-success"></i> 全国包邮</li>
<li><i class="bi bi-check-circle text-success"></i> 售后服务</li>
</ul>
</div>
</div>
</div>
<!-- 商品详情标签页 -->
<div class="row mt-5">
<div class="col-12">
<ul class="nav nav-tabs" id="productDetailTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="description-tab" data-bs-toggle="tab"
data-bs-target="#description" type="button" role="tab">商品详情</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="specs-tab" data-bs-toggle="tab"
data-bs-target="#specs" type="button" role="tab">规格参数</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="reviews-tab" data-bs-toggle="tab"
data-bs-target="#reviews" type="button" role="tab"
onclick="loadProductReviews({{ product.id }})">商品评价</button>
</li>
</ul>
<div class="tab-content" id="productDetailTabContent">
<!-- 商品详情 -->
<div class="tab-pane fade show active" id="description" role="tabpanel">
<div class="card">
<div class="card-body">
{% if product.description %}
<div class="product-description">
{{ product.description|replace('\n', '<br>')|safe }}
</div>
{% else %}
<p class="text-muted">暂无详细描述</p>
{% endif %}
</div>
</div>
</div>
<!-- 规格参数 -->
<div class="tab-pane fade" id="specs" role="tabpanel">
<div class="card">
<div class="card-body">
<table class="table table-striped">
<tbody>
<tr>
<td width="150"><strong>商品名称</strong></td>
<td>{{ product.name }}</td>
</tr>
{% if product.brand %}
<tr>
<td><strong>商品品牌</strong></td>
<td>{{ product.brand }}</td>
</tr>
{% endif %}
<tr>
<td><strong>商品分类</strong></td>
<td>{{ product.category.name }}</td>
</tr>
{% if product.weight %}
<tr>
<td><strong>商品重量</strong></td>
<td>{{ product.weight }}kg</td>
</tr>
{% endif %}
<tr>
<td><strong>上架时间</strong></td>
<td>{{ product.created_at.strftime('%Y-%m-%d') }}</td>
</tr>
{% if inventory_list %}
<tr>
<td><strong>库存信息</strong></td>
<td>
{% if inventory_list|length == 1 %}
{{ inventory_list[0].stock }}件
{% else %}
多规格商品,请选择具体规格查看库存
{% endif %}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- 商品评价 -->
<div class="tab-pane fade" id="reviews" role="tabpanel">
<div class="card">
<div class="card-body">
<div id="reviewsContainer">
<div class="text-center p-4 text-muted">
<i class="bi bi-star"></i> 点击标签页加载评价
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 推荐商品 -->
{% if recommended_products %}
<div class="row mt-5">
<div class="col-12">
<h4><i class="bi bi-heart-fill text-danger"></i> 相关推荐</h4>
<hr>
</div>
{% for rec_product in recommended_products %}
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 product-card">
<a href="{{ url_for('main.product_detail', product_id=rec_product.id) }}" class="text-decoration-none">
{% if rec_product.main_image %}
<img src="{{ rec_product.main_image }}" class="card-img-top" alt="{{ rec_product.name }}"
style="height: 200px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
style="height: 200px;">
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
</div>
{% endif %}
</a>
<div class="card-body">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=rec_product.id) }}"
class="text-decoration-none text-dark">
{{ rec_product.name[:40] }}{% if rec_product.name|length > 40 %}...{% endif %}
</a>
</h6>
<div class="d-flex justify-content-between align-items-center">
<span class="text-danger fw-bold">¥{{ "%.2f"|format(rec_product.price) }}</span>
<small class="text-muted">销量{{ rec_product.sales_count }}</small>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<!-- 库存数据用于JavaScript -->
<script type="application/json" id="inventoryData">
{% if inventory_data %}
{{ inventory_data|tojson }}
{% else %}
[]
{% endif %}
</script>
<script>
// 设置全局变量供JS使用
window.productId = {{ product.id }};
window.currentProductId = {{ product.id }};
window.isLoggedIn = {% if session.user_id %}true{% else %}false{% endif %};
</script>
<!-- 图片查看模态框 -->
<div class="modal fade" id="imageModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">查看图片</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<img id="modalImage" src="" class="img-fluid" alt="评价图片">
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/product_detail.js') }}"></script>
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
<script>
// 处理登录状态检查
{% if not session.user_id %}
function addToCart() {
if (confirm('请先登录后再加入购物车,是否前往登录?')) {
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
}
}
function buyNow() {
if (confirm('请先登录后再购买,是否前往登录?')) {
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
}
}
function addToFavorites() {
if (confirm('请先登录后再收藏,是否前往登录?')) {
window.location.href = '/auth/login?next=' + encodeURIComponent(window.location.pathname);
}
}
{% endif %}
</script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/product_list.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<!-- 搜索框 -->
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-search"></i> 商品搜索</h6>
</div>
<div class="card-body">
<form method="GET" action="{{ url_for('main.product_list') }}">
<div class="input-group">
<input type="text" class="form-control" name="search"
placeholder="搜索商品..." value="{{ search or '' }}">
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
<!-- 保持其他筛选条件 -->
{% if category_id %}<input type="hidden" name="category_id" value="{{ category_id }}">{% endif %}
{% if sort %}<input type="hidden" name="sort" value="{{ sort }}">{% endif %}
</form>
</div>
</div>
<!-- 分类筛选 -->
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-grid"></i> 商品分类</h6>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
<a href="{{ url_for('main.product_list') }}"
class="list-group-item list-group-item-action {% if not category_id %}active{% endif %}">
全部商品
</a>
{% for category in categories %}
<a href="{{ url_for('main.product_list', category_id=category.id) }}"
class="list-group-item list-group-item-action {% if category_id == category.id %}active{% endif %}">
{% if category.icon_url %}
<img src="{{ category.icon_url }}" alt="{{ category.name }}"
style="width: 20px; height: 20px; margin-right: 8px;">
{% endif %}
{{ category.name }}
</a>
{% endfor %}
</div>
</div>
</div>
<!-- 价格筛选 -->
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-currency-dollar"></i> 价格筛选</h6>
</div>
<div class="card-body">
<form method="GET" action="{{ url_for('main.product_list') }}" id="priceForm">
<div class="row">
<div class="col-6">
<input type="number" class="form-control form-control-sm"
name="min_price" placeholder="最低价"
value="{{ min_price or '' }}" step="0.01">
</div>
<div class="col-6">
<input type="number" class="form-control form-control-sm"
name="max_price" placeholder="最高价"
value="{{ max_price or '' }}" step="0.01">
</div>
</div>
<div class="mt-2">
<button type="submit" class="btn btn-primary btn-sm me-2">筛选</button>
<a href="{{ url_for('main.product_list') }}" class="btn btn-outline-secondary btn-sm">重置</a>
</div>
<!-- 保持其他条件 -->
{% if search %}<input type="hidden" name="search" value="{{ search }}">{% endif %}
{% if category_id %}<input type="hidden" name="category_id" value="{{ category_id }}">{% endif %}
{% if sort %}<input type="hidden" name="sort" value="{{ sort }}">{% endif %}
</form>
<!-- 快速价格选择 -->
<div class="mt-3">
<div class="d-grid gap-1">
{% set filtered_args = request.args.copy() %}
{% set _ = filtered_args.pop('min_price', None) %}
{% set _ = filtered_args.pop('max_price', None) %}
<a href="{{ url_for('main.product_list', max_price=100, **filtered_args) }}"
class="btn btn-outline-secondary btn-sm">100元以下</a>
<a href="{{ url_for('main.product_list', min_price=100, max_price=500, **filtered_args) }}"
class="btn btn-outline-secondary btn-sm">100-500元</a>
<a href="{{ url_for('main.product_list', min_price=500, max_price=1000, **filtered_args) }}"
class="btn btn-outline-secondary btn-sm">500-1000元</a>
<a href="{{ url_for('main.product_list', min_price=1000, **filtered_args) }}"
class="btn btn-outline-secondary btn-sm">1000元以上</a>
</div>
</div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="col-md-9">
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
{% if current_category %}
<li class="breadcrumb-item active">{{ current_category.name }}</li>
{% elif search %}
<li class="breadcrumb-item active">搜索结果</li>
{% else %}
<li class="breadcrumb-item active">全部商品</li>
{% endif %}
</ol>
</nav>
<!-- 筛选和排序栏 -->
<div class="row mb-3">
<div class="col-md-6">
<h5>
{% if current_category %}
{{ current_category.name }}
{% elif search %}
搜索"{{ search }}"
{% else %}
全部商品
{% endif %}
<small class="text-muted">(共{{ products.total }}个商品)</small>
</h5>
</div>
<div class="col-md-6 text-end">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="sort" id="sort_default"
{% if not sort or sort == 'default' %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="sort_default"
onclick="changeSort('default')">综合</label>
<input type="radio" class="btn-check" name="sort" id="sort_newest"
{% if sort == 'newest' %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="sort_newest"
onclick="changeSort('newest')">最新</label>
<input type="radio" class="btn-check" name="sort" id="sort_sales"
{% if sort == 'sales' %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="sort_sales"
onclick="changeSort('sales')">销量</label>
<input type="radio" class="btn-check" name="sort" id="sort_price_asc"
{% if sort == 'price_asc' %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="sort_price_asc"
onclick="changeSort('price_asc')">价格↑</label>
<input type="radio" class="btn-check" name="sort" id="sort_price_desc"
{% if sort == 'price_desc' %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="sort_price_desc"
onclick="changeSort('price_desc')">价格↓</label>
</div>
</div>
</div>
<!-- 商品列表 -->
{% if products.items %}
<div class="row">
{% for product in products.items %}
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100 product-card">
<a href="{{ url_for('main.product_detail', product_id=product.id) }}" class="text-decoration-none">
{% if product.main_image %}
<img src="{{ product.main_image }}" class="card-img-top" alt="{{ product.name }}"
style="height: 200px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
style="height: 200px;">
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
</div>
{% endif %}
</a>
<div class="card-body">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=product.id) }}"
class="text-decoration-none text-dark">
{{ product.name[:60] }}{% if product.name|length > 60 %}...{% endif %}
</a>
</h6>
{% if product.brand %}
<p class="card-text text-muted small">{{ product.brand }}</p>
{% endif %}
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-danger fw-bold h6">¥{{ "%.2f"|format(product.price) }}</span>
{% if product.original_price and product.original_price > product.price %}
<small class="text-muted text-decoration-line-through d-block">
¥{{ "%.2f"|format(product.original_price) }}
</small>
{% endif %}
</div>
<div class="text-end">
<small class="text-muted d-block">销量{{ product.sales_count }}</small>
<small class="text-muted">浏览{{ product.view_count }}</small>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if products.pages > 1 %}
<nav aria-label="商品分页">
<ul class="pagination justify-content-center">
{% if products.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.product_list', page=products.prev_num, **request.args) }}">
<i class="bi bi-chevron-left"></i> 上一页
</a>
</li>
{% endif %}
{% for page_num in products.iter_pages() %}
{% if page_num %}
{% if page_num != products.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.product_list', page=page_num, **request.args) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if products.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.product_list', page=products.next_num, **request.args) }}">
下一页 <i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- 无商品提示 -->
<div class="text-center py-5">
<i class="bi bi-search text-muted" style="font-size: 5rem;"></i>
<h4 class="text-muted mt-3">暂无找到相关商品</h4>
<p class="text-muted">请尝试调整搜索条件或浏览其他分类</p>
<a href="{{ url_for('main.product_list') }}" class="btn btn-primary">
<i class="bi bi-arrow-left"></i> 返回全部商品
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/product_list.js') }}"></script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-star"></i> 我的评价</h5>
</div>
<div class="card-body">
{% if reviews.items %}
{% for review in reviews.items %}
<div class="review-item">
<div class="row">
<div class="col-md-3">
<img src="{{ review.product.main_image or '/static/images/default-product.jpg' }}"
class="img-fluid rounded" alt="{{ review.product.name }}">
</div>
<div class="col-md-9">
<h6 class="mb-2">
<a href="{{ url_for('main.product_detail', product_id=review.product_id) }}"
class="text-decoration-none">{{ review.product.name }}</a>
</h6>
<!-- 评分 -->
<div class="rating-display mb-2">
<span class="stars">{{ review.get_rating_stars() }}</span>
<span class="text-muted">{{ review.rating }}分</span>
</div>
<!-- 评价内容 -->
{% if review.content %}
<p class="review-content">{{ review.content }}</p>
{% endif %}
<!-- 评价图片 -->
{% if review.get_images() %}
<div class="review-images mb-2">
{% for image_url in review.get_images() %}
<img src="{{ image_url }}" class="review-image-thumb" alt="评价图片"
onclick="showImageModal('{{ image_url }}')">
{% endfor %}
</div>
{% endif %}
<!-- 评价信息 -->
<div class="review-meta">
<small class="text-muted">
评价时间:{{ review.created_at.strftime('%Y-%m-%d %H:%M') }}
{% if review.is_anonymous %}
| 匿名评价
{% endif %}
</small>
<div class="float-end">
<button class="btn btn-outline-danger btn-sm"
onclick="deleteReview({{ review.id }})">
<i class="bi bi-trash"></i> 删除
</button>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
{% if reviews.pages > 1 %}
<nav aria-label="评价分页">
<ul class="pagination justify-content-center">
{% if reviews.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('review.my_reviews', page=reviews.prev_num) }}">上一页</a>
</li>
{% endif %}
{% for page_num in reviews.iter_pages() %}
{% if page_num %}
{% if page_num != reviews.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('review.my_reviews', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if reviews.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('review.my_reviews', page=reviews.next_num) }}">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center empty-state">
<i class="bi bi-star display-1 text-muted"></i>
<h5 class="mt-3 text-muted">暂无评价</h5>
<p class="text-muted">您还没有发表过商品评价</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-shop"></i> 去购物
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 图片查看模态框 -->
<div class="modal fade" id="imageModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">查看图片</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<img id="modalImage" src="" class="img-fluid" alt="评价图片">
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
<script>
function showImageModal(imageUrl) {
document.getElementById('modalImage').src = imageUrl;
new bootstrap.Modal(document.getElementById('imageModal')).show();
}
function deleteReview(reviewId) {
if (confirm('确定要删除这条评价吗?')) {
fetch(`/review/delete/${reviewId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('删除失败', 'error');
});
}
}
</script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/review/write.html
📊 大小: 6165 bytes (6.02 KB)
🕒 修改时间: 2025-07-08 19:07:34
🔸==============================================================================
{% extends "base.html" %}
{% block title %}评价商品 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/review.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('order.list') }}">我的订单</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('order.detail', order_id=order.id) }}">订单详情</a></li>
<li class="breadcrumb-item active">评价商品</li>
</ol>
</nav>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-star"></i> 评价商品</h5>
</div>
<div class="card-body">
<!-- 商品信息 -->
<div class="product-info mb-4">
<div class="row align-items-center">
<div class="col-md-2">
<img src="{{ order_item.product_image or '/static/images/default-product.jpg' }}"
class="img-fluid rounded" alt="{{ order_item.product_name }}">
</div>
<div class="col-md-10">
<h6 class="mb-1">{{ order_item.product_name }}</h6>
{% if order_item.spec_combination %}
<p class="text-muted mb-1">{{ order_item.spec_combination }}</p>
{% endif %}
<p class="text-muted mb-0">
单价:¥{{ "%.2f"|format(order_item.price) }} × {{ order_item.quantity }}
</p>
</div>
</div>
</div>
<!-- 评价表单 -->
<form id="reviewForm">
<input type="hidden" id="orderId" value="{{ order.id }}">
<input type="hidden" id="productId" value="{{ order_item.product_id }}">
<!-- 评分 -->
<div class="mb-4">
<label class="form-label"><strong>商品评分:</strong></label>
<div class="rating-container">
<div class="star-rating" id="starRating">
<span class="star" data-rating="1"></span>
<span class="star" data-rating="2"></span>
<span class="star" data-rating="3"></span>
<span class="star" data-rating="4"></span>
<span class="star" data-rating="5"></span>
</div>
<span class="rating-text" id="ratingText">请选择评分</span>
</div>
<input type="hidden" id="rating" name="rating" required>
</div>
<!-- 评价内容 -->
<div class="mb-4">
<label for="content" class="form-label"><strong>评价内容:</strong></label>
<textarea class="form-control" id="content" name="content" rows="5"
placeholder="请分享您对商品的使用感受,帮助其他买家更好地了解商品..."></textarea>
<div class="form-text">字数限制500字以内</div>
</div>
<!-- 图片上传 -->
<div class="mb-4">
<label class="form-label"><strong>上传图片:</strong>(可选)</label>
<div class="image-upload-container">
<div class="upload-area" id="uploadArea">
<i class="bi bi-cloud-upload"></i>
<p class="mb-0">点击或拖拽上传图片</p>
<small class="text-muted">支持 JPG、PNG、GIF 格式最大5MB</small>
</div>
<input type="file" id="imageInput" multiple accept="image/*" style="display: none;">
<div class="uploaded-images" id="uploadedImages"></div>
</div>
</div>
<!-- 匿名评价 -->
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isAnonymous" name="is_anonymous">
<label class="form-check-label" for="isAnonymous">
匿名评价(其他用户将看不到您的用户名)
</label>
</div>
</div>
<!-- 提交按钮 -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ url_for('order.detail', order_id=order.id) }}"
class="btn btn-outline-secondary me-md-2">取消</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="bi bi-check-circle"></i> 提交评价
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/review.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/test_upload.html
📊 大小: 14836 bytes (14.49 KB)
🕒 修改时间: 2025-07-03 04:21:18
🔸==============================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>COS上传测试</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4>腾讯云COS上传测试</h4>
</div>
<div class="card-body">
<!-- 头像上传测试 -->
<div class="mb-4">
<h5>头像上传测试</h5>
<form id="avatarForm" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" class="form-control" id="avatarFile" name="avatar" accept="image/*">
</div>
<button type="submit" class="btn btn-primary">上传头像</button>
</form>
<div id="avatarResult" class="mt-3"></div>
</div>
<hr>
<!-- 通用图片上传测试 -->
<div class="mb-4">
<h5>通用图片上传测试</h5>
<form id="imageForm" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" class="form-control" id="imageFile" name="image" accept="image/*">
</div>
<div class="mb-3">
<select class="form-select" name="folder_type">
<option value="temp">临时文件</option>
<option value="product">商品图片</option>
<option value="review">评价图片</option>
</select>
</div>
<button type="submit" class="btn btn-success">上传图片</button>
</form>
<div id="imageResult" class="mt-3"></div>
</div>
<!-- 上传历史 -->
<div class="mb-4">
<h5>上传历史</h5>
<div id="uploadHistory"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 上传历史记录
let uploadHistory = [];
// 头像上传
document.getElementById('avatarForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData();
const fileInput = document.getElementById('avatarFile');
if (!fileInput.files[0]) {
showResult('avatarResult', false, '请选择文件');
return;
}
formData.append('avatar', fileInput.files[0]);
uploadFile('/upload/avatar', formData, 'avatarResult', '头像');
});
// 通用图片上传
document.getElementById('imageForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const fileInput = document.getElementById('imageFile');
if (!fileInput.files[0]) {
showResult('imageResult', false, '请选择文件');
return;
}
uploadFile('/upload/image', formData, 'imageResult', '图片');
});
// 上传文件函数
function uploadFile(url, formData, resultId, fileType) {
const resultDiv = document.getElementById(resultId);
resultDiv.innerHTML = '<div class="alert alert-info">上传中...</div>';
fetch(url, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showResult(resultId, true, `${fileType}上传成功!`, data.url);
addToHistory(fileType, data.url, data.file_key);
} else {
showResult(resultId, false, data.error || `${fileType}上传失败`);
}
})
.catch(error => {
showResult(resultId, false, `上传失败: ${error.message}`);
});
}
// 显示结果
function showResult(resultId, success, message, imageUrl = null) {
const resultDiv = document.getElementById(resultId);
const alertClass = success ? 'alert-success' : 'alert-danger';
let html = `<div class="alert ${alertClass}">${message}</div>`;
if (success && imageUrl) {
html += `
<div class="mt-2">
<img src="${imageUrl}" class="img-thumbnail" style="max-width: 200px;">
<p class="mt-2"><small>访问地址: <a href="${imageUrl}" target="_blank">${imageUrl}</a></small></p>
</div>
`;
}
resultDiv.innerHTML = html;
}
// 添加到历史记录
function addToHistory(type, url, fileKey) {
uploadHistory.unshift({
type: type,
url: url,
fileKey: fileKey,
time: new Date().toLocaleString()
});
updateHistoryDisplay();
}
// 更新历史记录显示
function updateHistoryDisplay() {
const historyDiv = document.getElementById('uploadHistory');
if (uploadHistory.length === 0) {
historyDiv.innerHTML = '<p class="text-muted">暂无上传记录</p>';
return;
}
let html = '<div class="list-group">';
uploadHistory.slice(0, 5).forEach(item => {
html += `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">${item.type}</h6>
<p class="mb-1"><small>${item.fileKey}</small></p>
<small class="text-muted">${item.time}</small>
</div>
<div>
<a href="${item.url}" target="_blank" class="btn btn-sm btn-outline-primary">查看</a>
</div>
</div>
</div>
`;
});
html += '</div>';
historyDiv.innerHTML = html;
}
// 初始化历史记录显示
updateHistoryDisplay();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>COS上传测试</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4>腾讯云COS上传测试</h4>
</div>
<div class="card-body">
<!-- 头像上传测试 -->
<div class="mb-4">
<h5>头像上传测试</h5>
<form id="avatarForm" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" class="form-control" id="avatarFile" name="avatar" accept="image/*">
</div>
<button type="submit" class="btn btn-primary">上传头像</button>
</form>
<div id="avatarResult" class="mt-3"></div>
</div>
<hr>
<!-- 通用图片上传测试 -->
<div class="mb-4">
<h5>通用图片上传测试</h5>
<form id="imageForm" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" class="form-control" id="imageFile" name="image" accept="image/*">
</div>
<div class="mb-3">
<select class="form-select" name="folder_type">
<option value="temp">临时文件</option>
<option value="product">商品图片</option>
<option value="review">评价图片</option>
</select>
</div>
<button type="submit" class="btn btn-success">上传图片</button>
</form>
<div id="imageResult" class="mt-3"></div>
</div>
<!-- 上传历史 -->
<div class="mb-4">
<h5>上传历史</h5>
<div id="uploadHistory"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 上传历史记录
let uploadHistory = [];
// 头像上传
document.getElementById('avatarForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData();
const fileInput = document.getElementById('avatarFile');
if (!fileInput.files[0]) {
showResult('avatarResult', false, '请选择文件');
return;
}
formData.append('avatar', fileInput.files[0]);
uploadFile('/upload/avatar', formData, 'avatarResult', '头像');
});
// 通用图片上传
document.getElementById('imageForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const fileInput = document.getElementById('imageFile');
if (!fileInput.files[0]) {
showResult('imageResult', false, '请选择文件');
return;
}
uploadFile('/upload/image', formData, 'imageResult', '图片');
});
// 上传文件函数
function uploadFile(url, formData, resultId, fileType) {
const resultDiv = document.getElementById(resultId);
resultDiv.innerHTML = '<div class="alert alert-info">上传中...</div>';
fetch(url, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showResult(resultId, true, `${fileType}上传成功!`, data.url);
addToHistory(fileType, data.url, data.file_key);
} else {
showResult(resultId, false, data.error || `${fileType}上传失败`);
}
})
.catch(error => {
showResult(resultId, false, `上传失败: ${error.message}`);
});
}
// 显示结果
function showResult(resultId, success, message, imageUrl = null) {
const resultDiv = document.getElementById(resultId);
const alertClass = success ? 'alert-success' : 'alert-danger';
let html = `<div class="alert ${alertClass}">${message}</div>`;
if (success && imageUrl) {
html += `
<div class="mt-2">
<img src="${imageUrl}" class="img-thumbnail" style="max-width: 200px;">
<p class="mt-2"><small>访问地址: <a href="${imageUrl}" target="_blank">${imageUrl}</a></small></p>
</div>
`;
}
resultDiv.innerHTML = html;
}
// 添加到历史记录
function addToHistory(type, url, fileKey) {
uploadHistory.unshift({
type: type,
url: url,
fileKey: fileKey,
time: new Date().toLocaleString()
});
updateHistoryDisplay();
}
// 更新历史记录显示
function updateHistoryDisplay() {
const historyDiv = document.getElementById('uploadHistory');
if (uploadHistory.length === 0) {
historyDiv.innerHTML = '<p class="text-muted">暂无上传记录</p>';
return;
}
let html = '<div class="list-group">';
uploadHistory.slice(0, 5).forEach(item => {
html += `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">${item.type}</h6>
<p class="mb-1"><small>${item.fileKey}</small></p>
<small class="text-muted">${item.time}</small>
</div>
<div>
<a href="${item.url}" target="_blank" class="btn btn-sm btn-outline-primary">查看</a>
</div>
</div>
</div>
`;
});
html += '</div>';
historyDiv.innerHTML = html;
}
// 初始化历史记录显示
updateHistoryDisplay();
</script>
</body>
</html>
🔸==============================================================================
📄 文件: app/templates/user/address_form.html
📊 大小: 7575 bytes (7.40 KB)
🕒 修改时间: 2025-07-04 04:02:42
🔸==============================================================================
{% extends "base.html" %}
{% block title %}{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %} - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/address_form.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="#" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="#" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header">
<h5>
<i class="bi bi-geo-alt"></i>
{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %}
</h5>
</div>
<div class="card-body">
<!-- 调试信息 -->
<div class="alert alert-info" id="debugAlert" style="display: none;">
<strong>调试信息:</strong>
<div id="debugInfo">加载中...</div>
</div>
<form method="POST" id="addressForm">
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">{{ form.receiver_name.label.text }} <span class="text-danger">*</span></label>
{{ form.receiver_name(class="form-control") }}
{% if form.receiver_name.errors %}
<div class="text-danger">{{ form.receiver_name.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{{ form.receiver_phone.label.text }} <span class="text-danger">*</span></label>
{{ form.receiver_phone(class="form-control") }}
{% if form.receiver_phone.errors %}
<div class="text-danger">{{ form.receiver_phone.errors[0] }}</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">省份 <span class="text-danger">*</span></label>
<select class="form-select" id="province" name="province" required>
<option value="">加载中...</option>
</select>
<input type="hidden" id="provinceValue" value="{% if address %}{{ address.province }}{% endif %}">
{% if form.province.errors %}
<div class="text-danger">{{ form.province.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<label class="form-label">城市 <span class="text-danger">*</span></label>
<select class="form-select" id="city" name="city" required>
<option value="">请选择城市</option>
</select>
<input type="hidden" id="cityValue" value="{% if address %}{{ address.city }}{% endif %}">
{% if form.city.errors %}
<div class="text-danger">{{ form.city.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<label class="form-label">区县 <span class="text-danger">*</span></label>
<select class="form-select" id="district" name="district" required>
<option value="">请选择区县</option>
</select>
<input type="hidden" id="districtValue" value="{% if address %}{{ address.district }}{% endif %}">
{% if form.district.errors %}
<div class="text-danger">{{ form.district.errors[0] }}</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-8 mb-3">
<label class="form-label">{{ form.detail_address.label.text }} <span class="text-danger">*</span></label>
{{ form.detail_address(class="form-control", placeholder="街道、门牌号等详细信息") }}
{% if form.detail_address.errors %}
<div class="text-danger">{{ form.detail_address.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<label class="form-label">{{ form.postal_code.label.text }}</label>
{{ form.postal_code(class="form-control", placeholder="选填") }}
{% if form.postal_code.errors %}
<div class="text-danger">{{ form.postal_code.errors[0] }}</div>
{% endif %}
</div>
</div>
<div class="mb-3">
<div class="form-check">
{{ form.is_default(class="form-check-input") }}
<label class="form-check-label" for="{{ form.is_default.id }}">
{{ form.is_default.label.text }}
</label>
</div>
</div>
<div class="d-flex gap-2">
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('address.index') }}" class="btn btn-outline-secondary">取消</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- 在scripts块中引入城市数据确保在base.html的脚本之后加载 -->
<script src="{{ url_for('static', filename='js/city_data.js') }}"></script>
<script src="{{ url_for('static', filename='js/address_form.js') }}"></script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/addresses.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-geo-alt"></i> 收货地址</h5>
<a href="{{ url_for('address.add') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> 添加地址
</a>
</div>
<div class="card-body">
{% if addresses %}
<div class="row">
{% for address in addresses %}
<div class="col-md-6 mb-3">
<div class="card address-card {% if address.is_default %}border-primary{% endif %}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<h6 class="card-title mb-1">
{{ address.receiver_name }}
{% if address.is_default %}
<span class="badge bg-primary ms-2">默认</span>
{% endif %}
</h6>
<p class="text-muted mb-0">{{ address.receiver_phone }}</p>
</div>
<div class="dropdown">
<button class="btn btn-link btn-sm" type="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('address.edit', address_id=address.id) }}">
<i class="bi bi-pencil"></i> 编辑
</a>
</li>
{% if not address.is_default %}
<li>
<a class="dropdown-item" href="#" onclick="setDefaultAddress({{ address.id }})">
<i class="bi bi-star"></i> 设为默认
</a>
</li>
{% endif %}
<li>
<a class="dropdown-item text-danger" href="#" onclick="deleteAddress({{ address.id }})">
<i class="bi bi-trash"></i> 删除
</a>
</li>
</ul>
</div>
</div>
<p class="card-text">
<i class="bi bi-geo-alt text-muted"></i>
{{ address.get_full_address() }}
</p>
{% if address.postal_code %}
<p class="text-muted mb-0">
<small>邮编:{{ address.postal_code }}</small>
</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center empty-state">
<i class="bi bi-geo-alt-fill display-1 text-muted"></i>
<h5 class="mt-3 text-muted">暂无收货地址</h5>
<p class="text-muted">请添加您的收货地址,方便下单购物</p>
<a href="{{ url_for('address.add') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> 添加地址
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/addresses.js') }}"></script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/favorites.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-heart text-danger"></i> 我的收藏</h5>
<div>
<span class="badge bg-secondary">共 {{ total_count }} 件商品</span>
{% if total_count > 0 %}
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="toggleSelectAll()">
<i class="bi bi-check-square"></i> 全选
</button>
<button class="btn btn-sm btn-outline-danger ms-1" onclick="batchRemove()">
<i class="bi bi-trash"></i> 批量删除
</button>
{% endif %}
</div>
</div>
<div class="card-body">
{% if favorites.items %}
<div class="row">
{% for favorite in favorites.items %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card favorite-item h-100" data-product-id="{{ favorite.product_id }}">
<div class="card-body">
<div class="d-flex">
<div class="form-check me-3">
<input class="form-check-input favorite-checkbox" type="checkbox"
value="{{ favorite.product_id }}" id="favorite-{{ favorite.product_id }}">
</div>
<div class="flex-shrink-0 me-3">
<a href="{{ url_for('main.product_detail', product_id=favorite.product_id) }}">
{% if favorite.product.main_image %}
<img src="{{ favorite.product.main_image }}"
alt="{{ favorite.product.name }}"
class="favorite-image">
{% else %}
<div class="favorite-image-placeholder">
<i class="bi bi-image"></i>
</div>
{% endif %}
</a>
</div>
<div class="flex-grow-1">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=favorite.product_id) }}"
class="text-decoration-none">
{{ favorite.product.name }}
</a>
</h6>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-danger fw-bold">
¥{{ "%.2f"|format(favorite.product.price) }}
</span>
<small class="text-muted">
销量 {{ favorite.product.sales_count }}
</small>
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ favorite.created_at.strftime('%Y-%m-%d') }}
</small>
{% if favorite.product.status == 1 %}
<span class="badge bg-success">有货</span>
{% else %}
<span class="badge bg-secondary">下架</span>
{% endif %}
</div>
<div class="mt-3">
<div class="btn-group btn-group-sm w-100 icon-buttons">
{% if favorite.product.status == 1 %}
<button class="btn btn-outline-primary"
onclick="addToCart({{ favorite.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="加入购物车">
<i class="bi bi-cart-plus"></i>
</button>
{% endif %}
<button class="btn btn-outline-danger"
onclick="removeFavorite({{ favorite.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="取消收藏">
<i class="bi bi-heart-fill"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if favorites.pages > 1 %}
<nav aria-label="收藏分页">
<ul class="pagination justify-content-center">
{% if favorites.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('favorite.index', page=favorites.prev_num) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in favorites.iter_pages() %}
{% if page_num %}
{% if page_num != favorites.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('favorite.index', page=page_num) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if favorites.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('favorite.index', page=favorites.next_num) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- 空状态 -->
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-heart display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有收藏任何商品</h4>
<p class="text-muted">去逛逛,收藏心仪的商品吧~</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> 去首页逛逛
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="confirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="confirmMessage">确定要取消收藏这些商品吗?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmBtn">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/favorites.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/user/history.html
📊 大小: 12588 bytes (12.29 KB)
🕒 修改时间: 2025-07-09 04:52:12
🔸==============================================================================
{% extends "base.html" %}
{% block title %}浏览历史 - 太白购物商城{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/history.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-clock-history text-primary"></i> 浏览历史</h5>
<div>
<span class="badge bg-secondary">共 {{ total_count }} 件商品</span>
{% if total_count > 0 %}
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="toggleSelectAll()">
<i class="bi bi-check-square"></i> 全选
</button>
<button class="btn btn-sm btn-outline-danger ms-1" onclick="batchRemove()">
<i class="bi bi-trash"></i> 批量删除
</button>
<button class="btn btn-sm btn-outline-warning ms-1" onclick="clearHistory()">
<i class="bi bi-arrow-clockwise"></i> 清空历史
</button>
{% endif %}
</div>
</div>
<div class="card-body">
{% if history.items %}
<div class="row">
{% for item in history.items %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card history-item h-100" data-product-id="{{ item.product_id }}">
<div class="card-body">
<div class="d-flex">
<div class="form-check me-3">
<input class="form-check-input history-checkbox" type="checkbox"
value="{{ item.product_id }}" id="history-{{ item.product_id }}">
</div>
<div class="flex-shrink-0 me-3">
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}">
{% if item.product.main_image %}
<img src="{{ item.product.main_image }}"
alt="{{ item.product.name }}"
class="history-image">
{% else %}
<div class="history-image-placeholder">
<i class="bi bi-image"></i>
</div>
{% endif %}
</a>
</div>
<div class="flex-grow-1">
<h6 class="card-title">
<a href="{{ url_for('main.product_detail', product_id=item.product_id) }}"
class="text-decoration-none">
{{ item.product.name }}
</a>
</h6>
<div class="mb-2">
<span class="text-danger fw-bold">
¥{{ "%.2f"|format(item.product.price) }}
</span>
<small class="text-muted ms-2">
销量 {{ item.product.sales_count }}
</small>
</div>
<div class="mb-2">
<span class="badge bg-light text-dark border">
<i class="bi bi-tag"></i> {{ item.product.category.name if item.product.category else "未分类" }}
</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ item.viewed_at.strftime('%Y-%m-%d %H:%M') }}
</small>
{% if item.product.status == 1 %}
<span class="badge bg-success">有货</span>
{% else %}
<span class="badge bg-secondary">下架</span>
{% endif %}
</div>
<div class="mt-3">
<div class="btn-group btn-group-sm w-100">
{% if item.product.status == 1 %}
<button class="btn btn-outline-primary"
onclick="addToCart({{ item.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="加入购物车">
<i class="bi bi-cart-plus"></i>
</button>
<button class="btn btn-outline-danger"
onclick="addToFavorites({{ item.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="收藏商品">
<i class="bi bi-heart"></i>
</button>
{% endif %}
<button class="btn btn-outline-secondary"
onclick="removeHistory({{ item.product_id }})"
data-bs-toggle="tooltip" data-bs-placement="top" title="删除记录">
<i class="bi bi-x-circle"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if history.pages > 1 %}
<nav aria-label="浏览历史分页">
<ul class="pagination justify-content-center">
{% if history.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('history.index', page=history.prev_num) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in history.iter_pages() %}
{% if page_num %}
{% if page_num != history.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('history.index', page=page_num) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if history.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('history.index', page=history.next_num) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- 空状态 -->
<div class="empty-state">
<div class="text-center py-5">
<i class="bi bi-clock-history display-1 text-muted"></i>
<h4 class="mt-3 text-muted">还没有浏览任何商品</h4>
<p class="text-muted">去逛逛,看看有什么好商品~</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-house"></i> 去首页逛逛
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="confirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="confirmMessage">确定要删除这些浏览记录吗?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmBtn">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/history.js') }}"></script>
{% endblock %}
🔸==============================================================================
📄 文件: app/templates/user/login.html
📊 大小: 2715 bytes (2.65 KB)
🕒 修改时间: 2025-07-04 03:55:20
🔸==============================================================================
{% extends "base.html" %}
{% block title %}用户登录 - 太白购物商城{% endblock %}
{% block styles %}
<link href="{{ url_for('static', filename='css/auth.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="auth-container">
<div class="row justify-content-center w-100">
<div class="col-md-6 col-lg-4">
<div class="card auth-card">
<div class="card-header text-center">
<h4><i class="bi bi-person-circle"></i> 用户登录</h4>
</div>
<div class="card-body">
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
{% if form.username.errors %}
<div class="invalid-feedback">
{% for error in form.username.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
{% if form.password.errors %}
<div class="invalid-feedback">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
<hr>
<div class="text-center">
<p class="mb-0">还没有账户? <a href="{{ url_for('auth.register') }}" class="auth-link">立即注册</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/orders.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('order.list') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="#" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
</a>
<a href="#" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-bag"></i> 我的订单</h5>
</div>
<div class="card-body">
<!-- 订单状态筛选 -->
<ul class="nav nav-pills mb-4">
<li class="nav-item">
<a class="nav-link {% if not current_status %}active{% endif %}"
href="{{ url_for('order.list') }}">全部订单</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_status == 1 %}active{% endif %}"
href="{{ url_for('order.list', status=1) }}">待支付</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_status == 2 %}active{% endif %}"
href="{{ url_for('order.list', status=2) }}">待发货</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_status == 3 %}active{% endif %}"
href="{{ url_for('order.list', status=3) }}">待收货</a>
</li>
<li class="nav-item">
<a class="nav-link {% if current_status == 5 %}active{% endif %}"
href="{{ url_for('order.list', status=5) }}">已完成</a>
</li>
</ul>
<!-- 订单列表 -->
{% if orders.items %}
{% for order in orders.items %}
<div class="order-card">
<!-- 订单头部 -->
<div class="order-header">
<div class="row align-items-center">
<div class="col-md-3">
<strong>订单号:</strong>{{ order.order_sn }}
</div>
<div class="col-md-3">
<strong>下单时间:</strong>{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}
</div>
<div class="col-md-3">
{% if order.status == 1 %}
<span class="badge bg-warning status-badge">{{ order.get_status_text() }}</span>
{% elif order.status == 2 %}
<span class="badge bg-info status-badge">{{ order.get_status_text() }}</span>
{% elif order.status == 3 %}
<span class="badge bg-primary status-badge">{{ order.get_status_text() }}</span>
{% elif order.status == 5 %}
<span class="badge bg-success status-badge">{{ order.get_status_text() }}</span>
{% elif order.status == 6 %}
<span class="badge bg-secondary status-badge">{{ order.get_status_text() }}</span>
{% else %}
<span class="badge bg-dark status-badge">{{ order.get_status_text() }}</span>
{% endif %}
</div>
<div class="col-md-3 text-end">
<a href="{{ url_for('order.detail', order_id=order.id) }}"
class="btn btn-outline-primary btn-sm">查看详情</a>
</div>
</div>
</div>
<!-- 订单商品 -->
{% for item in order.order_items[:3] %}
<div class="order-item">
<div class="row align-items-center">
<div class="col-md-2">
<img src="{{ item.product_image or '/static/images/default-product.jpg' }}"
class="product-image" alt="{{ item.product_name }}">
</div>
<div class="col-md-6">
<h6 class="mb-1">{{ item.product_name }}</h6>
{% if item.spec_combination %}
<p class="text-muted mb-0">{{ item.spec_combination }}</p>
{% endif %}
</div>
<div class="col-md-2 text-center">
<span class="text-muted">× {{ item.quantity }}</span>
</div>
<div class="col-md-2 text-end">
<span class="fw-bold">¥{{ "%.2f"|format(item.total_price) }}</span>
</div>
</div>
</div>
{% endfor %}
{% if order.order_items|length > 3 %}
<div class="order-item text-center text-muted">
<small>还有 {{ order.order_items|length - 3 }} 件商品...</small>
</div>
{% endif %}
<!-- 订单底部 -->
<div class="order-footer">
<div class="row align-items-center">
<div class="col-md-6">
<div class="d-flex gap-2 flex-wrap">
{% if order.can_pay() %}
<a href="{{ url_for('order.pay', payment_sn=order.payments[0].payment_sn) }}"
class="btn btn-danger btn-sm">立即支付</a>
{% endif %}
{% if order.can_cancel() %}
<button class="btn btn-outline-secondary btn-sm"
onclick="cancelOrder({{ order.id }})">取消订单</button>
{% endif %}
{% if order.can_confirm_receipt() %}
<button class="btn btn-success btn-sm"
onclick="confirmReceipt({{ order.id }})">确认收货</button>
{% endif %}
{% if order.status == 4 %}
<!-- 评价商品按钮,根据商品数量展示 -->
{% if order.order_items|length == 1 %}
<a href="{{ url_for('review.write_review', order_id=order.id, product_id=order.order_items[0].product_id) }}"
class="btn btn-outline-warning btn-sm">评价商品</a>
{% else %}
<div class="dropdown">
<button class="btn btn-outline-warning btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
评价商品
</button>
<ul class="dropdown-menu">
{% for item in order.order_items %}
<li>
<a class="dropdown-item"
href="{{ url_for('review.write_review', order_id=order.id, product_id=item.product_id) }}">
{{ item.product_name[:20] }}{% if item.product_name|length > 20 %}...{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="col-md-6 text-end">
<div>
<span class="text-muted">共 {{ order.order_items|length }} 件商品,</span>
<span class="text-muted">应付:</span>
<span class="order-amount">¥{{ "%.2f"|format(order.actual_amount) }}</span>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
{% if orders.pages > 1 %}
<nav aria-label="订单分页">
<ul class="pagination justify-content-center">
{% if orders.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('order.list', page=orders.prev_num, status=current_status) }}">上一页</a>
</li>
{% endif %}
{% for page_num in orders.iter_pages() %}
{% if page_num %}
{% if page_num != orders.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('order.list', page=page_num, status=current_status) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if orders.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('order.list', page=orders.next_num, status=current_status) }}">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center empty-state">
<i class="bi bi-bag-x display-1 text-muted"></i>
<h5 class="mt-3 text-muted">
{% if current_status %}
暂无该状态订单
{% else %}
暂无订单
{% endif %}
</h5>
<p class="text-muted">您还没有任何订单,快去购物吧!</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">
<i class="bi bi-shop"></i> 去购物
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/orders.js') }}"></script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/profile.css') }}">
{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-circle"></i> 个人中心</h5>
</div>
<div class="list-group list-group-flush">
<a href="{{ url_for('user.profile') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-person"></i> 基本信息
</a>
<a href="{{ url_for('user.orders') }}" class="list-group-item list-group-item-action">
<i class="bi bi-bag"></i> 我的订单
</a>
<a href="{{ url_for('address.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-geo-alt"></i> 收货地址
</a>
<a href="{{ url_for('favorite.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-heart"></i> 我的收藏
<a href="{{ url_for('review.my_reviews') }}" class="list-group-item list-group-item-action">
<i class="bi bi-star"></i> 我的评价
</a>
<a href="{{ url_for('history.index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-clock-history"></i> 浏览历史
</a>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person"></i> 基本信息</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<table class="table table-borderless">
<tr>
<td width="120"><strong>用户名:</strong></td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td><strong>昵称:</strong></td>
<td>{{ user.nickname or '未设置' }}</td>
</tr>
<tr>
<td><strong>手机号:</strong></td>
<td>{{ user.phone or '未绑定' }}</td>
</tr>
<tr>
<td><strong>邮箱:</strong></td>
<td>{{ user.email or '未绑定' }}</td>
</tr>
<tr>
<td><strong>性别:</strong></td>
<td>
{% if user.gender == 1 %}男
{% elif user.gender == 2 %}女
{% else %}未设置
{% endif %}
</td>
</tr>
<tr>
<td><strong>注册时间:</strong></td>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M:%S') if user.created_at else '未知' }}</td>
</tr>
</table>
<div class="mt-3">
<button class="btn btn-primary me-2">
<i class="bi bi-pencil"></i> 编辑资料
</button>
<button class="btn btn-outline-secondary">
<i class="bi bi-key"></i> 修改密码
</button>
</div>
</div>
<div class="col-md-4 text-center">
<div class="mb-3">
<div class="avatar-upload">
{% if user.avatar_url %}
<img src="{{ user.avatar_url }}" alt="头像" class="avatar-preview" id="avatarPreview">
{% else %}
<div class="avatar-placeholder" id="avatarPlaceholder">
<i class="bi bi-person display-4 text-muted"></i>
</div>
{% endif %}
<div class="upload-overlay" onclick="triggerFileInput()">
<i class="bi bi-camera text-white fs-3"></i>
</div>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input type="file" id="avatarInput" accept="image/*" style="display: none;" onchange="handleFileSelect(event)">
<!-- 上传进度 -->
<div class="upload-progress" id="uploadProgress">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted">上传中...</small>
</div>
<button class="btn btn-outline-primary btn-sm" onclick="triggerFileInput()">
<i class="bi bi-camera"></i> 更换头像
</button>
<div class="mt-2">
<small class="text-muted">支持 JPG、PNG 格式,大小不超过 2MB</small>
</div>
</div>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="row mt-4">
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('user.orders') }}" class="text-decoration-none">
<i class="bi bi-bag display-4 text-primary mb-2"></i>
<h6 class="card-title">我的订单</h6>
<small class="text-muted">查看所有订单</small>
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('cart.index') }}" class="text-decoration-none">
<i class="bi bi-cart display-4 text-success mb-2"></i>
<h6 class="card-title">购物车</h6>
<small class="text-muted">查看购物车</small>
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('favorite.index') }}" class="text-decoration-none">
<i class="bi bi-heart display-4 text-danger mb-2"></i>
<h6 class="card-title">我的收藏</h6>
<small class="text-muted">收藏的商品</small>
</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('history.index') }}" class="text-decoration-none">
<i class="bi bi-clock-history display-4 text-info mb-2"></i>
<h6 class="card-title">浏览历史</h6>
<small class="text-muted">查看浏览记录</small>
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('address.index') }}" class="text-decoration-none">
<i class="bi bi-geo-alt display-4 text-warning mb-2"></i>
<h6 class="card-title">收货地址</h6>
<small class="text-muted">管理收货地址</small>
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center">
<div class="card-body">
<a href="{{ url_for('review.my_reviews') }}" class="text-decoration-none">
<i class="bi bi-star display-4 text-secondary mb-2"></i>
<h6 class="card-title">我的评价</h6>
<small class="text-muted">查看我的评价</small>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 重新设计的图片预览模态框 -->
<div class="modal fade image-preview-modal" id="imagePreviewModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title">
<i class="bi bi-image text-primary"></i> 头像预览
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="preview-container">
<div class="preview-image-wrapper">
<img src="" alt="预览图片" class="preview-image" id="previewImage">
</div>
<div class="preview-info">
<div class="mb-2">
<i class="bi bi-info-circle text-primary"></i>
<strong>图片信息</strong>
</div>
<div class="preview-stats">
<div class="stat-item">
<div class="stat-value" id="imageWidth">-</div>
<div class="stat-label">宽度(px)</div>
</div>
<div class="stat-item">
<div class="stat-value" id="imageHeight">-</div>
<div class="stat-label">高度(px)</div>
</div>
<div class="stat-item">
<div class="stat-value" id="imageSize">-</div>
<div class="stat-label">大小</div>
</div>
<div class="stat-item">
<div class="stat-value" id="imageType">-</div>
<div class="stat-label">格式</div>
</div>
</div>
</div>
<div class="mt-3">
<small class="text-muted">
<i class="bi bi-check-circle text-success"></i>
图片将被自动调整为合适的头像尺寸
</small>
</div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> 取消
</button>
<button type="button" class="btn btn-primary" onclick="confirmUpload()">
<i class="bi bi-upload"></i> 确认上传
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/profile.js') }}"></script>
{% 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/register.css') }}">
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow">
<div class="card-header text-center">
<h4><i class="bi bi-person-plus"></i> 用户注册</h4>
</div>
<div class="card-body">
<form method="POST" id="registerForm">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
{% if form.username.errors %}
<div class="invalid-feedback">
{% for error in form.username.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">用户名只能包含字母、数字和下划线3-20个字符</div>
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
<div class="input-group">
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else ""), id="emailInput") }}
<button type="button" class="btn btn-outline-primary" id="sendEmailCodeBtn">
<span id="btnText">发送验证码</span>
</button>
</div>
{% if form.email.errors %}
<div class="invalid-feedback d-block">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.email_code.label(class="form-label") }}
{{ form.email_code(class="form-control" + (" is-invalid" if form.email_code.errors else ""), placeholder="请输入6位数字验证码") }}
{% if form.email_code.errors %}
<div class="invalid-feedback">
{% for error in form.email_code.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.phone.label(class="form-label") }}
{{ form.phone(class="form-control" + (" is-invalid" if form.phone.errors else "")) }}
{% if form.phone.errors %}
<div class="invalid-feedback">
{% for error in form.phone.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else ""), id="passwordInput") }}
{% if form.password.errors %}
<div class="invalid-feedback">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">密码必须包含至少一个字母和一个数字6-20个字符</div>
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control" + (" is-invalid" if form.confirm_password.errors else ""), id="confirmPasswordInput") }}
{% if form.confirm_password.errors %}
<div class="invalid-feedback">
{% for error in form.confirm_password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div id="passwordMatchMessage" class="form-text"></div>
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-success") }}
</div>
</form>
<hr>
<div class="text-center">
<p class="mb-0">已有账户? <a href="{{ url_for('auth.login') }}">立即登录</a></p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/register.js') }}"></script>
{% 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"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>验证码邮件</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;">
<h2 style="color: #007bff;">太白购物平台</h2>
</div>
<div style="background: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 20px;">
<h3>您好!</h3>
<p>您正在进行<strong>{type_map.get(code_type, "验证")}</strong>操作,验证码为:</p>
<div style="text-align: center; margin: 20px 0;">
<span style="font-size: 24px; font-weight: bold; color: #007bff; background: #e9ecef; padding: 10px 20px; border-radius: 5px; letter-spacing: 2px;">{code}</span>
</div>
<p style="color: #666;">验证码有效期为10分钟请及时使用。</p>
<p style="color: #666;">如果这不是您的操作,请忽略此邮件。</p>
</div>
<div style="text-align: center; color: #666; font-size: 12px;">
<p>此邮件由系统自动发送,请勿回复。</p>
<p>© 2024 太白购物平台 版权所有</p>
</div>
</div>
</body>
</html>
"""
return send_email(email, subject, html_template)
🔸==============================================================================
📄 文件: app/utils/file_upload.py
📊 大小: 12684 bytes (12.39 KB)
🕒 修改时间: 2025-07-03 04:20:25
🔸==============================================================================
"""
文件上传处理工具
"""
import os
import magic
from PIL import Image
from io import BytesIO
from werkzeug.utils import secure_filename
from config.cos_config import COSConfig
from .cos_client import cos_client
class FileUploadHandler:
"""文件上传处理器"""
@staticmethod
def validate_file(file_obj, file_type='image'):
"""
验证文件
Args:
file_obj: 文件对象
file_type: 文件类型 (image, file)
Returns:
dict: 验证结果
"""
if not file_obj or not file_obj.filename:
return {'valid': False, 'error': '请选择文件'}
# 检查文件扩展名
filename = secure_filename(file_obj.filename)
if '.' not in filename:
return {'valid': False, 'error': '文件格式不正确'}
file_ext = filename.rsplit('.', 1)[1].lower()
if file_type == 'image':
allowed_extensions = COSConfig.ALLOWED_IMAGE_EXTENSIONS
max_size = COSConfig.MAX_IMAGE_SIZE
else:
allowed_extensions = COSConfig.ALLOWED_FILE_EXTENSIONS
max_size = COSConfig.MAX_FILE_SIZE
if file_ext not in allowed_extensions:
return {
'valid': False,
'error': f'不支持的文件格式,支持格式: {", ".join(allowed_extensions)}'
}
# 检查文件大小
file_obj.seek(0, 2) # 移动到文件末尾
file_size = file_obj.tell()
file_obj.seek(0) # 重置文件指针
if file_size > max_size:
max_size_mb = max_size / (1024 * 1024)
return {'valid': False, 'error': f'文件大小不能超过 {max_size_mb:.1f}MB'}
# 验证文件内容类型(防止恶意文件)
try:
file_content = file_obj.read(1024) # 读取前1KB用于检测
file_obj.seek(0) # 重置文件指针
mime_type = magic.from_buffer(file_content, mime=True)
if file_type == 'image' and not mime_type.startswith('image/'):
return {'valid': False, 'error': '文件内容不是有效的图片格式'}
except Exception:
# 如果magic检测失败继续处理某些环境可能没有libmagic
pass
return {'valid': True, 'filename': filename, 'size': file_size}
@staticmethod
def process_image(file_obj, max_width=1200, max_height=1200, quality=None):
"""
处理图片(压缩、调整尺寸)
Args:
file_obj: 图片文件对象
max_width: 最大宽度
max_height: 最大高度
quality: 压缩质量
Returns:
BytesIO: 处理后的图片数据
"""
try:
# 打开图片
image = Image.open(file_obj)
# 转换RGBA到RGB处理PNG透明背景
if image.mode in ('RGBA', 'LA', 'P'):
background = Image.new('RGB', image.size, (255, 255, 255))
if image.mode == 'P':
image = image.convert('RGBA')
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
image = background
# 调整图片尺寸
if image.width > max_width or image.height > max_height:
image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
# 保存处理后的图片
output = BytesIO()
image.save(
output,
format='JPEG',
quality=quality or COSConfig.IMAGE_QUALITY,
optimize=True
)
output.seek(0)
return output
except Exception as e:
raise Exception(f"图片处理失败: {str(e)}")
@staticmethod
def upload_image(file_obj, folder_type='temp', process_image=True):
"""
上传图片到COS
Args:
file_obj: 图片文件对象
folder_type: 存储文件夹类型
process_image: 是否处理图片
Returns:
dict: 上传结果
"""
# 验证文件
validation = FileUploadHandler.validate_file(file_obj, 'image')
if not validation['valid']:
return {
'success': False,
'error': validation['error'],
'url': None,
'file_key': None
}
try:
# 处理图片
if process_image:
processed_file = FileUploadHandler.process_image(file_obj)
upload_file = processed_file
else:
file_obj.seek(0)
upload_file = file_obj
# 上传到COS
result = cos_client.upload_file(
upload_file,
folder_type,
validation['filename']
)
return result
except Exception as e:
return {
'success': False,
'error': f"上传失败: {str(e)}",
'url': None,
'file_key': None
}
@staticmethod
def upload_file(file_obj, folder_type='temp'):
"""
上传普通文件到COS
Args:
file_obj: 文件对象
folder_type: 存储文件夹类型
Returns:
dict: 上传结果
"""
# 验证文件
validation = FileUploadHandler.validate_file(file_obj, 'file')
if not validation['valid']:
return {
'success': False,
'error': validation['error'],
'url': None,
'file_key': None
}
try:
file_obj.seek(0)
# 上传到COS
result = cos_client.upload_file(
file_obj,
folder_type,
validation['filename']
)
return result
except Exception as e:
return {
'success': False,
'error': f"上传失败: {str(e)}",
'url': None,
'file_key': None
}
# 创建全局文件上传处理器实例
file_upload_handler = FileUploadHandler()
"""
文件上传处理工具
"""
import os
import magic
from PIL import Image
from io import BytesIO
from werkzeug.utils import secure_filename
from config.cos_config import COSConfig
from .cos_client import cos_client
class FileUploadHandler:
"""文件上传处理器"""
@staticmethod
def validate_file(file_obj, file_type='image'):
"""
验证文件
Args:
file_obj: 文件对象
file_type: 文件类型 (image, file)
Returns:
dict: 验证结果
"""
if not file_obj or not file_obj.filename:
return {'valid': False, 'error': '请选择文件'}
# 检查文件扩展名
filename = secure_filename(file_obj.filename)
if '.' not in filename:
return {'valid': False, 'error': '文件格式不正确'}
file_ext = filename.rsplit('.', 1)[1].lower()
if file_type == 'image':
allowed_extensions = COSConfig.ALLOWED_IMAGE_EXTENSIONS
max_size = COSConfig.MAX_IMAGE_SIZE
else:
allowed_extensions = COSConfig.ALLOWED_FILE_EXTENSIONS
max_size = COSConfig.MAX_FILE_SIZE
if file_ext not in allowed_extensions:
return {
'valid': False,
'error': f'不支持的文件格式,支持格式: {", ".join(allowed_extensions)}'
}
# 检查文件大小
file_obj.seek(0, 2) # 移动到文件末尾
file_size = file_obj.tell()
file_obj.seek(0) # 重置文件指针
if file_size > max_size:
max_size_mb = max_size / (1024 * 1024)
return {'valid': False, 'error': f'文件大小不能超过 {max_size_mb:.1f}MB'}
# 验证文件内容类型(防止恶意文件)
try:
file_content = file_obj.read(1024) # 读取前1KB用于检测
file_obj.seek(0) # 重置文件指针
mime_type = magic.from_buffer(file_content, mime=True)
if file_type == 'image' and not mime_type.startswith('image/'):
return {'valid': False, 'error': '文件内容不是有效的图片格式'}
except Exception:
# 如果magic检测失败继续处理某些环境可能没有libmagic
pass
return {'valid': True, 'filename': filename, 'size': file_size}
@staticmethod
def process_image(file_obj, max_width=1200, max_height=1200, quality=None):
"""
处理图片(压缩、调整尺寸)
Args:
file_obj: 图片文件对象
max_width: 最大宽度
max_height: 最大高度
quality: 压缩质量
Returns:
BytesIO: 处理后的图片数据
"""
try:
# 打开图片
image = Image.open(file_obj)
# 转换RGBA到RGB处理PNG透明背景
if image.mode in ('RGBA', 'LA', 'P'):
background = Image.new('RGB', image.size, (255, 255, 255))
if image.mode == 'P':
image = image.convert('RGBA')
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
image = background
# 调整图片尺寸
if image.width > max_width or image.height > max_height:
image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
# 保存处理后的图片
output = BytesIO()
image.save(
output,
format='JPEG',
quality=quality or COSConfig.IMAGE_QUALITY,
optimize=True
)
output.seek(0)
return output
except Exception as e:
raise Exception(f"图片处理失败: {str(e)}")
@staticmethod
def upload_image(file_obj, folder_type='temp', process_image=True):
"""
上传图片到COS
Args:
file_obj: 图片文件对象
folder_type: 存储文件夹类型
process_image: 是否处理图片
Returns:
dict: 上传结果
"""
# 验证文件
validation = FileUploadHandler.validate_file(file_obj, 'image')
if not validation['valid']:
return {
'success': False,
'error': validation['error'],
'url': None,
'file_key': None
}
try:
# 处理图片
if process_image:
processed_file = FileUploadHandler.process_image(file_obj)
upload_file = processed_file
else:
file_obj.seek(0)
upload_file = file_obj
# 上传到COS
result = cos_client.upload_file(
upload_file,
folder_type,
validation['filename']
)
return result
except Exception as e:
return {
'success': False,
'error': f"上传失败: {str(e)}",
'url': None,
'file_key': None
}
@staticmethod
def upload_file(file_obj, folder_type='temp'):
"""
上传普通文件到COS
Args:
file_obj: 文件对象
folder_type: 存储文件夹类型
Returns:
dict: 上传结果
"""
# 验证文件
validation = FileUploadHandler.validate_file(file_obj, 'file')
if not validation['valid']:
return {
'success': False,
'error': validation['error'],
'url': None,
'file_key': None
}
try:
file_obj.seek(0)
# 上传到COS
result = cos_client.upload_file(
file_obj,
folder_type,
validation['filename']
)
return result
except Exception as e:
return {
'success': False,
'error': f"上传失败: {str(e)}",
'url': None,
'file_key': None
}
# 创建全局文件上传处理器实例
file_upload_handler = FileUploadHandler()
🔸==============================================================================
📄 文件: app/utils/helpers.py
📊 大小: 0 bytes (0.00 KB)
🕒 修改时间: 2025-07-03 02:46:14
🔸==============================================================================
🔸==============================================================================
📄 文件: app/utils/sms.py
📊 大小: 0 bytes (0.00 KB)
🕒 修改时间: 2025-07-03 02:46:14
🔸==============================================================================
🔸==============================================================================
📄 文件: app/utils/wechat_pay.py
📊 大小: 0 bytes (0.00 KB)
🕒 修改时间: 2025-07-03 02:46:14
🔸==============================================================================
🔸==============================================================================
📄 文件: app/views/__init__.py
📊 大小: 0 bytes (0.00 KB)
🕒 修改时间: 2025-07-03 02:46:14
🔸==============================================================================
🔸==============================================================================
📄 文件: app/views/address.py
📊 大小: 8172 bytes (7.98 KB)
🕒 修改时间: 2025-07-04 03:10:28
🔸==============================================================================
"""
地址管理视图
"""
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash
from app.models.address import UserAddress
from app.models.user import User
from app.forms import AddressForm
from app.utils.decorators import login_required
from config.database import db
address_bp = Blueprint('address', __name__, url_prefix='/address')
@address_bp.route('/')
@login_required
def index():
"""地址管理页面"""
user_id = session['user_id']
addresses = UserAddress.get_user_addresses(user_id)
return render_template('user/addresses.html', addresses=addresses)
@address_bp.route('/add', methods=['GET', 'POST'])
@login_required
def add():
"""添加地址"""
form = AddressForm()
if request.method == 'POST':
# 手动验证必填字段
if not all([
form.receiver_name.data,
form.receiver_phone.data,
form.province.data,
form.city.data,
form.district.data,
form.detail_address.data
]):
flash('请填写所有必填信息', 'error')
return render_template('user/address_form.html', form=form, action='add')
# 验证手机号格式
import re
if not re.match(r'^1[3-9]\d{9}$', form.receiver_phone.data):
flash('请输入有效的手机号', 'error')
return render_template('user/address_form.html', form=form, action='add')
try:
user_id = session['user_id']
# 如果是第一个地址或设为默认,处理默认地址
if form.is_default.data or not UserAddress.query.filter_by(user_id=user_id).first():
UserAddress.query.filter_by(user_id=user_id).update({'is_default': 0})
is_default = 1
else:
is_default = 0
address = UserAddress(
user_id=user_id,
receiver_name=form.receiver_name.data.strip(),
receiver_phone=form.receiver_phone.data.strip(),
province=form.province.data.strip(),
city=form.city.data.strip(),
district=form.district.data.strip(),
detail_address=form.detail_address.data.strip(),
postal_code=form.postal_code.data.strip() if form.postal_code.data else None,
is_default=is_default
)
db.session.add(address)
db.session.commit()
flash('地址添加成功', 'success')
return redirect(url_for('address.index'))
except Exception as e:
db.session.rollback()
flash(f'添加失败: {str(e)}', 'error')
return render_template('user/address_form.html', form=form, action='add')
@address_bp.route('/edit/<int:address_id>', methods=['GET', 'POST'])
@login_required
def edit(address_id):
"""编辑地址"""
user_id = session['user_id']
address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first_or_404()
form = AddressForm()
if request.method == 'GET':
# 预填充表单数据
form.receiver_name.data = address.receiver_name
form.receiver_phone.data = address.receiver_phone
form.province.data = address.province
form.city.data = address.city
form.district.data = address.district
form.detail_address.data = address.detail_address
form.postal_code.data = address.postal_code
form.is_default.data = bool(address.is_default)
elif request.method == 'POST':
# 手动验证必填字段
if not all([
form.receiver_name.data,
form.receiver_phone.data,
form.province.data,
form.city.data,
form.district.data,
form.detail_address.data
]):
flash('请填写所有必填信息', 'error')
return render_template('user/address_form.html', form=form, action='edit', address=address)
# 验证手机号格式
import re
if not re.match(r'^1[3-9]\d{9}$', form.receiver_phone.data):
flash('请输入有效的手机号', 'error')
return render_template('user/address_form.html', form=form, action='edit', address=address)
try:
# 如果设为默认地址,先取消其他默认地址
if form.is_default.data and not address.is_default:
UserAddress.query.filter_by(user_id=user_id).update({'is_default': 0})
address.is_default = 1
elif not form.is_default.data and address.is_default:
# 如果取消当前默认地址,需要检查是否还有其他地址
other_addresses = UserAddress.query.filter(
UserAddress.user_id == user_id,
UserAddress.id != address_id
).first()
if other_addresses:
address.is_default = 0
else:
flash('至少需要保留一个默认地址', 'warning')
return render_template('user/address_form.html', form=form, action='edit', address=address)
# 更新地址信息
address.receiver_name = form.receiver_name.data.strip()
address.receiver_phone = form.receiver_phone.data.strip()
address.province = form.province.data.strip()
address.city = form.city.data.strip()
address.district = form.district.data.strip()
address.detail_address = form.detail_address.data.strip()
address.postal_code = form.postal_code.data.strip() if form.postal_code.data else None
db.session.commit()
flash('地址更新成功', 'success')
return redirect(url_for('address.index'))
except Exception as e:
db.session.rollback()
flash(f'更新失败: {str(e)}', 'error')
return render_template('user/address_form.html', form=form, action='edit', address=address)
@address_bp.route('/delete/<int:address_id>', methods=['POST'])
@login_required
def delete(address_id):
"""删除地址"""
try:
user_id = session['user_id']
address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first()
if not address:
return jsonify({'success': False, 'message': '地址不存在'})
# 检查是否是唯一地址
address_count = UserAddress.query.filter_by(user_id=user_id).count()
if address_count <= 1:
return jsonify({'success': False, 'message': '至少需要保留一个地址'})
# 如果删除的是默认地址,需要设置新的默认地址
if address.is_default:
other_address = UserAddress.query.filter(
UserAddress.user_id == user_id,
UserAddress.id != address_id
).first()
if other_address:
other_address.is_default = 1
db.session.delete(address)
db.session.commit()
return jsonify({'success': True, 'message': '地址删除成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
@address_bp.route('/set_default/<int:address_id>', methods=['POST'])
@login_required
def set_default(address_id):
"""设置默认地址"""
try:
user_id = session['user_id']
success = UserAddress.set_default_address(user_id, address_id)
if success:
return jsonify({'success': True, 'message': '默认地址设置成功'})
else:
return jsonify({'success': False, 'message': '地址不存在'})
except Exception as e:
return jsonify({'success': False, 'message': f'设置失败: {str(e)}'})
@address_bp.route('/api/list')
@login_required
def api_list():
"""获取用户地址列表API"""
user_id = session['user_id']
addresses = UserAddress.get_user_addresses(user_id)
return jsonify({
'success': True,
'addresses': [addr.to_dict() for addr in addresses]
})
🔸==============================================================================
📄 文件: app/views/admin.py
📊 大小: 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/<int:user_id>/detail')
@admin_required
def user_detail(user_id):
"""获取用户详情"""
try:
user = User.query.get_or_404(user_id)
return jsonify({
'success': True,
'user': user.to_dict()
})
except Exception as e:
return jsonify({
'success': False,
'message': str(e)
})
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@admin_required
@log_operation('切换用户状态')
def toggle_user_status(user_id):
"""切换用户状态"""
try:
user = User.query.get_or_404(user_id)
data = request.get_json()
new_status = data.get('status')
if new_status not in [0, 1]:
return jsonify({
'success': False,
'message': '无效的状态值'
})
user.status = new_status
db.session.commit()
action_text = '启用' if new_status == 1 else '禁用'
return jsonify({
'success': True,
'message': f'用户已{action_text}'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': str(e)
})
@admin_bp.route('/orders')
@admin_required
def orders():
"""订单管理"""
page = request.args.get('page', 1, type=int)
per_page = 20
query = Order.query.order_by(Order.created_at.desc())
# 搜索功能
search = request.args.get('search', '').strip()
if search:
query = query.filter(
or_(
Order.order_sn.like(f'%{search}%'),
Order.user.has(User.username.like(f'%{search}%')),
Order.user.has(User.phone.like(f'%{search}%'))
)
)
# 状态筛选
status = request.args.get('status', '', type=str)
if status:
query = query.filter(Order.status == int(status))
# 日期筛选
start_date = request.args.get('start_date', '').strip()
end_date = request.args.get('end_date', '').strip()
if start_date:
try:
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d')
query = query.filter(Order.created_at >= start_date_obj)
except ValueError:
pass
if end_date:
try:
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)
query = query.filter(Order.created_at < end_date_obj)
except ValueError:
pass
orders = query.paginate(page=page, per_page=per_page, error_out=False)
# 获取订单统计
order_stats = {}
for status_code, status_name in Order.STATUS_CHOICES.items():
count = Order.query.filter_by(status=status_code).count()
order_stats[status_code] = {'name': status_name, 'count': count}
return render_template('admin/orders.html',
orders=orders,
search=search,
status=status,
start_date=start_date,
end_date=end_date,
order_stats=order_stats,
ORDER_STATUS=Order.STATUS_CHOICES)
@admin_bp.route('/orders/<int:order_id>')
@admin_required
def order_detail(order_id):
"""订单详情"""
order = Order.query.get_or_404(order_id)
# 获取支付记录
payment = Payment.query.filter_by(order_id=order_id).first()
# 获取物流信息
shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first()
return render_template('admin/order_detail.html',
order=order,
payment=payment,
shipping_info=shipping_info)
@admin_bp.route('/orders/<int:order_id>/ship', methods=['POST'])
@admin_required
@log_operation('订单发货')
def ship_order(order_id):
"""订单发货"""
try:
order = Order.query.get_or_404(order_id)
if order.status != Order.STATUS_PENDING_SHIPMENT:
return jsonify({'success': False, 'message': '订单状态不允许发货'})
# 获取发货信息
shipping_company = request.form.get('shipping_company', '').strip()
tracking_number = request.form.get('tracking_number', '').strip()
if not shipping_company or not tracking_number:
return jsonify({'success': False, 'message': '请填写完整的物流信息'})
# 更新订单状态
order.status = Order.STATUS_SHIPPED
order.shipped_at = datetime.utcnow()
# 创建或更新物流信息
shipping_info = ShippingInfo.query.filter_by(order_id=order_id).first()
if not shipping_info:
shipping_info = ShippingInfo(order_id=order_id)
db.session.add(shipping_info)
shipping_info.shipping_company = shipping_company
shipping_info.tracking_number = tracking_number
shipping_info.shipping_status = 1 # 已发货
db.session.commit()
return jsonify({'success': True, 'message': '发货成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'发货失败: {str(e)}'})
@admin_bp.route('/orders/<int:order_id>/refund', methods=['POST'])
@admin_required
@log_operation('订单退款')
def refund_order(order_id):
"""订单退款"""
try:
order = Order.query.get_or_404(order_id)
if order.status not in [Order.STATUS_PENDING_SHIPMENT, Order.STATUS_SHIPPED, Order.STATUS_REFUNDING]:
return jsonify({'success': False, 'message': '订单状态不允许退款'})
# 获取退款信息
refund_reason = request.form.get('refund_reason', '').strip()
if not refund_reason:
return jsonify({'success': False, 'message': '请填写退款原因'})
# 更新订单状态
order.status = Order.STATUS_REFUNDING
# 更新支付记录状态
payment = Payment.query.filter_by(order_id=order_id).first()
if payment:
payment.status = Payment.STATUS_REFUNDED
# 恢复库存
from app.models.product import ProductInventory
for item in order.order_items:
if item.sku_code:
sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first()
if sku_info:
sku_info.stock += item.quantity
# 减少销量
if item.product:
item.product.sales_count = max(0, item.product.sales_count - item.quantity)
db.session.commit()
return jsonify({'success': True, 'message': '退款处理成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'退款失败: {str(e)}'})
@admin_bp.route('/orders/<int:order_id>/cancel', methods=['POST'])
@admin_required
@log_operation('取消订单')
def cancel_order(order_id):
"""取消订单"""
try:
order = Order.query.get_or_404(order_id)
if not order.can_cancel():
return jsonify({'success': False, 'message': '订单状态不允许取消'})
# 获取取消原因
cancel_reason = request.form.get('cancel_reason', '').strip()
# 更新订单状态
order.status = Order.STATUS_CANCELLED
# 恢复库存
from app.models.product import ProductInventory
for item in order.order_items:
if item.sku_code:
sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first()
if sku_info:
sku_info.stock += item.quantity
# 减少销量
if item.product:
item.product.sales_count = max(0, item.product.sales_count - item.quantity)
db.session.commit()
return jsonify({'success': True, 'message': '订单已取消'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'})
@admin_bp.route('/logs')
@admin_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/<int:log_id>/detail')
@admin_required
def log_detail(log_id):
"""获取日志详情"""
try:
log = OperationLog.query.get_or_404(log_id)
return jsonify({
'success': True,
'log': log.to_dict()
})
except Exception as e:
return jsonify({
'success': False,
'message': str(e)
})
@admin_bp.route('/logs/clear', methods=['POST'])
@admin_required
@log_operation('清理操作日志')
def clear_logs():
"""清理操作日志"""
try:
data = request.get_json()
days_to_keep = data.get('days_to_keep', 30)
# 计算删除日期
delete_before = datetime.now() - timedelta(days=days_to_keep)
# 删除旧日志
deleted_count = OperationLog.query.filter(
OperationLog.created_at < delete_before
).delete()
db.session.commit()
return jsonify({
'success': True,
'message': f'已清理 {deleted_count} 条历史日志'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': str(e)
})
🔸==============================================================================
📄 文件: 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/<int:product_id>')
@login_required
def check(product_id):
"""检查商品是否已收藏"""
try:
is_favorited = UserFavorite.is_favorited(session['user_id'], product_id)
return jsonify({
'success': True,
'is_favorited': is_favorited
})
except Exception as e:
print(f"检查收藏状态失败: {str(e)}")
return jsonify({'success': False, 'message': str(e)})
@favorite_bp.route('/batch-remove', methods=['POST'])
@login_required
def batch_remove():
"""批量取消收藏"""
try:
data = request.get_json()
product_ids = data.get('product_ids', [])
if not product_ids:
return jsonify({'success': False, 'message': '请选择要取消的商品'})
success_count = 0
fail_count = 0
for product_id in product_ids:
try:
product_id = int(product_id)
success, _ = UserFavorite.remove_favorite(session['user_id'], product_id)
if success:
success_count += 1
else:
fail_count += 1
except (ValueError, TypeError):
fail_count += 1
message = f'成功取消收藏 {success_count} 个商品'
if fail_count > 0:
message += f',失败 {fail_count} 个'
# 获取更新后的收藏数量
favorite_count = UserFavorite.get_user_favorites_count(session['user_id'])
return jsonify({
'success': True,
'message': message,
'favorite_count': favorite_count
})
except Exception as e:
print(f"批量操作失败: {str(e)}")
return jsonify({'success': False, 'message': f'批量操作失败: {str(e)}'})
@favorite_bp.route('/count')
@login_required
def count():
"""获取收藏数量"""
try:
favorite_count = UserFavorite.get_user_favorites_count(session['user_id'])
return jsonify({
'success': True,
'favorite_count': favorite_count
})
except Exception as e:
print(f"获取收藏数量失败: {str(e)}")
return jsonify({'success': False, 'message': str(e)})
🔸==============================================================================
📄 文件: 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/<int:product_id>')
def product_detail(product_id):
"""商品详情页面"""
product = Product.query.filter_by(id=product_id, status=1).first_or_404()
# 增加浏览量
try:
product.view_count += 1
from config.database import db
db.session.commit()
except Exception as e:
current_app.logger.error(f"更新浏览量失败: {str(e)}")
# 获取商品图片(按排序)
images = product.images
if images:
images = sorted(images, key=lambda x: x.sort_order)
# 获取商品库存信息并转换为字典
inventory_list = product.inventory
inventory_data = []
if inventory_list:
for inventory in inventory_list:
inventory_data.append({
'id': inventory.id,
'sku_code': inventory.sku_code,
'spec_combination': inventory.spec_combination,
'price_adjustment': float(inventory.price_adjustment) if inventory.price_adjustment else 0,
'stock': inventory.stock,
'warning_stock': inventory.warning_stock,
'is_default': inventory.is_default,
'status': inventory.status,
'final_price': inventory.get_final_price()
})
# 获取推荐商品(同分类的其他商品)
recommended_products = Product.query.filter(
Product.category_id == product.category_id,
Product.id != product.id,
Product.status == 1
).order_by(Product.sales_count.desc()).limit(4).all()
return render_template('product/detail.html',
product=product,
images=images,
inventory_list=inventory_list,
inventory_data=inventory_data,
recommended_products=recommended_products)
@main_bp.route('/category/<int:category_id>')
def category_products(category_id):
"""分类商品页面(重定向到商品列表)"""
return redirect(url_for('main.product_list', category_id=category_id))
@main_bp.route('/search')
def search():
"""搜索页面(重定向到商品列表)"""
search_query = request.args.get('q', '').strip()
return redirect(url_for('main.product_list', search=search_query))
@main_bp.route('/about')
def about():
"""关于我们"""
return render_template('about.html')
🔸==============================================================================
📄 文件: app/views/order.py
📊 大小: 10973 bytes (10.72 KB)
🕒 修改时间: 2025-07-04 02:27:02
🔸==============================================================================
"""
订单视图
"""
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash, g
from app.models.order import Order, OrderItem
from app.models.cart import Cart
from app.models.address import UserAddress
from app.models.product import ProductInventory
from app.models.payment import Payment
from app.forms import CheckoutForm
from app.utils.decorators import login_required
from config.database import db
import json
order_bp = Blueprint('order', __name__, url_prefix='/order')
@order_bp.route('/checkout')
@login_required
def checkout():
"""订单结算页面"""
user_id = session['user_id']
selected_items = request.args.getlist('items')
if not selected_items:
flash('请选择要购买的商品', 'error')
return redirect(url_for('cart.index'))
# 获取选中的购物车项目
cart_items = Cart.query.filter(
Cart.id.in_(selected_items),
Cart.user_id == user_id
).all()
if not cart_items:
flash('选中的商品不存在', 'error')
return redirect(url_for('cart.index'))
# 检查商品可用性和库存
unavailable_items = []
total_amount = 0
for item in cart_items:
if not item.is_available():
unavailable_items.append(item.product.name)
else:
total_amount += item.get_total_price()
if unavailable_items:
flash(f'以下商品库存不足或已下架:{", ".join(unavailable_items)}', 'error')
return redirect(url_for('cart.index'))
# 获取用户地址
addresses = UserAddress.get_user_addresses(user_id)
if not addresses:
flash('请先添加收货地址', 'warning')
return redirect(url_for('address.add'))
# 计算运费
shipping_fee = 0 # 默认免运费
# 创建表单并设置地址选项
form = CheckoutForm()
form.address_id.choices = [(addr.id, f"{addr.receiver_name} - {addr.get_full_address()}")
for addr in addresses]
# 设置默认地址
default_address = UserAddress.get_default_address(user_id)
if default_address:
form.address_id.data = default_address.id
return render_template('order/checkout.html',
cart_items=cart_items,
addresses=addresses,
form=form,
total_amount=total_amount,
shipping_fee=shipping_fee,
final_amount=total_amount + shipping_fee)
@order_bp.route('/create', methods=['POST'])
@login_required
def create():
"""创建订单"""
try:
user_id = session['user_id']
data = request.get_json()
selected_items = data.get('selected_items', [])
address_id = data.get('address_id')
shipping_method = data.get('shipping_method', 'standard')
payment_method = data.get('payment_method', 'wechat')
remark = data.get('remark', '')
if not selected_items or not address_id:
return jsonify({'success': False, 'message': '参数错误'})
# 获取购物车商品
cart_items = Cart.query.filter(
Cart.id.in_(selected_items),
Cart.user_id == user_id
).all()
if not cart_items:
return jsonify({'success': False, 'message': '购物车商品不存在'})
# 验证地址
address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first()
if not address:
return jsonify({'success': False, 'message': '收货地址不存在'})
# 再次检查库存和计算总价
total_amount = 0
order_items_data = []
for cart_item in cart_items:
if not cart_item.is_available():
return jsonify({
'success': False,
'message': f'商品"{cart_item.product.name}"库存不足或已下架'
})
# 检查库存是否足够
current_stock = cart_item.get_stock()
if current_stock < cart_item.quantity:
return jsonify({
'success': False,
'message': f'商品"{cart_item.product.name}"库存不足,仅剩{current_stock}件'
})
item_total = cart_item.get_total_price()
total_amount += item_total
order_items_data.append({
'product_id': cart_item.product_id,
'sku_code': cart_item.sku_code,
'product_name': cart_item.product.name,
'product_image': cart_item.product.main_image,
'spec_combination': cart_item.spec_combination,
'price': cart_item.get_price(),
'quantity': cart_item.quantity,
'total_price': item_total
})
# 计算运费
shipping_fee_map = {
'standard': 0,
'express': 10,
'same_day': 20
}
shipping_fee = shipping_fee_map.get(shipping_method, 0)
actual_amount = total_amount + shipping_fee
# 创建订单
order = Order(
user_id=user_id,
order_sn=Order.generate_order_sn(),
total_amount=total_amount,
actual_amount=actual_amount,
shipping_fee=shipping_fee,
payment_method=payment_method,
shipping_method=shipping_method,
remark=remark
)
# 设置收货人信息
order.set_receiver_info({
'receiver_name': address.receiver_name,
'receiver_phone': address.receiver_phone,
'province': address.province,
'city': address.city,
'district': address.district,
'detail_address': address.detail_address,
'postal_code': address.postal_code,
'full_address': address.get_full_address()
})
db.session.add(order)
db.session.flush() # 获取订单ID
# 创建订单商品明细
for item_data in order_items_data:
order_item = OrderItem(
order_id=order.id,
**item_data
)
db.session.add(order_item)
# 扣减库存
for cart_item in cart_items:
if cart_item.sku_code:
sku_info = ProductInventory.query.filter_by(sku_code=cart_item.sku_code).first()
if sku_info:
sku_info.stock -= cart_item.quantity
# 增加销量
cart_item.product.sales_count += cart_item.quantity
# 删除购物车商品
for cart_item in cart_items:
db.session.delete(cart_item)
# 创建支付记录
payment = Payment(
order_id=order.id,
payment_sn=Payment.generate_payment_sn(),
payment_method=payment_method,
amount=actual_amount
)
db.session.add(payment)
db.session.commit()
return jsonify({
'success': True,
'message': '订单创建成功',
'order_id': order.id,
'order_sn': order.order_sn,
'payment_sn': payment.payment_sn
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'创建订单失败: {str(e)}'})
@order_bp.route('/list')
@login_required
def list():
"""订单列表"""
user_id = session['user_id']
status = request.args.get('status', type=int)
page = request.args.get('page', 1, type=int)
per_page = 10
query = Order.query.filter_by(user_id=user_id)
if status:
query = query.filter_by(status=status)
orders = query.order_by(Order.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return render_template('user/orders.html', orders=orders, current_status=status)
@order_bp.route('/detail/<int:order_id>')
@login_required
def detail(order_id):
"""订单详情"""
user_id = session['user_id']
order = Order.query.filter_by(id=order_id, user_id=user_id).first_or_404()
return render_template('order/detail.html', order=order)
@order_bp.route('/cancel/<int:order_id>', methods=['POST'])
@login_required
def cancel(order_id):
"""取消订单"""
try:
user_id = session['user_id']
order = Order.query.filter_by(id=order_id, user_id=user_id).first()
if not order:
return jsonify({'success': False, 'message': '订单不存在'})
if not order.can_cancel():
return jsonify({'success': False, 'message': '订单状态不允许取消'})
# 更新订单状态
order.status = Order.STATUS_CANCELLED
# 恢复库存
for item in order.order_items:
if item.sku_code:
sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first()
if sku_info:
sku_info.stock += item.quantity
# 减少销量
if item.product:
item.product.sales_count = max(0, item.product.sales_count - item.quantity)
db.session.commit()
return jsonify({'success': True, 'message': '订单已取消'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'取消失败: {str(e)}'})
@order_bp.route('/confirm_receipt/<int:order_id>', methods=['POST'])
@login_required
def confirm_receipt(order_id):
"""确认收货"""
try:
user_id = session['user_id']
order = Order.query.filter_by(id=order_id, user_id=user_id).first()
if not order:
return jsonify({'success': False, 'message': '订单不存在'})
if not order.can_confirm_receipt():
return jsonify({'success': False, 'message': '订单状态不允许确认收货'})
# 更新订单状态
order.status = Order.STATUS_PENDING_REVIEW
order.received_at = db.func.now()
db.session.commit()
return jsonify({'success': True, 'message': '确认收货成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'确认收货失败: {str(e)}'})
@order_bp.route('/pay/<payment_sn>')
@login_required
def pay(payment_sn):
"""支付页面"""
user_id = session['user_id']
payment = Payment.query.filter_by(payment_sn=payment_sn).first_or_404()
order = payment.order
# 验证订单所有权
if order.user_id != user_id:
flash('订单不存在', 'error')
return redirect(url_for('order.list'))
# 检查是否可以支付
if not order.can_pay():
flash('订单不可支付', 'error')
return redirect(url_for('order.detail', order_id=order.id))
return render_template('order/pay.html', order=order, payment=payment)
🔸==============================================================================
📄 文件: app/views/payment.py
📊 大小: 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/<payment_sn>')
@login_required
def check_status(payment_sn):
"""检查支付状态"""
try:
user_id = session['user_id']
payment = Payment.query.filter_by(payment_sn=payment_sn).first()
if not payment or payment.order.user_id != user_id:
return jsonify({'success': False, 'message': '支付记录不存在'})
return jsonify({
'success': True,
'status': payment.status,
'status_text': payment.get_status_text(),
'paid_at': payment.paid_at.isoformat() if payment.paid_at else None
})
except Exception as e:
return jsonify({'success': False, 'message': f'查询失败: {str(e)}'})
@payment_bp.route('/simulate_success/<payment_sn>', methods=['POST'])
@login_required
def simulate_success(payment_sn):
"""模拟支付成功(开发测试用)"""
try:
user_id = session['user_id']
payment = Payment.query.filter_by(payment_sn=payment_sn).first()
if not payment or payment.order.user_id != user_id:
return jsonify({'success': False, 'message': '支付记录不存在'})
if payment.status == Payment.STATUS_SUCCESS:
return jsonify({'success': False, 'message': '订单已支付'})
# 模拟支付成功
result = handle_payment_success(payment_sn, f'SIMULATE_{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/<payment_sn>', methods=['POST'])
@login_required
def simulate_fail(payment_sn):
"""模拟支付失败(开发测试用)"""
try:
user_id = session['user_id']
payment = Payment.query.filter_by(payment_sn=payment_sn).first()
if not payment or payment.order.user_id != user_id:
return jsonify({'success': False, 'message': '支付记录不存在'})
if payment.status == Payment.STATUS_SUCCESS:
return jsonify({'success': False, 'message': '订单已支付,无法模拟失败'})
# 模拟支付失败
payment.status = Payment.STATUS_FAILED
payment.third_party_sn = f'SIMULATE_FAIL_{int(time.time())}_{random.randint(1000, 9999)}'
db.session.commit()
return jsonify({'success': True, 'message': '模拟支付失败'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'模拟支付失败操作失败: {str(e)}'})
🔸==============================================================================
📄 文件: app/views/product.py
📊 大小: 23747 bytes (23.19 KB)
🕒 修改时间: 2025-07-03 15:17:18
🔸==============================================================================
"""
商品管理视图
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, g
from werkzeug.utils import secure_filename
from app.models.product import Product, Category, ProductImage, SpecName, SpecValue, ProductInventory
from app.models.admin import AdminUser
from app.utils.decorators import admin_required, log_operation
from app.utils.cos_client import cos_client
from config.database import db
from sqlalchemy import func
import time
import uuid
import json
product_bp = Blueprint('product', __name__, url_prefix='/admin/products')
@product_bp.route('/')
@admin_required
def index():
"""商品列表"""
page = request.args.get('page', 1, type=int)
per_page = 20
query = Product.query.order_by(Product.created_at.desc())
# 搜索功能
search = request.args.get('search', '').strip()
if search:
query = query.filter(Product.name.like(f'%{search}%'))
# 分类筛选
category_id = request.args.get('category_id', '', type=str)
if category_id:
query = query.filter(Product.category_id == int(category_id))
# 状态筛选
status = request.args.get('status', '', type=str)
if status:
query = query.filter(Product.status == int(status))
products = query.paginate(page=page, per_page=per_page, error_out=False)
# 获取所有分类用于筛选
categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all()
return render_template('admin/products.html',
products=products,
categories=categories,
search=search,
category_id=category_id,
status=status)
@product_bp.route('/add')
@admin_required
def add():
"""添加商品页面"""
categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all()
spec_names = SpecName.query.order_by(SpecName.sort_order).all()
return render_template('admin/product_form.html',
product=None,
categories=categories,
spec_names=spec_names)
@product_bp.route('/edit/<int:product_id>')
@admin_required
def edit(product_id):
"""编辑商品页面"""
product = Product.query.get_or_404(product_id)
categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all()
spec_names = SpecName.query.order_by(SpecName.sort_order).all()
return render_template('admin/product_form.html',
product=product,
categories=categories,
spec_names=spec_names)
@product_bp.route('/save', methods=['POST'])
@admin_required
@log_operation('保存商品信息', 'product')
def save():
"""保存商品信息"""
try:
product_id = request.form.get('product_id', type=int)
# 基本信息
name = request.form.get('name', '').strip()
category_id = request.form.get('category_id', type=int)
brand = request.form.get('brand', '').strip()
price = request.form.get('price', type=float)
original_price = request.form.get('original_price', type=float)
description = request.form.get('description', '').strip()
weight = request.form.get('weight', type=float)
status = request.form.get('status', 1, type=int)
# 验证必填字段
if not name or not category_id or not price:
flash('请填写完整的商品基本信息', 'error')
return redirect(request.referrer)
# 创建或更新商品
if product_id:
product = Product.query.get_or_404(product_id)
else:
product = Product()
product.name = name
product.category_id = category_id
product.brand = brand
product.price = price
product.original_price = original_price
product.description = description
product.weight = weight
product.status = status
# 处理库存类型
inventory_type = request.form.get('inventory_type', 'single')
if inventory_type == 'single':
product.has_specs = 0
else:
product.has_specs = 1
if not product_id:
db.session.add(product)
db.session.flush() # 获取product.id
# 处理库存信息
if inventory_type == 'single':
# 单规格处理
single_stock = request.form.get('single_stock', 0, type=int)
warning_stock = request.form.get('warning_stock', 10, type=int)
# 删除现有库存记录(如果是编辑模式)
if product_id:
ProductInventory.query.filter_by(product_id=product.id).delete()
# 创建单个SKU
sku_code = f"{product.name[:3].upper()}-DEFAULT-{product.id}"
inventory = ProductInventory(
product_id=product.id,
sku_code=sku_code,
spec_combination=None,
price_adjustment=0,
stock=single_stock,
warning_stock=warning_stock,
is_default=1,
status=1
)
db.session.add(inventory)
else:
# 多规格处理
sku_codes = request.form.getlist('sku_codes[]')
spec_combinations = request.form.getlist('spec_combinations[]')
price_adjustments = request.form.getlist('price_adjustments[]')
stocks = request.form.getlist('stocks[]')
warning_stocks = request.form.getlist('warning_stocks[]')
default_sku_index = request.form.get('default_sku', 0, type=int)
if not sku_codes:
flash('请至少添加一个SKU', 'error')
return redirect(request.referrer)
# 删除现有库存记录(如果是编辑模式)
if product_id:
ProductInventory.query.filter_by(product_id=product.id).delete()
# 创建多个SKU
for i, sku_code in enumerate(sku_codes):
try:
spec_combination = json.loads(spec_combinations[i]) if spec_combinations[i] else None
except:
spec_combination = None
inventory = ProductInventory(
product_id=product.id,
sku_code=sku_code,
spec_combination=spec_combination,
price_adjustment=float(price_adjustments[i]) if price_adjustments[i] else 0,
stock=int(stocks[i]) if stocks[i] else 0,
warning_stock=int(warning_stocks[i]) if warning_stocks[i] else 10,
is_default=1 if i == default_sku_index else 0,
status=1
)
db.session.add(inventory)
db.session.commit()
flash('商品信息保存成功', 'success')
return redirect(url_for('product.edit', product_id=product.id))
except Exception as e:
db.session.rollback()
flash(f'保存失败: {str(e)}', 'error')
return redirect(request.referrer)
@product_bp.route('/upload-images/<int:product_id>', methods=['POST'])
@admin_required
@log_operation('上传商品图片', 'productdef index():')
def upload_images(product_id):
"""上传商品图片"""
try:
product = Product.query.get_or_404(product_id)
if 'files' not in request.files:
return jsonify({'success': False, 'message': '没有选择文件'})
files = request.files.getlist('files')
uploaded_images = []
for file in files:
if file.filename == '':
continue
# 检查文件类型
allowed_extensions = {'jpg', 'jpeg', 'png', 'gif', 'webp'}
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
if file_ext not in allowed_extensions:
continue
# 上传到COS
result = cos_client.upload_file(file, 'product', file.filename)
if result['success']:
# 检查是否是第一张图片
existing_images_count = ProductImage.query.filter_by(product_id=product_id).count()
is_first_image = (existing_images_count == 0)
# 保存图片记录
image = ProductImage(
product_id=product_id,
image_url=result['url'],
sort_order=existing_images_count,
is_main=1 if is_first_image else 0 # 第一张图片自动设为主图
)
db.session.add(image)
# 如果是第一张图片,同时更新商品主图
if is_first_image:
product.main_image = result['url']
uploaded_images.append({
'id': None, # 临时ID提交后会更新
'url': result['url'],
'sort_order': image.sort_order,
'is_main': is_first_image
})
db.session.commit()
# 更新图片ID
for i, uploaded_image in enumerate(uploaded_images):
image = ProductImage.query.filter_by(
product_id=product_id,
image_url=uploaded_image['url']
).first()
if image:
uploaded_images[i]['id'] = image.id
return jsonify({
'success': True,
'message': f'成功上传 {len(uploaded_images)} 张图片',
'images': uploaded_images
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'上传失败: {str(e)}'})
@product_bp.route('/delete-image/<int:image_id>', methods=['DELETE'])
@admin_required
@log_operation('删除商品图片', 'product_image')
def delete_image(image_id):
"""删除商品图片"""
try:
image = ProductImage.query.get_or_404(image_id)
# 从COS删除文件
if image.image_url:
file_key = image.image_url.split('/')[-4:] # 提取文件路径
file_key = '/'.join(file_key)
cos_client.delete_file(file_key)
db.session.delete(image)
db.session.commit()
return jsonify({'success': True, 'message': '图片删除成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
@product_bp.route('/set-main-image/<int:image_id>', methods=['POST'])
@admin_required
@log_operation('设置主图', 'product')
def set_main_image(image_id):
"""设置主图"""
try:
image = ProductImage.query.get_or_404(image_id)
product = image.product
# 清除当前主图
ProductImage.query.filter_by(product_id=product.id, is_main=1).update({'is_main': 0})
# 设置新主图
image.is_main = 1
product.main_image = image.image_url
db.session.commit()
return jsonify({'success': True, 'message': '主图设置成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'设置失败: {str(e)}'})
@product_bp.route('/sort-images/<int:product_id>', methods=['POST'])
@admin_required
@log_operation('排序商品图片', 'product')
def sort_images(product_id):
"""图片排序"""
try:
image_ids = request.json.get('image_ids', [])
for index, image_id in enumerate(image_ids):
ProductImage.query.filter_by(id=image_id).update({'sort_order': index})
db.session.commit()
return jsonify({'success': True, 'message': '排序保存成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'排序失败: {str(e)}'})
@product_bp.route('/delete/<int:product_id>', methods=['DELETE'])
@admin_required
@log_operation('删除商品', 'product')
def delete(product_id):
"""删除商品"""
try:
product = Product.query.get_or_404(product_id)
# 删除商品图片
for image in product.images:
if image.image_url:
file_key = image.image_url.split('/')[-4:]
file_key = '/'.join(file_key)
cos_client.delete_file(file_key)
# 删除商品记录
db.session.delete(product)
db.session.commit()
return jsonify({'success': True, 'message': '商品删除成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
# 分类管理相关路由
@product_bp.route('/categories')
@admin_required
def categories():
"""分类管理"""
categories = Category.query.order_by(Category.sort_order).all()
return render_template('admin/categories.html', categories=categories)
@product_bp.route('/categories/save', methods=['POST'])
@admin_required
@log_operation('保存商品分类', 'category')
def save_category():
"""保存分类"""
try:
category_id = request.form.get('category_id', type=int)
name = request.form.get('name', '').strip()
parent_id = request.form.get('parent_id', 0, type=int)
sort_order = request.form.get('sort_order', 0, type=int)
is_active = request.form.get('is_active', 1, type=int)
if not name:
flash('分类名称不能为空', 'error')
return redirect(url_for('product.categories'))
# 检查分类名称是否重复
existing = Category.query.filter_by(name=name, parent_id=parent_id).first()
if existing and (not category_id or existing.id != category_id):
flash('同一层级下分类名称不能重复', 'error')
return redirect(url_for('product.categories'))
if category_id:
category = Category.query.get_or_404(category_id)
# 防止将分类设为自己的子分类
if parent_id == category_id:
flash('不能将分类设为自己的子分类', 'error')
return redirect(url_for('product.categories'))
# 防止循环引用
if parent_id != 0:
parent = Category.query.get(parent_id)
temp_parent = parent
while temp_parent and temp_parent.parent_id != 0:
if temp_parent.parent_id == category_id:
flash('不能创建循环引用的分类层级', 'error')
return redirect(url_for('product.categories'))
temp_parent = Category.query.get(temp_parent.parent_id)
else:
category = Category()
category.name = name
category.parent_id = parent_id
category.sort_order = sort_order
category.is_active = is_active
# 设置层级
if parent_id == 0:
category.level = 1
else:
parent = Category.query.get(parent_id)
if parent:
category.level = parent.level + 1
if category.level > 3:
flash('分类层级不能超过3级', 'error')
return redirect(url_for('product.categories'))
else:
category.level = 1
# 处理图标上传
if 'icon' in request.files:
icon_file = request.files['icon']
if icon_file and icon_file.filename:
# 删除旧图标
if category.icon_url:
old_file_key = category.icon_url.split('/')[-4:]
old_file_key = '/'.join(old_file_key)
cos_client.delete_file(old_file_key)
# 上传新图标
result = cos_client.upload_file(icon_file, 'category', icon_file.filename)
if result['success']:
category.icon_url = result['url']
else:
flash(f'图标上传失败: {result["error"]}', 'error')
return redirect(url_for('product.categories'))
if not category_id:
db.session.add(category)
db.session.commit()
flash('分类保存成功', 'success')
except Exception as e:
db.session.rollback()
flash(f'保存失败: {str(e)}', 'error')
return redirect(url_for('product.categories'))
@product_bp.route('/categories/<int:category_id>', methods=['GET'])
@admin_required
def get_category(category_id):
"""获取分类详情"""
try:
category = Category.query.get_or_404(category_id)
return jsonify({
'success': True,
'category': category.to_dict()
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@product_bp.route('/categories/<int:category_id>', methods=['DELETE'])
@admin_required
@log_operation('删除商品分类', 'category')
def delete_category(category_id):
"""删除分类"""
try:
category = Category.query.get_or_404(category_id)
# 检查是否有子分类
children = Category.query.filter_by(parent_id=category_id).count()
if children > 0:
return jsonify({'success': False, 'message': '该分类下还有子分类,无法删除'})
# 检查是否有商品使用此分类
products = Product.query.filter_by(category_id=category_id).count()
if products > 0:
return jsonify({'success': False, 'message': f'该分类下还有 {products} 个商品,无法删除'})
# 删除分类图标
if category.icon_url:
file_key = category.icon_url.split('/')[-4:]
file_key = '/'.join(file_key)
cos_client.delete_file(file_key)
db.session.delete(category)
db.session.commit()
return jsonify({'success': True, 'message': '分类删除成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
@product_bp.route('/categories/sort', methods=['POST'])
@admin_required
@log_operation('分类排序', 'category')
def sort_categories():
"""分类排序"""
try:
category_orders = request.json.get('orders', [])
for item in category_orders:
category_id = item.get('id')
sort_order = item.get('sort_order')
Category.query.filter_by(id=category_id).update({'sort_order': sort_order})
db.session.commit()
return jsonify({'success': True, 'message': '排序保存成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'排序失败: {str(e)}'})
# 库存管理相关路由
@product_bp.route('/inventory/<int:product_id>')
@admin_required
def get_inventory(product_id):
"""获取商品库存详情"""
try:
product = Product.query.get_or_404(product_id)
inventory_list = ProductInventory.query.filter_by(product_id=product_id) \
.order_by(ProductInventory.is_default.desc(), ProductInventory.id).all()
inventory_data = []
for inventory in inventory_list:
inventory_data.append({
'id': inventory.id,
'sku_code': inventory.sku_code,
'spec_combination': inventory.spec_combination,
'stock': inventory.stock,
'warning_stock': inventory.warning_stock,
'price_adjustment': float(inventory.price_adjustment) if inventory.price_adjustment else 0,
'is_default': inventory.is_default,
'status': inventory.status,
'final_price': inventory.get_final_price()
})
return jsonify({
'success': True,
'inventory': inventory_data,
'product_name': product.name
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@product_bp.route('/inventory/update', methods=['POST'])
@admin_required
@log_operation('更新库存', 'inventory')
def update_inventory():
"""批量更新库存"""
try:
inventory_data = request.json.get('inventory_list', [])
for item in inventory_data:
inventory_id = item.get('id')
new_stock = item.get('stock')
if inventory_id and new_stock is not None:
inventory = ProductInventory.query.get(inventory_id)
if inventory:
old_stock = inventory.stock
inventory.stock = new_stock
# 记录库存变更日志
from app.models.product import InventoryLog
InventoryLog.create_log(
product_id=inventory.product_id,
sku_code=inventory.sku_code,
change_type=3, # 调整
change_quantity=new_stock - old_stock,
before_stock=old_stock,
after_stock=new_stock,
remark='管理员手动调整'
)
db.session.commit()
return jsonify({'success': True, 'message': '库存更新成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'更新失败: {str(e)}'})
@product_bp.route('/inventory/log/<int:product_id>')
@admin_required
def inventory_log(product_id):
"""查看库存变更日志"""
product = Product.query.get_or_404(product_id)
page = request.args.get('page', 1, type=int)
per_page = 20
from app.models.product import InventoryLog
logs = InventoryLog.query.filter_by(product_id=product_id) \
.order_by(InventoryLog.created_at.desc()) \
.paginate(page=page, per_page=per_page, error_out=False)
return render_template('admin/inventory_log.html',
product=product,
logs=logs)
@product_bp.route('/generate-sku-code', methods=['POST'])
@admin_required
def generate_sku_code():
"""生成SKU编码"""
try:
product_name = request.json.get('product_name', '')
spec_combination = request.json.get('spec_combination', {})
# 生成SKU编码逻辑
short_name = product_name[:3].upper() if product_name else 'PRD'
spec_code = ''.join([v[:2].upper() for v in spec_combination.values()]) if spec_combination else 'DEFAULT'
timestamp = str(int(time.time()))[-4:]
sku_code = f"{short_name}-{spec_code}-{timestamp}"
return jsonify({'success': True, 'sku_code': sku_code})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@product_bp.route('/check-sku-code', methods=['POST'])
@admin_required
def check_sku_code():
"""检查SKU编码是否重复"""
try:
sku_code = request.json.get('sku_code', '')
product_id = request.json.get('product_id', None)
query = ProductInventory.query.filter_by(sku_code=sku_code)
if product_id:
query = query.filter(ProductInventory.product_id != product_id)
exists = query.first() is not None
return jsonify({
'success': True,
'exists': exists,
'message': 'SKU编码已存在' if exists else 'SKU编码可用'
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
🔸==============================================================================
📄 文件: app/views/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/<int:product_id>')
def product_reviews(product_id):
"""商品评价列表AJAX接口"""
try:
page = request.args.get('page', 1, type=int)
rating_filter = request.args.get('rating', type=int)
# 基础查询
query = Review.query.filter_by(product_id=product_id, status=1)
# 评分筛选
if rating_filter:
query = query.filter_by(rating=rating_filter)
# 分页查询
reviews = query.order_by(desc(Review.created_at)).paginate(
page=page, per_page=10, error_out=False
)
# 评价统计
stats = db.session.query(
Review.rating,
func.count(Review.id).label('count')
).filter_by(product_id=product_id, status=1).group_by(Review.rating).all()
rating_stats = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
total_reviews = 0
for stat in stats:
rating_stats[stat.rating] = stat.count
total_reviews += stat.count
# 好评率计算
good_rate = 0
if total_reviews > 0:
good_reviews = rating_stats[4] + rating_stats[5]
good_rate = round(good_reviews / total_reviews * 100, 1)
# 转换为字典
reviews_data = []
for review in reviews.items:
review_dict = review.to_dict()
# 添加用户头像
if review.user:
review_dict['user_avatar'] = review.user.avatar_url
reviews_data.append(review_dict)
return jsonify({
'success': True,
'reviews': reviews_data,
'pagination': {
'page': reviews.page,
'pages': reviews.pages,
'per_page': reviews.per_page,
'total': reviews.total,
'has_next': reviews.has_next,
'has_prev': reviews.has_prev
},
'stats': {
'total_reviews': total_reviews,
'good_rate': good_rate,
'rating_stats': rating_stats
}
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@review_bp.route('/write/<int:order_id>/<int:product_id>')
@login_required
def write_review(order_id, product_id):
"""写评价页面"""
# 验证订单和商品
order = Order.query.filter_by(id=order_id, user_id=session["user_id"]).first()
if not order:
flash('订单不存在', 'error')
return redirect(url_for('order.list'))
# 检查订单状态
if order.status not in [4, 5]: # 待评价或已完成
flash('该订单暂时无法评价', 'error')
return redirect(url_for('order.detail', order_id=order_id))
# 检查商品是否在订单中
order_item = OrderItem.query.filter_by(order_id=order_id, product_id=product_id).first()
if not order_item:
flash('商品不在此订单中', 'error')
return redirect(url_for('order.detail', order_id=order_id))
# 检查是否已经评价过
existing_review = Review.query.filter_by(
user_id=session["user_id"],
product_id=product_id,
order_id=order_id
).first()
if existing_review:
flash('您已经评价过该商品', 'info')
return redirect(url_for('order.detail', order_id=order_id))
return render_template('review/write.html',
order=order,
order_item=order_item,
product=order_item.product)
@review_bp.route('/submit', methods=['POST'])
@login_required
@log_operation('提交商品评价')
def submit_review():
"""提交评价"""
try:
data = request.get_json()
order_id = data.get('order_id')
product_id = data.get('product_id')
rating = data.get('rating')
content = data.get('content', '').strip()
is_anonymous = data.get('is_anonymous', False)
images = data.get('images', [])
# 参数验证
if not all([order_id, product_id, rating]):
return jsonify({'success': False, 'message': '参数不完整'})
if not (1 <= rating <= 5):
return jsonify({'success': False, 'message': '评分必须在1-5星之间'})
# 验证订单
order = Order.query.filter_by(id=order_id, user_id=session["user_id"]).first()
if not order:
return jsonify({'success': False, 'message': '订单不存在'})
if order.status not in [4, 5]:
return jsonify({'success': False, 'message': '该订单暂时无法评价'})
# 验证商品在订单中
order_item = OrderItem.query.filter_by(order_id=order_id, product_id=product_id).first()
if not order_item:
return jsonify({'success': False, 'message': '商品不在此订单中'})
# 检查是否已评价
existing_review = Review.query.filter_by(
user_id=session["user_id"],
product_id=product_id,
order_id=order_id
).first()
if existing_review:
return jsonify({'success': False, 'message': '您已经评价过该商品'})
# 创建评价
review = Review(
user_id=session["user_id"],
product_id=product_id,
order_id=order_id,
rating=rating,
content=content if content else None,
is_anonymous=1 if is_anonymous else 0
)
# 设置图片
if images:
review.set_images(images)
db.session.add(review)
# 检查订单中所有商品是否都已评价
total_items = OrderItem.query.filter_by(order_id=order_id).count()
reviewed_items = Review.query.filter_by(order_id=order_id).count() + 1 # +1 是当前这个评价
# 如果所有商品都已评价,更新订单状态为已完成
if reviewed_items >= total_items and order.status == 4:
order.status = 5 # 已完成
db.session.commit()
return jsonify({
'success': True,
'message': '评价提交成功',
'review_id': review.id
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'提交失败: {str(e)}'})
@review_bp.route('/upload_image', methods=['POST'])
@login_required
def upload_review_image():
"""上传评价图片"""
try:
if 'file' not in request.files:
return jsonify({'success': False, 'message': '没有选择文件'})
file = request.files['file']
if file.filename == '':
return jsonify({'success': False, 'message': '没有选择文件'})
# 使用现有的文件上传处理器
result = file_upload_handler.upload_image(file, 'reviews', process_image=True)
if result['success']:
return jsonify({
'success': True,
'message': '图片上传成功',
'url': result['url']
})
else:
return jsonify({'success': False, 'message': result['error']})
except Exception as e:
return jsonify({'success': False, 'message': f'上传失败: {str(e)}'})
@review_bp.route('/my_reviews')
@login_required
def my_reviews():
"""我的评价列表"""
page = request.args.get('page', 1, type=int)
reviews = Review.query.filter_by(user_id=session["user_id"]).order_by(
desc(Review.created_at)
).paginate(page=page, per_page=10, error_out=False)
return render_template('review/my_reviews.html', reviews=reviews)
@review_bp.route('/delete/<int:review_id>', methods=['POST'])
@login_required
@log_operation('删除商品评价')
def delete_review(review_id):
"""删除评价(仅限自己的评价)"""
try:
review = Review.query.filter_by(id=review_id, user_id=session["user_id"]).first()
if not review:
return jsonify({'success': False, 'message': '评价不存在'})
db.session.delete(review)
db.session.commit()
return jsonify({'success': True, 'message': '评价删除成功'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
🔸==============================================================================
📄 文件: app/views/upload.py
📊 大小: 5464 bytes (5.34 KB)
🕒 修改时间: 2025-07-03 04:48:37
🔸==============================================================================
"""
文件上传视图
"""
from flask import Blueprint, request, jsonify, session, current_app
from werkzeug.utils import secure_filename
from app.utils.decorators import login_required
from app.models.user import User
from app.utils.cos_client import cos_client
from config.database import db
from config.cos_config import COSConfig
import os
upload_bp = Blueprint('upload', __name__)
@upload_bp.route('/avatar', methods=['POST'])
@login_required
def upload_avatar():
"""
上传用户头像
"""
try:
# 检查是否有文件
if 'avatar' not in request.files:
return jsonify({
'success': False,
'message': '没有选择文件'
}), 400
file = request.files['avatar']
# 检查文件名
if file.filename == '':
return jsonify({
'success': False,
'message': '没有选择文件'
}), 400
# 验证文件类型
if not allowed_file(file.filename, COSConfig.ALLOWED_IMAGE_EXTENSIONS):
return jsonify({
'success': False,
'message': f'不支持的文件格式,只支持: {", ".join(COSConfig.ALLOWED_IMAGE_EXTENSIONS)}'
}), 400
# 验证文件大小
file.seek(0, 2) # 移动到文件末尾
file_size = file.tell()
file.seek(0) # 重置文件指针
if file_size > COSConfig.MAX_IMAGE_SIZE:
size_mb = COSConfig.MAX_IMAGE_SIZE / 1024 / 1024
return jsonify({
'success': False,
'message': f'文件大小超过限制,最大允许 {size_mb:.1f}MB'
}), 400
# 获取当前用户
user = User.query.get(session['user_id'])
if not user:
return jsonify({
'success': False,
'message': '用户不存在'
}), 404
# 上传到COS
upload_result = cos_client.upload_file(
file_obj=file,
folder_type='avatar',
original_filename=file.filename
)
if not upload_result['success']:
current_app.logger.error(f"COS上传失败: {upload_result['error']}")
return jsonify({
'success': False,
'message': '文件上传失败,请重试'
}), 500
# 删除旧头像(如果存在)
if user.avatar_url:
try:
# 从URL中提取文件路径
old_file_key = extract_file_key_from_url(user.avatar_url)
if old_file_key:
cos_client.delete_file(old_file_key)
current_app.logger.info(f"删除旧头像: {old_file_key}")
except Exception as e:
current_app.logger.warning(f"删除旧头像失败: {str(e)}")
# 更新用户头像URL
user.avatar_url = upload_result['url']
db.session.commit()
current_app.logger.info(f"用户 {user.username} 头像上传成功: {upload_result['file_key']}")
return jsonify({
'success': True,
'message': '头像上传成功',
'avatar_url': upload_result['url'],
'file_key': upload_result['file_key']
})
except Exception as e:
current_app.logger.error(f"头像上传异常: {str(e)}")
db.session.rollback()
return jsonify({
'success': False,
'message': '服务器内部错误'
}), 500
def allowed_file(filename, allowed_extensions):
"""
检查文件扩展名是否允许
"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in allowed_extensions
def extract_file_key_from_url(url):
"""
从COS URL中提取文件路径
"""
try:
if not url:
return None
# 移除域名部分,只保留文件路径
if COSConfig.BUCKET_DOMAIN in url:
return url.split(COSConfig.BUCKET_DOMAIN + '/')[-1]
return None
except Exception:
return None
@upload_bp.route('/test', methods=['GET', 'POST'])
@login_required
def test_upload():
"""
测试上传功能
"""
if request.method == 'GET':
return '''
<!DOCTYPE html>
<html>
<head>
<title>测试上传</title>
<meta charset="utf-8">
</head>
<body>
<h2>测试文件上传</h2>
<form method="post" enctype="multipart/form-data">
<input type="file" name="test_file" accept="image/*" required>
<button type="submit">上传测试</button>
</form>
</body>
</html>
'''
# POST 请求处理
if 'test_file' not in request.files:
return '没有文件'
file = request.files['test_file']
if file.filename == '':
return '没有选择文件'
# 上传到COS
result = cos_client.upload_file(
file_obj=file,
folder_type='temp',
original_filename=file.filename
)
if result['success']:
return f'''
<h2>上传成功!</h2>
<p>文件路径: {result['file_key']}</p>
<p>访问URL: <a href="{result['url']}" target="_blank">{result['url']}</a></p>
<img src="{result['url']}" style="max-width: 300px;">
'''
else:
return f'上传失败: {result["error"]}'
🔸==============================================================================
📄 文件: app/views/user.py
📊 大小: 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
================================================================================