online_shopping_07_04
This commit is contained in:
commit
5fcd6b7017
178
.gitignore
vendored
Normal file
178
.gitignore
vendored
Normal file
@ -0,0 +1,178 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# poetry
|
||||
poetry.lock
|
||||
|
||||
# Environments (保留配置文件,只排除本地环境变量)
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 操作系统
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
*.lnk
|
||||
|
||||
# 虚拟环境
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# 日志文件
|
||||
logs/*.log
|
||||
*.log
|
||||
|
||||
# 上传文件(用户上传的内容不需要版本控制)
|
||||
app/static/uploads/*
|
||||
!app/static/uploads/.gitkeep
|
||||
|
||||
# 数据库文件(如果使用SQLite)
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 临时文件
|
||||
temp/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# 测试输出
|
||||
test_output/
|
||||
test_results/
|
||||
|
||||
# 缓存文件
|
||||
.cache/
|
||||
|
||||
# 压缩文件
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# 编译文件
|
||||
*.pyc
|
||||
|
||||
# 性能分析
|
||||
*.prof
|
||||
|
||||
# 本地开发临时文件
|
||||
scratch/
|
||||
notes.txt
|
||||
todo.txt
|
||||
|
||||
# Docker临时文件(保留主要Docker文件)
|
||||
.dockerignore.bak
|
||||
|
||||
# 部署相关临时文件
|
||||
deploy_temp/
|
||||
211
README.md
Normal file
211
README.md
Normal file
@ -0,0 +1,211 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
# 🛍️ Python Flask 电商系统 🛍️
|
||||
|
||||
**一个功能完善、技术栈现代、可直接部署的线上电商平台毕业设计项目。**
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
95
app/__init__.py
Normal file
95
app/__init__.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""
|
||||
Flask应用工厂
|
||||
"""
|
||||
from flask import Flask
|
||||
from flask_mail import Mail
|
||||
from config.config import Config
|
||||
from config.database import db
|
||||
import re
|
||||
|
||||
# 初始化邮件服务
|
||||
mail = Mail()
|
||||
|
||||
|
||||
def create_app(config_name='default'):
|
||||
app = Flask(__name__)
|
||||
|
||||
# 加载配置
|
||||
app.config.from_object(Config)
|
||||
|
||||
# 初始化数据库
|
||||
db.init_app(app)
|
||||
|
||||
# 初始化邮件服务
|
||||
mail.init_app(app)
|
||||
|
||||
# 注册自定义过滤器
|
||||
register_filters(app)
|
||||
|
||||
# 注册蓝图
|
||||
register_blueprints(app)
|
||||
|
||||
# 创建数据库表
|
||||
with app.app_context():
|
||||
try:
|
||||
db.create_all()
|
||||
print("✅ 数据库表创建/同步成功")
|
||||
except Exception as e:
|
||||
print(f"❌ 数据库表创建失败: {str(e)}")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_filters(app):
|
||||
"""注册自定义过滤器"""
|
||||
|
||||
@app.template_filter('nl2br')
|
||||
def nl2br_filter(text):
|
||||
"""将换行符转换为HTML <br> 标签"""
|
||||
if not text:
|
||||
return ''
|
||||
# 将换行符替换为 <br> 标签
|
||||
return text.replace('\n', '<br>')
|
||||
|
||||
@app.template_filter('truncate_chars')
|
||||
def truncate_chars_filter(text, length=50):
|
||||
"""截断字符串"""
|
||||
if not text:
|
||||
return ''
|
||||
if len(text) <= length:
|
||||
return text
|
||||
return text[:length] + '...'
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
"""注册蓝图"""
|
||||
from app.views.main import main_bp
|
||||
from app.views.auth import auth_bp
|
||||
from app.views.user import user_bp
|
||||
from app.views.admin import admin_bp
|
||||
from app.views.product import product_bp
|
||||
from app.views.cart import cart_bp
|
||||
from app.views.address import address_bp
|
||||
from app.views.order import order_bp
|
||||
from app.views.payment import payment_bp
|
||||
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(user_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(product_bp)
|
||||
app.register_blueprint(cart_bp)
|
||||
app.register_blueprint(address_bp)
|
||||
app.register_blueprint(order_bp)
|
||||
app.register_blueprint(payment_bp)
|
||||
|
||||
# 修复:正确注册upload蓝图并设置URL前缀
|
||||
try:
|
||||
from app.views.upload import upload_bp
|
||||
app.register_blueprint(upload_bp, url_prefix='/upload') # 添加URL前缀
|
||||
print("✅ 上传功能蓝图注册成功")
|
||||
except ImportError as e:
|
||||
print(f"⚠️ 上传功能暂时不可用: {str(e)}")
|
||||
|
||||
print("✅ 商品管理蓝图注册成功")
|
||||
print("✅ 购物车蓝图注册成功")
|
||||
128
app/forms.py
Normal file
128
app/forms.py
Normal file
@ -0,0 +1,128 @@
|
||||
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('提交订单')
|
||||
18
app/models/__init__.py
Normal file
18
app/models/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
from app.models.user import User
|
||||
from app.models.verification import EmailVerification
|
||||
from app.models.admin import AdminUser
|
||||
from app.models.operation_log import OperationLog
|
||||
from app.models.product import Category, Product, ProductImage, SpecName, SpecValue, ProductInventory, InventoryLog, ProductSpecRelation
|
||||
from app.models.cart import Cart
|
||||
from app.models.address import UserAddress
|
||||
from app.models.order import Order, OrderItem, ShippingInfo
|
||||
from app.models.payment import Payment
|
||||
from app.models.review import Review
|
||||
|
||||
__all__ = [
|
||||
'User', 'EmailVerification', 'AdminUser', 'OperationLog',
|
||||
'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue',
|
||||
'ProductInventory', 'InventoryLog', 'ProductSpecRelation',
|
||||
'Cart', 'UserAddress', 'Order', 'OrderItem', 'ShippingInfo',
|
||||
'Payment', 'Review'
|
||||
]
|
||||
79
app/models/address.py
Normal file
79
app/models/address.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""
|
||||
用户地址模型
|
||||
"""
|
||||
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()}>'
|
||||
52
app/models/admin.py
Normal file
52
app/models/admin.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
管理员模型
|
||||
"""
|
||||
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}>'
|
||||
136
app/models/cart.py
Normal file
136
app/models/cart.py
Normal file
@ -0,0 +1,136 @@
|
||||
"""
|
||||
购物车模型
|
||||
"""
|
||||
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}>'
|
||||
57
app/models/operation_log.py
Normal file
57
app/models/operation_log.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""
|
||||
操作日志模型
|
||||
"""
|
||||
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}>'
|
||||
182
app/models/order.py
Normal file
182
app/models/order.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""
|
||||
订单模型
|
||||
"""
|
||||
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}>'
|
||||
68
app/models/payment.py
Normal file
68
app/models/payment.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""
|
||||
支付模型
|
||||
"""
|
||||
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}>'
|
||||
265
app/models/product.py
Normal file
265
app/models/product.py
Normal file
@ -0,0 +1,265 @@
|
||||
"""
|
||||
商品相关模型
|
||||
"""
|
||||
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}>'
|
||||
64
app/models/review.py
Normal file
64
app/models/review.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""
|
||||
评价模型
|
||||
"""
|
||||
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}星>'
|
||||
50
app/models/user.py
Normal file
50
app/models/user.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""
|
||||
用户模型
|
||||
"""
|
||||
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}>'
|
||||
61
app/models/verification.py
Normal file
61
app/models/verification.py
Normal file
@ -0,0 +1,61 @@
|
||||
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
|
||||
96
app/static/css/address_form.css
Normal file
96
app/static/css/address_form.css
Normal file
@ -0,0 +1,96 @@
|
||||
/* 地址表单页面样式 */
|
||||
.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;
|
||||
}
|
||||
79
app/static/css/addresses.css
Normal file
79
app/static/css/addresses.css
Normal file
@ -0,0 +1,79 @@
|
||||
/* 地址管理页面样式 */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
117
app/static/css/admin_base.css
Normal file
117
app/static/css/admin_base.css
Normal file
@ -0,0 +1,117 @@
|
||||
: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);
|
||||
}
|
||||
155
app/static/css/admin_categories.css
Normal file
155
app/static/css/admin_categories.css
Normal file
@ -0,0 +1,155 @@
|
||||
/* 分类管理页面样式 */
|
||||
.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;
|
||||
}
|
||||
39
app/static/css/admin_dashboard.css
Normal file
39
app/static/css/admin_dashboard.css
Normal file
@ -0,0 +1,39 @@
|
||||
/* 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;
|
||||
}
|
||||
82
app/static/css/admin_login.css
Normal file
82
app/static/css/admin_login.css
Normal file
@ -0,0 +1,82 @@
|
||||
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;
|
||||
}
|
||||
231
app/static/css/admin_profile.css
Normal file
231
app/static/css/admin_profile.css
Normal file
@ -0,0 +1,231 @@
|
||||
/* 管理员个人资料页面样式 */
|
||||
.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;
|
||||
}
|
||||
65
app/static/css/auth.css
Normal file
65
app/static/css/auth.css
Normal file
@ -0,0 +1,65 @@
|
||||
/* 认证页面样式 */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
47
app/static/css/base.css
Normal file
47
app/static/css/base.css
Normal file
@ -0,0 +1,47 @@
|
||||
/* 基础样式 */
|
||||
.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;
|
||||
}
|
||||
26
app/static/css/cart.css
Normal file
26
app/static/css/cart.css
Normal file
@ -0,0 +1,26 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
46
app/static/css/checkout.css
Normal file
46
app/static/css/checkout.css
Normal file
@ -0,0 +1,46 @@
|
||||
/* 订单结算页面样式 */
|
||||
.checkout-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.address-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.address-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.address-card.selected {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9ff;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.product-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
}
|
||||
68
app/static/css/index.css
Normal file
68
app/static/css/index.css
Normal file
@ -0,0 +1,68 @@
|
||||
/* 首页样式 */
|
||||
.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%);
|
||||
}
|
||||
|
||||
/* 商品图片样式 */
|
||||
.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);
|
||||
}
|
||||
80
app/static/css/order_detail.css
Normal file
80
app/static/css/order_detail.css
Normal file
@ -0,0 +1,80 @@
|
||||
/* 订单详情页面样式 */
|
||||
.order-status-timeline {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
top: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item.completed::before {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.timeline-item.current::before {
|
||||
background-color: #007bff;
|
||||
box-shadow: 0 0 0 4px rgba(0,123,255,0.2);
|
||||
}
|
||||
|
||||
.timeline-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
top: 12px;
|
||||
width: 2px;
|
||||
height: calc(100% - 12px);
|
||||
background-color: #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-item.completed::after {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.order-detail-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.product-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
160
app/static/css/orders.css
Normal file
160
app/static/css/orders.css
Normal file
@ -0,0 +1,160 @@
|
||||
/* 订单页面样式 */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
49
app/static/css/pay.css
Normal file
49
app/static/css/pay.css
Normal file
@ -0,0 +1,49 @@
|
||||
/* 订单支付页面样式 */
|
||||
.pay-container {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.payment-method {
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.payment-method:hover {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9ff;
|
||||
}
|
||||
|
||||
.payment-method.selected {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9ff;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 1.2em;
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.payment-status {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
71
app/static/css/product_detail.css
Normal file
71
app/static/css/product_detail.css
Normal file
@ -0,0 +1,71 @@
|
||||
.product-card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.spec-option {
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.spec-option:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.thumbnail-image {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.thumbnail-image:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.price-section {
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffeee8 100%);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ffe6e6;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
line-height: 1.8;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.service-promises li {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
/* 规格选择动效 */
|
||||
.spec-option.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
color: white;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 按钮禁用状态样式 */
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.price-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-buttons .d-md-flex {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
14
app/static/css/product_list.css
Normal file
14
app/static/css/product_list.css
Normal file
@ -0,0 +1,14 @@
|
||||
.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;
|
||||
}
|
||||
231
app/static/css/profile.css
Normal file
231
app/static/css/profile.css
Normal file
@ -0,0 +1,231 @@
|
||||
/* 个人中心页面样式 */
|
||||
|
||||
/* 头像上传相关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;
|
||||
}
|
||||
36
app/static/css/register.css
Normal file
36
app/static/css/register.css
Normal file
@ -0,0 +1,36 @@
|
||||
/* 注册页面样式 */
|
||||
.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;
|
||||
}
|
||||
229
app/static/js/address_form.js
Normal file
229
app/static/js/address_form.js
Normal file
@ -0,0 +1,229 @@
|
||||
// 地址表单页面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);
|
||||
}
|
||||
}
|
||||
64
app/static/js/addresses.js
Normal file
64
app/static/js/addresses.js
Normal file
@ -0,0 +1,64 @@
|
||||
// 地址管理页面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 = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
276
app/static/js/admin_categories.js
Normal file
276
app/static/js/admin_categories.js
Normal file
@ -0,0 +1,276 @@
|
||||
// 分类管理页面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);
|
||||
}
|
||||
}
|
||||
80
app/static/js/admin_dashboard.js
Normal file
80
app/static/js/admin_dashboard.js
Normal file
@ -0,0 +1,80 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
app/static/js/base.js
Normal file
76
app/static/js/base.js
Normal file
@ -0,0 +1,76 @@
|
||||
// 基础JavaScript功能
|
||||
|
||||
// 返回顶部功能
|
||||
window.addEventListener('scroll', function() {
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
if (window.pageYOffset > 300) {
|
||||
backToTop.style.display = 'block';
|
||||
} else {
|
||||
backToTop.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// 购物车数量更新
|
||||
function updateCartBadge(count) {
|
||||
const badge = document.getElementById('cartBadge');
|
||||
if (count > 0) {
|
||||
badge.textContent = count;
|
||||
badge.style.display = 'inline-block';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后的初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 当前页面高亮
|
||||
const currentPath = window.location.pathname;
|
||||
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
|
||||
|
||||
navLinks.forEach(link => {
|
||||
if (link.getAttribute('href') === currentPath) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化购物车数量
|
||||
// TODO: 实现购物车数量获取
|
||||
});
|
||||
|
||||
// 通用AJAX错误处理
|
||||
function handleAjaxError(xhr) {
|
||||
if (xhr.status === 401) {
|
||||
alert('请先登录');
|
||||
window.location.href = '/auth/login';
|
||||
} else if (xhr.status === 403) {
|
||||
alert('没有权限执行此操作');
|
||||
} else {
|
||||
alert('操作失败,请稍后再试');
|
||||
}
|
||||
}
|
||||
|
||||
// 通用成功提示
|
||||
function showSuccessMessage(message) {
|
||||
// 创建临时提示框
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed success-toast';
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.parentNode.removeChild(alertDiv);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
219
app/static/js/cart.js
Normal file
219
app/static/js/cart.js
Normal file
@ -0,0 +1,219 @@
|
||||
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()}`;
|
||||
}
|
||||
114
app/static/js/checkout.js
Normal file
114
app/static/js/checkout.js
Normal file
@ -0,0 +1,114 @@
|
||||
// 订单结算页面脚本
|
||||
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');
|
||||
});
|
||||
|
||||
document.querySelector(`[data-address-id="${addressId}"]`).classList.add('selected');
|
||||
|
||||
// 更新单选按钮
|
||||
document.querySelector(`input[value="${addressId}"]`).checked = true;
|
||||
}
|
||||
|
||||
// 更新运费
|
||||
function updateShippingFee() {
|
||||
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value;
|
||||
let fee = 0;
|
||||
|
||||
switch(shippingMethod) {
|
||||
case 'express':
|
||||
fee = 10;
|
||||
break;
|
||||
case 'same_day':
|
||||
fee = 20;
|
||||
break;
|
||||
default:
|
||||
fee = 0;
|
||||
}
|
||||
|
||||
document.getElementById('shippingFee').textContent = `¥${fee.toFixed(2)}`;
|
||||
document.getElementById('totalAmount').textContent = `¥${(subtotal + fee).toFixed(2)}`;
|
||||
}
|
||||
|
||||
// 提交订单
|
||||
function submitOrder() {
|
||||
if (!selectedAddressId) {
|
||||
showAlert('请选择收货地址', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value;
|
||||
const paymentMethod = document.querySelector('input[name="payment_method"]:checked').value;
|
||||
const remark = document.getElementById('orderRemark').value;
|
||||
|
||||
// 获取选中的购物车商品ID
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const selectedItems = urlParams.getAll('items');
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
showAlert('没有选中的商品', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const orderData = {
|
||||
selected_items: selectedItems,
|
||||
address_id: selectedAddressId,
|
||||
shipping_method: shippingMethod,
|
||||
payment_method: paymentMethod,
|
||||
remark: remark
|
||||
};
|
||||
|
||||
// 显示加载状态
|
||||
const submitBtn = document.querySelector('.btn-danger');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
fetch('/order/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(orderData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('订单创建成功!正在跳转到支付页面...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = `/order/pay/${data.payment_sn}`;
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('提交订单失败,请重试', 'error');
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
466
app/static/js/city_data.js
Normal file
466
app/static/js/city_data.js
Normal file
@ -0,0 +1,466 @@
|
||||
// 中国省市区数据
|
||||
const cityData = {
|
||||
'北京市': {
|
||||
'北京市': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '门头沟区', '房山区', '通州区', '顺义区', '昌平区', '大兴区', '怀柔区', '平谷区', '密云区', '延庆区']
|
||||
},
|
||||
'上海市': {
|
||||
'上海市': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '虹口区', '杨浦区', '闵行区', '宝山区', '嘉定区', '浦东新区', '金山区', '松江区', '青浦区', '奉贤区', '崇明区']
|
||||
},
|
||||
'天津市': {
|
||||
'天津市': ['和平区', '河东区', '河西区', '南开区', '河北区', '红桥区', '东丽区', '西青区', '津南区', '北辰区', '武清区', '宝坻区', '滨海新区', '宁河区', '静海区', '蓟州区']
|
||||
},
|
||||
'重庆市': {
|
||||
'重庆市': ['万州区', '涪陵区', '渝中区', '大渡口区', '江北区', '沙坪坝区', '九龙坡区', '南岸区', '北碚区', '綦江区', '大足区', '渝北区', '巴南区', '黔江区', '长寿区', '江津区', '合川区', '永川区', '南川区', '璧山区', '铜梁区', '潼南区', '荣昌区', '开州区', '梁平区', '武隆区', '城口县', '丰都县', '垫江县', '忠县', '云阳县', '奉节县', '巫山县', '巫溪县', '石柱土家族自治县', '秀山土家族苗族自治县', '酉阳土家族苗族自治县', '彭水苗族土家族自治县']
|
||||
},
|
||||
'河北省': {
|
||||
'石家庄市': ['长安区', '桥西区', '新华区', '井陉矿区', '裕华区', '藁城区', '鹿泉区', '栾城区', '井陉县', '正定县', '行唐县', '灵寿县', '高邑县', '深泽县', '赞皇县', '无极县', '平山县', '元氏县', '赵县', '辛集市', '晋州市', '新乐市'],
|
||||
'唐山市': ['路南区', '路北区', '古冶区', '开平区', '丰南区', '丰润区', '曹妃甸区', '滦州市', '滦南县', '乐亭县', '迁西县', '玉田县', '遵化市', '迁安市'],
|
||||
'秦皇岛市': ['海港区', '山海关区', '北戴河区', '抚宁区', '青龙满族自治县', '昌黎县', '卢龙县'],
|
||||
'邯郸市': ['邯山区', '丛台区', '复兴区', '峰峰矿区', '肥乡区', '永年区', '临漳县', '成安县', '大名县', '涉县', '磁县', '邱县', '鸡泽县', '广平县', '馆陶县', '魏县', '曲周县', '武安市'],
|
||||
'邢台市': ['桥东区', '桥西区', '邢台县', '临城县', '内丘县', '柏乡县', '隆尧县', '任县', '南和县', '宁晋县', '巨鹿县', '新河县', '广宗县', '平乡县', '威县', '清河县', '临西县', '南宫市', '沙河市'],
|
||||
'保定市': ['竞秀区', '莲池区', '满城区', '清苑区', '徐水区', '涞水县', '阜平县', '定兴县', '唐县', '高阳县', '容城县', '涞源县', '望都县', '安新县', '易县', '曲阳县', '蠡县', '顺平县', '博野县', '雄县', '涿州市', '定州市', '安国市', '高碑店市'],
|
||||
'张家口市': ['桥东区', '桥西区', '宣化区', '下花园区', '万全区', '崇礼区', '张北县', '康保县', '沽源县', '尚义县', '蔚县', '阳原县', '怀安县', '怀来县', '涿鹿县', '赤城县'],
|
||||
'承德市': ['双桥区', '双滦区', '鹰手营子矿区', '承德县', '兴隆县', '平泉市', '滦平县', '隆化县', '丰宁满族自治县', '宽城满族自治县', '围场满族蒙古族自治县'],
|
||||
'沧州市': ['新华区', '运河区', '沧县', '青县', '东光县', '海兴县', '盐山县', '肃宁县', '南皮县', '吴桥县', '献县', '孟村回族自治县', '泊头市', '任丘市', '黄骅市', '河间市'],
|
||||
'廊坊市': ['安次区', '广阳区', '固安县', '永清县', '香河县', '大城县', '文安县', '大厂回族自治县', '霸州市', '三河市'],
|
||||
'衡水市': ['桃城区', '冀州区', '枣强县', '武邑县', '武强县', '饶阳县', '安平县', '故城县', '景县', '阜城县', '深州市']
|
||||
},
|
||||
'山西省': {
|
||||
'太原市': ['小店区', '迎泽区', '杏花岭区', '尖草坪区', '万柏林区', '晋源区', '清徐县', '阳曲县', '娄烦县', '古交市'],
|
||||
'大同市': ['平城区', '云冈区', '新荣区', '左云县', '阳高县', '天镇县', '广灵县', '灵丘县', '浑源县', '云州区'],
|
||||
'阳泉市': ['城区', '矿区', '郊区', '平定县', '盂县'],
|
||||
'长治市': ['潞州区', '上党区', '屯留区', '潞城区', '襄垣县', '平顺县', '黎城县', '壶关县', '长子县', '武乡县', '沁县', '沁源县'],
|
||||
'晋城市': ['城区', '沁水县', '阳城县', '陵川县', '泽州县', '高平市'],
|
||||
'朔州市': ['朔城区', '平鲁区', '山阴县', '应县', '右玉县', '怀仁市'],
|
||||
'晋中市': ['榆次区', '榆社县', '左权县', '和顺县', '昔阳县', '寿阳县', '太谷县', '祁县', '平遥县', '灵石县', '介休市'],
|
||||
'运城市': ['盐湖区', '临猗县', '万荣县', '闻喜县', '稷山县', '新绛县', '绛县', '垣曲县', '夏县', '平陆县', '芮城县', '永济市', '河津市'],
|
||||
'忻州市': ['忻府区', '定襄县', '五台县', '代县', '繁峙县', '宁武县', '静乐县', '神池县', '五寨县', '岢岚县', '河曲县', '保德县', '偏关县', '原平市'],
|
||||
'临汾市': ['尧都区', '曲沃县', '翼城县', '襄汾县', '洪洞县', '古县', '安泽县', '浮山县', '吉县', '乡宁县', '大宁县', '隰县', '永和县', '蒲县', '汾西县', '侯马市', '霍州市'],
|
||||
'吕梁市': ['离石区', '文水县', '交城县', '兴县', '临县', '柳林县', '石楼县', '岚县', '方山县', '中阳县', '交口县', '孝义市', '汾阳市']
|
||||
},
|
||||
'内蒙古自治区': {
|
||||
'呼和浩特市': ['新城区', '回民区', '玉泉区', '赛罕区', '土默特左旗', '托克托县', '和林格尔县', '清水河县', '武川县'],
|
||||
'包头市': ['东河区', '昆都仑区', '青山区', '石拐区', '白云鄂博矿区', '九原区', '土默特右旗', '固阳县', '达尔罕茂明安联合旗'],
|
||||
'乌海市': ['海勃湾区', '海南区', '乌达区'],
|
||||
'赤峰市': ['红山区', '元宝山区', '松山区', '阿鲁科尔沁旗', '巴林左旗', '巴林右旗', '林西县', '克什克腾旗', '翁牛特旗', '喀喇沁旗', '宁城县', '敖汉旗'],
|
||||
'通辽市': ['科尔沁区', '科尔沁左翼中旗', '科尔沁左翼后旗', '开鲁县', '库伦旗', '奈曼旗', '扎鲁特旗', '霍林郭勒市'],
|
||||
'鄂尔多斯市': ['东胜区', '康巴什区', '达拉特旗', '准格尔旗', '鄂托克前旗', '鄂托克旗', '杭锦旗', '乌审旗', '伊金霍洛旗'],
|
||||
'呼伦贝尔市': ['海拉尔区', '扎赉诺尔区', '阿荣旗', '莫力达瓦达斡尔族自治旗', '鄂伦春自治旗', '鄂温克族自治旗', '陈巴尔虎旗', '新巴尔虎左旗', '新巴尔虎右旗', '满洲里市', '牙克石市', '扎兰屯市', '额尔古纳市', '根河市'],
|
||||
'巴彦淖尔市': ['临河区', '五原县', '磴口县', '乌拉特前旗', '乌拉特中旗', '乌拉特后旗', '杭锦后旗'],
|
||||
'乌兰察布市': ['集宁区', '卓资县', '化德县', '商都县', '兴和县', '凉城县', '察哈尔右翼前旗', '察哈尔右翼中旗', '察哈尔右翼后旗', '四子王旗', '丰镇市'],
|
||||
'兴安盟': ['乌兰浩特市', '阿尔山市', '科尔沁右翼前旗', '科尔沁右翼中旗', '扎赉特旗', '突泉县'],
|
||||
'锡林郭勒盟': ['锡林浩特市', '阿巴嘎旗', '苏尼特左旗', '苏尼特右旗', '东乌珠穆沁旗', '西乌珠穆沁旗', '太仆寺旗', '镶黄旗', '正镶白旗', '正蓝旗', '多伦县', '二连浩特市'],
|
||||
'阿拉善盟': ['阿拉善左旗', '阿拉善右旗', '额济纳旗']
|
||||
},
|
||||
'辽宁省': {
|
||||
'沈阳市': ['和平区', '沈河区', '大东区', '皇姑区', '铁西区', '苏家屯区', '浑南区', '沈北新区', '于洪区', '辽中区', '康平县', '法库县', '新民市'],
|
||||
'大连市': ['中山区', '西岗区', '沙河口区', '甘井子区', '旅顺口区', '金州区', '普兰店区', '长海县', '瓦房店市', '庄河市'],
|
||||
'鞍山市': ['铁东区', '铁西区', '立山区', '千山区', '台安县', '岫岩满族自治县', '海城市'],
|
||||
'抚顺市': ['新抚区', '东洲区', '望花区', '顺城区', '抚顺县', '新宾满族自治县', '清原满族自治县'],
|
||||
'本溪市': ['平山区', '溪湖区', '明山区', '南芬区', '本溪满族自治县', '桓仁满族自治县'],
|
||||
'丹东市': ['元宝区', '振兴区', '振安区', '宽甸满族自治县', '东港市', '凤城市'],
|
||||
'锦州市': ['古塔区', '凌河区', '太和区', '黑山县', '义县', '凌海市', '北镇市'],
|
||||
'营口市': ['站前区', '西市区', '鲅鱼圈区', '老边区', '盖州市', '大石桥市'],
|
||||
'阜新市': ['海州区', '新邱区', '太平区', '清河门区', '细河区', '阜新蒙古族自治县', '彰武县'],
|
||||
'辽阳市': ['白塔区', '文圣区', '宏伟区', '弓长岭区', '太子河区', '辽阳县', '灯塔市'],
|
||||
'盘锦市': ['双台子区', '兴隆台区', '大洼区', '盘山县'],
|
||||
'铁岭市': ['银州区', '清河区', '铁岭县', '西丰县', '昌图县', '调兵山市', '开原市'],
|
||||
'朝阳市': ['双塔区', '龙城区', '朝阳县', '建平县', '喀喇沁左翼蒙古族自治县', '北票市', '凌源市'],
|
||||
'葫芦岛市': ['连山区', '龙港区', '南票区', '绥中县', '建昌县', '兴城市']
|
||||
},
|
||||
'吉林省': {
|
||||
'长春市': ['南关区', '宽城区', '朝阳区', '二道区', '绿园区', '双阳区', '九台区', '农安县', '榆树市', '德惠市'],
|
||||
'吉林市': ['昌邑区', '龙潭区', '船营区', '丰满区', '永吉县', '蛟河市', '桦甸市', '舒兰市', '磐石市'],
|
||||
'四平市': ['铁西区', '铁东区', '梨树县', '伊通满族自治县', '公主岭市', '双辽市'],
|
||||
'辽源市': ['龙山区', '西安区', '东丰县', '东辽县'],
|
||||
'通化市': ['东昌区', '二道江区', '通化县', '辉南县', '柳河县', '梅河口市', '集安市'],
|
||||
'白山市': ['浑江区', '江源区', '抚松县', '靖宇县', '长白朝鲜族自治县', '临江市'],
|
||||
'松原市': ['宁江区', '前郭尔罗斯蒙古族自治县', '长岭县', '乾安县', '扶余市'],
|
||||
'白城市': ['洮北区', '镇赖县', '通榆县', '洮南市', '大安市'],
|
||||
'延边朝鲜族自治州': ['延吉市', '图们市', '敦化市', '珲春市', '龙井市', '和龙市', '汪清县', '安图县']
|
||||
},
|
||||
'黑龙江省': {
|
||||
'哈尔滨市': ['道里区', '南岗区', '道外区', '平房区', '松北区', '香坊区', '呼兰区', '阿城区', '双城区', '依兰县', '方正县', '宾县', '巴彦县', '木兰县', '通河县', '延寿县', '尚志市', '五常市'],
|
||||
'齐齐哈尔市': ['龙沙区', '建华区', '铁锋区', '昂昂溪区', '富拉尔基区', '碾子山区', '梅里斯达斡尔族区', '龙江县', '依安县', '泰来县', '甘南县', '富裕县', '克山县', '克东县', '拜泉县', '讷河市'],
|
||||
'鸡西市': ['鸡冠区', '恒山区', '滴道区', '梨树区', '城子河区', '麻山区', '鸡东县', '虎林市', '密山市'],
|
||||
'鹤岗市': ['向阳区', '工农区', '南山区', '兴安区', '东山区', '兴山区', '萝北县', '绥滨县'],
|
||||
'双鸭山市': ['尖山区', '岭东区', '四方台区', '宝山区', '集贤县', '友谊县', '宝清县', '饶河县'],
|
||||
'大庆市': ['萨尔图区', '龙凤区', '让胡路区', '红岗区', '大同区', '肇州县', '肇源县', '林甸县', '杜尔伯特蒙古族自治县'],
|
||||
'伊春市': ['伊春区', '南岔区', '友好区', '西林区', '翠峦区', '新青区', '美溪区', '金山屯区', '五营区', '乌马河区', '汤旺河区', '带岭区', '乌伊岭区', '红星区', '上甘岭区', '嘉荫县', '铁力市'],
|
||||
'佳木斯市': ['向阳区', '前进区', '东风区', '郊区', '桦南县', '桦川县', '汤原县', '抚远市', '同江市', '富锦市'],
|
||||
'七台河市': ['新兴区', '桃山区', '茄子河区', '勃利县'],
|
||||
'牡丹江市': ['东安区', '阳明区', '爱民区', '西安区', '林口县', '绥芬河市', '海林市', '宁安市', '穆棱市', '东宁市'],
|
||||
'黑河市': ['爱辉区', '嫩江县', '逊克县', '孙吴县', '北安市', '五大连池市'],
|
||||
'绥化市': ['北林区', '望奎县', '兰西县', '青冈县', '庆安县', '明水县', '绥棱县', '安达市', '肇东市', '海伦市'],
|
||||
'大兴安岭地区': ['呼玛县', '塔河县', '漠河市']
|
||||
},
|
||||
'江苏省': {
|
||||
'南京市': ['玄武区', '秦淮区', '建邺区', '鼓楼区', '浦口区', '栖霞区', '雨花台区', '江宁区', '六合区', '溧水区', '高淳区'],
|
||||
'无锡市': ['锡山区', '惠山区', '滨湖区', '梁溪区', '新吴区', '江阴市', '宜兴市'],
|
||||
'徐州市': ['鼓楼区', '云龙区', '贾汪区', '泉山区', '铜山区', '丰县', '沛县', '睢宁县', '新沂市', '邳州市'],
|
||||
'常州市': ['天宁区', '钟楼区', '新北区', '武进区', '金坛区', '溧阳市'],
|
||||
'苏州市': ['虎丘区', '吴中区', '相城区', '姑苏区', '吴江区', '常熟市', '张家港市', '昆山市', '太仓市'],
|
||||
'南通市': ['崇川区', '港闸区', '通州区', '海安市', '如东县', '启东市', '如皋市', '海门市'],
|
||||
'连云港市': ['连云区', '海州区', '赣榆区', '东海县', '灌云县', '灌南县'],
|
||||
'淮安市': ['淮安区', '淮阴区', '清江浦区', '洪泽区', '涟水县', '盱眙县', '金湖县'],
|
||||
'盐城市': ['亭湖区', '盐都区', '大丰区', '响水县', '滨海县', '阜宁县', '射阳县', '建湖县', '东台市'],
|
||||
'扬州市': ['广陵区', '邗江区', '江都区', '宝应县', '仪征市', '高邮市'],
|
||||
'镇江市': ['京口区', '润州区', '丹徒区', '丹阳市', '扬中市', '句容市'],
|
||||
'泰州市': ['海陵区', '高港区', '姜堰区', '兴化市', '靖江市', '泰兴市'],
|
||||
'宿迁市': ['宿城区', '宿豫区', '沭阳县', '泗阳县', '泗洪县']
|
||||
},
|
||||
'浙江省': {
|
||||
'杭州市': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区', '萧山区', '余杭区', '富阳区', '临安区', '桐庐县', '淳安县', '建德市'],
|
||||
'宁波市': ['海曙区', '江北区', '北仑区', '镇海区', '鄞州区', '奉化区', '象山县', '宁海县', '余姚市', '慈溪市'],
|
||||
'温州市': ['鹿城区', '龙湾区', '瓯海区', '洞头区', '永嘉县', '平阳县', '苍南县', '文成县', '泰顺县', '瑞安市', '乐清市'],
|
||||
'嘉兴市': ['南湖区', '秀洲区', '嘉善县', '海盐县', '海宁市', '平湖市', '桐乡市'],
|
||||
'湖州市': ['吴兴区', '南浔区', '德清县', '长兴县', '安吉县'],
|
||||
'绍兴市': ['越城区', '柯桥区', '上虞区', '新昌县', '诸暨市', '嵊州市'],
|
||||
'金华市': ['婺城区', '金东区', '武义县', '浦江县', '磐安县', '兰溪市', '义乌市', '东阳市', '永康市'],
|
||||
'衢州市': ['柯城区', '衢江区', '常山县', '开化县', '龙游县', '江山市'],
|
||||
'舟山市': ['定海区', '普陀区', '岱山县', '嵊泗县'],
|
||||
'台州市': ['椒江区', '黄岩区', '路桥区', '三门县', '天台县', '仙居县', '温岭市', '临海市', '玉环市'],
|
||||
'丽水市': ['莲都区', '青田县', '缙云县', '遂昌县', '松阳县', '云和县', '庆元县', '景宁畲族自治县', '龙泉市']
|
||||
},
|
||||
'安徽省': {
|
||||
'合肥市': ['瑶海区', '庐阳区', '蜀山区', '包河区', '长丰县', '肥东县', '肥西县', '庐江县', '巢湖市'],
|
||||
'芜湖市': ['镜湖区', '弋江区', '鸠江区', '三山区', '芜湖县', '繁昌县', '南陵县', '无为市'],
|
||||
'蚌埠市': ['龙子湖区', '蚌山区', '禹会区', '淮上区', '怀远县', '五河县', '固镇县'],
|
||||
'淮南市': ['大通区', '田家庵区', '谢家集区', '八公山区', '潘集区', '凤台县', '寿县'],
|
||||
'马鞍山市': ['花山区', '雨山区', '博望区', '当涂县', '含山县', '和县'],
|
||||
'淮北市': ['杜集区', '相山区', '烈山区', '濉溪县'],
|
||||
'铜陵市': ['铜官区', '义安区', '郊区', '枞阳县'],
|
||||
'安庆市': ['迎江区', '大观区', '宜秀区', '怀宁县', '潜山市', '太湖县', '宿松县', '望江县', '岳西县', '桐城市'],
|
||||
'黄山市': ['屯溪区', '黄山区', '徽州区', '歙县', '休宁县', '黟县', '祁门县'],
|
||||
'滁州市': ['琅琊区', '南谯区', '来安县', '全椒县', '定远县', '凤阳县', '天长市', '明光市'],
|
||||
'阜阳市': ['颍州区', '颍东区', '颍泉区', '临泉县', '太和县', '阜南县', '颍上县', '界首市'],
|
||||
'宿州市': ['埇桥区', '砀山县', '萧县', '灵璧县', '泗县'],
|
||||
'六安市': ['金安区', '裕安区', '叶集区', '霍邱县', '舒城县', '金寨县', '霍山县'],
|
||||
'亳州市': ['谯城区', '涡阳县', '蒙城县', '利辛县'],
|
||||
'池州市': ['贵池区', '东至县', '石台县', '青阳县'],
|
||||
'宣城市': ['宣州区', '郎溪县', '广德市', '泾县', '绩溪县', '旌德县', '宁国市']
|
||||
},
|
||||
'福建省': {
|
||||
'福州市': ['鼓楼区', '台江区', '仓山区', '马尾区', '晋安区', '长乐区', '闽侯县', '连江县', '罗源县', '闽清县', '永泰县', '平潭县', '福清市'],
|
||||
'厦门市': ['思明区', '海沧区', '湖里区', '集美区', '同安区', '翔安区'],
|
||||
'莆田市': ['城厢区', '涵江区', '荔城区', '秀屿区', '仙游县'],
|
||||
'三明市': ['梅列区', '三元区', '明溪县', '清流县', '宁化县', '大田县', '尤溪县', '沙县', '将乐县', '泰宁县', '建宁县', '永安市'],
|
||||
'泉州市': ['鲤城区', '丰泽区', '洛江区', '泉港区', '惠安县', '安溪县', '永春县', '德化县', '金门县', '石狮市', '晋江市', '南安市'],
|
||||
'漳州市': ['芗城区', '龙文区', '云霄县', '漳浦县', '诏安县', '长泰县', '东山县', '南靖县', '平和县', '华安县', '龙海市'],
|
||||
'南平市': ['延平区', '建阳区', '顺昌县', '浦城县', '光泽县', '松溪县', '政和县', '邵武市', '武夷山市', '建瓯市'],
|
||||
'龙岩市': ['新罗区', '永定区', '长汀县', '上杭县', '武平县', '连城县', '漳平市'],
|
||||
'宁德市': ['蕉城区', '霞浦县', '古田县', '屏南县', '寿宁县', '周宁县', '柘荣县', '福安市', '福鼎市']
|
||||
},
|
||||
'江西省': {
|
||||
'南昌市': ['东湖区', '西湖区', '青云谱区', '湾里区', '青山湖区', '新建区', '南昌县', '安义县', '进贤县'],
|
||||
'景德镇市': ['昌江区', '珠山区', '浮梁县', '乐平市'],
|
||||
'萍乡市': ['安源区', '湘东区', '莲花县', '上栗县', '芦溪县'],
|
||||
'九江市': ['濂溪区', '浔阳区', '柴桑区', '武宁县', '修水县', '永修县', '德安县', '都昌县', '湖口县', '彭泽县', '瑞昌市', '共青城市', '庐山市'],
|
||||
'新余市': ['渝水区', '分宜县'],
|
||||
'鹰潭市': ['月湖区', '余江区', '贵溪市'],
|
||||
'赣州市': ['章贡区', '南康区', '赣县区', '信丰县', '大余县', '上犹县', '崇义县', '安远县', '龙南县', '定南县', '全南县', '宁都县', '于都县', '兴国县', '会昌县', '寻乌县', '石城县', '瑞金市'],
|
||||
'吉安市': ['吉州区', '青原区', '吉安县', '吉水县', '峡江县', '新干县', '永丰县', '泰和县', '遂川县', '万安县', '安福县', '永新县', '井冈山市'],
|
||||
'宜春市': ['袁州区', '奉新县', '万载县', '上高县', '宜丰县', '靖安县', '铜鼓县', '丰城市', '樟树市', '高安市'],
|
||||
'抚州市': ['临川区', '东乡区', '南城县', '黎川县', '南丰县', '崇仁县', '乐安县', '宜黄县', '金溪县', '资溪县', '广昌县'],
|
||||
'上饶市': ['信州区', '广丰区', '广信区', '玉山县', '铅山县', '横峰县', '弋阳县', '余干县', '鄱阳县', '万年县', '婺源县', '德兴市']
|
||||
},
|
||||
'山东省': {
|
||||
'济南市': ['历下区', '市中区', '槐荫区', '天桥区', '历城区', '长清区', '章丘区', '济阳区', '莱芜区', '钢城区', '平阴县', '商河县'],
|
||||
'青岛市': ['市南区', '市北区', '黄岛区', '崂山区', '李沧区', '城阳区', '即墨区', '胶州市', '平度市', '莱西市'],
|
||||
'淄博市': ['淄川区', '张店区', '博山区', '临淄区', '周村区', '桓台县', '高青县', '沂源县'],
|
||||
'枣庄市': ['市中区', '薛城区', '峄城区', '台儿庄区', '山亭区', '滕州市'],
|
||||
'东营市': ['东营区', '河口区', '垦利区', '利津县', '广饶县'],
|
||||
'烟台市': ['芝罘区', '福山区', '牟平区', '莱山区', '长岛县', '龙口市', '莱阳市', '莱州市', '蓬莱市', '招远市', '栖霞市', '海阳市'],
|
||||
'潍坊市': ['潍城区', '寒亭区', '坊子区', '奎文区', '临朐县', '昌乐县', '青州市', '诸城市', '寿光市', '安丘市', '高密市', '昌邑市'],
|
||||
'济宁市': ['任城区', '兖州区', '微山县', '鱼台县', '金乡县', '嘉祥县', '汶上县', '泗水县', '梁山县', '曲阜市', '邹城市'],
|
||||
'泰安市': ['泰山区', '岱岳区', '宁阳县', '东平县', '新泰市', '肥城市'],
|
||||
'威海市': ['环翠区', '文登区', '荣成市', '乳山市'],
|
||||
'日照市': ['东港区', '岚山区', '五莲县', '莒县'],
|
||||
'临沂市': ['兰山区', '罗庄区', '河东区', '沂南县', '郯城县', '沂水县', '兰陵县', '费县', '平邑县', '莒南县', '蒙阴县', '临沭县'],
|
||||
'德州市': ['德城区', '陵城区', '宁津县', '庆云县', '临邑县', '齐河县', '平原县', '夏津县', '武城县', '乐陵市', '禹城市'],
|
||||
'聊城市': ['东昌府区', '茌平区', '阳谷县', '莘县', '茌平县', '东阿县', '冠县', '高唐县', '临清市'],
|
||||
'滨州市': ['滨城区', '沾化区', '惠民县', '阳信县', '无棣县', '博兴县', '邹平市'],
|
||||
'菏泽市': ['牡丹区', '定陶区', '曹县', '单县', '成武县', '巨野县', '郓城县', '鄄城县', '东明县']
|
||||
},
|
||||
'河南省': {
|
||||
'郑州市': ['中原区', '二七区', '管城回族区', '金水区', '上街区', '惠济区', '中牟县', '巩义市', '荥阳市', '新密市', '新郑市', '登封市'],
|
||||
'开封市': ['龙亭区', '顺河回族区', '鼓楼区', '禹王台区', '祥符区', '杞县', '通许县', '尉氏县', '兰考县'],
|
||||
'洛阳市': ['老城区', '西工区', '瀍河回族区', '涧西区', '吉利区', '洛龙区', '孟津县', '新安县', '栾川县', '嵩县', '汝阳县', '宜阳县', '洛宁县', '伊川县', '偃师市'],
|
||||
'平顶山市': ['新华区', '卫东区', '石龙区', '湛河区', '宝丰县', '叶县', '鲁山县', '郏县', '舞钢市', '汝州市'],
|
||||
'安阳市': ['文峰区', '北关区', '殷都区', '龙安区', '安阳县', '汤阴县', '滑县', '内黄县', '林州市'],
|
||||
'鹤壁市': ['鹤山区', '山城区', '淇滨区', '浚县', '淇县'],
|
||||
'新乡市': ['红旗区', '卫滨区', '凤泉区', '牧野区', '新乡县', '获嘉县', '原阳县', '延津县', '封丘县', '长垣市', '卫辉市', '辉县市'],
|
||||
'焦作市': ['解放区', '中站区', '马村区', '山阳区', '修武县', '博爱县', '武陟县', '温县', '沁阳市', '孟州市'],
|
||||
'濮阳市': ['华龙区', '清丰县', '南乐县', '范县', '台前县', '濮阳县'],
|
||||
'许昌市': ['魏都区', '建安区', '鄢陵县', '襄城县', '禹州市', '长葛市'],
|
||||
'漯河市': ['源汇区', '郾城区', '召陵区', '舞阳县', '临颍县'],
|
||||
'三门峡市': ['湖滨区', '陕州区', '渑池县', '卢氏县', '义马市', '灵宝市'],
|
||||
'南阳市': ['宛城区', '卧龙区', '南召县', '方城县', '西峡县', '镇平县', '内乡县', '淅川县', '社旗县', '唐河县', '新野县', '桐柏县', '邓州市'],
|
||||
'商丘市': ['梁园区', '睢阳区', '民权县', '睢县', '宁陵县', '柘城县', '虞城县', '夏邑县', '永城市'],
|
||||
'信阳市': ['浉河区', '平桥区', '罗山县', '光山县', '新县', '商城县', '固始县', '潢川县', '淮滨县', '息县'],
|
||||
'周口市': ['川汇区', '扶沟县', '西华县', '商水县', '沈丘县', '郸城县', '淮阳区', '太康县', '鹿邑县', '项城市'],
|
||||
'驻马店市': ['驿城区', '西平县', '上蔡县', '平舆县', '正阳县', '确山县', '泌阳县', '汝南县', '遂平县', '新蔡县'],
|
||||
'济源市': ['济源市']
|
||||
},
|
||||
'湖北省': {
|
||||
'武汉市': ['江岸区', '江汉区', '硚口区', '汉阳区', '武昌区', '青山区', '洪山区', '东西湖区', '汉南区', '蔡甸区', '江夏区', '黄陂区', '新洲区'],
|
||||
'黄石市': ['黄石港区', '西塞山区', '下陆区', '铁山区', '阳新县', '大冶市'],
|
||||
'十堰市': ['茅箭区', '张湾区', '郧阳区', '郧西县', '竹山县', '竹溪县', '房县', '丹江口市'],
|
||||
'宜昌市': ['西陵区', '伍家岗区', '点军区', '猇亭区', '夷陵区', '远安县', '兴山县', '秭归县', '长阳土家族自治县', '五峰土家族自治县', '宜都市', '当阳市', '枝江市'],
|
||||
'襄阳市': ['襄城区', '樊城区', '襄州区', '南漳县', '谷城县', '保康县', '老河口市', '枣阳市', '宜城市'],
|
||||
'鄂州市': ['梁子湖区', '华容区', '鄂城区'],
|
||||
'荆门市': ['东宝区', '掇刀区', '京山市', '沙洋县', '钟祥市'],
|
||||
'孝感市': ['孝南区', '孝昌县', '大悟县', '云梦县', '应城市', '安陆市', '汉川市'],
|
||||
'荆州市': ['沙市区', '荆州区', '公安县', '监利县', '江陵县', '石首市', '洪湖市', '松滋市'],
|
||||
'黄冈市': ['黄州区', '团风县', '红安县', '罗田县', '英山县', '浠水县', '蕲春县', '黄梅县', '麻城市', '武穴市'],
|
||||
'咸宁市': ['咸安区', '嘉鱼县', '通城县', '崇阳县', '通山县', '赤壁市'],
|
||||
'随州市': ['曾都区', '随县', '广水市'],
|
||||
'恩施土家族苗族自治州': ['恩施市', '利川市', '建始县', '巴东县', '宣恩县', '咸丰县', '来凤县', '鹤峰县'],
|
||||
'仙桃市': ['仙桃市'],
|
||||
'潜江市': ['潜江市'],
|
||||
'天门市': ['天门市'],
|
||||
'神农架林区': ['神农架林区']
|
||||
},
|
||||
'湖南省': {
|
||||
'长沙市': ['芙蓉区', '天心区', '岳麓区', '开福区', '雨花区', '望城区', '长沙县', '宁乡市', '浏阳市'],
|
||||
'株洲市': ['荷塘区', '芦淞区', '石峰区', '天元区', '渌口区', '攸县', '茶陵县', '炎陵县', '醴陵市'],
|
||||
'湘潭市': ['雨湖区', '岳塘区', '湘潭县', '湘乡市', '韶山市'],
|
||||
'衡阳市': ['珠晖区', '雁峰区', '石鼓区', '蒸湘区', '南岳区', '衡阳县', '衡南县', '衡山县', '衡东县', '祁东县', '耒阳市', '常宁市'],
|
||||
'邵阳市': ['双清区', '大祥区', '北塔区', '邵东市', '新邵县', '邵阳县', '隆回县', '洞口县', '绥宁县', '新宁县', '城步苗族自治县', '武冈市'],
|
||||
'岳阳市': ['岳阳楼区', '云溪区', '君山区', '岳阳县', '华容县', '湘阴县', '平江县', '汨罗市', '临湘市'],
|
||||
'常德市': ['武陵区', '鼎城区', '安乡县', '汉寿县', '澧县', '临澧县', '桃源县', '石门县', '津市市'],
|
||||
'张家界市': ['永定区', '武陵源区', '慈利县', '桑植县'],
|
||||
'益阳市': ['资阳区', '赫山区', '南县', '桃江县', '安化县', '沅江市'],
|
||||
'郴州市': ['北湖区', '苏仙区', '桂阳县', '宜章县', '永兴县', '嘉禾县', '临武县', '汝城县', '桂东县', '安仁县', '资兴市'],
|
||||
'永州市': ['零陵区', '冷水滩区', '祁阳县', '东安县', '双牌县', '道县', '江永县', '宁远县', '蓝山县', '新田县', '江华瑶族自治县'],
|
||||
'怀化市': ['鹤城区', '中方县', '沅陵县', '辰溪县', '溆浦县', '会同县', '麻阳苗族自治县', '新晃侗族自治县', '芷江侗族自治县', '靖州苗族侗族自治县', '通道侗族自治县', '洪江市'],
|
||||
'娄底市': ['娄星区', '双峰县', '新化县', '冷水江市', '涟源市'],
|
||||
'湘西土家族苗族自治州': ['吉首市', '泸溪县', '凤凰县', '花垣县', '保靖县', '古丈县', '永顺县', '龙山县']
|
||||
},
|
||||
'广东省': {
|
||||
'广州市': ['荔湾区', '越秀区', '海珠区', '天河区', '白云区', '黄埔区', '番禺区', '花都区', '南沙区', '从化区', '增城区'],
|
||||
'深圳市': ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区', '龙华区', '坪山区', '光明区', '大鹏新区'],
|
||||
'珠海市': ['香洲区', '斗门区', '金湾区'],
|
||||
'汕头市': ['龙湖区', '金平区', '濠江区', '潮阳区', '潮南区', '澄海区', '南澳县'],
|
||||
'佛山市': ['禅城区', '南海区', '顺德区', '三水区', '高明区'],
|
||||
'韶关市': ['武江区', '浈江区', '曲江区', '始兴县', '仁化县', '翁源县', '乳源瑶族自治县', '新丰县', '乐昌市', '南雄市'],
|
||||
'湛江市': ['赤坎区', '霞山区', '坡头区', '麻章区', '遂溪县', '徐闻县', '廉江市', '雷州市', '吴川市'],
|
||||
'肇庆市': ['端州区', '鼎湖区', '高要区', '广宁县', '怀集县', '封开县', '德庆县', '四会市'],
|
||||
'江门市': ['蓬江区', '江海区', '新会区', '台山市', '开平市', '鹤山市', '恩平市'],
|
||||
'茂名市': ['茂南区', '电白区', '高州市', '化州市', '信宜市'],
|
||||
'惠州市': ['惠城区', '惠阳区', '博罗县', '惠东县', '龙门县'],
|
||||
'梅州市': ['梅江区', '梅县区', '大埔县', '丰顺县', '五华县', '平远县', '蕉岭县', '兴宁市'],
|
||||
'汕尾市': ['城区', '海丰县', '陆河县', '陆丰市'],
|
||||
'河源市': ['源城区', '紫金县', '龙川县', '连平县', '和平县', '东源县'],
|
||||
'阳江市': ['江城区', '阳东区', '阳西县', '阳春市'],
|
||||
'清远市': ['清城区', '清新区', '佛冈县', '阳山县', '连山壮族瑶族自治县', '连南瑶族自治县', '英德市', '连州市'],
|
||||
'东莞市': ['东莞市'],
|
||||
'中山市': ['中山市'],
|
||||
'潮州市': ['湘桥区', '潮安区', '饶平县'],
|
||||
'揭阳市': ['榕城区', '揭东区', '揭西县', '惠来县', '普宁市'],
|
||||
'云浮市': ['云城区', '云安区', '新兴县', '郁南县', '罗定市']
|
||||
},
|
||||
'广西壮族自治区': {
|
||||
'南宁市': ['兴宁区', '青秀区', '江南区', '西乡塘区', '良庆区', '邕宁区', '武鸣区', '隆安县', '马山县', '上林县', '宾阳县', '横县'],
|
||||
'柳州市': ['城中区', '鱼峰区', '柳南区', '柳北区', '柳江区', '柳城县', '鹿寨县', '融安县', '融水苗族自治县', '三江侗族自治县'],
|
||||
'桂林市': ['秀峰区', '叠彩区', '象山区', '七星区', '雁山区', '临桂区', '阳朔县', '灵川县', '全州县', '兴安县', '永福县', '灌阳县', '龙胜各族自治县', '资源县', '平乐县', '荔浦市', '恭城瑶族自治县'],
|
||||
'梧州市': ['万秀区', '长洲区', '龙圩区', '苍梧县', '藤县', '蒙山县', '岑溪市'],
|
||||
'北海市': ['海城区', '银海区', '铁山港区', '合浦县'],
|
||||
'防城港市': ['港口区', '防城区', '上思县', '东兴市'],
|
||||
'钦州市': ['钦南区', '钦北区', '灵山县', '浦北县'],
|
||||
'贵港市': ['港北区', '港南区', '覃塘区', '平南县', '桂平市'],
|
||||
'玉林市': ['玉州区', '福绵区', '容县', '陆川县', '博白县', '兴业县', '北流市'],
|
||||
'百色市': ['右江区', '田阳区', '田东县', '平果市', '德保县', '那坡县', '凌云县', '乐业县', '田林县', '西林县', '隆林各族自治县', '靖西市'],
|
||||
'贺州市': ['八步区', '平桂区', '昭平县', '钟山县', '富川瑶族自治县'],
|
||||
'河池市': ['金城江区', '宜州区', '南丹县', '天峨县', '凤山县', '东兰县', '罗城仫佬族自治县', '环江毛南族自治县', '巴马瑶族自治县', '都安瑶族自治县', '大化瑶族自治县'],
|
||||
'来宾市': ['兴宾区', '忻城县', '象州县', '武宣县', '金秀瑶族自治县', '合山市'],
|
||||
'崇左市': ['江州区', '扶绥县', '宁明县', '龙州县', '大新县', '天等县', '凭祥市']
|
||||
},
|
||||
'海南省': {
|
||||
'海口市': ['秀英区', '龙华区', '琼山区', '美兰区'],
|
||||
'三亚市': ['海棠区', '吉阳区', '天涯区', '崖州区'],
|
||||
'三沙市': ['西沙群岛', '南沙群岛', '中沙群岛'],
|
||||
'儋州市': ['儋州市'],
|
||||
'五指山市': ['五指山市'],
|
||||
'琼海市': ['琼海市'],
|
||||
'文昌市': ['文昌市'],
|
||||
'万宁市': ['万宁市'],
|
||||
'东方市': ['东方市'],
|
||||
'定安县': ['定安县'],
|
||||
'屯昌县': ['屯昌县'],
|
||||
'澄迈县': ['澄迈县'],
|
||||
'临高县': ['临高县'],
|
||||
'白沙黎族自治县': ['白沙黎族自治县'],
|
||||
'昌江黎族自治县': ['昌江黎族自治县'],
|
||||
'乐东黎族自治县': ['乐东黎族自治县'],
|
||||
'陵水黎族自治县': ['陵水黎族自治县'],
|
||||
'保亭黎族苗族自治县': ['保亭黎族苗族自治县'],
|
||||
'琼中黎族苗族自治县': ['琼中黎族苗族自治县']
|
||||
},
|
||||
'四川省': {
|
||||
'成都市': ['锦江区', '青羊区', '金牛区', '武侯区', '成华区', '龙泉驿区', '青白江区', '新都区', '温江区', '双流区', '郫都区', '新津区', '金堂县', '大邑县', '蒲江县', '都江堰市', '彭州市', '邛崃市', '崇州市', '简阳市'],
|
||||
'自贡市': ['自流井区', '贡井区', '大安区', '沿滩区', '荣县', '富顺县'],
|
||||
'攀枝花市': ['东区', '西区', '仁和区', '米易县', '盐边县'],
|
||||
'泸州市': ['江阳区', '纳溪区', '龙马潭区', '泸县', '合江县', '叙永县', '古蔺县'],
|
||||
'德阳市': ['旌阳区', '罗江区', '中江县', '广汉市', '什邡市', '绵竹市'],
|
||||
'绵阳市': ['涪城区', '游仙区', '安州区', '三台县', '盐亭县', '梓潼县', '北川羌族自治县', '平武县', '江油市'],
|
||||
'广元市': ['利州区', '昭化区', '朝天区', '旺苍县', '青川县', '剑阁县', '苍溪县'],
|
||||
'遂宁市': ['船山区', '安居区', '蓬溪县', '射洪市', '大英县'],
|
||||
'内江市': ['市中区', '东兴区', '威远县', '资中县', '隆昌市'],
|
||||
'乐山市': ['市中区', '沙湾区', '五通桥区', '金口河区', '犍为县', '井研县', '夹江县', '沐川县', '峨边彝族自治县', '马边彝族自治县', '峨眉山市'],
|
||||
'南充市': ['顺庆区', '高坪区', '嘉陵区', '南部县', '营山县', '蓬安县', '仪陇县', '西充县', '阆中市'],
|
||||
'眉山市': ['东坡区', '彭山区', '仁寿县', '洪雅县', '丹棱县', '青神县'],
|
||||
'宜宾市': ['翠屏区', '南溪区', '叙州区', '江安县', '长宁县', '高县', '珙县', '筠连县', '兴文县', '屏山县'],
|
||||
'广安市': ['广安区', '前锋区', '岳池县', '武胜县', '邻水县', '华蓥市'],
|
||||
'达州市': ['通川区', '达川区', '宣汉县', '开江县', '大竹县', '渠县', '万源市'],
|
||||
'雅安市': ['雨城区', '名山区', '荥经县', '汉源县', '石棉县', '天全县', '芦山县', '宝兴县'],
|
||||
'巴中市': ['巴州区', '恩阳区', '通江县', '南江县', '平昌县'],
|
||||
'资阳市': ['雁江区', '安岳县', '乐至县'],
|
||||
'阿坝藏族羌族自治州': ['马尔康市', '汶川县', '理县', '茂县', '松潘县', '九寨沟县', '金川县', '小金县', '黑水县', '壤塘县', '阿坝县', '若尔盖县', '红原县'],
|
||||
'甘孜藏族自治州': ['康定市', '泸定县', '丹巴县', '九龙县', '雅江县', '道孚县', '炉霍县', '甘孜县', '新龙县', '德格县', '白玉县', '石渠县', '色达县', '理塘县', '巴塘县', '乡城县', '稻城县', '得荣县'],
|
||||
'凉山彝族自治州': ['西昌市', '木里藏族自治县', '盐源县', '德昌县', '会理市', '会东县', '宁南县', '普格县', '布拖县', '金阳县', '昭觉县', '喜德县', '冕宁县', '越西县', '甘洛县', '美姑县', '雷波县']
|
||||
},
|
||||
'贵州省': {
|
||||
'贵阳市': ['南明区', '云岩区', '花溪区', '乌当区', '白云区', '观山湖区', '开阳县', '息烽县', '修文县', '清镇市'],
|
||||
'六盘水市': ['钟山区', '六枝特区', '水城区', '盘州市'],
|
||||
'遵义市': ['红花岗区', '汇川区', '播州区', '桐梓县', '绥阳县', '正安县', '道真仡佬族苗族自治县', '务川仡佬族苗族自治县', '凤冈县', '湄潭县', '余庆县', '习水县', '赤水市', '仁怀市'],
|
||||
'安顺市': ['西秀区', '平坝区', '普定县', '镇宁布依族苗族自治县', '关岭布依族苗族自治县', '紫云苗族布依族自治县'],
|
||||
'毕节市': ['七星关区', '大方县', '黔西市', '金沙县', '织金县', '纳雍县', '威宁彝族回族苗族自治县', '赫章县'],
|
||||
'铜仁市': ['碧江区', '万山区', '江口县', '玉屏侗族自治县', '石阡县', '思南县', '印江土家族苗族自治县', '德江县', '沿河土家族自治县', '松桃苗族自治县'],
|
||||
'黔西南布依族苗族自治州': ['兴义市', '兴仁市', '普安县', '晴隆县', '贞丰县', '望谟县', '册亨县', '安龙县'],
|
||||
'黔东南苗族侗族自治州': ['凯里市', '黄平县', '施秉县', '三穗县', '镇远县', '岑巩县', '天柱县', '锦屏县', '剑河县', '台江县', '黎平县', '榕江县', '从江县', '雷山县', '麻江县', '丹寨县'],
|
||||
'黔南布依族苗族自治州': ['都匀市', '福泉市', '荔波县', '贵定县', '瓮安县', '独山县', '平塘县', '罗甸县', '长顺县', '龙里县', '惠水县', '三都水族自治县']
|
||||
},
|
||||
'云南省': {
|
||||
'昆明市': ['五华区', '盘龙区', '官渡区', '西山区', '东川区', '呈贡区', '晋宁区', '富民县', '宜良县', '石林彝族自治县', '嵩明县', '禄劝彝族苗族自治县', '寻甸回族彝族自治县', '安宁市'],
|
||||
'曲靖市': ['麒麟区', '沾益区', '马龙区', '陆良县', '师宗县', '罗平县', '富源县', '会泽县', '宣威市'],
|
||||
'玉溪市': ['红塔区', '江川区', '澄江市', '通海县', '华宁县', '易门县', '峨山彝族自治县', '新平彝族傣族自治县', '元江哈尼族彝族傣族自治县'],
|
||||
'保山市': ['隆阳区', '施甸县', '龙陵县', '昌宁县', '腾冲市'],
|
||||
'昭通市': ['昭阳区', '鲁甸县', '巧家县', '盐津县', '大关县', '永善县', '绥江县', '镇雄县', '彝良县', '威信县', '水富市'],
|
||||
'丽江市': ['古城区', '玉龙纳西族自治县', '永胜县', '华坪县', '宁蒗彝族自治县'],
|
||||
'普洱市': ['思茅区', '宁洱哈尼族彝族自治县', '墨江哈尼族自治县', '景东彝族自治县', '景谷傣族彝族自治县', '镇沅彝族哈尼族拉祜族自治县', '江城哈尼族彝族自治县', '孟连傣族拉祜族佤族自治县', '澜沧拉祜族自治县', '西盟佤族自治县'],
|
||||
'临沧市': ['临翔区', '凤庆县', '云县', '永德县', '镇康县', '双江拉祜族佤族布朗族傣族自治县', '耿马傣族佤族自治县', '沧源佤族自治县'],
|
||||
'楚雄彝族自治州': ['楚雄市', '双柏县', '牟定县', '南华县', '姚安县', '大姚县', '永仁县', '元谋县', '武定县', '禄丰市'],
|
||||
'红河哈尼族彝族自治州': ['个旧市', '开远市', '蒙自市', '弥勒市', '屏边苗族自治县', '建水县', '石屏县', '泸西县', '元阳县', '红河县', '金平苗族瑶族傣族自治县', '绿春县', '河口瑶族自治县'],
|
||||
'文山壮族苗族自治州': ['文山市', '砚山县', '西畴县', '麻栗坡县', '马关县', '丘北县', '广南县', '富宁县'],
|
||||
'西双版纳傣族自治州': ['景洪市', '勐海县', '勐腊县'],
|
||||
'大理白族自治州': ['大理市', '漾濞彝族自治县', '祥云县', '宾川县', '弥渡县', '南涧彝族自治县', '巍山彝族回族自治县', '永平县', '云龙县', '洱源县', '剑川县', '鹤庆县'],
|
||||
'德宏傣族景颇族自治州': ['瑞丽市', '芒市', '梁河县', '盈江县', '陇川县'],
|
||||
'怒江傈僳族自治州': ['泸水市', '福贡县', '贡山独龙族怒族自治县', '兰坪白族普米族自治县'],
|
||||
'迪庆藏族自治州': ['香格里拉市', '德钦县', '维西傈僳族自治县']
|
||||
},
|
||||
'西藏自治区': {
|
||||
'拉萨市': ['城关区', '堆龙德庆区', '达孜区', '林周县', '当雄县', '尼木县', '曲水县', '墨竹工卡县'],
|
||||
'日喀则市': ['桑珠孜区', '南木林县', '江孜县', '定日县', '萨迦县', '拉孜县', '昂仁县', '谢通门县', '白朗县', '仁布县', '康马县', '定结县', '仲巴县', '亚东县', '吉隆县', '聂拉木县', '萨嘎县', '岗巴县'],
|
||||
'昌都市': ['卡若区', '江达县', '贡觉县', '类乌齐县', '丁青县', '察雅县', '八宿县', '左贡县', '芒康县', '洛隆县', '边坝县'],
|
||||
'林芝市': ['巴宜区', '工布江达县', '米林县', '墨脱县', '波密县', '察隅县', '朗县'],
|
||||
'山南市': ['乃东区', '扎囊县', '贡嘎县', '桑日县', '琼结县', '曲松县', '措美县', '洛扎县', '加查县', '隆子县', '错那县', '浪卡子县'],
|
||||
'那曲市': ['色尼区', '嘉黎县', '比如县', '聂荣县', '安多县', '申扎县', '索县', '班戈县', '巴青县', '尼玛县', '双湖县'],
|
||||
'阿里地区': ['普兰县', '札达县', '噶尔县', '日土县', '革吉县', '改则县', '措勤县']
|
||||
},
|
||||
'陕西省': {
|
||||
'西安市': ['新城区', '碑林区', '莲湖区', '灞桥区', '未央区', '雁塔区', '阎良区', '临潼区', '长安区', '高陵区', '鄠邑区', '蓝田县', '周至县'],
|
||||
'铜川市': ['王益区', '印台区', '耀州区', '宜君县'],
|
||||
'宝鸡市': ['渭滨区', '金台区', '陈仓区', '凤翔区', '岐山县', '扶风县', '眉县', '陇县', '千阳县', '麟游县', '凤县', '太白县'],
|
||||
'咸阳市': ['秦都区', '杨陵区', '渭城区', '三原县', '泾阳县', '乾县', '礼泉县', '永寿县', '长武县', '旬邑县', '淳化县', '武功县', '兴平市', '彬州市'],
|
||||
'渭南市': ['临渭区', '华州区', '潼关县', '大荔县', '合阳县', '澄城县', '蒲城县', '白水县', '富平县', '韩城市', '华阴市'],
|
||||
'延安市': ['宝塔区', '安塞区', '延长县', '延川县', '志丹县', '吴起县', '甘泉县', '富县', '洛川县', '宜川县', '黄龙县', '黄陵县', '子长市'],
|
||||
'汉中市': ['汉台区', '南郑区', '城固县', '洋县', '西乡县', '勉县', '宁强县', '略阳县', '镇巴县', '留坝县', '佛坪县'],
|
||||
'榆林市': ['榆阳区', '横山区', '府谷县', '靖边县', '定边县', '绥德县', '米脂县', '佳县', '吴堡县', '清涧县', '子洲县', '神木市'],
|
||||
'安康市': ['汉滨区', '汉阴县', '石泉县', '宁陕县', '紫阳县', '岚皋县', '平利县', '镇坪县', '旬阳县', '白河县'],
|
||||
'商洛市': ['商州区', '洛南县', '丹凤县', '商南县', '山阳县', '镇安县', '柞水县']
|
||||
},
|
||||
'甘肃省': {
|
||||
'兰州市': ['城关区', '七里河区', '西固区', '安宁区', '红古区', '永登县', '皋兰县', '榆中县'],
|
||||
'嘉峪关市': ['嘉峪关市'],
|
||||
'金昌市': ['金川区', '永昌县'],
|
||||
'白银市': ['白银区', '平川区', '靖远县', '会宁县', '景泰县'],
|
||||
'天水市': ['秦州区', '麦积区', '清水县', '秦安县', '甘谷县', '武山县', '张家川回族自治县'],
|
||||
'武威市': ['凉州区', '民勤县', '古浪县', '天祝藏族自治县'],
|
||||
'张掖市': ['甘州区', '肃南裕固族自治县', '民乐县', '临泽县', '高台县', '山丹县'],
|
||||
'平凉市': ['崆峒区', '泾川县', '灵台县', '崇信县', '华亭市', '庄浪县', '静宁县'],
|
||||
'酒泉市': ['肃州区', '金塔县', '瓜州县', '肃北蒙古族自治县', '阿克塞哈萨克族自治县', '玉门市', '敦煌市'],
|
||||
'庆阳市': ['西峰区', '庆城县', '环县', '华池县', '合水县', '正宁县', '宁县', '镇原县'],
|
||||
'定西市': ['安定区', '通渭县', '陇西县', '渭源县', '临洮县', '漳县', '岷县'],
|
||||
'陇南市': ['武都区', '成县', '文县', '宕昌县', '康县', '西和县', '礼县', '徽县', '两当县'],
|
||||
'临夏回族自治州': ['临夏市', '临夏县', '康乐县', '永靖县', '广河县', '和政县', '东乡族自治县', '积石山保安族东乡族撒拉族自治县'],
|
||||
'甘南藏族自治州': ['合作市', '临潭县', '卓尼县', '舟曲县', '迭部县', '玛曲县', '碌曲县', '夏河县']
|
||||
},
|
||||
'青海省': {
|
||||
'西宁市': ['城东区', '城中区', '城西区', '城北区', '大通回族土族自治县', '湟中区', '湟源县'],
|
||||
'海东市': ['乐都区', '平安区', '民和回族土族自治县', '互助土族自治县', '化隆回族自治县', '循化撒拉族自治县'],
|
||||
'海北藏族自治州': ['门源回族自治县', '祁连县', '海晏县', '刚察县'],
|
||||
'黄南藏族自治州': ['同仁市', '尖扎县', '泽库县', '河南蒙古族自治县'],
|
||||
'海南藏族自治州': ['共和县', '同德县', '贵德县', '兴海县', '贵南县'],
|
||||
'果洛藏族自治州': ['玛沁县', '班玛县', '甘德县', '达日县', '久治县', '玛多县'],
|
||||
'玉树藏族自治州': ['玉树市', '杂多县', '称多县', '治多县', '囊谦县', '曲麻莱县'],
|
||||
'海西蒙古族藏族自治州': ['德令哈市', '格尔木市', '茫崖市', '乌兰县', '都兰县', '天峻县', '大柴旦行委']
|
||||
},
|
||||
'宁夏回族自治区': {
|
||||
'银川市': ['兴庆区', '西夏区', '金凤区', '永宁县', '贺兰县', '灵武市'],
|
||||
'石嘴山市': ['大武口区', '惠农区', '平罗县'],
|
||||
'吴忠市': ['利通区', '红寺堡区', '盐池县', '同心县', '青铜峡市'],
|
||||
'固原市': ['原州区', '西吉县', '隆德县', '泾源县', '彭阳县'],
|
||||
'中卫市': ['沙坡头区', '中宁县', '海原县']
|
||||
},
|
||||
'新疆维吾尔自治区': {
|
||||
'乌鲁木齐市': ['天山区', '沙依巴克区', '新市区', '水磨沟区', '头屯河区', '达坂城区', '米东区', '乌鲁木齐县'],
|
||||
'克拉玛依市': ['独山子区', '克拉玛依区', '白碱滩区', '乌尔禾区'],
|
||||
'吐鲁番市': ['高昌区', '鄯善县', '托克逊县'],
|
||||
'哈密市': ['伊州区', '巴里坤哈萨克自治县', '伊吾县'],
|
||||
'昌吉回族自治州': ['昌吉市', '阜康市', '呼图壁县', '玛纳斯县', '奇台县', '吉木萨尔县', '木垒哈萨克自治县'],
|
||||
'博尔塔拉蒙古自治州': ['博乐市', '阿拉山口市', '精河县', '温泉县'],
|
||||
'巴音郭楞蒙古自治州': ['库尔勒市', '轮台县', '尉犁县', '若羌县', '且末县', '焉耆回族自治县', '和静县', '和硕县', '博湖县'],
|
||||
'阿克苏地区': ['阿克苏市', '温宿县', '库车市', '沙雅县', '新和县', '拜城县', '乌什县', '阿瓦提县', '柯坪县'],
|
||||
'克孜勒苏柯尔克孜自治州': ['阿图什市', '阿克陶县', '阿合奇县', '乌恰县'],
|
||||
'喀什地区': ['喀什市', '疏附县', '疏勒县', '英吉沙县', '泽普县', '莎车县', '叶城县', '麦盖提县', '岳普湖县', '伽师县', '巴楚县', '塔什库尔干塔吉克自治县'],
|
||||
'和田地区': ['和田市', '和田县', '墨玉县', '皮山县', '洛浦县', '策勒县', '于田县', '民丰县'],
|
||||
'伊犁哈萨克自治州': ['伊宁市', '奎屯市', '霍尔果斯市', '伊宁县', '察布查尔锡伯自治县', '霍城县', '巩留县', '新源县', '昭苏县', '特克斯县', '尼勒克县'],
|
||||
'塔城地区': ['塔城市', '乌苏市', '额敏县', '沙湾市', '托里县', '裕民县', '和布克赛尔蒙古自治县'],
|
||||
'阿勒泰地区': ['阿勒泰市', '布尔津县', '富蕴县', '福海县', '哈巴河县', '青河县', '吉木乃县'],
|
||||
'石河子市': ['石河子市'],
|
||||
'阿拉尔市': ['阿拉尔市'],
|
||||
'图木舒克市': ['图木舒克市'],
|
||||
'五家渠市': ['五家渠市'],
|
||||
'北屯市': ['北屯市'],
|
||||
'铁门关市': ['铁门关市'],
|
||||
'双河市': ['双河市'],
|
||||
'可克达拉市': ['可克达拉市'],
|
||||
'昆玉市': ['昆玉市'],
|
||||
'胡杨河市': ['胡杨河市']
|
||||
},
|
||||
'香港特别行政区': {
|
||||
'香港岛': ['中西区', '湾仔区', '东区', '南区'],
|
||||
'九龙': ['油尖旺区', '深水埗区', '九龙城区', '黄大仙区', '观塘区'],
|
||||
'新界': ['北区', '大埔区', '沙田区', '西贡区', '荃湾区', '屯门区', '元朗区', '葵青区', '离岛区']
|
||||
},
|
||||
'澳门特别行政区': {
|
||||
'澳门半岛': ['花地玛堂区', '圣安多尼堂区', '大堂区', '望德堂区', '风顺堂区'],
|
||||
'氹仔': ['氹仔'],
|
||||
'路环': ['路环']
|
||||
},
|
||||
'台湾省': {
|
||||
'台北市': ['中正区', '大同区', '中山区', '松山区', '大安区', '万华区', '信义区', '士林区', '北投区', '内湖区', '南港区', '文山区'],
|
||||
'新北市': ['万里区', '金山区', '板桥区', '汐止区', '深坑区', '石碇区', '瑞芳区', '平溪区', '双溪区', '贡寮区', '新店区', '坪林区', '乌来区', '永和区', '中和区', '土城区', '三峡区', '树林区', '莺歌区', '三重区', '新庄区', '泰山区', '林口区', '芦洲区', '五股区', '八里区', '淡水区', '三芝区', '石门区'],
|
||||
'桃园市': ['中坜区', '平镇区', '龙潭区', '杨梅区', '新屋区', '观音区', '桃园区', '龟山区', '八德区', '大溪区', '复兴区', '大园区', '芦竹区'],
|
||||
'台中市': ['中区', '东区', '南区', '西区', '北区', '北屯区', '西屯区', '南屯区', '太平区', '大里区', '雾峰区', '乌日区', '丰原区', '后里区', '石冈区', '东势区', '和平区', '新社区', '潭子区', '大雅区', '神冈区', '大肚区', '沙鹿区', '龙井区', '梧栖区', '清水区', '大甲区', '外埔区', '大安区'],
|
||||
'台南市': ['中西区', '东区', '南区', '北区', '安平区', '安南区', '永康区', '归仁区', '新化区', '左镇区', '玉井区', '楠西区', '南化区', '仁德区', '关庙区', '龙崎区', '官田区', '麻豆区', '佳里区', '西港区', '七股区', '将军区', '学甲区', '北门区', '新营区', '后壁区', '白河区', '东山区', '六甲区', '下营区', '柳营区', '盐水区', '善化区', '大内区', '山上区', '新市区', '安定区'],
|
||||
'高雄市': ['新兴区', '前金区', '苓雅区', '盐埕区', '鼓山区', '旗津区', '前镇区', '三民区', '楠梓区', '小港区', '左营区', '仁武区', '大社区', '冈山区', '路竹区', '阿莲区', '田寮区', '燕巢区', '桥头区', '梓官区', '弥陀区', '永安区', '湖内区', '凤山区', '大寮区', '林园区', '鸟松区', '大树区', '旗山区', '美浓区', '六龟区', '内门区', '杉林区', '甲仙区', '桃源区', '那玛夏区', '茂林区', '茄萣区'],
|
||||
'基隆市': ['仁爱区', '信义区', '中正区', '中山区', '安乐区', '暖暖区', '七堵区'],
|
||||
'新竹市': ['东区', '北区', '香山区'],
|
||||
'嘉义市': ['东区', '西区'],
|
||||
'新竹县': ['竹北市', '湖口乡', '新丰乡', '新埔镇', '关西镇', '芎林乡', '宝山乡', '竹东镇', '五峰乡', '横山乡', '尖石乡', '北埔乡', '峨眉乡'],
|
||||
'苗栗县': ['竹南镇', '头份市', '三湾乡', '南庄乡', '狮潭乡', '后龙镇', '通霄镇', '苑里镇', '苗栗市', '造桥乡', '头屋乡', '公馆乡', '大湖乡', '泰安乡', '铜锣乡', '三义乡', '西湖乡', '卓兰镇'],
|
||||
'彰化县': ['彰化市', '芬园乡', '花坛乡', '秀水乡', '鹿港镇', '福兴乡', '线西乡', '和美镇', '伸港乡', '员林市', '社头乡', '永靖乡', '埔心乡', '溪湖镇', '大村乡', '埔盐乡', '田中镇', '北斗镇', '田尾乡', '埤头乡', '溪州乡', '竹塘乡', '二林镇', '大城乡', '芳苑乡', '二水乡'],
|
||||
'南投县': ['南投市', '中寮乡', '草屯镇', '国姓乡', '埔里镇', '仁爱乡', '名间乡', '集集镇', '水里乡', '鱼池乡', '信义乡', '竹山镇', '鹿谷乡'],
|
||||
'云林县': ['斗南镇', '大埤乡', '虎尾镇', '土库镇', '褒忠乡', '东势乡', '台西乡', '仑背乡', '麦寮乡', '斗六市', '林内乡', '古坑乡', '莿桐乡', '西螺镇', '二仑乡', '北港镇', '水林乡', '口湖乡', '四湖乡', '元长乡'],
|
||||
'嘉义县': ['番路乡', '梅山乡', '竹崎乡', '阿里山乡', '中埔乡', '大埔乡', '水上乡', '鹿草乡', '太保市', '朴子市', '东石乡', '六脚乡', '新港乡', '民雄乡', '大林镇', '溪口乡', '义竹乡', '布袋镇'],
|
||||
'屏东县': ['屏东市', '三地门乡', '雾台乡', '玛家乡', '九如乡', '里港乡', '高树乡', '盐埔乡', '长治乡', '麟洛乡', '竹田乡', '内埔乡', '万丹乡', '潮州镇', '泰武乡', '来义乡', '万峦乡', '崁顶乡', '新埤乡', '南州乡', '林边乡', '东港镇', '琉球乡', '佳冬乡', '新园乡', '枋寮乡', '枋山乡', '春日乡', '狮子乡', '车城乡', '牡丹乡', '恒春镇', '满州乡'],
|
||||
'宜兰县': ['宜兰市', '头城镇', '礁溪乡', '壮围乡', '员山乡', '罗东镇', '三星乡', '大同乡', '五结乡', '冬山乡', '苏澳镇', '南澳乡'],
|
||||
'花莲县': ['花莲市', '新城乡', '秀林乡', '吉安乡', '寿丰乡', '凤林镇', '光复乡', '丰滨乡', '瑞穗乡', '万荣乡', '玉里镇', '卓溪乡', '富里乡'],
|
||||
'台东县': ['台东市', '绿岛乡', '兰屿乡', '延平乡', '卑南乡', '鹿野乡', '关山镇', '海端乡', '池上乡', '东河乡', '成功镇', '长滨乡', '太麻里乡', '金峰乡', '大武乡', '达仁乡'],
|
||||
'澎湖县': ['马公市', '西屿乡', '望安乡', '七美乡', '白沙乡', '湖西乡'],
|
||||
'金门县': ['金沙镇', '金湖镇', '金宁乡', '金城镇', '烈屿乡', '乌坵乡'],
|
||||
'连江县': ['南竿乡', '北竿乡', '莒光乡', '东引乡']
|
||||
}
|
||||
};
|
||||
49
app/static/js/order_detail.js
Normal file
49
app/static/js/order_detail.js
Normal file
@ -0,0 +1,49 @@
|
||||
// 订单详情页面脚本
|
||||
|
||||
// 取消订单
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
109
app/static/js/orders.js
Normal file
109
app/static/js/orders.js
Normal file
@ -0,0 +1,109 @@
|
||||
// 订单页面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');
|
||||
});
|
||||
}
|
||||
}
|
||||
200
app/static/js/pay.js
Normal file
200
app/static/js/pay.js
Normal file
@ -0,0 +1,200 @@
|
||||
// 订单支付页面脚本
|
||||
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() {
|
||||
countdownTimer = setInterval(() => {
|
||||
timeLeft--;
|
||||
|
||||
const minutes = Math.floor(timeLeft / 60);
|
||||
const seconds = timeLeft % 60;
|
||||
|
||||
document.getElementById('countdown').textContent =
|
||||
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(countdownTimer);
|
||||
showAlert('订单已过期,请重新下单', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/order/list';
|
||||
}, 2000);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 开始支付
|
||||
function startPayment() {
|
||||
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
|
||||
const paymentMethod = document.querySelector('[data-payment-method]')?.dataset.paymentMethod;
|
||||
|
||||
if (!paymentSn || !paymentMethod) {
|
||||
showAlert('支付信息获取失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/payment/process', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
payment_sn: paymentSn,
|
||||
payment_method: paymentMethod
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.payment_type === 'qrcode') {
|
||||
showQRCode(data.qr_code_url);
|
||||
startStatusCheck();
|
||||
} else if (data.payment_type === 'redirect') {
|
||||
window.open(data.pay_url, '_blank');
|
||||
startStatusCheck();
|
||||
}
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('支付启动失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 显示二维码
|
||||
function showQRCode(qrUrl) {
|
||||
const qrArea = document.getElementById('qrCodeArea');
|
||||
const qrImage = document.getElementById('qrCodeImage');
|
||||
|
||||
// 这里应该使用真实的二维码生成库,现在用文本模拟
|
||||
qrImage.innerHTML = `
|
||||
<div style="width: 200px; height: 200px; margin: 0 auto; background: #f0f0f0;
|
||||
display: flex; align-items: center; justify-content: center; border: 1px solid #ddd;">
|
||||
<div style="text-align: center;">
|
||||
<i class="bi bi-qr-code display-4"></i><br>
|
||||
<small>微信支付二维码</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
qrArea.style.display = 'block';
|
||||
}
|
||||
|
||||
// 开始检查支付状态
|
||||
function startStatusCheck() {
|
||||
statusCheckTimer = setInterval(() => {
|
||||
checkPaymentStatus();
|
||||
}, 3000); // 每3秒检查一次
|
||||
}
|
||||
|
||||
// 检查支付状态
|
||||
function checkPaymentStatus() {
|
||||
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
|
||||
|
||||
if (!paymentSn) return;
|
||||
|
||||
fetch(`/payment/check_status/${paymentSn}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.status === 2) { // 支付成功
|
||||
clearInterval(statusCheckTimer);
|
||||
clearInterval(countdownTimer);
|
||||
showPaymentSuccess();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('状态检查失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示支付成功
|
||||
function showPaymentSuccess() {
|
||||
document.getElementById('paymentArea').style.display = 'none';
|
||||
document.getElementById('paymentStatus').style.display = 'block';
|
||||
|
||||
const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
|
||||
setTimeout(() => {
|
||||
window.location.href = `/order/detail/${orderId}`;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
function cancelOrder() {
|
||||
const orderId = document.querySelector('[data-order-id]')?.dataset.orderId;
|
||||
|
||||
if (!orderId) {
|
||||
showAlert('订单信息获取失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('确定要取消这个订单吗?')) {
|
||||
fetch(`/order/cancel/${orderId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('订单已取消', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/order/list';
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('取消失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟支付成功(开发测试用)
|
||||
function simulatePayment() {
|
||||
const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn;
|
||||
|
||||
if (!paymentSn) {
|
||||
showAlert('支付信息获取失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('这是测试功能,确定要模拟支付成功吗?')) {
|
||||
fetch(`/payment/simulate_success/${paymentSn}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('模拟支付成功', 'success');
|
||||
setTimeout(() => {
|
||||
showPaymentSuccess();
|
||||
}, 1000);
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('模拟支付失败', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
236
app/static/js/product_detail.js
Normal file
236
app/static/js/product_detail.js
Normal file
@ -0,0 +1,236 @@
|
||||
// 获取库存数据
|
||||
const inventoryData = JSON.parse(document.getElementById('inventoryData').textContent);
|
||||
let selectedSpecs = {};
|
||||
let currentSku = null;
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 如果只有一个SKU,自动选择
|
||||
if (inventoryData.length === 1) {
|
||||
currentSku = inventoryData[0];
|
||||
updateStockInfo();
|
||||
}
|
||||
|
||||
// 绑定规格选择事件
|
||||
document.querySelectorAll('.spec-option').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
selectSpec(this);
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化购物车数量显示
|
||||
if (typeof loadCartCount === 'function') {
|
||||
loadCartCount();
|
||||
}
|
||||
});
|
||||
|
||||
// 规格选择
|
||||
function selectSpec(button) {
|
||||
const specName = button.getAttribute('data-spec-name');
|
||||
const specValue = button.getAttribute('data-spec-value');
|
||||
|
||||
// 清除同组其他选择
|
||||
document.querySelectorAll(`[data-spec-name="${specName}"]`).forEach(btn => {
|
||||
btn.classList.remove('btn-primary');
|
||||
btn.classList.add('btn-outline-secondary');
|
||||
});
|
||||
|
||||
// 选中当前项
|
||||
button.classList.remove('btn-outline-secondary');
|
||||
button.classList.add('btn-primary');
|
||||
|
||||
// 更新选择状态
|
||||
selectedSpecs[specName] = specValue;
|
||||
|
||||
// 查找匹配的SKU
|
||||
findMatchingSku();
|
||||
}
|
||||
|
||||
// 查找匹配的SKU
|
||||
function findMatchingSku() {
|
||||
for (let sku of inventoryData) {
|
||||
if (sku.spec_combination) {
|
||||
let isMatch = true;
|
||||
for (let [specName, specValue] of Object.entries(selectedSpecs)) {
|
||||
if (sku.spec_combination[specName] !== specValue) {
|
||||
isMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isMatch && Object.keys(selectedSpecs).length === Object.keys(sku.spec_combination).length) {
|
||||
currentSku = sku;
|
||||
updateStockInfo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 未找到完全匹配的SKU
|
||||
currentSku = null;
|
||||
updateStockInfo();
|
||||
}
|
||||
|
||||
// 更新库存信息
|
||||
function updateStockInfo() {
|
||||
const stockElement = document.getElementById('stockCount');
|
||||
const priceElement = document.getElementById('currentPrice');
|
||||
const addToCartBtn = document.getElementById('addToCartBtn');
|
||||
const buyNowBtn = document.getElementById('buyNowBtn');
|
||||
const quantityInput = document.getElementById('quantity');
|
||||
|
||||
if (currentSku) {
|
||||
stockElement.textContent = currentSku.stock;
|
||||
stockElement.className = currentSku.stock > 0 ? 'text-success' : 'text-danger';
|
||||
priceElement.textContent = currentSku.final_price.toFixed(2);
|
||||
|
||||
if (currentSku.stock > 0) {
|
||||
addToCartBtn.disabled = false;
|
||||
buyNowBtn.disabled = false;
|
||||
quantityInput.max = currentSku.stock;
|
||||
} else {
|
||||
addToCartBtn.disabled = true;
|
||||
buyNowBtn.disabled = true;
|
||||
quantityInput.max = 0;
|
||||
}
|
||||
} else if (inventoryData.length > 1) {
|
||||
stockElement.textContent = '请选择规格';
|
||||
stockElement.className = 'text-warning';
|
||||
addToCartBtn.disabled = true;
|
||||
buyNowBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 数量变更
|
||||
function changeQuantity(delta) {
|
||||
const quantityInput = document.getElementById('quantity');
|
||||
let quantity = parseInt(quantityInput.value) + delta;
|
||||
|
||||
const min = parseInt(quantityInput.min) || 1;
|
||||
const max = parseInt(quantityInput.max) || 999;
|
||||
|
||||
quantity = Math.max(min, Math.min(max, quantity));
|
||||
quantityInput.value = quantity;
|
||||
}
|
||||
|
||||
// 轮播图跳转
|
||||
function goToSlide(index) {
|
||||
const carousel = new bootstrap.Carousel(document.getElementById('productImageCarousel'));
|
||||
carousel.to(index);
|
||||
}
|
||||
|
||||
// 加载购物车数量
|
||||
function loadCartCount() {
|
||||
fetch('/cart/count')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateCartBadge(data.cart_count);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading cart count:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 加入购物车
|
||||
function addToCart() {
|
||||
if (!currentSku) {
|
||||
alert('请选择商品规格');
|
||||
return;
|
||||
}
|
||||
|
||||
const quantity = parseInt(document.getElementById('quantity').value);
|
||||
if (quantity <= 0 || quantity > currentSku.stock) {
|
||||
alert('请选择正确的购买数量');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮,防止重复点击
|
||||
const addToCartBtn = document.getElementById('addToCartBtn');
|
||||
addToCartBtn.disabled = true;
|
||||
addToCartBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 添加中...';
|
||||
|
||||
// 提交到购物车
|
||||
fetch('/cart/add', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: window.productId,
|
||||
sku_code: currentSku.sku_code,
|
||||
spec_combination: Object.keys(selectedSpecs).length > 0 ? JSON.stringify(selectedSpecs) : '',
|
||||
quantity: quantity
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSuccessMessage(data.message);
|
||||
updateCartBadge(data.cart_count);
|
||||
|
||||
// 询问是否查看购物车
|
||||
setTimeout(() => {
|
||||
if (confirm('商品已添加到购物车,是否查看购物车?')) {
|
||||
window.location.href = '/cart/';
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('加入购物车失败,请稍后再试');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
addToCartBtn.disabled = false;
|
||||
addToCartBtn.innerHTML = '<i class="bi bi-cart-plus"></i> 加入购物车';
|
||||
});
|
||||
}
|
||||
|
||||
// 立即购买
|
||||
function buyNow() {
|
||||
if (!currentSku) {
|
||||
alert('请选择商品规格');
|
||||
return;
|
||||
}
|
||||
|
||||
const quantity = parseInt(document.getElementById('quantity').value);
|
||||
if (quantity <= 0 || quantity > currentSku.stock) {
|
||||
alert('请选择正确的购买数量');
|
||||
return;
|
||||
}
|
||||
|
||||
// 先添加到购物车,然后跳转到结算页面
|
||||
fetch('/cart/add', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: window.productId,
|
||||
sku_code: currentSku.sku_code,
|
||||
spec_combination: Object.keys(selectedSpecs).length > 0 ? JSON.stringify(selectedSpecs) : '',
|
||||
quantity: quantity
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 立即跳转到购物车结算
|
||||
window.location.href = '/cart/';
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('购买失败,请稍后再试');
|
||||
});
|
||||
}
|
||||
|
||||
// 收藏商品
|
||||
function addToFavorites() {
|
||||
// TODO: 实现收藏功能
|
||||
alert('收藏功能开发中...');
|
||||
}
|
||||
6
app/static/js/product_list.js
Normal file
6
app/static/js/product_list.js
Normal file
@ -0,0 +1,6 @@
|
||||
function changeSort(sortType) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('sort', sortType);
|
||||
url.searchParams.set('page', '1'); // 重置到第一页
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
343
app/static/js/profile.js
Normal file
343
app/static/js/profile.js
Normal file
@ -0,0 +1,343 @@
|
||||
// 个人中心页面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); // 每秒检查一次
|
||||
116
app/static/js/register.js
Normal file
116
app/static/js/register.js
Normal file
@ -0,0 +1,116 @@
|
||||
// 注册页面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;
|
||||
}
|
||||
});
|
||||
});
|
||||
0
app/static/uploads/.gitkeep
Normal file
0
app/static/uploads/.gitkeep
Normal file
123
app/templates/admin/base.html
Normal file
123
app/templates/admin/base.html
Normal file
@ -0,0 +1,123 @@
|
||||
<!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 == 'admin.orders' %}active{% endif %}"
|
||||
href="#">
|
||||
<i class="bi bi-receipt"></i>
|
||||
订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'admin.logs' %}active{% endif %}"
|
||||
href="{{ url_for('admin.logs') }}">
|
||||
<i class="bi bi-journal-text"></i>
|
||||
操作日志
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'admin.profile' %}active{% endif %}"
|
||||
href="{{ url_for('admin.profile') }}">
|
||||
<i class="bi bi-person-gear"></i>
|
||||
个人资料
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="admin-main">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="admin-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h4 class="mb-0">{% block page_title %}管理后台{% endblock %}</h4>
|
||||
<small class="text-muted">{% block page_description %}{% endblock %}</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="dropdown">
|
||||
<a class="btn btn-link text-decoration-none dropdown-toggle" href="#" role="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
{{ session.admin_username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin.profile') }}">
|
||||
<i class="bi bi-person-gear"></i> 个人资料
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin.logout') }}">
|
||||
<i class="bi bi-box-arrow-right"></i> 退出登录
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="admin-content">
|
||||
<!-- 消息提示 -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
326
app/templates/admin/categories.html
Normal file
326
app/templates/admin/categories.html
Normal file
@ -0,0 +1,326 @@
|
||||
{% 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 %}
|
||||
207
app/templates/admin/dashboard.html
Normal file
207
app/templates/admin/dashboard.html
Normal file
@ -0,0 +1,207 @@
|
||||
{% 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 %}
|
||||
69
app/templates/admin/login.html
Normal file
69
app/templates/admin/login.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!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>
|
||||
0
app/templates/admin/orders.html
Normal file
0
app/templates/admin/orders.html
Normal file
859
app/templates/admin/product_form.html
Normal file
859
app/templates/admin/product_form.html
Normal file
@ -0,0 +1,859 @@
|
||||
{% 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);
|
||||