我们将主要使用 Python 作为后端语言,因为它在处理数据和API请求方面非常强大和便捷,前端界面则使用 HTML + CSS + JavaScript,这是最基础和通用的组合。


纯前端静态页面(最简单)

这个方案不涉及后端服务器,直接在浏览器中运行,它使用一个模拟的、硬编码的物流数据,非常适合快速原型演示或学习前端基础。

文件结构

/logistics-query
├── index.html
├── style.css
└── script.js

index.html (页面结构)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">物流查询</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>物流信息查询</h1>
        <div class="input-area">
            <input type="text" id="tracking-number" placeholder="请输入快递单号">
            <button id="query-btn">查询</button>
        </div>
        <div id="result-area" class="hidden">
            <h2>物流轨迹</h2>
            <div id="logistics-info"></div>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

style.css (页面样式)

body {
    font-family: 'Arial', sans-serif;
    background-color: #f4f7f6;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
}
.container {
    background-color: #ffffff;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
    width: 90%;
    max-width: 600px;
    text-align: center;
}
h1 {
    color: #333;
}
.input-area {
    margin: 20px 0;
}
#tracking-number {
    width: 70%;
    padding: 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 16px;
}
#query-btn {
    width: 25%;
    padding: 12px;
    border: none;
    background-color: #007bff;
    color: white;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s;
}
#query-btn:hover {
    background-color: #0056b3;
}
#result-area {
    text-align: left;
    margin-top: 20px;
}
#result-area.hidden {
    display: none;
}
.logistics-item {
    border-left: 3px solid #007bff;
    padding-left: 15px;
    margin-bottom: 15px;
    position: relative;
}
.logistics-item::before {
    content: '';
    position: absolute;
    left: -8px;
    top: 5px;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    background-color: #007bff;
}
.time {
    font-size: 0.9em;
    color: #666;
    margin-bottom: 5px;
}
.status {
    font-weight: bold;
    color: #333;
}

script.js (页面交互逻辑)

document.addEventListener('DOMContentLoaded', () => {
    const queryBtn = document.getElementById('query-btn');
    const trackingNumberInput = document.getElementById('tracking-number');
    const resultArea = document.getElementById('result-area');
    const logisticsInfo = document.getElementById('logistics-info');
    // 模拟的物流数据
    const mockData = {
        'SF1234567890': [
            { time: '2025-10-27 10:30:00', status: '快件已签收,签收人:本人' },
            { time: '2025-10-27 09:15:00', status: '【深圳市南山区】快件已由派件员[张三]本人签收' },
            { time: '2025-10-27 08:00:00', status: '【深圳市南山区】快件正在派送途中,请保持电话畅通' },
            { time: '2025-10-26 22:30:00', status: '【深圳市南山区】快件已到达【南山营业点】,派件员[张三]电话:13800138000' },
            { time: '2025-10-26 20:15:00', status: '【广州市】快件已离开广州转运中心' },
            { time: '2025-10-26 15:00:00', status: '【广州市】快件已到达广州转运中心' },
        ],
        'YT9876543210': [
            { time: '2025-10-27 11:00:00', status: '快件已由菜鸟驿站代收' },
            { time: '2025-10-27 10:00:00', status: '【上海市浦东新区】快递员[李四]正在派送中' },
            { time: '2025-10-26 18:00:00', status: '【上海市浦东新区】快件已到达【浦东新区营业点】' },
        ]
    };
    queryBtn.addEventListener('click', () => {
        const trackingNumber = trackingNumberInput.value.trim();
        if (!trackingNumber) {
            alert('请输入快递单号!');
            return;
        }
        // 清空之前的结果
        logisticsInfo.innerHTML = '';
        // 模拟网络延迟
        resultArea.classList.add('hidden');
        setTimeout(() => {
            const data = mockData[trackingNumber];
            if (data) {
                // 将数据倒序显示,最新的在上面
                data.reverse().forEach(item => {
                    const div = document.createElement('div');
                    div.className = 'logistics-item';
                    div.innerHTML = `
                        <div class="time">${item.time}</div>
                        <div class="status">${item.status}</div>
                    `;
                    logisticsInfo.appendChild(div);
                });
                resultArea.classList.remove('hidden');
            } else {
                logisticsInfo.innerHTML = '<p>未找到相关物流信息,请检查单号是否正确。</p>';
                resultArea.classList.remove('hidden');
            }
        }, 500); // 模拟0.5秒的加载时间
    });
    // 回车键也可以触发查询
    trackingNumberInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            queryBtn.click();
        }
    });
});

后端 API + 前端调用(真实项目常用)

这个方案更接近真实世界的应用,前端通过JavaScript向后端服务器发送请求,后端再去调用真实的物流查询API(如快递100、聚合数据等),然后将结果返回给前端。

后端实现 (使用 Python + Flask)

你需要安装 Flask 库: pip install Flask

创建一个简单的后端服务 app.py

注意: 你需要先在快递100等平台注册并获取一个 API Key

# app.py
from flask import Flask, request, jsonify
import requests
import json
app = Flask(__name__)
# !!! 重要:请替换成你自己的快递100 API Key 和 Customer
KUAIDI100_API_KEY = '你的快递100APIKey'
KUAIDI100_CUSTOMER = '你的快递100客户ID'
# 模拟的物流数据(当API调用失败时使用)
MOCK_DATA = {
    'SF1234567890': [
        {'time': '2025-10-27 10:30:00', 'status': '快件已签收,签收人:本人'},
        {'time': '2025-10-27 09:15:00', 'status': '【深圳市南山区】快件已由派件员[张三]本人签收'},
        {'time': '2025-10-27 08:00:00', 'status': '【深圳市南山区】快件正在派送途中,请保持电话畅通'},
        {'time': '2025-10-26 22:30:00', 'status': '【深圳市南山区】快件已到达【南山营业点】,派件员[张三]电话:13800138000'},
        {'time': '2025-10-26 20:15:00', 'status': '【广州市】快件已离开广州转运中心'},
        {'time': '2025-10-26 15:00:00', 'status': '【广州市】快件已到达广州转运中心'},
    ],
    'YT9876543210': [
        {'time': '2025-10-27 11:00:00', 'status': '快件已由菜鸟驿站代收'},
        {'time': '2025-10-27 10:00:00', 'status': '【上海市浦东新区】快递员[李四]正在派送中'},
        {'time': '2025-10-26 18:00:00', 'status': '【上海市浦东新区】快件已到达【浦东新区营业点】'},
    ]
}
@app.route('/api/query', methods=['GET'])
def query_logistics():
    tracking_number = request.args.get('trackingNumber')
    company_code = request.args.get('company')
    if not tracking_number or not company_code:
        return jsonify({'error': '缺少参数: trackingNumber 或 company'}), 400
    # --- 调用真实API的代码 ---
    # try:
    #     url = f'http://poll.kuaidi100.com/poll/query.do'
    #     params = {
    #         'customer': KUAIDI100_CUSTOMER,
    #         'param': json.dumps({'com': company_code, 'num': tracking_number}, ensure_ascii=False),
    #     }
    #     headers = {
    #         'Authorization': 'APICode ' + KUAIDI100_API_KEY
    #     }
    #     response = requests.post(url, data=params, headers=headers)
    #     result = response.json()
    #
    #     if result.get('returnCode') == '200':
    #         # 提取物流轨迹数据
    #         tracks = result.get('data', {}).get('trails', [])
    #         formatted_tracks = []
    #         for track in tracks:
    #             formatted_tracks.append({
    #                 'time': track['time'],
    #                 'status': track['content']
    #             })
    #         return jsonify({'success': True, 'data': formatted_tracks})
    #     else:
    #         return jsonify({'success': False, 'message': result.get('message', '查询失败')}), 404
    #
    # except Exception as e:
    #     print(f"API调用失败: {e}")
    #     return jsonify({'success': False, 'message': '服务器内部错误'}), 500
    # --- 模拟API返回 ---
    # 在真实环境中,请注释掉上面的代码块,并取消注释下面的代码块
    data = MOCK_DATA.get(tracking_number)
    if data:
        return jsonify({'success': True, 'data': data})
    else:
        return jsonify({'success': False, 'message': '未找到物流信息'}), 404
if __name__ == '__main__':
    app.run(debug=True)

前端修改 (script.js)

前端代码需要修改,不再使用本地数据,而是通过 fetch API 调用我们刚刚创建的后端接口。

// script.js (修改版)
document.addEventListener('DOMContentLoaded', () => {
    const queryBtn = document.getElementById('query-btn');
    const trackingNumberInput = document.getElementById('tracking-number');
    const resultArea = document.getElementById('result-area');
    const logisticsInfo = document.getElementById('logistics-info');
    queryBtn.addEventListener('click', async () => {
        const trackingNumber = trackingNumberInput.value.trim();
        if (!trackingNumber) {
            alert('请输入快递单号!');
            return;
        }
        // 清空之前的结果
        logisticsInfo.innerHTML = '';
        resultArea.classList.add('hidden');
        // 在实际应用中,你可能需要一个下拉菜单来选择快递公司
        // 这里我们用一个硬编码的值作为示例
        const companyCode = 'SF'; // SF-顺丰, YT-圆通
        try {
            // 显示一个加载中的提示
            logisticsInfo.innerHTML = '<p>查询中,请稍候...</p>';
            resultArea.classList.remove('hidden');
            const response = await fetch(`/api/query?trackingNumber=${trackingNumber}&company=${companyCode}`);
            const result = await response.json();
            if (result.success) {
                // 将数据倒序显示,最新的在上面
                result.data.reverse().forEach(item => {
                    const div = document.createElement('div');
                    div.className = 'logistics-item';
                    div.innerHTML = `
                        <div class="time">${item.time}</div>
                        <div class="status">${item.status}</div>
                    `;
                    logisticsInfo.appendChild(div);
                });
            } else {
                logisticsInfo.innerHTML = `<p>${result.message}</p>`;
            }
        } catch (error) {
            console.error('请求失败:', error);
            logisticsInfo.innerHTML = '<p>服务器连接失败,请稍后再试。</p>';
        }
    });
    trackingNumberInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            queryBtn.click();
        }
    });
});

如何运行这个方案?

  1. 确保你已经安装了 Python 和 Flask。
  2. app.pyindex.html, style.css, script.js 放在同一个项目文件夹下。
  3. 打开终端,进入该文件夹,运行 python app.py
  4. 打开浏览器,访问 http://127.0.0.1:5000

更完整的前端实现(增加快递公司选择)

这是一个对方案二的增强版,在前端增加了一个下拉选择框,让用户可以选择快递公司。

修改 index.html

<!-- 在 input-area div 中增加一个下拉选择框 -->
<div class="input-area">
    <select id="company-select">
        <option value="">请选择快递公司</option>
        <option value="SF">顺丰速运</option>
        <option value="YT">圆通速递</option>
        <option value="STO">申通快递</option>
        <option value="YTO">中通快递</option>
        <option value="JD">京东物流</option>
    </select>
    <input type="text" id="tracking-number" placeholder="请输入快递单号">
    <button id="query-btn">查询</button>
</div>

修改 script.js

// script.js (完整版)
document.addEventListener('DOMContentLoaded', () => {
    const queryBtn = document.getElementById('query-btn');
    const trackingNumberInput = document.getElementById('tracking-number');
    const companySelect = document.getElementById('company-select');
    const resultArea = document.getElementById('result-area');
    const logisticsInfo = document.getElementById('logistics-info');
    queryBtn.addEventListener('click', async () => {
        const trackingNumber = trackingNumberInput.value.trim();
        const companyCode = companySelect.value;
        if (!trackingNumber) {
            alert('请输入快递单号!');
            return;
        }
        if (!companyCode) {
            alert('请选择快递公司!');
            return;
        }
        logisticsInfo.innerHTML = '<p>查询中,请稍候...</p>';
        resultArea.classList.remove('hidden');
        try {
            const response = await fetch(`/api/query?trackingNumber=${trackingNumber}&company=${companyCode}`);
            const result = await response.json();
            if (result.success) {
                logisticsInfo.innerHTML = ''; // 清空加载提示
                result.data.reverse().forEach(item => {
                    const div = document.createElement('div');
                    div.className = 'logistics-item';
                    div.innerHTML = `
                        <div class="time">${item.time}</div>
                        <div class="status">${item.status}</div>
                    `;
                    logisticsInfo.appendChild(div);
                });
            } else {
                logisticsInfo.innerHTML = `<p>${result.message}</p>`;
            }
        } catch (error) {
            console.error('请求失败:', error);
            logisticsInfo.innerHTML = '<p>服务器连接失败,请稍后再试。</p>';
        }
    });
    // 回车键触发查询
    trackingNumberInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            queryBtn.click();
        }
    });
});

总结与建议

方案 优点 缺点 适用场景
方案一 (纯前端) 实现简单,无需服务器,适合快速原型。 数据是硬编码的,无法查询真实物流。 学习、演示、个人静态页面。
方案二 (后端API) 结构清晰,前后端分离,可扩展性强,能接入真实数据。 需要搭建后端服务器,有服务器成本。 小型Web应用、企业内部工具。
方案三 (完整前端) 用户体验好,功能更完善。 在方案二基础上增加了前端复杂度。 对用户体验要求较高的公开Web应用。

给你的建议:

  1. 如果你是初学者,从 方案一 开始,理解HTML、CSS、JS是如何协同工作的。
  2. 如果你想做一个能用的真实项目,直接上手 方案三,并配合 方案二 的后端代码,这是目前最主流和推荐的实践方式。
  3. 关于物流API:国内的快递100、聚合数据等平台都提供免费或付费的API服务,它们能自动识别快递公司,你只需要提供单号即可,这样前端就可以省去选择快递公司的步骤,体验会更好。