鸿蒙HarmonyOS 5中使用Uniapp完成一个出行类的应用
地图导航:集成地图服务,提供路线规划和实时导航行程规划:多种出行方式组合规划交通查询:实时公交、地铁信息查询出行服务:打车、共享单车、租车等服务接入鸿蒙适配:利用鸿蒙特有能力和优化体验这种开发方式既保留了跨平台开发的效率优势,又能充分利用鸿蒙系统的特性,为用户提供高质量的出行服务体验。您可以根据实际需求进一步扩展功能,如添加AI行程建议、社交分享或AR导航等
·
概述
本指南将介绍如何使用Uniapp框架开发一个适配鸿蒙HarmonyOS 5的出行类应用,包含地图导航、行程规划、交通查询和出行服务等核心功能。
功能设计
- 地图导航:集成地图服务,提供路线规划和实时导航
- 行程规划:多种出行方式组合规划
- 交通查询:实时公交、地铁信息查询
- 出行服务:打车、共享单车、租车等服务接入
- 个人中心:行程记录、收藏地点管理
开发准备
1. 环境配置
- 安装HUAWEI DevEco Studio
- 安装Node.js (建议v14.x或更高版本)
- 安装Uniapp开发工具HBuilderX
- 配置HarmonyOS SDK
2. 创建Uniapp项目
- 打开HBuilderX
- 选择"文件" -> "新建" -> "项目"
- 选择"uni-app"模板
- 项目名称填写"TravelApp"
- 选择默认模板
项目结构
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应用
- 在HBuilderX中选择"发行" -> "原生App-云打包"
- 选择"HarmonyOS"平台
- 配置证书和签名信息
- 点击"打包"生成HAP文件
2. 应用上架
- 登录华为开发者联盟
- 进入"我的项目"创建新应用
- 上传生成的HAP文件
- 填写应用信息和截图
- 提交审核
性能优化建议
-
图片资源优化:
- 使用WebP格式替代PNG/JPG
- 实现懒加载和渐进式加载
- 根据设备分辨率加载不同尺寸的图片
-
数据缓存策略:
- 使用本地存储缓存常用数据
- 实现离线地图和路线缓存
- 设置合理的API缓存策略
-
代码优化:
- 使用组件复用
- 将复杂逻辑拆分为多个子组件
- 避免在UI线程执行耗时操作
-
鸿蒙特有优化:
- 使用鸿蒙的分布式能力实现多设备协同
- 利用鸿蒙的原子化服务特性
- 适配鸿蒙的卡片式交互
总结
通过Uniapp框架开发鸿蒙HarmonyOS 5出行类应用,我们实现了以下核心功能:
- 地图导航:集成地图服务,提供路线规划和实时导航
- 行程规划:多种出行方式组合规划
- 交通查询:实时公交、地铁信息查询
- 出行服务:打车、共享单车、租车等服务接入
- 鸿蒙适配:利用鸿蒙特有能力和优化体验
这种开发方式既保留了跨平台开发的效率优势,又能充分利用鸿蒙系统的特性,为用户提供高质量的出行服务体验。您可以根据实际需求进一步扩展功能,如添加AI行程建议、社交分享或AR导航等高级功能。
更多推荐



所有评论(0)