当前位置: 首页 > article >正文

fastapi+angular实现菜鸟驿站系统

说明:我计划用fastapi+angular实现菜鸟驿站系统
userid 和stationid 暂时先写死 全部写成1 也就是用户1 驿站1 这样就可以简化流程

1.新增包裹入库 增加一个添加入库的按钮 然后填写信息 然后入库

2.新增包裹取件按钮 post 请求,弹窗填写取件码, 取件成功 需要刷新包裹状态

3.获取超时列表 比如有些包裹 严重超时 我需要查看超时包裹的信息 和位置

4.还需要取件记录表 用弹窗 每次用户取件 都必须有一条取件记录

5.还需要 新增包裹 比如入库 有一个新增按钮 可以插入包裹列表
效果图:
在这里插入图片描述

step1:sql

-- Users Table (Stores Recipient Information)
CREATE TABLE db_school.users (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    phone VARCHAR(11) NOT NULL UNIQUE,   -- Recipient's phone number
    full_name VARCHAR(50) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Stations Table (Multiple Service Points)
CREATE TABLE db_school.stations (
    station_id INT PRIMARY KEY AUTO_INCREMENT,
    station_name VARCHAR(100) NOT NULL,  -- Station name
    address VARCHAR(255) NOT NULL,       -- Physical address
    manager_id INT,                      -- Manager ID (foreign key)
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (manager_id) REFERENCES staff(staff_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Staff Table (Station Operators)
CREATE TABLE db_school.staff (
    staff_id INT PRIMARY KEY AUTO_INCREMENT,
    station_id INT NOT NULL,             -- Associated station
    full_name VARCHAR(50) NOT NULL,
    role ENUM('admin', 'operator') NOT NULL DEFAULT 'operator',
    phone VARCHAR(11) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL, -- Password hash
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (station_id) REFERENCES stations(station_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Packages Table (Core Business Data)
CREATE TABLE db_school.packages (
    tracking_no VARCHAR(20) PRIMARY KEY,  -- Tracking number (PK)
    user_id INT NOT NULL,                 -- Recipient ID
    station_id INT NOT NULL,             -- Current station
    carrier VARCHAR(50) NOT NULL,        -- Logistics company
    status ENUM('pending', 'picked_up', 'returned') DEFAULT 'pending',
    check_in_time DATETIME NOT NULL,     -- Storage timestamp
    pickup_code CHAR(6) NOT NULL,        -- Pickup verification code
    expire_time DATETIME,                 -- Latest pickup deadline
    shelf_location VARCHAR(20),          -- Storage position (optional)
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (station_id) REFERENCES stations(station_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Pickup Records Table
CREATE TABLE db_school.pickup_records (
    record_id INT PRIMARY KEY AUTO_INCREMENT,
    tracking_no VARCHAR(20) NOT NULL,
    user_id INT NOT NULL,
    pickup_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    operator_id INT NOT NULL,            -- Operator ID
    FOREIGN KEY (tracking_no) REFERENCES packages(tracking_no),
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (operator_id) REFERENCES staff(staff_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Notifications Table
CREATE TABLE db_school.notifications (
    notification_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    tracking_no VARCHAR(20) NOT NULL,
    content TEXT NOT NULL,               -- Notification message
    sent_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    is_read TINYINT DEFAULT 0,          -- 0=unread, 1=read
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (tracking_no) REFERENCES packages(tracking_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Sample Data Insertions
INSERT INTO users (phone, full_name) VALUES
('13800138001', 'Zhang San'),
('13800138002', 'Li Si'),
('13800138003', 'Wang Wu');

INSERT INTO stations (station_name, address) VALUES
('South Campus Station', '1 Keji Road'),
('North Campus Station', '88 Chuangxin Road');

INSERT INTO staff (station_id, full_name, role, phone, password_hash) VALUES
(1, 'Station Admin A', 'admin', '13900139001', '$2a$10$N7h/Zq'),
(1, 'Operator A1', 'operator', '13900139002', '$2a$10$N7h/Zq');

UPDATE stations SET manager_id = 1 WHERE station_id = 1;

INSERT INTO packages VALUES
('YT001', 1, 1, 'YTO', 'pending', NOW(), 'A1B2C3', NOW() + INTERVAL 7 DAY, 'A3'),
('STO002', 2, 1, 'STO', 'picked_up', NOW(), 'D4E5F6', NOW() + INTERVAL 7 DAY, 'B2');

INSERT INTO pickup_records (tracking_no, user_id, operator_id) VALUES
('STO002', 2, 2);

INSERT INTO notifications (user_id, tracking_no, content) VALUES
(1, 'YT001', 'Your package has arrived at South Campus Station. Pickup code: A1B2C3');

step2:fastapi

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pymysql import MySQLError
import pymysql.cursors
from datetime import datetime, timedelta
import secrets
from typing import Optional
from pydantic import BaseModel
app = FastAPI()

# CORS配置
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:4200"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 数据库配置
DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',
    'password': '123456',
    'db': 'db_school',
    'charset': 'utf8mb4',
    'cursorclass': pymysql.cursors.DictCursor
}


# 数据库工具函数
def db_operation(query: str, params=None, fetch: bool = False):
    connection = pymysql.connect(**DB_CONFIG)
    try:
        with connection.cursor() as cursor:
            cursor.execute(query, params or ())
            if fetch:
                result = cursor.fetchall()
            else:
                result = cursor.lastrowid
            connection.commit()
            return result
    except MySQLError as e:
        connection.rollback()
        raise HTTPException(status_code=400, detail=f"Database error: {e}")
    finally:
        connection.close()

# 新增请求体模型
class PackageCreate(BaseModel):
    tracking_no: str
    user_id: int
    station_id: int
    carrier: str
    shelf_location: Optional[str] = None

class PickupRequest(BaseModel):
    tracking_no: str
    pickup_code: str
    operator_id: int

# ---------- 基础接口 ----------
@app.post("/packages/")
async def create_package(package: PackageCreate):
    """包裹入库接口"""
    # 生成6位取件码
    pickup_code = f"{secrets.randbelow(999999):06d}"
    expire_time = datetime.now() + timedelta(days=3)

    query = """
    INSERT INTO packages 
    (tracking_no, user_id, station_id, carrier, check_in_time, 
     pickup_code, expire_time, shelf_location)
    VALUES (%s, %s, %s, %s, NOW(), %s, %s, %s)
    """
    params = (
        package.tracking_no,
        package.user_id,
        package.station_id,
        package.carrier,
        pickup_code,
        expire_time,
        package.shelf_location
    )

    try:
        db_operation(query, params)
        # 创建通知记录
        noti_query = """
        INSERT INTO notifications 
        (user_id, tracking_no, content)
        VALUES (%s, %s, %s)
        """
        content = f"您的包裹已到达驿站,取件码:{pickup_code}"
        db_operation(noti_query, (package.user_id, package.tracking_no, content))
        return {"code": pickup_code, "expire_time": expire_time}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))


@app.get("/users/{user_id}/packages")
async def get_user_packages(user_id: int, status: Optional[str] = None):
    """查询用户包裹"""
    base_query = """
    SELECT p.*, s.station_name, u.phone 
    FROM packages p
    JOIN stations s ON p.station_id = s.station_id
    JOIN users u ON p.user_id = u.user_id
    WHERE p.user_id = %s
    """
    params = [user_id]

    if status:
        base_query += " AND p.status = %s"
        params.append(status)

    data = db_operation(base_query, params, fetch=True)
    return {"data": data}


@app.post("/packages/pickup")
async def pickup_package(request: PickupRequest):
    """包裹取件接口"""
    # 验证取件码
    check_query = """
    SELECT user_id, station_id 
    FROM packages 
    WHERE tracking_no = %s AND pickup_code = %s AND status = 'pending'
    """
    package = db_operation(check_query, (request.tracking_no, request.pickup_code), fetch=True)
    if not package:
        raise HTTPException(status_code=404, detail="取件码无效或包裹已取走")

    # 更新包裹状态
    update_query = """
    UPDATE packages 
    SET status = 'picked_up' 
    WHERE tracking_no = %s
    """
    db_operation(update_query, (request.tracking_no,))

    # 创建取件记录
    record_query = """
    INSERT INTO pickup_records 
    (tracking_no, user_id, operator_id)
    VALUES (%s, %s, %s)
    """
    db_operation(record_query,
                 (request.tracking_no, package[0]['user_id'], request.operator_id))

    return {"message": "取件成功"}


# ---------- 管理接口 ----------
@app.get("/stations/{station_id}/daily-stats")
async def get_daily_stats(station_id: int):
    """驿站当日统计"""
    query = """
    SELECT 
        COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending_count,
        COUNT(CASE WHEN status = 'picked_up' THEN 1 END) AS picked_count,
        COUNT(CASE WHEN status = 'returned' THEN 1 END) AS returned_count
    FROM packages
    WHERE station_id = %s 
      AND DATE(check_in_time) = CURDATE()
    """
    data = db_operation(query, (station_id,), fetch=True)
    return data[0] if data else {}


@app.get("/stations/{station_id}/expired-packages")
async def get_expired_packages(station_id: int):
    """获取超期未取包裹"""
    query = """
    SELECT tracking_no, user_id, check_in_time 
    FROM packages
    WHERE station_id = %s 
      AND status = 'pending' 
      AND expire_time < NOW()
    """
    return {"data": db_operation(query, (station_id,), fetch=True)}


# ---------- 通知接口 ----------
@app.get("/users/{user_id}/notifications")
async def get_unread_notifications(user_id: int):
    """获取用户未读通知"""
    query = """
    SELECT * FROM notifications 
    WHERE user_id = %s AND is_read = 0
    ORDER BY sent_time DESC
    """
    return {"data": db_operation(query, (user_id,), fetch=True)}


@app.post("/notifications/{notification_id}/read")
async def mark_as_read(notification_id: int):
    """标记通知已读"""
    db_operation("""
    UPDATE notifications 
    SET is_read = 1 
    WHERE notification_id = %s
    """, (notification_id,))
    return {"message": "标记成功"}


# ---------- 取件记录接口 ----------
from typing import Optional


@app.get("/stations/{station_id}/detailed-pickup-records")
async def get_detailed_pickup_records(
        station_id: int,
        page: int = 1,
        size: int = 20
):
    """获取驿站详细取件记录(分页版)"""
    offset = (page - 1) * size

    query = """
    SELECT
        pr.tracking_no,
        pr.operator_id,
        pr.pickup_time,
        u.phone AS user_phone,
        p.carrier,
        p.station_id,
        p.shelf_location
    FROM pickup_records pr
    JOIN packages p ON pr.tracking_no = p.tracking_no
    JOIN users u ON pr.user_id = u.user_id
    WHERE p.station_id = %s
    ORDER BY pr.pickup_time DESC
    LIMIT %s OFFSET %s
    """

    try:
        # 获取分页数据
        records = db_operation(query, (station_id, size, offset), fetch=True)

        # 获取总数
        count_query = """
        SELECT COUNT(*) AS total 
        FROM pickup_records pr
        JOIN packages p ON pr.tracking_no = p.tracking_no
        WHERE p.station_id = %s
        """
        total = db_operation(count_query, (station_id,), fetch=True)[0]['total']

        return {
            "data": records,
            "pagination": {
                "total": total,
                "page": page,
                "size": size
            }
        }

    except MySQLError as e:
        raise HTTPException(
            status_code=500,
            detail=f"数据库查询失败: {e.args[1]}"
        )

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

step3:C:\Users\wangrusheng\WebstormProjects\untitled4\src\app\services\station.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StationService {
  private apiUrl = 'http://localhost:8000';

  constructor(private http: HttpClient) { }

  getUserPackages(userId: number, status?: string): Observable<any> {
    const params: any = { user_id: userId };
    if (status) params.status = status;
    return this.http.get(`${this.apiUrl}/users/${userId}/packages`, { params });
  }

  createPackage(
    tracking_no: string,       // ✅ 统一使用下划线
    user_id: number,            // ✅
    station_id: number,        // ✅
    carrier: string,
    shelf_location?: string
  ) {
    return this.http.post(`${this.apiUrl}/packages/`, {
      tracking_no,             // ✅ ES6 简写语法
      user_id,
      station_id,
      carrier,
      shelf_location
    });
  }


  pickupPackage(tracking_no: string, pickup_code: string, operator_id: number) {
    console.log("tracking_no",tracking_no);
    console.log("pickup_code",pickup_code);
    console.log("operator_id",operator_id);
    return this.http.post(`${this.apiUrl}/packages/pickup`, {
      tracking_no,
      pickup_code,
      operator_id
    }, {
      headers: { 'Content-Type': 'application/json' }  // ✅ 明确指定请求头
    });
  }

  // 新增方法
  getExpiredPackages(stationId: number): Observable<any> {
    return this.http.get(`${this.apiUrl}/stations/${stationId}/expired-packages`);
  }


  getDetailedPickupRecords(stationId: number): Observable<any> {
    return this.http.get(`${this.apiUrl}/stations/${stationId}/detailed-pickup-records`);
  }

}

step4:C:\Users\wangrusheng\WebstormProjects\untitled4\src\app\station-list\status-translate.pipe.ts

// status-translate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'statusTranslate'
})
export class StatusTranslatePipe implements PipeTransform {
  transform(value: string): string {
    const statusMap: { [key: string]: string } = {
      pending: '待取件',
      picked_up: '已取件',
      returned: '已退回'
    };
    return statusMap[value] || value;
  }
}

step5:C:\Users\wangrusheng\WebstormProjects\untitled4\src\app\station-list\station.model.ts

// package.model.ts
export interface Station {
  tracking_no: string;
  carrier: string;
  status: 'pending' | 'picked_up' | 'returned';
  pickup_code: string;
  shelf_location: string;
  check_in_time: string;
  station_name: string;
  phone: string;
}

step6:C:\Users\wangrusheng\WebstormProjects\untitled4\src\app\station-list\station-list.component.ts

import { Component, OnInit } from '@angular/core';
import { Station } from './station.model';
import { StationService } from '../services/station.service';
import {DatePipe, NgClass, NgForOf, NgIf} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {StatusTranslatePipe} from './status-translate.pipe';


@Component({
  selector: 'app-package',
  templateUrl: './station-list.component.html',
  imports: [
    NgForOf,
    NgClass,
    FormsModule,
    StatusTranslatePipe,
    DatePipe,
    NgIf
  ],
  styleUrls: ['./station-list.component.css']
})
export class StationComponent implements OnInit {
  packages: Station[] = [];
  selectedStatus = '';
  currentUserId = 1; // 从认证服务获取实际用户ID

  // 新增表单相关属性
  showAddForm = false;
  newPackage = {
    tracking_no: '',
    user_id: 1, // 默认当前用户
    station_id: 1, // 默认驿站ID
    carrier: '',
    shelf_location: ''
  };


  // 新增取件相关状态
  showPickupModal = false;
  currentPickupTrackingNo = '';
  pickupCodeInput = '';
  operatorId = 1; // 假设当前操作员ID为1

  // 新增状态
  showExpiredModal = false;
  expiredPackages: any[] = [];
  currentStationId = 1; // 默认当前驿站ID

  // 新增状态
  showPickupRecordsModal = false;
  pickupRecords: any[] = [];


  constructor(private stationService: StationService) {}

  // 新增表单方法
  openAddForm() {
    this.showAddForm = true;
    this.resetForm();
  }

  ngOnInit() {
    this.loadPackages();
  }

  loadPackages() {
    this.stationService.getUserPackages(this.currentUserId, this.selectedStatus)
      .subscribe((res: any) => {
        this.packages = res.data;
      });
  }

  onStatusChange() {
    this.loadPackages();
  }

  // 新增关闭弹窗方法
  closeAddForm() {
    this.showAddForm = false;
    this.resetForm();
  }

 submitPackage() {
    this.stationService.createPackage(
      this.newPackage.tracking_no,
      this.newPackage.user_id,
      this.newPackage.station_id,
      this.newPackage.carrier,
      this.newPackage.shelf_location
    ).subscribe({
      next: (res: any) => {
        alert(`入库成功!\n取件码:${res.code}\n过期时间:${res.expire_time}`);
        this.closeAddForm();
        this.loadPackages();
      },
      error: (err) => {
        alert(`入库失败:${err.error.detail}`);
      }
    });
  }


  private resetForm() {
    this.newPackage = {
      tracking_no: '',
      user_id: 1,
      station_id: 1,
      carrier: '',
      shelf_location: ''
    };
  }

  // 打开取件弹窗
  openPickupModal(trackingNo: string) {
    this.currentPickupTrackingNo = trackingNo;
    this.showPickupModal = true;
    this.pickupCodeInput = ''; // 清空上次输入
  }

  // 提交取件
  confirmPickup() {
    this.stationService.pickupPackage(
      this.currentPickupTrackingNo,
      this.pickupCodeInput,
      this.operatorId
    ).subscribe({
      next: () => {
        alert('取件成功!');
        this.showPickupModal = false;
        this.loadPackages(); // 刷新包裹列表
        this.loadPickupRecords(); // 刷新取件记录
      },
      error: (err) => {
        const msg = err.status === 404 ? '取件码错误或包裹已取走' : '取件失败';
        alert(msg);
      }
    });
  }

  // 打开超期包裹弹窗
  openExpiredModal() {
    this.loadExpiredPackages();
    this.showExpiredModal = true;
  }

  // 加载超期包裹
  loadExpiredPackages() {
    this.stationService.getExpiredPackages(this.currentStationId)
      .subscribe((res: any) => {
        this.expiredPackages = res.data;
      });
  }

  // 关闭弹窗
  closeExpiredModal() {
    this.showExpiredModal = false;
  }

  // 打开取件记录弹窗
  openPickupRecords() {
    this.loadPickupRecords();
    this.showPickupRecordsModal = true;
  }

  // station-list.component.ts
  loadPickupRecords() {
    this.stationService.getDetailedPickupRecords(1).subscribe({
      next: (res) => this.pickupRecords = res.data,
      error: (err) => console.error('获取记录失败', err)
    });
  }



}

step7:C:\Users\wangrusheng\WebstormProjects\untitled4\src\app\station-list\station-list.component.html

<div class="container">
  <!-- 新增入库按钮 -->
  <div class="header-section">
    <h2>包裹管理</h2>
    <button class="btn-info" (click)="openAddForm()">+ 新增入库</button>
    <button class="btn-info" (click)="openExpiredModal()">查看超期包裹</button>
    <button class="btn-info" (click)="openPickupRecords()">取件记录</button>
  </div>
  <div class="filter-section">
    <select [(ngModel)]="selectedStatus" (change)="onStatusChange()">
      <option value="">全部状态</option>
      <option value="pending">待取件</option>
      <option value="picked_up">已取件</option>
      <option value="returned">已退回</option>
    </select>
  </div>

  <div class="package-list">
    <h2>我的包裹</h2>
    <table>
      <thead>
      <tr>
        <th>快递单号</th>
        <th>物流公司</th>
        <th>状态</th>
        <th>取件码</th>
        <th>货架位置</th>
        <th>入库时间</th>
        <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr *ngFor="let pkg of packages">
        <td>{{ pkg.tracking_no }}</td>
        <td>{{ pkg.carrier }}</td>

        <td [ngClass]="{
            'status-pending': pkg.status === 'pending',
            'status-picked': pkg.status === 'picked_up',
            'status-returned': pkg.status === 'returned'
          }">{{ pkg.status | statusTranslate }}</td>
        <td>{{ pkg.pickup_code }}</td>
        <td>{{ pkg.shelf_location }}</td>
        <td>{{ pkg.check_in_time | date: 'yyyy-MM-dd HH:mm' }}</td>

        <td>
          <button *ngIf="pkg.status === 'pending'"
                  (click)="openPickupModal(pkg.tracking_no)">
            取件
          </button>
        </td>
      </tr>
      </tbody>
    </table>
  </div>

  <!-- 弹窗背景层 -->
  <div *ngIf="showAddForm" class="modal-backdrop">
    <div class="comment-modal">
      <h3>包裹入库 <span class="required-tip">(* 为必填项)</span></h3>

      <!-- 表单内容 -->
      <div class="form-content">
        <div class="form-row">
          <label>快递单号 *</label>
          <input type="text" [(ngModel)]="newPackage.tracking_no" name="trackingNo" required>
        </div>

        <div class="form-row">
          <label>物流公司 *</label>
          <input type="text" [(ngModel)]="newPackage.carrier" name="carrier" required>
        </div>

        <div class="form-row">
          <label>驿站编号 *</label>
          <input type="number" [(ngModel)]="newPackage.station_id" name="stationId" required>
        </div>

        <div class="form-row">
          <label>货架位置</label>
          <input type="text" [(ngModel)]="newPackage.shelf_location" name="shelfLocation">
        </div>
      </div>

      <!-- 操作按钮 -->
      <div class="modal-actions">
        <button class="btn-cancel" (click)="closeAddForm()">取消</button>
        <button class="btn-submit" (click)="submitPackage()">提交入库</button>
      </div>
    </div>
  </div>


  <!-- 取件弹窗 -->
  <div *ngIf="showPickupModal" class="modal-backdrop">
    <div class="comment-modal">
      <h3>包裹取件 (单号: {{ currentPickupTrackingNo }})</h3>

      <div class="form-content">
        <div class="form-row">
          <label>取件码 *</label>
          <input type="text" [(ngModel)]="pickupCodeInput" required>
        </div>
      </div>

      <div class="modal-actions">
        <button class="btn-cancel" (click)="showPickupModal = false">取消</button>
        <button class="btn-submit"
                [disabled]="!pickupCodeInput"
                (click)="confirmPickup()">
          确认取件
        </button>
      </div>
    </div>
  </div>

  <!-- 超期包裹弹窗 -->
  <div *ngIf="showExpiredModal" class="modal-backdrop">
    <div class="comment-modal">
      <h3>超期未取包裹 <span class="tip">(驿站ID: {{ currentStationId }})</span></h3>

      <table class="expired-table">
        <thead>
        <tr>
          <th>快递单号</th>
          <th>物流公司</th>
          <th>货架位置</th>
          <th>入库时间</th>
          <th>过期时间</th>
        </tr>
        </thead>
        <tbody>
        <tr *ngFor="let pkg of expiredPackages">
          <td>{{ pkg.tracking_no }}</td>
          <td>{{ pkg.carrier }}</td>
          <td class="warning">{{ pkg.shelf_location || '未指定' }}</td>
          <td>{{ pkg.check_in_time | date: 'yyyy-MM-dd HH:mm' }}</td>
          <td class="warning">{{ pkg.expire_time | date: 'yyyy-MM-dd HH:mm' }}</td>
        </tr>
        </tbody>
      </table>

      <div class="modal-actions">
        <button class="btn-cancel" (click)="closeExpiredModal()">关闭</button>
      </div>
    </div>
  </div>

  <!-- 取件记录弹窗 -->
  <div *ngIf="showPickupRecordsModal" class="modal-backdrop">
    <div class="comment-modals" style="max-width: 1000px;">
      <h3>取件记录</h3>

      <table class="record-table">
        <thead>
        <tr>
          <th>快递单号</th>
          <th>操作员ID</th>
          <th>取件时间</th>
          <!-- 新增字段 -->
          <th>用户电话</th>
          <th>物流公司</th>
          <th>驿站ID</th>
          <th>货架位置</th>
        </tr>
        </thead>
        <tbody>
        <tr *ngFor="let record of pickupRecords">
          <td>{{ record.tracking_no }}</td>
          <td>{{ record.operator_id }}</td>
          <td>{{ record.pickup_time | date: 'yyyy-MM-dd HH:mm' }}</td>
          <!-- 新增数据绑定 -->
          <td>{{ record.user_phone }}</td>
          <td>{{ record.carrier }}</td>
          <td>{{ record.station_id }}</td>
          <td>{{ record.shelf_location || '未指定' }}</td>
        </tr>
        </tbody>
      </table>

      <div class="modal-actions">
        <button class="btn-cancel" (click)="showPickupRecordsModal = false">关闭</button>
      </div>
    </div>
  </div>
</div>




step8:C:\Users\wangrusheng\WebstormProjects\untitled4\src\app\station-list\station-list.component.css

.container {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.filter-section {
  margin-bottom: 20px;
}

select {
  padding: 8px 12px;
  border-radius: 4px;
  border: 1px solid #ccc;
}

.package-list table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 15px;
}

th, td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #ddd;
}

th {
  background-color: #f5f5f5;
}

.status-pending {
  color: #ff9800;
  font-weight: 500;
}

.status-picked {
  color: #4caf50;
}

.status-returned {
  color: #9e9e9e;
}


/* 弹窗样式 */
.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.comment-modal {
  background: white;
  border-radius: 8px;
  padding: 20px;
  width: 500px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

/* 修改弹窗容器样式 */
.comment-modals {
  background: white;
  padding: 20px;
  width: 95vw;          /* 占据视口宽度的95% */
  max-width: 1200px;     /* 最大宽度限制 */
  min-width: 800px;     /* 最小宽度保证 */
  overflow-x: auto;     /* 允许横向滚动 */
}

/* 表格容器添加滚动 */
.record-table-container {
  max-height: 70vh;     /* 限制高度 */
  overflow-x: auto;     /* 横向滚动 */
}


.comment-modal h3 {
  margin: 0 0 20px 0;
  color: #333;
  border-bottom: 1px solid #eee;
  padding-bottom: 10px;
}

.required-tip {
  color: #666;
  font-size: 0.9em;
}

.form-content {
  margin-bottom: 20px;
}

.form-row {
  margin-bottom: 15px;
}

.form-row label {
  display: block;
  margin-bottom: 5px;
  color: #444;
}

.form-row input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.btn-submit, .btn-cancel {
  padding: 8px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-submit {
  background: #1890ff;
  color: white;
}

.btn-cancel {
  background: #f5222d;
  color: white;
}

.btn-submit:hover {
  background: #40a9ff;
}

.btn-cancel:hover {
  background: #ff4d4f;
}

/* 操作按钮容器 */
.action-buttons {
  display: flex;
  gap: 10px;
}

/* 警告按钮样式 */
.btn-warning {
  background: #ff4d4f;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 3px;
}

/* 超期表格样式 */
.expired-table {
  width: 100%;
  margin: 15px 0;
  border-collapse: collapse;
}

.expired-table th,
.expired-table td {
  padding: 8px;
  border: 1px solid #f0f0f0;
}

.expired-table th {
  background: #fafafa;
}

.warning {
  color: #ff4d4f;
  font-weight: 500;
}


/* 信息按钮样式 */
.btn-info {
  background: #1890ff;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 10px;
}

/* 记录表格样式 */
.record-table {
  width: 100%;
  margin: 15px 0;
  border-collapse: collapse;
}

.record-table th,
.record-table td {
  padding: 8px;
  border: 1px solid #f0f0f0;
  text-align: left;
}

.record-table th {
  background: #fafafa;
}

/* 调整表格列宽和文本对齐 */
.record-table {
  width: 100%;
  table-layout: auto; /* 自动列宽 */
}

.record-table th,
.record-table td {
  padding: 8px 12px;
  border-bottom: 1px solid #f0f0f0;
  text-align: left; /* 左对齐更易阅读 */
}

.record-table th:nth-child(1) { width: 15%; }  /* 快递单号 */
.record-table th:nth-child(2) { width: 10%; }  /* 操作员ID */
.record-table th:nth-child(3) { width: 15%; }  /* 取件时间 */
.record-table th:nth-child(4) { width: 15%; }  /* 用户电话 */
.record-table th:nth-child(5) { width: 15%; }  /* 物流公司 */
.record-table th:nth-child(6) { width: 10%; }  /* 驿站ID */
.record-table th:nth-child(7) { width: 10%; }  /* 货架位置 */

end


http://www.kler.cn/a/595508.html

相关文章:

  • Bell-1量子计算机分析:开启量子计算2.0时代的创新引擎
  • 音视频系列——Websockets接口封装为Http接口
  • 《沉思录》
  • 计算机四级 - 数据库原理(操作系统部分)- 第3章「进程线程模型」
  • 边缘计算革命:重构软件架构的范式与未来
  • 第P8周:YOLOv5-C3模块实现
  • w264民族婚纱预定系统
  • Hive的分区分桶和数据抽样
  • 实用工具-Stirling-PDF
  • 基于RAG(Retrieval Augmented Generation)架构的简单问答系统的Python实现示例
  • 网络安全学习-博客文序记
  • PbootCMS经常被黑怎么办?
  • Java 中 PriorityQueue 的底层数据结构及相关分析
  • java 并发编程之线程池与自定义线程池介绍
  • 如何 使用 Docker 部署 ollama 和 MaxKB
  • 如何在 Linux 系统中部署 FTP 服务器:从基础配置到安全优化
  • Linux系统-TCPDump流量
  • C++Primer学习(13.1 拷贝、赋值与销毁)
  • 通过Knife4j在gateway中对swagger文档进行聚合
  • 流程优化的可配置文档结构化系统设计