HarmonyOS智慧农业管理应用开发教程--高高种地--第15篇:地图导航与路线规划

第15篇:地图导航与路线规划

📚 本篇导读

在智慧农业管理中,农户经常需要从当前位置前往农田、查找周边的农资店、农机租赁点等服务设施。本篇教程将带你实现完整的地图导航和POI(兴趣点)搜索功能,让应用更加实用。

本篇将实现

  • 🚗 驾车路线规划(从当前位置到目标地块)
  • 🗺️ 路线可视化展示(在地图上绘制导航路线)
  • 📍 POI周边搜索(农资店、农机租赁、加油站等)
  • 🎯 搜索结果展示(列表+地图标记)
  • 🌤️ 天气查询功能(基于当前位置)

🎯 学习目标

完成本篇教程后,你将掌握:

  1. 如何集成高德地图导航SDK
  2. 如何实现驾车路线规划功能
  3. 如何在地图上绘制路线
  4. 如何实现POI搜索功能
  5. 如何处理搜索结果并在地图上展示

📐 功能架构

导航与搜索功能架构

复制代码
地图导航与搜索模块
├── 路线规划
│   ├── RouteSearch(路线搜索服务)
│   ├── 驾车路线计算
│   ├── 路线结果处理
│   └── 路线可视化(Polyline绘制)
│
├── POI搜索
│   ├── PoiSearch(POI搜索服务)
│   ├── 周边圆形搜索
│   ├── 城市关键词搜索
│   └── 搜索结果标记
│
├── 天气查询
│   ├── WeatherSearch(天气搜索服务)
│   ├── 实时天气查询
│   └── 天气信息展示
│
└── 交互功能
    ├── 点击地块触发导航
    ├── 搜索半径调整
    └── 搜索结果点击定位

一、高德导航SDK集成

1.1 依赖配置

在项目的 oh-package.json5 中,我们已经添加了导航和搜索SDK:

json5 复制代码
{
  "dependencies": {
    "@amap/amap_lbs_map3d": "^1.0.0",      // 地图SDK
    "@amap/amap_lbs_search": "^1.0.0",     // 搜索SDK(包含路线规划)
    "@amap/amap_lbs_navi": "^1.0.0"        // 导航SDK
  }
}

说明

  • @amap/amap_lbs_search:提供路线规划、POI搜索、天气查询等功能
  • @amap/amap_lbs_navi:提供实时导航功能(本篇重点是路线规划)

1.2 导入必要的模块

FieldMapPage.ets 中导入搜索相关的类:

typescript 复制代码
// 搜索SDK导入
import { 
  RouteSearch,           // 路线搜索
  DriveRouteQuery,       // 驾车路线查询
  FromAndTo,             // 起点终点
  LatLonPoint,           // 经纬度点
  OnRouteSearchListener, // 路线搜索监听器
  DriveRouteResult,      // 驾车路线结果
  AMapException,         // 异常码
  PoiSearch,             // POI搜索
  PoiQuery,              // POI查询
  OnPoiSearchListener,   // POI搜索监听器
  PoiResult,             // POI结果
  PoiItem,               // POI项
  PoiSearchBound,        // POI搜索范围
  WeatherSearch,         // 天气搜索
  WeatherSearchQuery,    // 天气查询
  LocalWeatherLive,      // 实时天气
  LocalWeatherLiveResult,// 天气结果
  OnWeatherSearchListener // 天气监听器
} from '@amap/amap_lbs_search';

二、路线规划功能实现

2.1 初始化路线搜索服务

在页面的 aboutToAppear() 生命周期中初始化搜索服务:

typescript 复制代码
@Component
export struct FieldMapPage {
  // 路线搜索相关
  private routeSearch: RouteSearch | null = null;
  private routePolyline: Polyline | null = null;
  @State showRouteDialog: boolean = false;
  @State routeDistance: string = '';
  @State routeTime: string = '';
  @State isRouteSearching: boolean = false;
  
  aboutToAppear(): void {
    // 初始化路线搜索服务
    try {
      const context = getContext(this) as Context;
      
      // 创建RouteSearch实例
      this.routeSearch = new RouteSearch(context);
      console.info('[FieldMapPage] RouteSearch初始化成功');
      
    } catch (error) {
      console.error('[FieldMapPage] 搜索服务初始化失败:', error);
      promptAction.showToast({
        message: '路线规划功能初始化失败',
        duration: 2000
      });
    }
  }
}

关键点

  • RouteSearch 需要传入 Context 对象
  • 初始化失败时要给用户友好的提示

2.2 实现路线规划方法

创建一个方法来计算从当前位置到目标地块的路线:

typescript 复制代码
/**
 * 规划到指定地块的路线
 * @param fieldId 地块ID
 */
private planRouteToField(fieldId: string): void {
  // 1. 检查是否有当前位置
  if (!this.currentLocationInfo ||
      !this.currentLocationInfo.latitude ||
      !this.currentLocationInfo.longitude) {
    promptAction.showToast({
      message: '无法获取当前位置,请确保已开启定位权限',
      duration: 2000
    });
    return;
  }

  // 2. 查找目标地块
  const field = this.fields.find(f => f.id === fieldId);
  if (!field || !field.latitude || !field.longitude) {
    promptAction.showToast({
      message: '地块位置信息不完整',
      duration: 2000
    });
    return;
  }

  // 3. 获取起点和终点坐标
  const startLat = this.currentLocationInfo.latitude;
  const startLon = this.currentLocationInfo.longitude;
  const endLat = field.latitude;
  const endLon = field.longitude;

  console.info(`[FieldMapPage] 规划路线: (${startLat},${startLon}) -> (${endLat},${endLon})`);

  // 4. 调用路线搜索
  this.searchDriveRoute(startLat, startLon, endLat, endLon);
}

/**
 * 搜索驾车路线
 */
private searchDriveRoute(
  startLat: number,
  startLon: number,
  endLat: number,
  endLon: number
): void {
  if (!this.routeSearch || !globalAMap) {
    promptAction.showToast({
      message: '路线搜索服务不可用',
      duration: 2000
    });
    return;
  }

  try {
    console.info('[FieldMapPage] 开始路线搜索...');
    this.isRouteSearching = true;

    // 1. 创建起点和终点
    const startPoint = new LatLonPoint(startLat, startLon);
    const endPoint = new LatLonPoint(endLat, endLon);
    const fromAndTo = new FromAndTo(startPoint, endPoint);

    // 2. 创建驾车路线查询
    // mode参数: 0=速度优先, 1=费用优先, 2=距离优先, 3=不走高速
    const passPoints = new ArrayList<LatLonPoint>();  // 途经点(空)
    const avoidPolygons = new ArrayList<ArrayList<LatLonPoint>>();  // 避让区域(空)
    const query = new DriveRouteQuery(
      fromAndTo,      // 起点终点
      0,              // 模式:速度优先
      passPoints,     // 途经点
      avoidPolygons,  // 避让区域
      ''              // 避让道路
    );

    // 3. 设置监听器
    const listener: OnRouteSearchListener = {
      // 驾车路线搜索回调
      onDriveRouteSearched: (result: DriveRouteResult | undefined, errorCode: number): void => {
        this.isRouteSearching = false;
        this.handleRouteResult(result, errorCode);
      },
      // 其他路线类型(不使用)
      onWalkRouteSearched: (): void => {},
      onRideRouteSearched: (): void => {},
      onBusRouteSearched: (): void => {}
    };

    // 4. 设置监听器并开始搜索
    this.routeSearch.setRouteSearchListener(listener);

    // 延时执行,确保监听器设置完成
    setTimeout(() => {
      if (this.routeSearch) {
        this.routeSearch.calculateDriveRouteAsyn(query);
        console.info('[FieldMapPage] 路线计算已启动');
      }
    }, 100);

    promptAction.showToast({
      message: '正在规划路线...',
      duration: 1500
    });

  } catch (error) {
    this.isRouteSearching = false;
    console.error('[FieldMapPage] 路线搜索失败:', error);
    promptAction.showToast({
      message: '路线规划失败',
      duration: 2000
    });
  }
}

代码说明

步骤 说明
1. 参数验证 检查当前位置和目标地块的坐标是否有效
2. 创建查询对象 使用 DriveRouteQuery 创建驾车路线查询
3. 设置监听器 通过 OnRouteSearchListener 接收搜索结果
4. 异步搜索 调用 calculateDriveRouteAsyn() 开始搜索

DriveRouteQuery参数说明

  • mode=0:速度优先(推荐用于农业场景)
  • mode=1:费用优先(避开收费路段)
  • mode=2:距离优先(最短路径)
  • mode=3:不走高速(适合农村道路)

2.3 处理路线搜索结果

typescript 复制代码
/**
 * 处理路线搜索结果
 */
private handleRouteResult(result: DriveRouteResult | undefined, errorCode: number): void {
  console.info('[FieldMapPage] 路线搜索回调, errorCode:', errorCode);

  if (!globalAMap) {
    console.error('[FieldMapPage] 地图对象为空');
    return;
  }

  // 检查是否成功
  if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    console.info('[FieldMapPage] ✅ 路线搜索成功!');

    // 获取路线列表(通常返回多条路线,取第一条)
    const paths = result.getPaths();

    if (paths && paths.length > 0) {
      const path = paths[0];  // 取第一条路线

      // 获取路线信息
      const distance = path.getDistance();  // 距离(米)
      const duration = path.getDuration();  // 时长(秒)

      console.info(`[FieldMapPage] 路线信息 - 距离:${distance}米, 时长:${duration}秒`);

      // 格式化距离显示
      this.routeDistance = distance > 1000 ?
        `${(distance / 1000).toFixed(2)}公里` :
        `${distance.toFixed(0)}米`;

      // 格式化时间显示
      this.routeTime = duration > 3600 ?
        `${Math.floor(duration / 3600)}小时${Math.floor((duration % 3600) / 60)}分钟` :
        `${Math.floor(duration / 60)}分钟`;

      // 绘制路线
      const steps = path.getSteps();  // 获取路线分段
      this.drawRoute(steps);

      // 显示路线信息对话框
      this.showRouteDialog = true;

      promptAction.showToast({
        message: `路线规划成功: ${this.routeDistance},用时约${this.routeTime}`,
        duration: 3000
      });
    } else {
      console.warn('[FieldMapPage] 未找到路线');
      promptAction.showToast({
        message: '未找到合适的路线',
        duration: 2000
      });
    }
  } else {
    // 搜索失败
    console.error('[FieldMapPage] 路线搜索失败, errorCode:', errorCode);

    let errorMessage = '路线搜索失败';
    switch (errorCode) {
      case 1000: errorMessage = '请求超时,请重试'; break;
      case 1001: errorMessage = '网络错误,请检查网络连接'; break;
      case 1002: errorMessage = '无效的参数'; break;
      case 2000: errorMessage = '无效的用户Key'; break;
      case 3000: errorMessage = '服务请求超出限制'; break;
      default: errorMessage = `路线搜索失败 (错误码: ${errorCode})`;
    }

    promptAction.showToast({
      message: errorMessage,
      duration: 3000
    });
  }
}

关键点

  • result.getPaths() 返回多条路线方案,通常取第一条
  • path.getDistance() 返回距离(单位:米)
  • path.getDuration() 返回预计时长(单位:秒)
  • path.getSteps() 返回路线分段信息,用于绘制路线

2.4 在地图上绘制路线

typescript 复制代码
/**
 * 绘制路线到地图上
 * @param steps 路线分段信息
 */
private drawRoute(steps: ESObject): void {
  console.info('[FieldMapPage] 开始绘制路线...');

  if (!globalAMap) {
    console.error('[FieldMapPage] 地图对象为空,无法绘制路线');
    return;
  }

  if (!steps) {
    console.error('[FieldMapPage] 路线分段信息为空');
    return;
  }

  // 1. 清除旧路线
  if (this.routePolyline) {
    console.info('[FieldMapPage] 清除旧路线');
    this.routePolyline.remove();
    this.routePolyline = null;
  }

  try {
    // 2. 收集所有路线点
    const polylineOptions: PolylineOptions = new PolylineOptions();
    let pointCount = 0;

    // 遍历所有分段
    const stepsArray: ESObject = steps;
    const length: number = stepsArray.length as number;
    console.info(`[FieldMapPage] 处理 ${length} 个路线分段`);

    for (let i = 0; i < length; i++) {
      const step: ESObject = stepsArray[i] as ESObject;
      const polyline: ESObject = step.getPolyline?.() as ESObject;

      if (polyline) {
        const polylineLength: number = polyline.length as number;
        // 遍历分段中的所有点
        for (let j = 0; j < polylineLength; j++) {
          const point: ESObject = polyline[j] as ESObject;
          const lat: number = point.getLatitude?.() as number;
          const lon: number = point.getLongitude?.() as number;

          if (lat && lon) {
            polylineOptions.add(new LatLng(lat, lon));
            pointCount++;
          }
        }
      }
    }

    console.info(`[FieldMapPage] 收集到 ${pointCount} 个路线点`);

    // 3. 绘制路线
    if (pointCount > 0) {
      polylineOptions.setWidth(12);           // 线条宽度
      polylineOptions.setColor(0xFF2196F3);   // 蓝色
      polylineOptions.setZIndex(50);          // 层级(确保在地块标记之上)

      const polyline = globalAMap.addPolyline(polylineOptions);
      this.routePolyline = polyline ? polyline : null;

      if (this.routePolyline) {
        console.info('[FieldMapPage] ✅ 路线绘制成功');
      } else {
        console.error('[FieldMapPage] ❌ 添加路线到地图失败');
      }
    } else {
      console.warn('[FieldMapPage] 没有有效的路线点');
      promptAction.showToast({
        message: '路线数据无效',
        duration: 2000
      });
    }
  } catch (error) {
    console.error('[FieldMapPage] 绘制路线失败:', error);
    promptAction.showToast({
      message: '绘制路线失败',
      duration: 2000
    });
  }
}

/**
 * 清除路线
 */
private clearRoute(): void {
  if (this.routePolyline) {
    this.routePolyline.remove();
    this.routePolyline = null;
  }
  this.showRouteDialog = false;
  this.routeDistance = '';
  this.routeTime = '';
}

绘制路线的关键步骤

步骤 说明
1. 清除旧路线 避免多条路线重叠显示
2. 遍历分段 路线由多个分段(step)组成
3. 收集坐标点 每个分段包含多个坐标点
4. 创建Polyline 使用 PolylineOptions 配置样式
5. 添加到地图 调用 addPolyline() 显示路线

Polyline样式配置

  • setWidth(12):线条宽度(像素)
  • setColor(0xFF2196F3):颜色(ARGB格式,蓝色)
  • setZIndex(50):层级,数值越大越在上层

三、POI搜索功能实现

3.1 初始化POI搜索服务

typescript 复制代码
@Component
export struct FieldMapPage {
  // POI搜索相关
  private poiSearch: PoiSearch | null = null;
  private poiMarkers: Marker[] = [];
  private searchCircle: MapCircle | null = null;

  @State showPoiSearch: boolean = false;
  @State poiKeyword: string = '农资店';
  @State poiResults: PoiItem[] = [];
  @State searchRadius: number = 5000;  // 搜索半径(米)

  aboutToAppear(): void {
    try {
      const context = getContext(this) as Context;

      // 初始化POI搜索(需要context和undefined参数)
      this.poiSearch = new PoiSearch(context, undefined);
      console.info('[FieldMapPage] PoiSearch初始化成功');

    } catch (error) {
      console.error('[FieldMapPage] POI搜索初始化失败:', error);
    }
  }
}

3.2 实现周边POI搜索

typescript 复制代码
/**
 * 搜索周边POI
 * @param keyword 搜索关键词(如:农资店、加油站)
 */
private searchNearbyPoi(keyword: string): void {
  // 1. 检查当前位置
  if (!this.poiSearch || !this.currentLocationInfo ||
      !this.currentLocationInfo.latitude || !this.currentLocationInfo.longitude) {
    promptAction.showToast({
      message: '无法获取当前位置',
      duration: 2000
    });
    return;
  }

  try {
    // 2. 创建中心点
    const centerPoint = new LatLonPoint(
      this.currentLocationInfo.latitude,
      this.currentLocationInfo.longitude
    );

    // 3. 创建圆形搜索区域
    const searchBound = PoiSearchBound.createCircleSearchBound(
      centerPoint,        // 中心点
      this.searchRadius   // 半径(米)
    );

    // 4. 创建POI查询
    const query = new PoiQuery(
      keyword,  // 关键词
      '',       // 类型(空表示所有类型)
      ''        // 城市(空表示全国)
    );
    query.setPageSize(20);      // 每页结果数
    query.setPageNum(0);        // 页码(从0开始)
    query.setExtensions('all'); // 获取完整信息

    console.info(`[FieldMapPage] POI搜索: ${keyword}, 半径: ${this.searchRadius}米`);

    // 5. 设置监听器
    const listener: OnPoiSearchListener = {
      onPoiSearched: (result: PoiResult | undefined, errorCode: number): void => {
        this.handlePoiResult(result, errorCode);
      },
      onPoiItemSearched: () => {}
    };

    // 6. 执行搜索
    this.poiSearch.setOnPoiSearchListener(listener);
    this.poiSearch.setBound(searchBound);
    this.poiSearch.setQuery(query);
    this.poiSearch.searchPOIAsyn();

    promptAction.showToast({
      message: `正在搜索${keyword}...`,
      duration: 1500
    });
  } catch (error) {
    console.error('[FieldMapPage] POI搜索失败:', error);
    promptAction.showToast({
      message: 'POI搜索失败',
      duration: 2000
    });
  }
}

POI搜索参数说明

参数 说明 示例
keyword 搜索关键词 '农资店'、'加油站'、'农机租赁'
category POI类型码 留空表示所有类型
city 城市名称 留空表示全国范围
searchRadius 搜索半径 5000(5公里)
pageSize 每页结果数 20(最大50)

3.3 处理POI搜索结果

typescript 复制代码
/**
 * 处理POI搜索结果
 */
private handlePoiResult(result: PoiResult | undefined, errorCode: number): void {
  console.info(`[FieldMapPage] POI搜索回调, errorCode: ${errorCode}`);

  if (!globalAMap) {
    console.error('[FieldMapPage] 地图对象为空');
    return;
  }

  // 清除旧结果
  this.clearPoiResults();

  if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    console.info('[FieldMapPage] ✅ POI搜索成功!');

    const pois = result.getPois();
    console.info(`[FieldMapPage] 找到 ${pois ? pois.length : 0} 个POI`);

    if (pois && pois.length > 0) {
      // 1. 将ArrayList转换为数组
      this.poiResults = [];
      for (let i = 0; i < pois.length; i++) {
        this.poiResults.push(pois[i]);
      }

      // 2. 在地图上添加POI标记
      for (const poi of this.poiResults) {
        const latLon = poi.getLatLonPoint();
        if (latLon) {
          const markerOptions: MarkerOptions = new MarkerOptions();
          markerOptions.setPosition(
            new LatLng(latLon.getLatitude(), latLon.getLongitude())
          );
          markerOptions.setTitle(poi.getTitle());
          markerOptions.setSnippet(poi.getSnippet());
          markerOptions.setZIndex(15);  // 层级高于地块标记
          markerOptions.setAnchor(0.5, 1);
          // 使用紫红色标记,区别于地块标记
          markerOptions.setIcon(
            BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_MAGENTA)
          );

          const marker = globalAMap.addMarker(markerOptions);
          if (marker) {
            this.poiMarkers.push(marker);
          }
        }
      }

      // 3. 绘制搜索范围圆圈
      if (this.currentLocationInfo?.latitude && this.currentLocationInfo?.longitude) {
        const circleOptions: CircleOptions = new CircleOptions();
        circleOptions.setCenter(
          new LatLng(
            this.currentLocationInfo.latitude,
            this.currentLocationInfo.longitude
          )
        );
        circleOptions.setRadius(this.searchRadius);
        circleOptions.setStrokeColor(0x88FF5722);  // 半透明橙色边框
        circleOptions.setStrokeWidth(2);
        circleOptions.setFillColor(0x22FF5722);    // 半透明橙色填充

        this.searchCircle = globalAMap.addCircle(circleOptions);
      }

      promptAction.showToast({
        message: `找到 ${this.poiResults.length} 个结果`,
        duration: 2000
      });
    } else {
      promptAction.showToast({
        message: '未找到相关POI',
        duration: 2000
      });
    }
  } else {
    console.error('[FieldMapPage] POI搜索失败, errorCode:', errorCode);
    promptAction.showToast({
      message: 'POI搜索失败',
      duration: 2000
    });
  }
}

/**
 * 清除POI搜索结果
 */
private clearPoiResults(): void {
  // 清除POI标记
  for (const marker of this.poiMarkers) {
    marker.remove();
  }
  this.poiMarkers = [];

  // 清除搜索范围圆圈
  if (this.searchCircle) {
    this.searchCircle.remove();
    this.searchCircle = null;
  }

  // 清除结果列表
  this.poiResults = [];
}

POI结果处理要点

步骤 说明
1. 清除旧结果 避免多次搜索结果叠加
2. 转换数据 将ArrayList转为TypeScript数组
3. 添加标记 使用紫红色标记区分POI和地块
4. 绘制范围 用圆圈显示搜索范围

3.4 POI搜索UI实现

在地图页面添加POI搜索面板:

typescript 复制代码
build() {
  Stack({ alignContent: Alignment.TopStart }) {
    // 地图组件
    MapViewComponent({ mapOptions: this.mapOptions })
      .width('100%')
      .height('100%')

    // POI搜索面板
    if (this.showPoiSearch) {
      Column() {
        // 搜索关键词选择
        Row() {
          Text('搜索类型:')
            .fontSize(14)
            .fontColor('#333333')

          // 快捷关键词按钮
          Button('农资店')
            .fontSize(12)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.poiKeyword === '农资店' ? '#4CAF50' : '#E0E0E0')
            .fontColor(this.poiKeyword === '农资店' ? '#FFFFFF' : '#666666')
            .onClick(() => {
              this.poiKeyword = '农资店';
              this.searchNearbyPoi(this.poiKeyword);
            })

          Button('农机租赁')
            .fontSize(12)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.poiKeyword === '农机租赁' ? '#4CAF50' : '#E0E0E0')
            .fontColor(this.poiKeyword === '农机租赁' ? '#FFFFFF' : '#666666')
            .onClick(() => {
              this.poiKeyword = '农机租赁';
              this.searchNearbyPoi(this.poiKeyword);
            })

          Button('加油站')
            .fontSize(12)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.poiKeyword === '加油站' ? '#4CAF50' : '#E0E0E0')
            .fontColor(this.poiKeyword === '加油站' ? '#FFFFFF' : '#666666')
            .onClick(() => {
              this.poiKeyword = '加油站';
              this.searchNearbyPoi(this.poiKeyword);
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .padding(12)

        // 搜索半径调整
        Row() {
          Text('搜索半径:')
            .fontSize(14)
            .fontColor('#333333')

          Slider({
            value: this.searchRadius,
            min: 1000,
            max: 50000,
            step: 1000
          })
            .width(150)
            .onChange((value: number) => {
              this.searchRadius = value;
            })

          Text(`${(this.searchRadius / 1000).toFixed(1)}公里`)
            .fontSize(12)
            .fontColor('#666666')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .padding({ left: 12, right: 12, bottom: 12 })

        // 搜索结果列表
        if (this.poiResults.length > 0) {
          List() {
            ForEach(this.poiResults, (poi: PoiItem, index: number) => {
              ListItem() {
                Row() {
                  Column() {
                    Text(poi.getTitle())
                      .fontSize(14)
                      .fontWeight(FontWeight.Medium)
                      .fontColor('#333333')

                    Text(poi.getSnippet())
                      .fontSize(12)
                      .fontColor('#999999')
                      .margin({ top: 4 })

                    // 距离信息
                    if (poi.getDistance()) {
                      Text(`距离: ${(poi.getDistance() / 1000).toFixed(2)}公里`)
                        .fontSize(11)
                        .fontColor('#4CAF50')
                        .margin({ top: 4 })
                    }
                  }
                  .alignItems(HorizontalAlign.Start)
                  .layoutWeight(1)

                  // 导航按钮
                  Button('导航')
                    .fontSize(12)
                    .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                    .backgroundColor('#2196F3')
                    .onClick(() => {
                      const latLon = poi.getLatLonPoint();
                      if (latLon && this.currentLocationInfo) {
                        this.searchDriveRoute(
                          this.currentLocationInfo.latitude!,
                          this.currentLocationInfo.longitude!,
                          latLon.getLatitude(),
                          latLon.getLongitude()
                        );
                      }
                    })
                }
                .width('100%')
                .padding(12)
                .backgroundColor('#FFFFFF')
              }
            }, (poi: PoiItem, index: number) => index.toString())
          }
          .width('100%')
          .maxHeight(300)
          .backgroundColor('#F5F5F5')
        }

        // 关闭按钮
        Button('关闭')
          .width('100%')
          .fontSize(14)
          .backgroundColor('#FF5722')
          .onClick(() => {
            this.showPoiSearch = false;
            this.clearPoiResults();
          })
          .margin({ top: 8 })
      }
      .width('90%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .shadow({ radius: 8, color: '#33000000' })
      .padding(16)
      .margin({ top: 80, left: '5%' })
    }
  }
  .width('100%')
  .height('100%')
}

UI设计要点

  • 提供快捷关键词按钮(农资店、农机租赁、加油站)
  • 可调节搜索半径(1-50公里)
  • 列表展示搜索结果,包含距离信息
  • 每个结果提供导航按钮,可直接规划路线

四、天气查询功能

4.1 实现天气查询

typescript 复制代码
/**
 * 查询当前城市天气
 * @param cityName 城市名称
 */
private searchWeather(cityName: string): void {
  if (!cityName) {
    console.warn('[FieldMapPage] 城市名称为空,无法查询天气');
    return;
  }

  try {
    const context = getContext(this) as Context;

    // 创建天气搜索监听器
    const weatherSearchListener: OnWeatherSearchListener = {
      onWeatherLiveSearched: (
        weatherLiveResult: LocalWeatherLiveResult | undefined,
        errorCode: number
      ): void => {
        if (errorCode === AMapException.CODE_AMAP_SUCCESS) {
          if (weatherLiveResult && weatherLiveResult.getLiveResult()) {
            const liveWeather: LocalWeatherLive = weatherLiveResult.getLiveResult();

            // 更新天气信息
            this.currentWeather = liveWeather.getWeather() || '';
            this.currentTemperature = liveWeather.getTemperature() || '';
            this.windDirection = liveWeather.getWindDirection() || '';
            this.windPower = liveWeather.getWindPower() || '';
            this.humidity = liveWeather.getHumidity() || '';
            this.reportTime = liveWeather.getReportTime() || '';

            console.info(`[FieldMapPage] ✅ 天气查询成功: ${this.currentWeather} ${this.currentTemperature}°C`);
          }
        } else {
          console.error(`[FieldMapPage] ❌ 天气查询失败,错误码: ${errorCode}`);
        }
      },
      onWeatherForecastSearched: (): void => {
        // 不处理预报天气
      }
    };

    // 创建天气查询
    const query = new WeatherSearchQuery(
      cityName,
      WeatherSearchQuery.WEATHER_TYPE_LIVE  // 实时天气
    );

    const weatherSearch = new WeatherSearch(context);
    weatherSearch.setOnWeatherSearchListener(weatherSearchListener);
    weatherSearch.setQuery(query);
    weatherSearch.searchWeatherAsyn();

    console.info(`[FieldMapPage] 开始查询天气: ${cityName}`);
  } catch (error) {
    console.error('[FieldMapPage] 天气查询异常:', error);
  }
}

/**
 * 根据天气描述返回对应图标
 */
private getWeatherIcon(weather: string): string {
  if (weather.includes('晴')) return '☀️';
  if (weather.includes('云') || weather.includes('阴')) return '☁️';
  if (weather.includes('雨')) return '🌧️';
  if (weather.includes('雪')) return '❄️';
  if (weather.includes('雾') || weather.includes('霾')) return '🌫️';
  if (weather.includes('雷')) return '⛈️';
  if (weather.includes('风')) return '💨';
  return '🌤️';
}

4.2 天气信息展示

在地图页面添加天气信息卡片:

typescript 复制代码
// 天气信息卡片
if (this.currentWeather) {
  Row() {
    Text(this.getWeatherIcon(this.currentWeather))
      .fontSize(24)

    Column() {
      Text(`${this.currentWeather} ${this.currentTemperature}°C`)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor('#333333')

      Text(`${this.windDirection}风 ${this.windPower}级 | 湿度${this.humidity}%`)
        .fontSize(11)
        .fontColor('#999999')
        .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .margin({ left: 8 })
  }
  .padding(12)
  .backgroundColor('#FFFFFF')
  .borderRadius(8)
  .shadow({ radius: 4, color: '#22000000' })
  .margin({ top: 80, left: 16 })
}

五、完整功能集成

5.1 地图工具栏

在地图页面添加工具栏,集成所有功能:

typescript 复制代码
// 地图工具栏
Row() {
  // POI搜索按钮
  Button() {
    Image($r('app.media.ic_search'))
      .width(24)
      .height(24)
      .fillColor('#FFFFFF')
  }
  .width(48)
  .height(48)
  .backgroundColor('#4CAF50')
  .borderRadius(24)
  .onClick(() => {
    this.showPoiSearch = !this.showPoiSearch;
  })

  // 定位按钮
  Button() {
    Image($r('app.media.ic_location'))
      .width(24)
      .height(24)
      .fillColor('#FFFFFF')
  }
  .width(48)
  .height(48)
  .backgroundColor('#2196F3')
  .borderRadius(24)
  .margin({ left: 12 })
  .onClick(() => {
    this.moveToCurrentLocation();
  })

  // 清除路线按钮
  if (this.routePolyline) {
    Button() {
      Image($r('app.media.ic_close'))
        .width(24)
        .height(24)
        .fillColor('#FFFFFF')
    }
    .width(48)
    .height(48)
    .backgroundColor('#FF5722')
    .borderRadius(24)
    .margin({ left: 12 })
    .onClick(() => {
      this.clearRoute();
    })
  }
}
.position({ x: 16, y: '50%' })

5.2 地块标记点击触发导航

修改地块标记的点击事件,添加导航功能:

typescript 复制代码
// 在addFieldMarkers方法中
globalAMap.setOnMarkerClickListener({
  onMarkerClick: (marker: Marker): boolean => {
    const title = marker.getTitle();
    console.info('[FieldMapPage] 标记被点击:', title);

    // 查找对应的地块
    const field = this.fields.find(f => f.name === title);
    if (field) {
      // 显示地块详情对话框
      AlertDialog.show({
        title: field.name,
        message: `面积: ${field.area}亩\n${field.currentCrop ? '作物: ' + field.currentCrop.cropName : '状态: 闲置'}`,
        primaryButton: {
          value: '导航',
          action: () => {
            // 规划到该地块的路线
            this.planRouteToField(field.id);
          }
        },
        secondaryButton: {
          value: '查看详情',
          action: () => {
            // 跳转到地块详情页
            router.pushUrl({
              url: 'pages/Field/FieldDetailPage',
              params: { fieldId: field.id }
            });
          }
        }
      });
    }

    return true;
  }
});

六、实操练习

练习1:实现路线规划

任务:点击地块标记,规划从当前位置到该地块的路线

步骤

  1. 确保已开启定位权限
  2. 点击地图上的任意地块标记
  3. 在弹出的对话框中点击"导航"按钮
  4. 观察地图上绘制的蓝色路线
  5. 查看路线信息(距离、时长)

预期结果

  • 地图上显示蓝色路线
  • 弹出提示显示路线距离和时长
  • 路线从当前位置连接到目标地块

练习2:搜索周边POI

任务:搜索周边的农资店

步骤

  1. 点击地图工具栏的搜索按钮
  2. 在POI搜索面板中点击"农资店"按钮
  3. 调整搜索半径(如5公里)
  4. 观察地图上的紫红色POI标记
  5. 查看搜索结果列表
  6. 点击某个结果的"导航"按钮

预期结果

  • 地图上显示紫红色POI标记
  • 显示搜索范围的橙色圆圈
  • 列表展示POI名称、地址、距离
  • 可以规划到POI的路线

练习3:调整搜索半径

任务:尝试不同的搜索半径

步骤

  1. 打开POI搜索面板
  2. 拖动搜索半径滑块(1-50公里)
  3. 点击搜索按钮
  4. 观察搜索结果数量的变化
  5. 观察地图上圆圈范围的变化

预期结果

  • 半径越大,搜索结果越多
  • 地图上的圆圈范围相应变化
  • 远距离的POI也会被搜索到

七、常见问题与解决方案

问题1:路线规划失败

现象:点击导航后提示"路线搜索失败"

可能原因

  1. 网络连接问题
  2. 起点或终点坐标无效
  3. API Key配置错误
  4. 搜索服务未正确初始化

解决方案

typescript 复制代码
// 1. 检查网络权限
// module.json5中添加:
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

// 2. 验证坐标有效性
if (!startLat || !startLon || !endLat || !endLon) {
  console.error('坐标无效');
  return;
}

// 3. 检查API Key
console.info('API Key:', MapConstants.AMAP_API_KEY);

// 4. 添加错误处理
try {
  this.routeSearch.calculateDriveRouteAsyn(query);
} catch (error) {
  console.error('路线搜索异常:', error);
}

问题2:POI搜索无结果

现象:搜索后提示"未找到相关POI"

可能原因

  1. 搜索半径太小
  2. 关键词不准确
  3. 当前位置偏远,周边确实没有相关POI

解决方案

typescript 复制代码
// 1. 增大搜索半径
this.searchRadius = 10000; // 10公里

// 2. 使用更通用的关键词
this.poiKeyword = '商店'; // 而不是'农资店'

// 3. 添加城市范围搜索
const query = new PoiQuery(keyword, '', '武汉市');

问题3:路线不显示在地图上

现象:路线规划成功,但地图上看不到路线

可能原因

  1. Polyline的ZIndex太低,被其他图层遮挡
  2. 路线颜色与地图背景相近
  3. 地图缩放级别不合适

解决方案

typescript 复制代码
// 1. 提高ZIndex
polylineOptions.setZIndex(100);

// 2. 使用更鲜艳的颜色
polylineOptions.setColor(0xFFFF0000); // 红色
polylineOptions.setWidth(15); // 增加宽度

// 3. 调整地图视角
const centerLat = (startLat + endLat) / 2;
const centerLon = (startLon + endLon) / 2;
const cameraUpdate = CameraUpdateFactory.newLatLngZoom(
  new LatLng(centerLat, centerLon),
  12
);
globalAMap.animateCamera(cameraUpdate);

问题4:定位权限未授权

现象:无法获取当前位置

解决方案

typescript 复制代码
// 在EntryAbility.ets的onCreate中请求权限
async requestPermissions() {
  const permissions: Permissions[] = [
    'ohos.permission.APPROXIMATELY_LOCATION',
    'ohos.permission.LOCATION'
  ];

  const atManager = abilityAccessCtrl.createAtManager();
  const result = await atManager.requestPermissionsFromUser(
    this.context,
    permissions
  );

  console.info('权限申请结果:', result.authResults);
}

八、功能扩展建议

8.1 多种出行方式

除了驾车路线,还可以添加步行、骑行路线:

typescript 复制代码
// 步行路线
private searchWalkRoute(startLat: number, startLon: number, endLat: number, endLon: number): void {
  const startPoint = new LatLonPoint(startLat, startLon);
  const endPoint = new LatLonPoint(endLat, endLon);
  const fromAndTo = new FromAndTo(startPoint, endPoint);

  const query = new WalkRouteQuery(fromAndTo);

  const listener: OnRouteSearchListener = {
    onWalkRouteSearched: (result: WalkRouteResult | undefined, errorCode: number): void => {
      // 处理步行路线结果
    },
    onDriveRouteSearched: (): void => {},
    onRideRouteSearched: (): void => {},
    onBusRouteSearched: (): void => {}
  };

  this.routeSearch.setRouteSearchListener(listener);
  this.routeSearch.calculateWalkRouteAsyn(query);
}

8.2 路线对比

显示多条路线方案供用户选择:

typescript 复制代码
private handleRouteResult(result: DriveRouteResult | undefined, errorCode: number): void {
  if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    const paths = result.getPaths();

    // 显示所有路线方案
    for (let i = 0; i < paths.length; i++) {
      const path = paths[i];
      console.info(`方案${i + 1}: ${path.getDistance()}米, ${path.getDuration()}秒`);

      // 可以用不同颜色绘制多条路线
      this.drawRouteWithColor(path.getSteps(), i === 0 ? 0xFF2196F3 : 0xFF9E9E9E);
    }
  }
}

8.3 实时导航

集成导航SDK实现实时导航:

typescript 复制代码
// 初始化导航实例
private naviInstance: AMapNavi | null = null;

// 开始导航
private startNavigation(endLat: number, endLon: number): void {
  if (!this.naviInstance) {
    const context = getContext(this) as Context;
    this.naviInstance = AMapNaviFactory.getAMapNaviInstance(
      context,
      MapConstants.AMAP_API_KEY
    );
  }

  // 设置终点
  const endPoint = new NaviLatLng(endLat, endLon);
  const endList = new ArrayList<NaviLatLng>();
  endList.add(endPoint);

  // 计算路线
  this.naviInstance.calculateDriveRoute(
    new ArrayList<NaviLatLng>(),  // 起点(空表示当前位置)
    endList,                       // 终点
    new ArrayList<NaviLatLng>(),  // 途经点
    AMapNaviStrategy.DRIVING_DEFAULT
  );

  // 开始导航
  this.naviInstance.startNavi(AMapNaviMode.GPS);
}

8.4 收藏常用地点

保存常用POI,方便快速导航:

typescript 复制代码
interface FavoritePoi {
  id: string;
  name: string;
  latitude: number;
  longitude: number;
  category: string;
}

private favoritePois: FavoritePoi[] = [];

// 添加收藏
private addFavorite(poi: PoiItem): void {
  const latLon = poi.getLatLonPoint();
  if (latLon) {
    const favorite: FavoritePoi = {
      id: Date.now().toString(),
      name: poi.getTitle(),
      latitude: latLon.getLatitude(),
      longitude: latLon.getLongitude(),
      category: this.poiKeyword
    };

    this.favoritePois.push(favorite);

    // 保存到本地存储
    StorageUtil.set('favoritePois', JSON.stringify(this.favoritePois));

    promptAction.showToast({
      message: '已添加到收藏',
      duration: 2000
    });
  }
}

九、性能优化建议

9.1 路线缓存

缓存已计算的路线,避免重复计算:

typescript 复制代码
private routeCache: Map<string, DriveRouteResult> = new Map();

private getCacheKey(startLat: number, startLon: number, endLat: number, endLon: number): string {
  return `${startLat.toFixed(4)},${startLon.toFixed(4)}-${endLat.toFixed(4)},${endLon.toFixed(4)}`;
}

private searchDriveRoute(startLat: number, startLon: number, endLat: number, endLon: number): void {
  const cacheKey = this.getCacheKey(startLat, startLon, endLat, endLon);

  // 检查缓存
  if (this.routeCache.has(cacheKey)) {
    console.info('[FieldMapPage] 使用缓存的路线');
    const cachedResult = this.routeCache.get(cacheKey);
    this.handleRouteResult(cachedResult, AMapException.CODE_AMAP_SUCCESS);
    return;
  }

  // 正常搜索...
}

9.2 POI标记优化

当POI数量较多时,使用聚合显示:

typescript 复制代码
// 根据地图缩放级别决定是否显示标记
private updatePoiMarkers(zoom: number): void {
  if (zoom < 12) {
    // 缩放级别较小时,隐藏标记或显示聚合
    for (const marker of this.poiMarkers) {
      marker.setVisible(false);
    }
  } else {
    // 缩放级别较大时,显示所有标记
    for (const marker of this.poiMarkers) {
      marker.setVisible(true);
    }
  }
}

9.3 搜索防抖

避免频繁搜索:

typescript 复制代码
private searchTimer: number = -1;

private debouncedSearch(keyword: string): void {
  // 清除之前的定时器
  if (this.searchTimer !== -1) {
    clearTimeout(this.searchTimer);
  }

  // 设置新的定时器
  this.searchTimer = setTimeout(() => {
    this.searchNearbyPoi(keyword);
  }, 500); // 500ms延迟
}

十、总结

本篇教程完成了地图导航与路线规划功能的实现,主要包括:

✅ 已实现功能

功能 说明
驾车路线规划 从当前位置到目标地块的路线计算
路线可视化 在地图上绘制蓝色路线
POI周边搜索 搜索农资店、农机租赁、加油站等
搜索结果展示 列表+地图标记双重展示
天气查询 基于当前位置的实时天气
交互优化 点击地块触发导航、POI导航

🎯 核心技术点

  1. RouteSearch:路线搜索服务
  2. DriveRouteQuery:驾车路线查询配置
  3. Polyline:路线绘制
  4. PoiSearch:POI搜索服务
  5. PoiSearchBound:圆形搜索范围
  6. WeatherSearch:天气查询服务

📊 数据流程

复制代码
用户操作 → 搜索服务 → 异步回调 → 结果处理 → UI更新
   ↓
点击地块
   ↓
获取坐标 → RouteSearch → 路线结果 → 绘制Polyline
   ↓
点击搜索
   ↓
输入关键词 → PoiSearch → POI结果 → 添加Marker

🚀 下一步

在下一篇教程中,我们将学习:

  • HarmonyOS AI能力集成
  • Vision Kit图像识别
  • 病虫害识别功能
  • 作物健康检测

教程版本 :v1.0
更新日期 :2024-01
作者:高高种地开发团队

相关资源


附录:完整代码示例

A. 路线规划完整代码

typescript 复制代码
/**
 * 地图页面 - 路线规划功能
 */
@Component
export struct FieldMapPage {
  // 路线搜索相关
  private routeSearch: RouteSearch | null = null;
  private routePolyline: Polyline | null = null;
  @State showRouteDialog: boolean = false;
  @State routeDistance: string = '';
  @State routeTime: string = '';

  aboutToAppear(): void {
    const context = getContext(this) as Context;
    this.routeSearch = new RouteSearch(context);
  }

  // 规划路线
  private planRouteToField(fieldId: string): void {
    const field = this.fields.find(f => f.id === fieldId);
    if (field && this.currentLocationInfo) {
      this.searchDriveRoute(
        this.currentLocationInfo.latitude!,
        this.currentLocationInfo.longitude!,
        field.latitude!,
        field.longitude!
      );
    }
  }

  // 搜索驾车路线
  private searchDriveRoute(
    startLat: number, startLon: number,
    endLat: number, endLon: number
  ): void {
    if (!this.routeSearch) return;

    const startPoint = new LatLonPoint(startLat, startLon);
    const endPoint = new LatLonPoint(endLat, endLon);
    const fromAndTo = new FromAndTo(startPoint, endPoint);

    const query = new DriveRouteQuery(
      fromAndTo, 0,
      new ArrayList<LatLonPoint>(),
      new ArrayList<ArrayList<LatLonPoint>>(),
      ''
    );

    const listener: OnRouteSearchListener = {
      onDriveRouteSearched: (result, errorCode) => {
        this.handleRouteResult(result, errorCode);
      },
      onWalkRouteSearched: () => {},
      onRideRouteSearched: () => {},
      onBusRouteSearched: () => {}
    };

    this.routeSearch.setRouteSearchListener(listener);
    this.routeSearch.calculateDriveRouteAsyn(query);
  }

  // 处理路线结果
  private handleRouteResult(result: DriveRouteResult | undefined, errorCode: number): void {
    if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
      const paths = result.getPaths();
      if (paths && paths.length > 0) {
        const path = paths[0];
        this.routeDistance = `${(path.getDistance() / 1000).toFixed(2)}公里`;
        this.routeTime = `${Math.floor(path.getDuration() / 60)}分钟`;
        this.drawRoute(path.getSteps());
        this.showRouteDialog = true;
      }
    }
  }

  // 绘制路线
  private drawRoute(steps: ESObject): void {
    if (!globalAMap) return;

    if (this.routePolyline) {
      this.routePolyline.remove();
    }

    const polylineOptions = new PolylineOptions();
    const stepsArray: ESObject = steps;
    const length: number = stepsArray.length as number;

    for (let i = 0; i < length; i++) {
      const step: ESObject = stepsArray[i] as ESObject;
      const polyline: ESObject = step.getPolyline?.() as ESObject;
      if (polyline) {
        const polylineLength: number = polyline.length as number;
        for (let j = 0; j < polylineLength; j++) {
          const point: ESObject = polyline[j] as ESObject;
          const lat: number = point.getLatitude?.() as number;
          const lon: number = point.getLongitude?.() as number;
          if (lat && lon) {
            polylineOptions.add(new LatLng(lat, lon));
          }
        }
      }
    }

    polylineOptions.setWidth(12);
    polylineOptions.setColor(0xFF2196F3);
    polylineOptions.setZIndex(50);

    this.routePolyline = globalAMap.addPolyline(polylineOptions);
  }
}

B. POI搜索完整代码

typescript 复制代码
/**
 * 地图页面 - POI搜索功能
 */
@Component
export struct FieldMapPage {
  // POI搜索相关
  private poiSearch: PoiSearch | null = null;
  private poiMarkers: Marker[] = [];
  @State poiResults: PoiItem[] = [];
  @State searchRadius: number = 5000;

  aboutToAppear(): void {
    const context = getContext(this) as Context;
    this.poiSearch = new PoiSearch(context, undefined);
  }

  // 搜索周边POI
  private searchNearbyPoi(keyword: string): void {
    if (!this.poiSearch || !this.currentLocationInfo) return;

    const centerPoint = new LatLonPoint(
      this.currentLocationInfo.latitude!,
      this.currentLocationInfo.longitude!
    );

    const searchBound = PoiSearchBound.createCircleSearchBound(
      centerPoint,
      this.searchRadius
    );

    const query = new PoiQuery(keyword, '', '');
    query.setPageSize(20);
    query.setPageNum(0);

    const listener: OnPoiSearchListener = {
      onPoiSearched: (result, errorCode) => {
        this.handlePoiResult(result, errorCode);
      },
      onPoiItemSearched: () => {}
    };

    this.poiSearch.setOnPoiSearchListener(listener);
    this.poiSearch.setBound(searchBound);
    this.poiSearch.setQuery(query);
    this.poiSearch.searchPOIAsyn();
  }

  // 处理POI结果
  private handlePoiResult(result: PoiResult | undefined, errorCode: number): void {
    if (!globalAMap) return;

    this.clearPoiResults();

    if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
      const pois = result.getPois();
      if (pois && pois.length > 0) {
        this.poiResults = [];
        for (let i = 0; i < pois.length; i++) {
          this.poiResults.push(pois[i]);
        }

        // 添加标记
        for (const poi of this.poiResults) {
          const latLon = poi.getLatLonPoint();
          if (latLon) {
            const markerOptions = new MarkerOptions();
            markerOptions.setPosition(
              new LatLng(latLon.getLatitude(), latLon.getLongitude())
            );
            markerOptions.setTitle(poi.getTitle());
            markerOptions.setIcon(
              BitmapDescriptorFactory.defaultMarker(
                BitmapDescriptorFactory.HUE_MAGENTA
              )
            );

            const marker = globalAMap.addMarker(markerOptions);
            if (marker) {
              this.poiMarkers.push(marker);
            }
          }
        }
      }
    }
  }

  // 清除POI结果
  private clearPoiResults(): void {
    for (const marker of this.poiMarkers) {
      marker.remove();
    }
    this.poiMarkers = [];
    this.poiResults = [];
  }
}

恭喜! 🎉 你已经完成了地图导航与路线规划功能的学习。现在你的应用具备了完整的导航能力,可以帮助农户快速找到农田和周边服务设施。

相关推荐
Easonmax2 小时前
基础入门 React Native 鸿蒙跨平台开发:栈操作可视化
react native·react.js·harmonyos
Easonmax2 小时前
基础入门 React Native 鸿蒙跨平台开发:链表操作可视化
react native·链表·harmonyos
AirDroid_cn2 小时前
鸿蒙NEXT:如何拦截第三方应用读取剪贴板内容?
华为·harmonyos
Easonmax2 小时前
基础入门 React Native 鸿蒙跨平台开发:简单模拟一个加油站
react native·react.js·harmonyos
不爱吃糖的程序媛2 小时前
Flutter OpenHarmony化工程的目录结构详解
flutter·华为·harmonyos
Easonmax2 小时前
基础入门 React Native 鸿蒙跨平台开发:八皇后问题可视化
react native·react.js·harmonyos
芒鸽2 小时前
鸿蒙应用自动化资源同步:Kuikly框架资源复制解决方案
华为·kotlin·自动化·harmonyos·kuikly
Danileaf_Guo2 小时前
超越ODL:直接使用ncclient通过NETCONF配置华为设备,实现真正的基础设施即代码
华为