扫码点餐系统

在这里插入图片描述

基于 FastAPI + HTML + SQLite3 的扫码点餐小程序及后台管理系统。

功能特性

顾客端

  • 📱 扫码点餐:扫描桌号二维码进入点餐页面
  • 🍽️ 浏览菜单:按分类查看菜品
  • 🛒 购物车:添加/删除商品,实时计算金额
  • 📝 提交订单:填写备注,一键下单

在这里插入图片描述

管理后台

  • 🔐 密码保护:后台登录密码 123456
  • 📊 数据概览:今日订单、销售额统计
  • 📋 订单管理:处理订单流程(待处理→制作中→待取餐→已完成)
  • 💰 收款管理:支持现金/微信/支付宝收款
  • 🍜 菜品管理:添加/编辑/删除菜品,上传图片
  • 🪑 桌号管理:生成二维码,下载打印

技术栈

  • 后端:FastAPI + SQLAlchemy + SQLite3
  • 前端:原生 HTML + CSS + JavaScript
  • 二维码:qrcode 库

项目结构

.
├── main.py                 # 主程序入口
├── requirements.txt        # 依赖包
├── README.md              # 项目说明
├── static/
│   └── uploads/           # 菜品图片上传目录
└── templates/
    ├── admin/
    │   ├── dashboard.html # 数据概览
    │   ├── orders.html    # 订单管理
    │   ├── menu.html      # 菜品管理
    │   └── tables.html    # 桌号管理
    └── customer/
        ├── order.html     # 点餐页面
        └── success.html   # 下单成功页

安装运行

1. 安装依赖

pip install fastapi uvicorn sqlalchemy python-multipart qrcode pillow jinja2

2. 运行项目

python main.py

3. 访问地址

  • 后台管理:http://localhost:8000/
  • 登录密码:123456
  • 顾客点餐:http://localhost:8000/order/{桌号二维码}

完整代码

main.py

from fastapi import FastAPI, Depends, HTTPException, Form, Request, File, UploadFile
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import create_engine, Column, Integer, String, Float, Boolean, DateTime, Text, ForeignKey, func
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session, relationship
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from typing import List, Optional
import qrcode
import io
import base64
import os
import uuid
import shutil

# 数据库配置
SQLALCHEMY_DATABASE_URL = "sqlite:///./ordering.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# 数据库模型
class Category(Base):
    __tablename__ = "categories"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    sort_order = Column(Integer, default=0)
    items = relationship("MenuItem", back_populates="category")

class MenuItem(Base):
    __tablename__ = "menu_items"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    description = Column(Text)
    price = Column(Float)
    image_url = Column(String)
    category_id = Column(Integer, ForeignKey("categories.id"))
    is_available = Column(Boolean, default=True)
    sort_order = Column(Integer, default=0)
    category = relationship("Category", back_populates="items")
    order_items = relationship("OrderItem", back_populates="menu_item")

class Table(Base):
    __tablename__ = "tables"
    id = Column(Integer, primary_key=True, index=True)
    table_number = Column(String, unique=True, index=True)
    qr_code = Column(String, unique=True)
    is_active = Column(Boolean, default=True)
    orders = relationship("Order", back_populates="table")

class Order(Base):
    __tablename__ = "orders"
    id = Column(Integer, primary_key=True, index=True)
    table_id = Column(Integer, ForeignKey("tables.id"))
    status = Column(String, default="pending")  # pending, preparing, ready, completed, cancelled
    payment_status = Column(String, default="unpaid")  # unpaid, paid
    payment_method = Column(String, default=None)  # cash, wechat, alipay
    total_amount = Column(Float, default=0)
    created_at = Column(DateTime, default=datetime.now)
    updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
    note = Column(Text)
    table = relationship("Table", back_populates="orders")
    items = relationship("OrderItem", back_populates="order")

class OrderItem(Base):
    __tablename__ = "order_items"
    id = Column(Integer, primary_key=True, index=True)
    order_id = Column(Integer, ForeignKey("orders.id"))
    menu_item_id = Column(Integer, ForeignKey("menu_items.id"))
    quantity = Column(Integer)
    unit_price = Column(Float)
    subtotal = Column(Float)
    order = relationship("Order", back_populates="items")
    menu_item = relationship("MenuItem", back_populates="order_items")

# 创建数据库表
Base.metadata.create_all(bind=engine)

# Pydantic模型
class CategoryCreate(BaseModel):
    name: str
    sort_order: int = 0

class CategoryResponse(CategoryCreate):
    id: int
    model_config = ConfigDict(from_attributes=True)

class MenuItemCreate(BaseModel):
    name: str
    description: str = ""
    price: float
    category_id: int
    is_available: bool = True
    sort_order: int = 0

class MenuItemResponse(MenuItemCreate):
    id: int
    image_url: Optional[str] = None
    model_config = ConfigDict(from_attributes=True)

class TableCreate(BaseModel):
    table_number: str

class TableResponse(TableCreate):
    id: int
    qr_code: str
    is_active: bool
    model_config = ConfigDict(from_attributes=True)

class OrderItemCreate(BaseModel):
    menu_item_id: int
    quantity: int

class OrderCreate(BaseModel):
    table_id: int
    items: List[OrderItemCreate]
    note: str = ""

class OrderItemResponse(BaseModel):
    id: int
    menu_item_id: Optional[int]
    menu_item_name: str
    quantity: int
    unit_price: float
    subtotal: float
    model_config = ConfigDict(from_attributes=True)

class OrderResponse(BaseModel):
    id: int
    table_id: int
    table_number: str
    status: str
    payment_status: str
    payment_method: Optional[str] = None
    total_amount: float
    created_at: datetime
    note: Optional[str] = None
    items: List[OrderItemResponse]
    model_config = ConfigDict(from_attributes=True)

# FastAPI应用
app = FastAPI(title="扫码点餐系统")

# CORS中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 静态文件和模板
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

# 后台管理密码
ADMIN_PASSWORD = "123456"

def check_auth(request: Request):
    """检查是否已登录"""
    return request.cookies.get("admin_auth") == "true"

# 数据库依赖
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# 初始化数据
def init_data():
    db = SessionLocal()
    try:
        # 检查是否已有数据
        if db.query(Category).first():
            return
        
        # 添加示例分类
        categories = [
            Category(name="热销推荐", sort_order=1),
            Category(name="主食", sort_order=2),
            Category(name="小吃", sort_order=3),
            Category(name="饮品", sort_order=4),
        ]
        db.add_all(categories)
        db.commit()
        
        # 添加示例菜品
        menu_items = [
            MenuItem(name="宫保鸡丁", description="经典川菜,鸡肉嫩滑,花生酥脆", price=38.0, category_id=1, is_available=True),
            MenuItem(name="麻婆豆腐", description="麻辣鲜香,下饭神器", price=22.0, category_id=1, is_available=True),
            MenuItem(name="红烧肉", description="肥而不腻,入口即化", price=48.0, category_id=2, is_available=True),
            MenuItem(name="扬州炒饭", description="配料丰富,香气四溢", price=28.0, category_id=2, is_available=True),
            MenuItem(name="小笼包", description="皮薄馅大,汤汁鲜美", price=18.0, category_id=3, is_available=True),
            MenuItem(name="可乐", description="冰镇可乐,清爽解渴", price=8.0, category_id=4, is_available=True),
        ]
        db.add_all(menu_items)
        
        # 添加示例桌号
        tables = [
            Table(table_number="A01", qr_code="table_a01"),
            Table(table_number="A02", qr_code="table_a02"),
            Table(table_number="B01", qr_code="table_b01"),
            Table(table_number="B02", qr_code="table_b02"),
        ]
        db.add_all(tables)
        
        db.commit()
    finally:
        db.close()

# 启动时初始化数据
@app.on_event("startup")
async def startup_event():
    init_data()

# ==================== 登录相关 ====================

@app.get("/login", response_class=HTMLResponse)
def login_page(request: Request):
    """登录页面"""
    return """
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>后台登录 - 扫码点餐系统</title>
        <style>
            * { margin: 0; padding: 0; box-sizing: border-box; }
            body {
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                min-height: 100vh;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .login-box {
                background: white;
                padding: 40px;
                border-radius: 16px;
                box-shadow: 0 10px 40px rgba(0,0,0,0.2);
                width: 90%;
                max-width: 400px;
            }
            .login-header {
                text-align: center;
                margin-bottom: 30px;
            }
            .login-header h1 {
                font-size: 28px;
                color: #333;
                margin-bottom: 10px;
            }
            .login-header p {
                color: #999;
            }
            .form-group {
                margin-bottom: 20px;
            }
            .form-group label {
                display: block;
                margin-bottom: 8px;
                color: #333;
                font-weight: 500;
            }
            .form-group input {
                width: 100%;
                padding: 12px 15px;
                border: 2px solid #e0e0e0;
                border-radius: 8px;
                font-size: 16px;
                transition: border-color 0.3s;
            }
            .form-group input:focus {
                outline: none;
                border-color: #667eea;
            }
            .btn-login {
                width: 100%;
                padding: 14px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                color: white;
                border: none;
                border-radius: 8px;
                font-size: 16px;
                font-weight: 600;
                cursor: pointer;
                transition: transform 0.2s;
            }
            .btn-login:hover {
                transform: translateY(-2px);
            }
        </style>
    </head>
    <body>
        <div class="login-box">
            <div class="login-header">
                <h1>🍽️ 点餐系统后台</h1>
                <p>请输入密码登录</p>
            </div>
            <form method="POST" action="/login">
                <div class="form-group">
                    <label>密码</label>
                    <input type="password" name="password" placeholder="请输入密码" required>
                </div>
                <button type="submit" class="btn-login">登录</button>
            </form>
        </div>
    </body>
    </html>
    """

@app.post("/login")
def login(request: Request, password: str = Form(...)):
    """登录验证"""
    if password == ADMIN_PASSWORD:
        response = RedirectResponse(url="/", status_code=302)
        response.set_cookie(key="admin_auth", value="true", httponly=True)
        return response
    else:
        return HTMLResponse(content="""
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <title>登录失败</title>
            <style>
                body {
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    background: #f5f5f5;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    min-height: 100vh;
                    margin: 0;
                }
                .error-box {
                    background: white;
                    padding: 40px;
                    border-radius: 12px;
                    text-align: center;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                }
                .error-box h2 { color: #e74c3c; margin-bottom: 15px; }
                .error-box a {
                    display: inline-block;
                    margin-top: 20px;
                    padding: 10px 20px;
                    background: #3498db;
                    color: white;
                    text-decoration: none;
                    border-radius: 6px;
                }
            </style>
        </head>
        <body>
            <div class="error-box">
                <h2>密码错误</h2>
                <p>请返回重新输入</p>
                <a href="/login">← 返回登录页</a>
            </div>
        </body>
        </html>
        """, status_code=401)

@app.get("/logout")
def logout():
    """退出登录"""
    response = RedirectResponse(url="/login", status_code=302)
    response.delete_cookie("admin_auth")
    return response

# ==================== 页面路由 ====================

@app.get("/", response_class=HTMLResponse)
def dashboard(request: Request):
    """后台首页 - 数据概览"""
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=302)
    return templates.TemplateResponse("admin/dashboard.html", {"request": request})

@app.get("/admin/orders", response_class=HTMLResponse)
def orders_page(request: Request):
    """订单管理页面"""
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=302)
    return templates.TemplateResponse("admin/orders.html", {"request": request})

@app.get("/admin/menu", response_class=HTMLResponse)
def menu_page(request: Request):
    """菜品管理页面"""
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=302)
    return templates.TemplateResponse("admin/menu.html", {"request": request})

@app.get("/admin/tables", response_class=HTMLResponse)
def tables_page(request: Request):
    """桌号管理页面"""
    if not check_auth(request):
        return RedirectResponse(url="/login", status_code=302)
    return templates.TemplateResponse("admin/tables.html", {"request": request})

@app.get("/order/{qr_code}", response_class=HTMLResponse)
def customer_order(request: Request, qr_code: str):
    """顾客点餐页面"""
    return templates.TemplateResponse("customer/order.html", {"request": request, "qr_code": qr_code})

@app.get("/order/success", response_class=HTMLResponse)
def order_success(request: Request):
    """下单成功页面"""
    return templates.TemplateResponse("customer/success.html", {"request": request})

# ==================== API接口 ====================

# 分类管理
@app.get("/api/categories", response_model=List[CategoryResponse])
def get_categories(db: Session = Depends(get_db)):
    return db.query(Category).order_by(Category.sort_order).all()

@app.post("/api/categories", response_model=CategoryResponse)
def create_category(category: CategoryCreate, db: Session = Depends(get_db)):
    db_category = Category(**category.dict())
    db.add(db_category)
    db.commit()
    db.refresh(db_category)
    return db_category

@app.put("/api/categories/{category_id}", response_model=CategoryResponse)
def update_category(category_id: int, category: CategoryCreate, db: Session = Depends(get_db)):
    db_category = db.query(Category).filter(Category.id == category_id).first()
    if not db_category:
        raise HTTPException(status_code=404, detail="分类不存在")
    for key, value in category.dict().items():
        setattr(db_category, key, value)
    db.commit()
    db.refresh(db_category)
    return db_category

@app.delete("/api/categories/{category_id}")
def delete_category(category_id: int, db: Session = Depends(get_db)):
    db_category = db.query(Category).filter(Category.id == category_id).first()
    if not db_category:
        raise HTTPException(status_code=404, detail="分类不存在")
    db.delete(db_category)
    db.commit()
    return {"message": "删除成功"}

# 菜品管理
@app.get("/api/menu-items", response_model=List[MenuItemResponse])
def get_menu_items(category_id: Optional[int] = None, db: Session = Depends(get_db)):
    query = db.query(MenuItem)
    if category_id:
        query = query.filter(MenuItem.category_id == category_id)
    return query.order_by(MenuItem.sort_order).all()

@app.post("/api/menu-items", response_model=MenuItemResponse)
def create_menu_item(item: MenuItemCreate, db: Session = Depends(get_db)):
    db_item = MenuItem(**item.dict())
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

@app.post("/api/menu-items/{item_id}/upload")
def upload_image(item_id: int, file: UploadFile = File(...), db: Session = Depends(get_db)):
    """上传菜品图片"""
    db_item = db.query(MenuItem).filter(MenuItem.id == item_id).first()
    if not db_item:
        raise HTTPException(status_code=404, detail="菜品不存在")
    
    # 确保上传目录存在
    upload_dir = "static/uploads"
    os.makedirs(upload_dir, exist_ok=True)
    
    # 生成唯一文件名
    file_ext = os.path.splitext(file.filename)[1]
    filename = f"{uuid.uuid4()}{file_ext}"
    file_path = os.path.join(upload_dir, filename)
    
    # 保存文件
    with open(file_path, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    
    # 更新数据库
    db_item.image_url = f"/static/uploads/{filename}"
    db.commit()
    
    return {"image_url": db_item.image_url}

@app.put("/api/menu-items/{item_id}", response_model=MenuItemResponse)
def update_menu_item(item_id: int, item: MenuItemCreate, db: Session = Depends(get_db)):
    db_item = db.query(MenuItem).filter(MenuItem.id == item_id).first()
    if not db_item:
        raise HTTPException(status_code=404, detail="菜品不存在")
    for key, value in item.dict().items():
        setattr(db_item, key, value)
    db.commit()
    db.refresh(db_item)
    return db_item

@app.delete("/api/menu-items/{item_id}")
def delete_menu_item(item_id: int, db: Session = Depends(get_db)):
    db_item = db.query(MenuItem).filter(MenuItem.id == item_id).first()
    if not db_item:
        raise HTTPException(status_code=404, detail="菜品不存在")
    db.delete(db_item)
    db.commit()
    return {"message": "删除成功"}

# 桌号管理
@app.get("/api/tables", response_model=List[TableResponse])
def get_tables(db: Session = Depends(get_db)):
    return db.query(Table).order_by(Table.id).all()

@app.post("/api/tables", response_model=TableResponse)
def create_table(table: TableCreate, db: Session = Depends(get_db)):
    # 生成二维码标识
    qr_code = f"table_{table.table_number.lower().replace(' ', '_')}"
    db_table = Table(table_number=table.table_number, qr_code=qr_code)
    db.add(db_table)
    db.commit()
    db.refresh(db_table)
    return db_table

@app.put("/api/tables/{table_id}")
def update_table(table_id: int, is_active: bool, db: Session = Depends(get_db)):
    db_table = db.query(Table).filter(Table.id == table_id).first()
    if not db_table:
        raise HTTPException(status_code=404, detail="桌号不存在")
    db_table.is_active = is_active
    db.commit()
    return {"message": "更新成功"}

@app.delete("/api/tables/{table_id}")
def delete_table(table_id: int, db: Session = Depends(get_db)):
    db_table = db.query(Table).filter(Table.id == table_id).first()
    if not db_table:
        raise HTTPException(status_code=404, detail="桌号不存在")
    db.delete(db_table)
    db.commit()
    return {"message": "删除成功"}

# 二维码生成
@app.get("/api/qrcode/{table_id}")
def generate_qrcode(table_id: int, request: Request, db: Session = Depends(get_db)):
    """生成桌号二维码"""
    table = db.query(Table).filter(Table.id == table_id).first()
    if not table:
        raise HTTPException(status_code=404, detail="桌号不存在")
    
    # 生成点餐链接
    order_url = f"{request.base_url}order/{table.qr_code}"
    
    # 生成二维码
    qr = qrcode.QRCode(version=1, box_size=10, border=2)
    qr.add_data(order_url)
    qr.make(fit=True)
    
    img = qr.make_image(fill_color="black", back_color="white")
    
    # 转换为base64
    buffer = io.BytesIO()
    img.save(buffer, format='PNG')
    img_str = base64.b64encode(buffer.getvalue()).decode()
    
    return {"qrcode": f"data:image/png;base64,{img_str}", "url": order_url}

# 订单管理
@app.get("/api/orders", response_model=List[OrderResponse])
def get_orders(status: Optional[str] = None, payment_status: Optional[str] = None, db: Session = Depends(get_db)):
    query = db.query(Order)
    if status:
        query = query.filter(Order.status == status)
    if payment_status:
        query = query.filter(Order.payment_status == payment_status)
    orders = query.order_by(Order.created_at.desc()).all()
    
    result = []
    for order in orders:
        order_data = {
            "id": order.id,
            "table_id": order.table_id,
            "table_number": order.table.table_number if order.table else "",
            "status": order.status,
            "payment_status": order.payment_status,
            "payment_method": order.payment_method,
            "total_amount": order.total_amount,
            "created_at": order.created_at,
            "note": order.note,
            "items": []
        }
        for item in order.items:
            order_data["items"].append({
                "id": item.id,
                "menu_item_id": item.menu_item_id,
                "menu_item_name": item.menu_item.name if item.menu_item else "",
                "quantity": item.quantity,
                "unit_price": item.unit_price,
                "subtotal": item.subtotal
            })
        result.append(order_data)
    return result

@app.post("/api/orders", response_model=OrderResponse)
def create_order(order: OrderCreate, db: Session = Depends(get_db)):
    # 计算总金额
    total_amount = 0
    order_items = []
    
    for item in order.items:
        menu_item = db.query(MenuItem).filter(MenuItem.id == item.menu_item_id).first()
        if not menu_item:
            raise HTTPException(status_code=404, detail=f"菜品ID {item.menu_item_id} 不存在")
        
        subtotal = menu_item.price * item.quantity
        total_amount += subtotal
        
        order_items.append({
            "menu_item_id": item.menu_item_id,
            "quantity": item.quantity,
            "unit_price": menu_item.price,
            "subtotal": subtotal
        })
    
    # 创建订单
    db_order = Order(
        table_id=order.table_id,
        status="pending",
        total_amount=total_amount,
        note=order.note
    )
    db.add(db_order)
    db.commit()
    db.refresh(db_order)
    
    # 创建订单项
    for item_data in order_items:
        db_order_item = OrderItem(order_id=db_order.id, **item_data)
        db.add(db_order_item)
    
    db.commit()
    db.refresh(db_order)
    
    # 返回完整订单信息
    return get_order_by_id(db_order.id, db)

def get_order_by_id(order_id: int, db: Session):
    order = db.query(Order).filter(Order.id == order_id).first()
    if not order:
        raise HTTPException(status_code=404, detail="订单不存在")
    
    order_data = {
        "id": order.id,
        "table_id": order.table_id,
        "table_number": order.table.table_number if order.table else "",
        "status": order.status,
        "payment_status": order.payment_status,
        "payment_method": order.payment_method,
        "total_amount": order.total_amount,
        "created_at": order.created_at,
        "note": order.note,
        "items": []
    }
    for item in order.items:
        order_data["items"].append({
            "id": item.id,
            "menu_item_id": item.menu_item_id,
            "menu_item_name": item.menu_item.name if item.menu_item else "",
            "quantity": item.quantity,
            "unit_price": item.unit_price,
            "subtotal": item.subtotal
        })
    return order_data

@app.get("/api/orders/{order_id}", response_model=OrderResponse)
def get_order(order_id: int, db: Session = Depends(get_db)):
    return get_order_by_id(order_id, db)

@app.put("/api/orders/{order_id}/status")
def update_order_status(order_id: int, status: str, db: Session = Depends(get_db)):
    db_order = db.query(Order).filter(Order.id == order_id).first()
    if not db_order:
        raise HTTPException(status_code=404, detail="订单不存在")
    db_order.status = status
    db_order.updated_at = datetime.now()
    db.commit()
    return {"message": "状态更新成功"}

@app.put("/api/orders/{order_id}/payment")
def update_payment(order_id: int, payment_status: str, payment_method: str, db: Session = Depends(get_db)):
    """更新订单支付状态"""
    db_order = db.query(Order).filter(Order.id == order_id).first()
    if not db_order:
        raise HTTPException(status_code=404, detail="订单不存在")
    db_order.payment_status = payment_status
    db_order.payment_method = payment_method
    db_order.updated_at = datetime.now()
    db.commit()
    return {"message": "收款成功"}

@app.get("/api/payment/stats")
def get_payment_stats(db: Session = Depends(get_db)):
    """获取收款统计"""
    from sqlalchemy import func
    
    # 今日收款统计
    today = datetime.now().date()
    today_start = datetime.combine(today, datetime.min.time())
    today_end = datetime.combine(today, datetime.max.time())
    
    # 今日已收款金额(排除已取消订单)
    today_paid = db.query(func.sum(Order.total_amount)).filter(
        Order.payment_status == "paid",
        Order.status != "cancelled",
        Order.created_at >= today_start,
        Order.created_at <= today_end
    ).scalar() or 0
    
    # 今日未收款金额(排除已取消订单)
    today_unpaid = db.query(func.sum(Order.total_amount)).filter(
        Order.payment_status == "unpaid",
        Order.status != "cancelled",
        Order.created_at >= today_start,
        Order.created_at <= today_end
    ).scalar() or 0
    
    # 各支付方式统计(排除已取消订单)
    payment_methods = db.query(
        Order.payment_method,
        func.count(Order.id),
        func.sum(Order.total_amount)
    ).filter(
        Order.payment_status == "paid",
        Order.status != "cancelled"
    ).group_by(Order.payment_method).all()
    
    return {
        "today_paid": today_paid,
        "today_unpaid": today_unpaid,
        "payment_methods": [
            {"method": m[0] or "unknown", "count": m[1], "amount": m[2]}
            for m in payment_methods
        ]
    }

@app.delete("/api/orders/{order_id}")
def delete_order(order_id: int, db: Session = Depends(get_db)):
    db_order = db.query(Order).filter(Order.id == order_id).first()
    if not db_order:
        raise HTTPException(status_code=404, detail="订单不存在")
    db.delete(db_order)
    db.commit()
    return {"message": "删除成功"}

# 扫码点餐 - 根据桌号获取信息
@app.get("/api/table/{qr_code}")
def get_table_by_qr(qr_code: str, db: Session = Depends(get_db)):
    table = db.query(Table).filter(Table.qr_code == qr_code).first()
    if not table:
        raise HTTPException(status_code=404, detail="桌号不存在")
    return {"id": table.id, "table_number": table.table_number, "is_active": table.is_active}

# 统计数据
@app.get("/api/stats")
def get_stats(db: Session = Depends(get_db)):
    from sqlalchemy import func
    
    today = datetime.now().date()
    today_start = datetime.combine(today, datetime.min.time())
    today_end = datetime.combine(today, datetime.max.time())
    
    # 今日订单数
    today_orders = db.query(Order).filter(
        Order.created_at >= today_start,
        Order.created_at <= today_end
    ).count()
    
    # 今日销售额
    today_sales = db.query(func.sum(Order.total_amount)).filter(
        Order.created_at >= today_start,
        Order.created_at <= today_end
    ).scalar() or 0
    
    # 待处理订单
    pending_orders = db.query(Order).filter(Order.status == "pending").count()
    
    # 总菜品数
    total_items = db.query(MenuItem).filter(MenuItem.is_available == True).count()
    
    # 最近订单
    recent_orders = db.query(Order).order_by(Order.created_at.desc()).limit(5).all()
    
    return {
        "today_orders": today_orders,
        "today_sales": today_sales,
        "pending_orders": pending_orders,
        "total_items": total_items,
        "recent_orders": [
            {
                "id": order.id,
                "table_number": order.table.table_number if order.table else "",
                "total_amount": order.total_amount,
                "status": order.status,
                "created_at": order.created_at.strftime("%H:%M")
            }
            for order in recent_orders
        ]
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

前端页面代码

templates/admin/dashboard.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>后台管理 - 扫码点餐系统</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f5f6fa;
        }
        .sidebar {
            position: fixed;
            left: 0;
            top: 0;
            bottom: 0;
            width: 220px;
            background: #2c3e50;
            color: white;
            padding: 20px 0;
        }
        .logo {
            text-align: center;
            padding: 0 20px 30px;
            border-bottom: 1px solid rgba(255,255,255,0.1);
        }
        .logo h2 { font-size: 20px; font-weight: 600; }
        .nav-menu { padding: 20px 0; }
        .nav-item {
            display: block;
            padding: 15px 25px;
            color: rgba(255,255,255,0.8);
            text-decoration: none;
            transition: all 0.3s;
            border-left: 3px solid transparent;
        }
        .nav-item:hover, .nav-item.active {
            background: rgba(255,255,255,0.1);
            color: white;
            border-left-color: #3498db;
        }
        .nav-item svg {
            width: 20px;
            height: 20px;
            margin-right: 10px;
            vertical-align: middle;
            fill: currentColor;
        }
        .main-content {
            margin-left: 220px;
            padding: 30px;
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 30px;
        }
        .header h1 { font-size: 24px; color: #333; }
        .btn-logout {
            padding: 8px 20px;
            background: #e74c3c;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            text-decoration: none;
        }
        .btn-logout:hover { background: #c0392b; }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        .stat-card {
            background: white;
            border-radius: 12px;
            padding: 25px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.05);
        }
        .stat-card.primary { border-left: 4px solid #3498db; }
        .stat-card.success { border-left: 4px solid #27ae60; }
        .stat-card.warning { border-left: 4px solid #f39c12; }
        .stat-card.danger { border-left: 4px solid #e74c3c; }
        .stat-title {
            font-size: 14px;
            color: #999;
            margin-bottom: 10px;
        }
        .stat-value {
            font-size: 32px;
            font-weight: bold;
            color: #333;
        }
        .section {
            background: white;
            border-radius: 12px;
            padding: 25px;
            margin-bottom: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.05);
        }
        .section h2 {
            font-size: 18px;
            color: #333;
            margin-bottom: 20px;
        }
        .order-list {
            display: flex;
            flex-direction: column;
            gap: 15px;
        }
        .order-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 15px;
            background: #f8f9fa;
            border-radius: 8px;
        }
        .order-info h4 {
            font-size: 16px;
            color: #333;
            margin-bottom: 5px;
        }
        .order-info p {
            font-size: 13px;
            color: #999;
        }
        .order-amount {
            font-size: 18px;
            font-weight: bold;
            color: #ff6b6b;
        }
        .status {
            padding: 5px 12px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: 500;
            margin-left: 15px;
        }
        .status-pending { background: #fff3e0; color: #f57c00; }
        .status-preparing { background: #e3f2fd; color: #1976d2; }
        .status-ready { background: #e8f5e9; color: #388e3c; }
        .status-completed { background: #f5f5f5; color: #999; }
    </style>
</head>
<body>
    <div class="sidebar">
        <div class="logo"><h2>🍽️ 点餐系统</h2></div>
        <nav class="nav-menu">
            <a href="/" class="nav-item active">
                <svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
                数据概览
            </a>
            <a href="/admin/orders" class="nav-item">
                <svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
                订单管理
            </a>
            <a href="/admin/menu" class="nav-item">
                <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
                菜品管理
            </a>
            <a href="/admin/tables" class="nav-item">
                <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
                桌号管理
            </a>
        </nav>
    </div>

    <div class="main-content">
        <div class="header">
            <h1>数据概览</h1>
            <a href="/logout" class="btn-logout">退出登录</a>
        </div>

        <div class="stats-grid">
            <div class="stat-card primary">
                <div class="stat-title">今日订单</div>
                <div class="stat-value" id="todayOrders">0</div>
            </div>
            <div class="stat-card success">
                <div class="stat-title">今日销售额</div>
                <div class="stat-value" id="todaySales">¥0</div>
            </div>
            <div class="stat-card warning">
                <div class="stat-title">待处理订单</div>
                <div class="stat-value" id="pendingOrders">0</div>
            </div>
            <div class="stat-card danger">
                <div class="stat-title">在售菜品</div>
                <div class="stat-value" id="totalItems">0</div>
            </div>
        </div>

        <div class="section">
            <h2>最近订单</h2>
            <div class="order-list" id="recentOrders">
                <p style="color: #999; text-align: center; padding: 40px;">加载中...</p>
            </div>
        </div>
    </div>

    <script>
        async function loadStats() {
            try {
                const res = await fetch('/api/stats');
                const data = await res.json();
                
                document.getElementById('todayOrders').textContent = data.today_orders;
                document.getElementById('todaySales').textContent = '¥' + data.today_sales.toFixed(2);
                document.getElementById('pendingOrders').textContent = data.pending_orders;
                document.getElementById('totalItems').textContent = data.total_items;
                
                const ordersHtml = data.recent_orders.map(order => {
                    const statusMap = {
                        'pending': { text: '待处理', class: 'status-pending' },
                        'preparing': { text: '制作中', class: 'status-preparing' },
                        'ready': { text: '待取餐', class: 'status-ready' },
                        'completed': { text: '已完成', class: 'status-completed' }
                    };
                    const status = statusMap[order.status] || { text: order.status, class: '' };
                    
                    return `
                        <div class="order-item">
                            <div class="order-info">
                                <h4>桌号 ${order.table_number} - 订单 #${String(order.id).padStart(4, '0')}</h4>
                                <p>${order.created_at}</p>
                            </div>
                            <div style="display: flex; align-items: center;">
                                <span class="order-amount">¥${order.total_amount.toFixed(2)}</span>
                                <span class="status ${status.class}">${status.text}</span>
                            </div>
                        </div>
                    `;
                }).join('');
                
                document.getElementById('recentOrders').innerHTML = ordersHtml || '<p style="color: #999; text-align: center; padding: 40px;">暂无订单</p>';
            } catch (err) {
                console.error('加载数据失败', err);
            }
        }
        
        loadStats();
        setInterval(loadStats, 10000);
    </script>
</body>
</html>

templates/admin/orders.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>订单管理 - 扫码点餐系统</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f5f6fa;
        }
        .sidebar {
            position: fixed;
            left: 0;
            top: 0;
            bottom: 0;
            width: 220px;
            background: #2c3e50;
            color: white;
            padding: 20px 0;
        }
        .logo {
            text-align: center;
            padding: 0 20px 30px;
            border-bottom: 1px solid rgba(255,255,255,0.1);
        }
        .logo h2 { font-size: 20px; font-weight: 600; }
        .nav-menu { padding: 20px 0; }
        .nav-item {
            display: block;
            padding: 15px 25px;
            color: rgba(255,255,255,0.8);
            text-decoration: none;
            transition: all 0.3s;
            border-left: 3px solid transparent;
        }
        .nav-item:hover, .nav-item.active {
            background: rgba(255,255,255,0.1);
            color: white;
            border-left-color: #3498db;
        }
        .nav-item svg {
            width: 20px;
            height: 20px;
            margin-right: 10px;
            vertical-align: middle;
            fill: currentColor;
        }
        .main-content {
            margin-left: 220px;
            padding: 30px;
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 30px;
        }
        .header h1 { font-size: 24px; color: #333; }
        .btn-logout {
            padding: 8px 20px;
            background: #e74c3c;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            text-decoration: none;
        }
        .btn-logout:hover { background: #c0392b; }
        
        /* 收款统计卡片 */
        .payment-stats {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
            margin-bottom: 20px;
        }
        .stat-card {
            background: white;
            border-radius: 12px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.05);
        }
        .stat-card.paid { border-left: 4px solid #27ae60; }
        .stat-card.unpaid { border-left: 4px solid #e74c3c; }
        .stat-title {
            font-size: 14px;
            color: #999;
            margin-bottom: 10px;
        }
        .stat-value {
            font-size: 28px;
            font-weight: bold;
        }
        .stat-card.paid .stat-value { color: #27ae60; }
        .stat-card.unpaid .stat-value { color: #e74c3c; }
        
        .filter-bar {
            background: white;
            border-radius: 12px;
            padding: 15px 20px;
            margin-bottom: 20px;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        .filter-btn {
            padding: 8px 16px;
            border: 1px solid #ddd;
            background: white;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.3s;
        }
        .filter-btn:hover { border-color: #3498db; color: #3498db; }
        .filter-btn.active {
            background: #3498db;
            color: white;
            border-color: #3498db;
        }
        .orders-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
            gap: 20px;
        }
        .order-card {
            background: white;
            border-radius: 12px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.05);
        }
        .order-card.paid { border: 2px solid #27ae60; }
        .order-card.unpaid { border: 2px solid #e74c3c; }
        .order-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 15px;
            border-bottom: 1px solid #f5f5f5;
        }
        .order-id {
            font-size: 16px;
            font-weight: 600;
            color: #333;
        }
        .order-time {
            font-size: 12px;
            color: #999;
        }
        .status-group {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .status {
            padding: 5px 12px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: 500;
        }
        .status-pending { background: #fff3e0; color: #f57c00; }
        .status-preparing { background: #e3f2fd; color: #1976d2; }
        .status-ready { background: #e8f5e9; color: #388e3c; }
        .status-completed { background: #f5f5f5; color: #999; }
        .status-cancelled { background: #ffebee; color: #d32f2f; }
        .status-paid { background: #e8f5e9; color: #27ae60; }
        .status-unpaid { background: #ffebee; color: #e74c3c; }
        .order-table {
            font-size: 14px;
            color: #666;
            margin-bottom: 15px;
        }
        .order-table span {
            color: #ff6b6b;
            font-weight: 600;
        }
        .order-items {
            margin-bottom: 15px;
        }
        .order-item {
            display: flex;
            justify-content: space-between;
            padding: 8px 0;
            font-size: 14px;
            border-bottom: 1px dashed #f5f5f5;
        }
        .order-item:last-child { border-bottom: none; }
        .item-name { color: #333; }
        .item-qty { color: #999; margin-left: 5px; }
        .item-price { color: #ff6b6b; font-weight: 500; }
        .order-note {
            background: #fff8e1;
            padding: 10px;
            border-radius: 6px;
            font-size: 13px;
            color: #666;
            margin-bottom: 15px;
        }
        .order-footer {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding-top: 15px;
            border-top: 1px solid #f5f5f5;
        }
        .order-total {
            font-size: 18px;
            color: #ff6b6b;
            font-weight: bold;
        }
        .order-total::before {
            content: '总计: ¥';
            font-size: 14px;
            color: #999;
            font-weight: normal;
        }
        .action-btns {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .btn {
            padding: 8px 16px;
            border-radius: 6px;
            border: none;
            cursor: pointer;
            font-size: 13px;
            transition: all 0.3s;
        }
        .btn-primary { background: #3498db; color: white; }
        .btn-primary:hover { background: #2980b9; }
        .btn-success { background: #27ae60; color: white; }
        .btn-success:hover { background: #219a52; }
        .btn-warning { background: #f39c12; color: white; }
        .btn-warning:hover { background: #e67e22; }
        .btn-danger { background: #e74c3c; color: white; }
        .btn-danger:hover { background: #c0392b; }
        .btn-pay { 
            background: linear-gradient(135deg, #11998e, #38ef7d); 
            color: white; 
            font-weight: 600;
        }
        .btn-pay:hover { opacity: 0.9; }
        .empty-state {
            text-align: center;
            padding: 60px;
            color: #999;
            grid-column: 1 / -1;
        }
        .loading {
            text-align: center;
            padding: 40px;
            color: #999;
            grid-column: 1 / -1;
        }
        .toast {
            position: fixed;
            top: 20px;
            right: 20px;
            background: #333;
            color: white;
            padding: 15px 25px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 1000;
            transform: translateX(150%);
            transition: transform 0.3s ease;
        }
        .toast.show { transform: translateX(0); }
        
        /* 收款弹窗 */
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 1001;
            align-items: center;
            justify-content: center;
        }
        .modal.active { display: flex; }
        .modal-content {
            background: white;
            border-radius: 16px;
            width: 90%;
            max-width: 400px;
            padding: 30px;
            text-align: center;
        }
        .modal-title {
            font-size: 20px;
            font-weight: 600;
            margin-bottom: 20px;
        }
        .payment-amount {
            font-size: 36px;
            color: #ff6b6b;
            font-weight: bold;
            margin-bottom: 20px;
        }
        .payment-methods {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 10px;
            margin-bottom: 20px;
        }
        .pay-method-btn {
            padding: 15px;
            border: 2px solid #ddd;
            background: white;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .pay-method-btn:hover { border-color: #3498db; }
        .pay-method-btn.selected {
            border-color: #27ae60;
            background: #e8f5e9;
        }
        .pay-method-btn span {
            display: block;
            font-size: 24px;
            margin-bottom: 5px;
        }
        .pay-method-btn label {
            font-size: 14px;
            color: #666;
        }
        .modal-actions {
            display: flex;
            gap: 10px;
        }
        .modal-actions .btn { flex: 1; padding: 12px; }
    </style>
</head>
<body>
    <div class="sidebar">
        <div class="logo"><h2>🍽️ 点餐系统</h2></div>
        <nav class="nav-menu">
            <a href="/" class="nav-item">
                <svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
                数据概览
            </a>
            <a href="/admin/orders" class="nav-item active">
                <svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
                订单管理
            </a>
            <a href="/admin/menu" class="nav-item">
                <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
                菜品管理
            </a>
            <a href="/admin/tables" class="nav-item">
                <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
                桌号管理
            </a>
        </nav>
    </div>

    <div class="main-content">
        <div class="header">
            <h1>订单管理 & 收款</h1>
            <a href="/logout" class="btn-logout">退出登录</a>
        </div>

        <!-- 收款统计 -->
        <div class="payment-stats">
            <div class="stat-card paid">
                <div class="stat-title">今日已收款</div>
                <div class="stat-value" id="todayPaid">¥0.00</div>
            </div>
            <div class="stat-card unpaid">
                <div class="stat-title">今日未收款</div>
                <div class="stat-value" id="todayUnpaid">¥0.00</div>
            </div>
        </div>

        <div class="filter-bar">
            <button class="filter-btn active" onclick="filterOrders('all')">全部订单</button>
            <button class="filter-btn" onclick="filterOrders('pending')">待处理</button>
            <button class="filter-btn" onclick="filterOrders('preparing')">制作中</button>
            <button class="filter-btn" onclick="filterOrders('ready')">待取餐</button>
            <button class="filter-btn" onclick="filterOrders('completed')">已完成</button>
            <button class="filter-btn" onclick="filterPayment('unpaid')" style="color: #e74c3c;">未收款</button>
            <button class="filter-btn" onclick="filterPayment('paid')" style="color: #27ae60;">已收款</button>
        </div>

        <div class="orders-grid" id="ordersGrid">
            <div class="loading">加载中...</div>
        </div>
    </div>

    <!-- 收款弹窗 -->
    <div class="modal" id="paymentModal">
        <div class="modal-content">
            <div class="modal-title">确认收款</div>
            <div class="payment-amount" id="paymentAmount">¥0.00</div>
            <div class="payment-methods">
                <button class="pay-method-btn" onclick="selectPaymentMethod('cash')">
                    <span>💵</span>
                    <label>现金</label>
                </button>
                <button class="pay-method-btn" onclick="selectPaymentMethod('wechat')">
                    <span>💚</span>
                    <label>微信</label>
                </button>
                <button class="pay-method-btn" onclick="selectPaymentMethod('alipay')">
                    <span>💙</span>
                    <label>支付宝</label>
                </button>
            </div>
            <div class="modal-actions">
                <button class="btn btn-secondary" onclick="closePaymentModal()">取消</button>
                <button class="btn btn-success" onclick="confirmPayment()">确认收款</button>
            </div>
        </div>
    </div>

    <div class="toast" id="toast"></div>

    <script>
        let allOrders = [];
        let currentFilter = 'all';
        let currentPaymentFilter = null;
        let selectedOrderId = null;
        let selectedPaymentMethod = null;
        
        async function loadData() {
            await Promise.all([loadOrders(), loadPaymentStats()]);
        }
        
        async function loadOrders() {
            try {
                const url = currentPaymentFilter 
                    ? `/api/orders?payment_status=${currentPaymentFilter}`
                    : '/api/orders';
                const res = await fetch(url);
                allOrders = await res.json();
                renderOrders();
            } catch (err) {
                console.error('加载订单失败', err);
            }
        }
        
        async function loadPaymentStats() {
            try {
                const res = await fetch('/api/payment/stats');
                const stats = await res.json();
                document.getElementById('todayPaid').textContent = '¥' + stats.today_paid.toFixed(2);
                document.getElementById('todayUnpaid').textContent = '¥' + stats.today_unpaid.toFixed(2);
            } catch (err) {
                console.error('加载收款统计失败', err);
            }
        }
        
        function
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐