导航菜单

在这一节中,我们将实现一个完整的快递查询功能。主要包括以下内容:

  1. 设计和实现快递查询页面
  2. 创建快递信息展示组件
  3. 集成聚合数据快递 API
  4. 实现查询历史记录功能

准备工作

  1. 注册聚合数据账号并获取 API Key:

    • 访问 聚合数据官网
    • 注册账号并实名认证
    • 申请全国快递物流查询 API
    • 获取 API Key
  2. 更新环境变量文件:

# .env
JUHE_EXPRESS_KEY=your_api_key_here

快递信息展示组件

首先,我们创建一个可复用的快递信息展示组件:

<!-- components/ExpressCard.vue -->
<script setup lang="ts">
import { computed } from 'vue';

// 定义组件属性
interface Props {
    info: {
        company: string;
        number: string;
        list: Array<{
            datetime: string;
            remark: string;
            zone: string;
        }>;
        status: string;
        isComplete: boolean;
    };
}

const props = defineProps<Props>();

// 根据快递状态选择图标和颜色
const statusInfo = computed(() => {
    const status = props.info.status;
    if (status === '已签收') {
        return {
            icon: '✅',
            color: 'text-success',
        };
    }
    if (status === '运输中') {
        return {
            icon: '🚚',
            color: 'text-info',
        };
    }
    return {
        icon: '📦',
        color: 'text-warning',
    };
});
</script>

<template>
    <div class="card w-full bg-base-200 shadow-xl">
        <div class="card-body">
            <h2 class="card-title text-2xl flex justify-between">
                <span>{{ info.company }}</span>
                <span :class="statusInfo.color"> {{ statusInfo.icon }} {{ info.status }} </span>
            </h2>
            <p class="text-lg mb-4">运单号:{{ info.number }}</p>

            <!-- 物流信息时间线 -->
            <ul class="timeline timeline-vertical">
                <li v-for="(item, index) in info.list" :key="index" class="timeline-item">
                    <div class="timeline-middle">
                        <svg
                            xmlns="http://www.w3.org/2000/svg"
                            viewBox="0 0 20 20"
                            fill="currentColor"
                            class="w-5 h-5"
                        >
                            <path
                                fill-rule="evenodd"
                                d="M10 18a8 8 0 100-16 8 8 0 000 16zm.75-13a.75.75 0 00-1.5 0v5c0 .414.336.75.75.75h4a.75.75 0 000-1.5h-3.25V5z"
                                clip-rule="evenodd"
                            />
                        </svg>
                    </div>
                    <div class="timeline-start md:text-end mb-10">
                        <time class="font-mono">{{ item.datetime }}</time>
                        <div class="text-lg">{{ item.remark }}</div>
                        <div class="text-sm text-base-content/60">{{ item.zone }}</div>
                    </div>
                    <hr />
                </li>
            </ul>
        </div>
    </div>
</template>

快递查询页面

接下来,创建快递查询页面:

<!-- pages/express/index.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';

// 查询表单
const company = ref('');
const number = ref('');
const loading = ref(false);
const error = ref('');

// 快递信息
const expressInfo = ref<any>(null);

// 查询历史
const history = ref<
    Array<{
        company: string;
        number: string;
        time: string;
    }>
>([]);

// 快递公司列表
const companies = [
    { name: '顺丰速运', code: 'SF' },
    { name: '中通快递', code: 'ZTO' },
    { name: '圆通速递', code: 'YTO' },
    { name: '韵达速递', code: 'YD' },
    { name: '申通快递', code: 'STO' },
    { name: '百世快递', code: 'HTKY' },
    { name: 'EMS', code: 'EMS' },
    { name: '京东物流', code: 'JD' },
];

// 加载查询历史
onMounted(() => {
    const savedHistory = localStorage.getItem('express_history');
    if (savedHistory) {
        history.value = JSON.parse(savedHistory);
    }
});

// 保存查询历史
function saveHistory(companyName: string, expressNumber: string) {
    const newRecord = {
        company: companyName,
        number: expressNumber,
        time: new Date().toLocaleString('zh-CN'),
    };

    history.value = [newRecord, ...history.value.slice(0, 9)];
    localStorage.setItem('express_history', JSON.stringify(history.value));
}

// 查询快递信息
async function queryExpress() {
    if (!company.value || !number.value) {
        error.value = '请选择快递公司并输入运单号';
        return;
    }

    try {
        loading.value = true;
        error.value = '';
        expressInfo.value = null;

        // 调用快递查询 API
        const response = await fetch('/api/express/query', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                company: company.value,
                number: number.value,
            }),
        });

        const data = await response.json();

        if (data.error) {
            error.value = data.error;
            return;
        }

        expressInfo.value = data;
        const companyName = companies.find(c => c.code === company.value)?.name || company.value;
        saveHistory(companyName, number.value);
    } catch (e) {
        error.value = '查询失败,请稍后重试';
    } finally {
        loading.value = false;
    }
}

// 从历史记录中查询
function queryFromHistory(record: (typeof history.value)[0]) {
    company.value = companies.find(c => c.name === record.company)?.code || '';
    number.value = record.number;
    queryExpress();
}

// 清空历史记录
function clearHistory() {
    history.value = [];
    localStorage.removeItem('express_history');
}
</script>

<template>
    <div class="max-w-4xl mx-auto">
        <h1 class="text-3xl font-bold mb-8">快递查询</h1>

        <!-- 查询表单 -->
        <div class="form-control w-full max-w-lg mb-8">
            <div class="flex gap-4 mb-4">
                <select v-model="company" class="select select-bordered w-full max-w-xs">
                    <option value="">选择快递公司</option>
                    <option v-for="item in companies" :key="item.code" :value="item.code">
                        {{ item.name }}
                    </option>
                </select>

                <input
                    v-model="number"
                    type="text"
                    placeholder="输入运单号"
                    class="input input-bordered w-full"
                />
            </div>

            <button class="btn btn-primary w-full" :disabled="loading" @click="queryExpress">
                查询
            </button>

            <!-- 错误提示 -->
            <div v-if="error" class="alert alert-error mt-4">
                {{ error }}
            </div>
        </div>

        <!-- 查询历史 -->
        <div v-if="history.length > 0" class="mb-8">
            <div class="flex justify-between items-center mb-4">
                <h2 class="text-xl font-bold">查询历史</h2>
                <button class="btn btn-sm btn-ghost" @click="clearHistory">清空</button>
            </div>

            <div class="overflow-x-auto">
                <table class="table table-zebra">
                    <thead>
                        <tr>
                            <th>快递公司</th>
                            <th>运单号</th>
                            <th>查询时间</th>
                            <th>操作</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="record in history" :key="record.number">
                            <td>{{ record.company }}</td>
                            <td>{{ record.number }}</td>
                            <td>{{ record.time }}</td>
                            <td>
                                <button
                                    class="btn btn-sm btn-ghost"
                                    @click="queryFromHistory(record)"
                                >
                                    重新查询
                                </button>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>

        <!-- 快递信息 -->
        <div v-if="expressInfo">
            <ExpressCard :info="expressInfo" />
        </div>

        <!-- 加载状态 -->
        <div v-if="loading" class="flex justify-center items-center h-40">
            <span class="loading loading-spinner loading-lg"></span>
        </div>
    </div>
</template>

API 路由

创建快递查询相关的 API 路由:

// server/api/express/query.ts
import { defineEventHandler, readBody } from 'h3';

import ReferenceAnswer from '@/components/common/ReferenceAnswer.vue';

export default defineEventHandler(async event => {
    try {
        const body = await readBody(event);
        const { company, number } = body;

        if (!company || !number) {
            return {
                error: '请提供快递公司和运单号',
            };
        }

        // 调用聚合数据快递 API
        const apiKey = process.env.JUHE_EXPRESS_KEY;
        const response = await fetch(
            `http://apis.juhe.cn/exp/index?com=${company}&no=${number}&key=${apiKey}`
        );

        const data = await response.json();

        if (data.error_code !== 0) {
            return {
                error: data.reason || '获取快递信息失败',
            };
        }

        // 格式化返回数据
        return {
            company: data.result.company,
            number: data.result.no,
            list: data.result.list,
            status: data.result.status,
            isComplete: data.result.isComplete,
        };
    } catch (error) {
        console.error('Express API Error:', error);
        return {
            error: '服务器错误,请稍后重试',
        };
    }
});

使用说明

  1. 查询快递信息:

    • 从下拉列表选择快递公司
    • 输入运单号
    • 点击查询按钮
  2. 查询历史功能:

    • 自动保存最近 10 条查询记录
    • 支持从历史记录中重新查询
    • 可以清空历史记录
  3. 快递信息展示:

    • 快递公司名称
    • 运单号
    • 物流状态和图标
    • 详细的物流信息时间线

优化建议

  1. 数据验证

    • 添加运单号格式验证
    • 支持扫描运单号二维码
  2. 用户体验

    • 添加运单号自动识别快递公司功能
    • 支持订阅物流更新提醒
    • 添加常用运单号收藏功能
  3. 性能优化

    • 实现查询结果缓存
    • 优化历史记录存储方式
    • 添加查询频率限制

下一步

接下来,我们将:

  1. 实现汇率换算功能
  2. 设计汇率计算器界面
  3. 集成汇率查询 API
  4. 添加货币选择功能

搜索