logo
Build Your Own Web Toolbox

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

  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'

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. 添加货币选择功能