概述

本指南将介绍如何使用Uniapp框架开发一个适配鸿蒙HarmonyOS 5的出行类应用,包含地图导航、行程规划、交通查询和出行服务等核心功能。

功能设计

  1. ​地图导航​​:集成地图服务,提供路线规划和实时导航
  2. ​行程规划​​:多种出行方式组合规划
  3. ​交通查询​​:实时公交、地铁信息查询
  4. ​出行服务​​:打车、共享单车、租车等服务接入
  5. ​个人中心​​:行程记录、收藏地点管理

开发准备

1. 环境配置

  1. 安装HUAWEI DevEco Studio
  2. 安装Node.js (建议v14.x或更高版本)
  3. 安装Uniapp开发工具HBuilderX
  4. 配置HarmonyOS SDK

2. 创建Uniapp项目

  1. 打开HBuilderX
  2. 选择"文件" -> "新建" -> "项目"
  3. 选择"uni-app"模板
  4. 项目名称填写"TravelApp"
  5. 选择默认模板

项目结构

TravelApp/
├── common/               # 公共资源
│   ├── css/             # 公共样式
│   ├── icons/           # 图标资源
│   └── js/              # 公共JS
├── components/           # 组件目录
├── pages/                # 页面目录
│   ├── index/           # 首页
│   ├── map/             # 地图页
│   ├── transit/         # 交通查询页
│   ├── service/         # 出行服务页
│   └── user/            # 用户中心
├── static/               # 静态资源
├── manifest.json         # 应用配置
└── pages.json           # 页面路由配置

核心功能实现

1. 首页实现 (pages/index/index.vue)

<template>
  <view class="container">
    <!-- 搜索框 -->
    <view class="search-box">
      <input 
        class="search-input" 
        placeholder="输入目的地" 
        v-model="destination"
        @confirm="handleSearch"
      />
      <button class="search-btn" @click="handleSearch">搜索</button>
    </view>
    
    <!-- 快捷入口 -->
    <view class="quick-entries">
      <view 
        class="entry-item" 
        v-for="item in quickEntries" 
        :key="item.id"
        @click="navigateTo(item.page)"
      >
        <image class="entry-icon" :src="item.icon"></image>
        <text class="entry-text">{{item.name}}</text>
      </view>
    </view>
    
    <!-- 推荐路线 -->
    <view class="section">
      <view class="section-header">
        <text class="section-title">推荐路线</text>
        <text class="section-more">查看更多</text>
      </view>
      <scroll-view scroll-x class="routes-scroll">
        <view 
          class="route-card" 
          v-for="route in recommendedRoutes" 
          :key="route.id"
          @click="viewRouteDetail(route)"
        >
          <image class="route-image" :src="route.image"></image>
          <text class="route-title">{{route.title}}</text>
          <text class="route-desc">{{route.description}}</text>
        </view>
      </scroll-view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      destination: '',
      quickEntries: [
        { id: 1, name: '公交地铁', icon: '/static/icons/bus.png', page: '/pages/transit/index' },
        { id: 2, name: '打车出行', icon: '/static/icons/taxi.png', page: '/pages/service/taxi' },
        { id: 3, name: '共享单车', icon: '/static/icons/bike.png', page: '/pages/service/bike' },
        { id: 4, name: '自驾租车', icon: '/static/icons/car.png', page: '/pages/service/car' }
      ],
      recommendedRoutes: [
        {
          id: 1,
          title: '城市经典一日游',
          description: '涵盖城市主要景点',
          image: '/static/images/route1.jpg'
        },
        // 更多推荐路线...
      ]
    }
  },
  methods: {
    handleSearch() {
      if (this.destination.trim()) {
        uni.navigateTo({
          url: `/pages/map/index?destination=${encodeURIComponent(this.destination)}`
        });
      }
    },
    navigateTo(page) {
      uni.navigateTo({
        url: page
      });
    },
    viewRouteDetail(route) {
      uni.navigateTo({
        url: `/pages/route/detail?id=${route.id}`
      });
    }
  }
}
</script>

<style>
.container {
  padding: 20rpx;
}

.search-box {
  display: flex;
  margin-bottom: 30rpx;
}

.search-input {
  flex: 1;
  height: 80rpx;
  padding: 0 20rpx;
  border: 1rpx solid #eee;
  border-radius: 40rpx;
}

.search-btn {
  width: 120rpx;
  height: 80rpx;
  margin-left: 20rpx;
  background-color: #007AFF;
  color: white;
  border-radius: 40rpx;
}

.quick-entries {
  display: flex;
  justify-content: space-between;
  margin-bottom: 40rpx;
}

.entry-item {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.entry-icon {
  width: 100rpx;
  height: 100rpx;
  margin-bottom: 10rpx;
}

.section {
  margin-top: 40rpx;
}

.section-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 20rpx;
}

.section-title {
  font-size: 36rpx;
  font-weight: bold;
}

.section-more {
  color: #999;
}

.routes-scroll {
  white-space: nowrap;
}

.route-card {
  display: inline-block;
  width: 300rpx;
  margin-right: 20rpx;
}

.route-image {
  width: 300rpx;
  height: 200rpx;
  border-radius: 10rpx;
}

.route-title {
  display: block;
  margin-top: 10rpx;
  font-weight: bold;
}

.route-desc {
  display: block;
  color: #666;
  font-size: 24rpx;
}
</style>

2. 地图页面实现 (pages/map/index.vue)

<template>
  <view class="map-container">
    <!-- 地图组件 -->
    <map 
      id="map" 
      :latitude="latitude" 
      :longitude="longitude" 
      :markers="markers"
      :polyline="polyline"
      :scale="scale"
      :show-location="true"
      class="map"
    ></map>
    
    <!-- 地图控制栏 -->
    <view class="map-controls">
      <button @click="zoomIn">+</button>
      <button @click="zoomOut">-</button>
      <button @click="locateMe">定位</button>
    </view>
    
    <!-- 路线选择 -->
    <view class="route-options" v-if="routes.length > 0">
      <view 
        class="route-option" 
        v-for="(route, index) in routes" 
        :key="index"
        @click="selectRoute(index)"
        :class="{active: selectedRoute === index}"
      >
        <text class="route-type">{{route.type}}</text>
        <text class="route-time">{{route.duration}}分钟</text>
        <text class="route-distance">{{route.distance}}公里</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      latitude: 39.90923, // 默认北京坐标
      longitude: 116.397428,
      scale: 15,
      markers: [],
      polyline: [],
      routes: [],
      selectedRoute: 0,
      destination: ''
    }
  },
  onLoad(options) {
    if (options.destination) {
      this.destination = decodeURIComponent(options.destination);
      this.searchDestination();
    } else {
      this.getLocation();
    }
  },
  methods: {
    getLocation() {
      uni.getLocation({
        type: 'gcj02',
        success: (res) => {
          this.latitude = res.latitude;
          this.longitude = res.longitude;
          this.addMyLocationMarker();
        },
        fail: (err) => {
          console.error('获取位置失败', err);
          uni.showToast({
            title: '获取位置失败',
            icon: 'none'
          });
        }
      });
    },
    addMyLocationMarker() {
      this.markers.push({
        id: 0,
        latitude: this.latitude,
        longitude: this.longitude,
        iconPath: '/static/icons/location.png',
        width: 30,
        height: 30
      });
    },
    searchDestination() {
      // 模拟搜索目的地
      const destinationMarker = {
        id: 1,
        latitude: 39.914,
        longitude: 116.404,
        iconPath: '/static/icons/destination.png',
        width: 30,
        height: 30,
        title: this.destination
      };
      
      this.markers.push(destinationMarker);
      
      // 模拟路线数据
      this.routes = [
        {
          type: '公交',
          duration: 45,
          distance: 12.5,
          polyline: [{
            points: [
              {latitude: this.latitude, longitude: this.longitude},
              {latitude: 39.911, longitude: 116.400},
              {latitude: 39.914, longitude: 116.404}
            ],
            color: '#00BFFF',
            width: 6
          }]
        },
        {
          type: '驾车',
          duration: 30,
          distance: 10.2,
          polyline: [{
            points: [
              {latitude: this.latitude, longitude: this.longitude},
              {latitude: 39.914, longitude: 116.404}
            ],
            color: '#FF6347',
            width: 6
          }]
        }
      ];
      
      // 默认选择第一条路线
      this.selectRoute(0);
    },
    selectRoute(index) {
      this.selectedRoute = index;
      this.polyline = this.routes[index].polyline;
      
      // 移动地图视角
      this.mapCtx = uni.createMapContext('map', this);
      this.mapCtx.includePoints({
        points: this.routes[index].polyline[0].points,
        padding: [50, 50, 50, 50]
      });
    },
    zoomIn() {
      this.scale += 1;
    },
    zoomOut() {
      this.scale -= 1;
    },
    locateMe() {
      this.getLocation();
      this.mapCtx.moveToLocation();
    }
  }
}
</script>

<style>
.map-container {
  position: relative;
  width: 100%;
  height: 100vh;
}

.map {
  width: 100%;
  height: 100%;
}

.map-controls {
  position: absolute;
  right: 20rpx;
  bottom: 200rpx;
  display: flex;
  flex-direction: column;
}

.map-controls button {
  width: 80rpx;
  height: 80rpx;
  margin-bottom: 20rpx;
  background-color: white;
  border-radius: 50%;
  box-shadow: 0 0 10rpx rgba(0,0,0,0.1);
}

.route-options {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: white;
  padding: 20rpx;
  border-top-left-radius: 20rpx;
  border-top-right-radius: 20rpx;
  box-shadow: 0 -5rpx 10rpx rgba(0,0,0,0.1);
}

.route-option {
  padding: 20rpx;
  margin-bottom: 20rpx;
  border-radius: 10rpx;
  border: 1rpx solid #eee;
}

.route-option.active {
  border-color: #007AFF;
  background-color: #F0F7FF;
}

.route-type {
  font-weight: bold;
  margin-right: 20rpx;
}

.route-time {
  color: #007AFF;
  margin-right: 20rpx;
}
</style>

3. 交通查询页面 (pages/transit/index.vue)

<template>
  <view class="transit-container">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <input 
        placeholder="输入公交/地铁线路" 
        v-model="searchQuery"
        @confirm="searchTransit"
      />
      <button @click="searchTransit">搜索</button>
    </view>
    
    <!-- 搜索结果 -->
    <view class="result-list">
      <view 
        class="result-item" 
        v-for="item in transitResults" 
        :key="item.id"
        @click="viewTransitDetail(item)"
      >
        <image class="transit-icon" :src="item.icon"></image>
        <view class="transit-info">
          <text class="transit-name">{{item.name}}</text>
          <text class="transit-desc">{{item.description}}</text>
        </view>
        <view class="transit-status">
          <text :class="['status', item.status]">{{item.statusText}}</text>
        </view>
      </view>
    </view>
    
    <!-- 附近站点 -->
    <view class="section">
      <view class="section-header">
        <text class="section-title">附近站点</text>
      </view>
      <view 
        class="station-item" 
        v-for="station in nearbyStations" 
        :key="station.id"
        @click="viewStation(station)"
      >
        <text class="station-name">{{station.name}}</text>
        <text class="station-distance">{{station.distance}}米</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      searchQuery: '',
      transitResults: [],
      nearbyStations: [
        {
          id: 1,
          name: '中关村南站',
          distance: 500,
          lines: ['地铁4号线', '公交320路']
        },
        // 更多附近站点...
      ]
    }
  },
  methods: {
    searchTransit() {
      if (this.searchQuery.trim()) {
        // 模拟API请求
        uni.showLoading({
          title: '搜索中'
        });
        
        setTimeout(() => {
          this.transitResults = [
            {
              id: 1,
              name: '地铁4号线',
              description: '安河桥北 - 天宫院',
              icon: '/static/icons/subway.png',
              status: 'normal',
              statusText: '正常运营'
            },
            {
              id: 2,
              name: '公交320路',
              description: '北京西站 - 西苑',
              icon: '/static/icons/bus.png',
              status: 'normal',
              statusText: '正常运营'
            }
          ];
          
          uni.hideLoading();
        }, 1000);
      }
    },
    viewTransitDetail(item) {
      uni.navigateTo({
        url: `/pages/transit/detail?id=${item.id}&type=${item.icon.includes('subway') ? 'subway' : 'bus'}`
      });
    },
    viewStation(station) {
      uni.navigateTo({
        url: `/pages/transit/station?id=${station.id}`
      });
    }
  }
}
</script>

<style>
.transit-container {
  padding: 20rpx;
}

.search-bar {
  display: flex;
  margin-bottom: 20rpx;
}

.search-bar input {
  flex: 1;
  height: 80rpx;
  padding: 0 20rpx;
  border: 1rpx solid #eee;
  border-radius: 40rpx;
}

.search-bar button {
  width: 120rpx;
  height: 80rpx;
  margin-left: 20rpx;
  background-color: #007AFF;
  color: white;
  border-radius: 40rpx;
}

.result-list {
  margin-bottom: 40rpx;
}

.result-item {
  display: flex;
  padding: 20rpx;
  margin-bottom: 20rpx;
  background-color: white;
  border-radius: 10rpx;
  align-items: center;
}

.transit-icon {
  width: 60rpx;
  height: 60rpx;
  margin-right: 20rpx;
}

.transit-info {
  flex: 1;
}

.transit-name {
  display: block;
  font-weight: bold;
}

.transit-desc {
  display: block;
  color: #666;
  font-size: 24rpx;
}

.transit-status {
  width: 120rpx;
  text-align: right;
}

.status {
  padding: 5rpx 10rpx;
  border-radius: 10rpx;
  font-size: 24rpx;
}

.status.normal {
  background-color: #E1F5E1;
  color: #4CAF50;
}

.station-item {
  display: flex;
  justify-content: space-between;
  padding: 20rpx;
  background-color: white;
  margin-bottom: 20rpx;
  border-radius: 10rpx;
}

.station-name {
  font-weight: bold;
}

.station-distance {
  color: #666;
}
</style>

4. 出行服务页面 (pages/service/taxi.vue)

<template>
  <view class="service-container">
    <!-- 地址选择 -->
    <view class="address-selector">
      <view class="address-item">
        <text class="address-label">起点</text>
        <input 
          class="address-input" 
          placeholder="我的位置" 
          v-model="startAddress"
          disabled
        />
      </view>
      <view class="address-item">
        <text class="address-label">终点</text>
        <input 
          class="address-input" 
          placeholder="输入目的地" 
          v-model="endAddress"
          @focus="showLocationPicker = true"
        />
      </view>
    </view>
    
    <!-- 车型选择 -->
    <view class="car-type-selector">
      <view 
        class="car-type" 
        v-for="type in carTypes" 
        :key="type.id"
        @click="selectCarType(type)"
        :class="{active: selectedCarType === type.id}"
      >
        <image class="car-icon" :src="type.icon"></image>
        <text class="car-name">{{type.name}}</text>
        <text class="car-price">约¥{{type.price}}</text>
      </view>
    </view>
    
    <!-- 呼叫按钮 -->
    <view class="call-btn-container">
      <button class="call-btn" @click="callTaxi">呼叫{{selectedCarTypeName}}</button>
    </view>
    
    <!-- 位置选择器 -->
    <location-picker 
      v-if="showLocationPicker" 
      @select="handleLocationSelect"
      @cancel="showLocationPicker = false"
    />
  </view>
</template>

<script>
export default {
  data() {
    return {
      startAddress: '我的位置',
      endAddress: '',
      showLocationPicker: false,
      carTypes: [
        {
          id: 1,
          name: '快车',
          icon: '/static/icons/taxi-normal.png',
          price: 35
        },
        {
          id: 2,
          name: '优享',
          icon: '/static/icons/taxi-premium.png',
          price: 50
        },
        {
          id: 3,
          name: '豪华车',
          icon: '/static/icons/taxi-luxury.png',
          price: 80
        }
      ],
      selectedCarType: 1
    }
  },
  computed: {
    selectedCarTypeName() {
      const type = this.carTypes.find(t => t.id === this.selectedCarType);
      return type ? type.name : '';
    }
  },
  methods: {
    selectCarType(type) {
      this.selectedCarType = type.id;
    },
    handleLocationSelect(location) {
      this.endAddress = location.name;
      this.showLocationPicker = false;
    },
    callTaxi() {
      if (!this.endAddress.trim()) {
        uni.showToast({
          title: '请选择目的地',
          icon: 'none'
        });
        return;
      }
      
      uni.showLoading({
        title: '呼叫中...'
      });
      
      // 模拟API调用
      setTimeout(() => {
        uni.hideLoading();
        uni.showToast({
          title: '呼叫成功,司机即将到达',
          icon: 'success'
        });
        
        // 跳转到订单详情页
        uni.navigateTo({
          url: '/pages/service/order?id=123'
        });
      }, 1500);
    }
  }
}
</script>

<style>
.service-container {
  padding: 20rpx;
}

.address-selector {
  background-color: white;
  border-radius: 10rpx;
  padding: 20rpx;
  margin-bottom: 30rpx;
}

.address-item {
  display: flex;
  align-items: center;
  padding: 20rpx 0;
}

.address-item:first-child {
  border-bottom: 1rpx solid #eee;
}

.address-label {
  width: 100rpx;
  font-weight: bold;
}

.address-input {
  flex: 1;
}

.car-type-selector {
  display: flex;
  justify-content: space-between;
  margin-bottom: 40rpx;
}

.car-type {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20rpx;
  width: 30%;
  background-color: white;
  border-radius: 10rpx;
  border: 1rpx solid #eee;
}

.car-type.active {
  border-color: #007AFF;
  background-color: #F0F7FF;
}

.car-icon {
  width: 80rpx;
  height: 80rpx;
  margin-bottom: 10rpx;
}

.car-name {
  font-weight: bold;
  margin-bottom: 5rpx;
}

.car-price {
  color: #FF6347;
  font-size: 24rpx;
}

.call-btn-container {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 40rpx;
  padding: 0 20rpx;
}

.call-btn {
  background-color: #007AFF;
  color: white;
  height: 90rpx;
  border-radius: 45rpx;
}
</style>

鸿蒙特有功能适配

1. 鸿蒙能力调用

manifest.json中配置鸿蒙特有功能:

{
  "harmonyos": {
    "package": "com.example.travelapp",
    "appName": "出行助手",
    "abilities": [
      {
        "name": "MainAbility",
        "type": "page",
        "label": "出行助手",
        "icon": "common/icons/app.png"
      }
    ],
    "reqPermissions": [
      {
        "name": "ohos.permission.LOCATION"
      },
      {
        "name": "ohos.permission.LOCATION_IN_BACKGROUND"
      },
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

2. 鸿蒙定位服务调用

// 在需要使用定位的页面中
methods: {
  getHarmonyLocation() {
    // 检查权限
    const result = await this.$harmony.requestPermissions([
      'ohos.permission.LOCATION'
    ]);
    
    if (result === 0) { // 0表示授权成功
      this.$harmony.getLocation({
        success: (res) => {
          this.latitude = res.latitude;
          this.longitude = res.longitude;
        },
        fail: (err) => {
          console.error('获取位置失败', err);
        }
      });
    }
  }
}

3. 鸿蒙后台持续定位

// 在app.vue中
export default {
  onLaunch() {
    // 申请后台定位权限
    this.$harmony.requestPermissions([
      'ohos.permission.LOCATION_IN_BACKGROUND'
    ]).then(result => {
      if (result === 0) {
        // 启动后台定位服务
        this.$harmony.startBackgroundLocation({
          interval: 5000, // 5秒更新一次
          callback: (res) => {
            // 处理位置更新
            this.$store.commit('updateLocation', {
              latitude: res.latitude,
              longitude: res.longitude
            });
          }
        });
      }
    });
  }
}

项目构建与发布

1. 构建HarmonyOS应用

  1. 在HBuilderX中选择"发行" -> "原生App-云打包"
  2. 选择"HarmonyOS"平台
  3. 配置证书和签名信息
  4. 点击"打包"生成HAP文件

2. 应用上架

  1. 登录华为开发者联盟
  2. 进入"我的项目"创建新应用
  3. 上传生成的HAP文件
  4. 填写应用信息和截图
  5. 提交审核

性能优化建议

  1. ​图片资源优化​​:

    • 使用WebP格式替代PNG/JPG
    • 实现懒加载和渐进式加载
    • 根据设备分辨率加载不同尺寸的图片
  2. ​数据缓存策略​​:

    • 使用本地存储缓存常用数据
    • 实现离线地图和路线缓存
    • 设置合理的API缓存策略
  3. ​代码优化​​:

    • 使用组件复用
    • 将复杂逻辑拆分为多个子组件
    • 避免在UI线程执行耗时操作
  4. ​鸿蒙特有优化​​:

    • 使用鸿蒙的分布式能力实现多设备协同
    • 利用鸿蒙的原子化服务特性
    • 适配鸿蒙的卡片式交互

总结

通过Uniapp框架开发鸿蒙HarmonyOS 5出行类应用,我们实现了以下核心功能:

  1. ​地图导航​​:集成地图服务,提供路线规划和实时导航
  2. ​行程规划​​:多种出行方式组合规划
  3. ​交通查询​​:实时公交、地铁信息查询
  4. ​出行服务​​:打车、共享单车、租车等服务接入
  5. ​鸿蒙适配​​:利用鸿蒙特有能力和优化体验

这种开发方式既保留了跨平台开发的效率优势,又能充分利用鸿蒙系统的特性,为用户提供高质量的出行服务体验。您可以根据实际需求进一步扩展功能,如添加AI行程建议、社交分享或AR导航等高级功能。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐