HarmonyOS开发:室内导航与路径规划

举报
Jack20 发表于 2026/06/22 11:59:34 2026/06/22
【摘要】 HarmonyOS开发:室内导航与路径规划核心要点:本文深入讲解基于HarmonyOS的室内导航与路径规划技术,涵盖室内地图数据模型、A*与Dijkstra路径规划算法、导航状态机设计、航位推算(PDR)辅助导航,以及完整的ArkTS代码实现。将定位能力转化为实用的室内导航服务。项目说明开发语言ArkTS核心APICanvas绘制、@ohos.sensor(PDR传感器)导航精度1-3米(...

HarmonyOS开发:室内导航与路径规划

核心要点:本文深入讲解基于HarmonyOS的室内导航与路径规划技术,涵盖室内地图数据模型、A*与Dijkstra路径规划算法、导航状态机设计、航位推算(PDR)辅助导航,以及完整的ArkTS代码实现。将定位能力转化为实用的室内导航服务。

项目 说明
开发语言 ArkTS
核心API Canvas绘制、@ohos.sensor(PDR传感器)
导航精度 1-3米(融合定位)
官方文档 传感器开发指导

一、背景与动机

1.1 从定位到导航的跨越

前几篇文章解决了"我在哪里"的问题,但室内定位的最终价值在于"如何到达目的地"。室内导航是将定位能力转化为用户价值的关键环节——用户需要的不是一组坐标数字,而是一条清晰的行走路线和实时的导航指引。

1.2 室内导航 vs 室外导航

对比维度 室内导航 室外导航
地图数据 建筑内部平面图 道路网络
定位源 蓝牙/WiFi/UWB GPS/BDS
路径约束 走廊、门、电梯 道路、交通规则
垂直维度 多楼层切换 基本忽略
精度要求 1-3m 5-10m
更新频率 实时(1-2Hz) 中等(1Hz)
地图更新 频繁(装修/布局变化) 较少

1.3 室内导航系统架构

flowchart TB
    subgraph 输入层["输入层"]
        direction LR
        A1["室内定位<br/>蓝牙/WiFi/UWB"]
        A2["传感器<br/>PDR/陀螺仪"]
        A3["用户交互<br/>目的地选择"]
    end

    subgraph 核心层["核心引擎"]
        direction TB
        B1["室内地图引擎"] --> B2["路径规划引擎"]
        B2 --> B3["导航状态机"]
        B3 --> B4["航向推算"]
        B4 --> B5["指令生成"]
    end

    subgraph 输出层["输出层"]
        direction LR
        C1["地图渲染"]
        C2["语音播报"]
        C3["振动提示"]
    end

    A1 --> B4
    A2 --> B4
    A3 --> B2
    B1 --> B2
    B5 --> C1
    B5 --> C2
    B5 --> C3

    classDef input fill:#533483,stroke:#e94560,color:#eee,stroke-width:2px
    classDef core fill:#16213e,stroke:#0f3460,color:#eee,stroke-width:2px
    classDef output fill:#0f3460,stroke:#4ade80,color:#eee,stroke-width:2px

    class A1,A2,A3 input
    class B1,B2,B3,B4,B5 core
    class C1,C2,C3 output

二、核心原理

2.1 室内地图数据模型

室内地图是导航的基础,需要表达建筑内部的拓扑结构和几何信息。

关键数据结构

  • 楼层平面图(FloorPlan):每层的几何轮廓和背景图
  • 可通行区域(WalkableArea):走廊、大厅等可步行区域
  • 不可通行区域(Obstacle):墙壁、柱子、家具等障碍物
  • 导航节点(NavNode):路径网络的节点(交叉口、门口等)
  • 导航边(NavEdge):节点之间的可通行路径
  • 兴趣点(POI):商店、办公室、卫生间等
  • 设施点(Facility):电梯、楼梯、扶梯等楼层连接点

2.2 路径规划算法

室内路径规划的核心是在导航图(NavGraph)上寻找最短路径。

Dijkstra算法:经典最短路径算法,保证全局最优,但搜索范围大。

A*算法:在Dijkstra基础上加入启发式函数,搜索效率显著提升。

f(n)=g(n)+h(n)f(n) = g(n) + h(n)

其中g(n)g(n)为从起点到节点nn的实际代价,h(n)h(n)为从节点nn到终点的启发式估计代价。

室内导航中,h(n)h(n)通常使用欧氏距离:

h(n)=(xnxgoal)2+(ynygoal)2h(n) = \sqrt{(x_n - x_{goal})^2 + (y_n - y_{goal})^2}

多楼层路径规划:将楼梯/电梯作为楼层间的"传送门"边,统一建模为图搜索问题。

2.3 导航状态机

导航过程可建模为有限状态机,管理导航的各个阶段:

stateDiagram-v2
    [*] --> Idle: 用户打开应用
    Idle --> Planning: 选择目的地
    Planning --> Guiding: 路径计算完成
    Planning --> Idle: 无可达路径
    Guiding --> Rerouting: 偏离路线
    Rerouting --> Guiding: 重新规划成功
    Guiding --> Arrived: 到达目的地
    Guiding --> Paused: 用户暂停
    Paused --> Guiding: 用户继续
    Arrived --> Idle: 导航结束

    classDef idle fill:#1a1a2e,stroke:#94a3b8,color:#eee
    classDef active fill:#16213e,stroke:#0f3460,color:#eee
    classDef alert fill:#0f3460,stroke:#e94560,color:#eee
    classDef success fill:#0f3460,stroke:#4ade80,color:#eee

2.4 航位推算(PDR)

PDR(Pedestrian Dead Reckoning,行人航位推算)利用手机传感器推算行人位置变化,辅助定位系统:

步频检测:通过加速度计检测步行周期
步长估计:根据步频和加速度特征估计步长
航向估计:通过陀螺仪和磁力计估计行走方向

pk+1=pk+Lk(cosθksinθk)p_{k+1} = p_k + L_k \cdot \begin{pmatrix} \cos\theta_k \\ \sin\theta_k \end{pmatrix}

其中LkL_k为步长,θk\theta_k为航向角。

PDR的优势是短期精度高(不受信号遮挡影响),但误差会累积,必须与绝对定位(蓝牙/WiFi/UWB)融合使用。


三、代码实战

3.1 室内地图数据模型

// IndoorMapModel.ets - 室内地图数据模型

// 导航节点类型
enum NavNodeType {
  INTERSECTION = 'intersection',  // 交叉口
  DOOR = 'door',                  // 门口
  ELEVATOR = 'elevator',          // 电梯
  STAIRS = 'stairs',              // 楼梯
  ESCALATOR = 'escalator',        // 扶梯
  POI = 'poi',                    // 兴趣点
  WAYPOINT = 'waypoint',          // 路径点
}

// 导航节点
interface NavNode {
  id: string;
  type: NavNodeType;
  x: number;           // 像素坐标X
  y: number;           // 像素坐标Y
  floor: number;       // 楼层
  building: string;    // 建筑
  name?: string;       // 名称
  connectedFloors?: number[];  // 连接的楼层(电梯/楼梯)
  poiId?: string;      // 关联的POI ID
}

// 导航边属性
interface NavEdgeAttr {
  length: number;      // 长度(米)
  width: number;       // 宽度(米)
  isAccessible: boolean;  // 是否可通行
  isWheelchair: boolean;  // 轮椅可通行
  speedFactor: number;   // 速度系数(1.0=正常, <1.0=慢)
}

// 导航边
interface NavEdge {
  fromId: string;
  toId: string;
  attr: NavEdgeAttr;
}

// 兴趣点
interface POI {
  id: string;
  name: string;
  category: string;    // 类别:shop/office/restroom/food...
  x: number;
  y: number;
  floor: number;
  building: string;
  navNodeId: string;   // 关联的导航节点ID
  description?: string;
  icon?: string;
}

// 楼层平面图
interface FloorPlan {
  building: string;
  floor: number;
  name: string;        // "B1", "1F", "2F"...
  width: number;       // 像素宽度
  height: number;      // 像素高度
  scale: number;       // 像素/米 比例尺
  nodes: NavNode[];
  edges: NavEdge[];
  pois: POI[];
  obstacles: Array<{   // 障碍物多边形
    points: Array<{ x: number; y: number }>;
    type: string;      // wall/pillar/furniture
  }>;
}

// 室内地图
export class IndoorMap {
  private floors: Map<string, FloorPlan> = new Map();  // key: "building_floor"
  private nodeIndex: Map<string, NavNode> = new Map(); // 快速查找节点
  private adjacencyList: Map<string, Array<{
    nodeId: string;
    edge: NavEdge;
  }>> = new Map();  // 邻接表

  // 添加楼层平面图
  addFloorPlan(plan: FloorPlan): void {
    const key = `${plan.building}_${plan.floor}`;
    this.floors.set(key, plan);

    // 构建索引
    for (const node of plan.nodes) {
      this.nodeIndex.set(node.id, node);
    }

    // 构建邻接表
    for (const edge of plan.edges) {
      if (!this.adjacencyList.has(edge.fromId)) {
        this.adjacencyList.set(edge.fromId, []);
      }
      this.adjacencyList.get(edge.fromId)!.push({
        nodeId: edge.toId,
        edge
      });

      // 无向图,添加反向边
      if (!this.adjacencyList.has(edge.toId)) {
        this.adjacencyList.set(edge.toId, []);
      }
      this.adjacencyList.get(edge.toId)!.push({
        nodeId: edge.fromId,
        edge
      });
    }

    console.info(`[IndoorMap] 加载楼层: ${key}, ${plan.nodes.length}节点, ${plan.edges.length}`);
  }

  // 获取楼层平面图
  getFloorPlan(building: string, floor: number): FloorPlan | undefined {
    return this.floors.get(`${building}_${floor}`);
  }

  // 获取节点
  getNode(nodeId: string): NavNode | undefined {
    return this.nodeIndex.get(nodeId);
  }

  // 获取邻接节点
  getAdjacentNodes(nodeId: string): Array<{ nodeId: string; edge: NavEdge }> {
    return this.adjacencyList.get(nodeId) || [];
  }

  // 获取所有POI
  getAllPOIs(building: string, floor: number): POI[] {
    const plan = this.getFloorPlan(building, floor);
    return plan?.pois || [];
  }

  // 按类别搜索POI
  searchPOIs(category: string, building: string, floor?: number): POI[] {
    const results: POI[] = [];
    for (const plan of this.floors.values()) {
      if (plan.building !== building) continue;
      if (floor !== undefined && plan.floor !== floor) continue;
      for (const poi of plan.pois) {
        if (poi.category === category) {
          results.push(poi);
        }
      }
    }
    return results;
  }

  // 按名称搜索POI
  searchPOIByName(name: string, building: string): POI[] {
    const results: POI[] = [];
    for (const plan of this.floors.values()) {
      if (plan.building !== building) continue;
      for (const poi of plan.pois) {
        if (poi.name.includes(name)) {
          results.push(poi);
        }
      }
    }
    return results;
  }

  // 查找最近的导航节点(给定坐标)
  findNearestNode(x: number, y: number, floor: number, building: string): NavNode | null {
    let nearestNode: NavNode | null = null;
    let minDist = Infinity;

    const plan = this.getFloorPlan(building, floor);
    if (!plan) return null;

    for (const node of plan.nodes) {
      const dist = Math.sqrt((node.x - x) ** 2 + (node.y - y) ** 2);
      if (dist < minDist) {
        minDist = dist;
        nearestNode = node;
      }
    }

    return nearestNode;
  }

  // 获取所有楼层
  getFloors(building: string): number[] {
    const floors: number[] = [];
    for (const key of this.floors.keys()) {
      if (key.startsWith(`${building}_`)) {
        floors.push(parseInt(key.split('_')[1]));
      }
    }
    return floors.sort((a, b) => a - b);
  }
}

3.2 A*路径规划算法

// PathPlanner.ets - A*路径规划引擎
import { IndoorMap, NavNode, NavEdge } from './IndoorMapModel';

// 路径段
interface PathSegment {
  fromNode: NavNode;
  toNode: NavNode;
  edge: NavEdge;
  direction: string;     // 方向描述
  distance: number;      // 距离(米)
  isFloorChange: boolean; // 是否楼层切换
}

// 导航路径
interface NavigationPath {
  segments: PathSegment[];
  totalDistance: number;   // 总距离(米)
  estimatedTime: number;   // 预计时间(秒)
  floorChanges: number;    // 楼层切换次数
  startNode: NavNode;
  endNode: NavNode;
  waypoints: Array<{ x: number; y: number; floor: number }>;  // 路径点坐标
}

// A*搜索节点
interface AStarNode {
  nodeId: string;
  gCost: number;        // 实际代价
  hCost: number;        // 启发式代价
  fCost: number;        // 总代价
  parentId: string | null;
  floor: number;
  building: string;
}

export class PathPlanner {
  private map: IndoorMap;
  private averageWalkSpeed: number = 1.2; // 平均步行速度 m/s

  constructor(map: IndoorMap) {
    this.map = map;
  }

  // A*路径规划
  planPath(
    startNodeId: string,
    endNodeId: string,
    options?: { avoidStairs?: boolean; wheelchair?: boolean }
  ): NavigationPath | null {
    const startNode = this.map.getNode(startNodeId);
    const endNode = this.map.getNode(endNodeId);

    if (!startNode || !endNode) {
      console.error('[PathPlanner] 起点或终点节点不存在');
      return null;
    }

    // A*搜索
    const openSet: Map<string, AStarNode> = new Map();
    const closedSet: Set<string> = new Set();

    const startAStar: AStarNode = {
      nodeId: startNodeId,
      gCost: 0,
      hCost: this.heuristic(startNode, endNode),
      fCost: this.heuristic(startNode, endNode),
      parentId: null,
      floor: startNode.floor,
      building: startNode.building
    };

    openSet.set(startNodeId, startAStar);

    let iterations = 0;
    const maxIterations = 10000;

    while (openSet.size > 0 && iterations < maxIterations) {
      iterations++;

      // 选择fCost最小的节点
      let currentId = '';
      let minFCost = Infinity;
      for (const [id, node] of openSet) {
        if (node.fCost < minFCost || (node.fCost === minFCost && node.hCost < (openSet.get(currentId)?.hCost ?? Infinity))) {
          minFCost = node.fCost;
          currentId = id;
        }
      }

      const current = openSet.get(currentId)!;

      // 到达终点
      if (currentId === endNodeId) {
        return this.reconstructPath(current, startNodeId, endNodeId);
      }

      // 移到关闭集
      openSet.delete(currentId);
      closedSet.add(currentId);

      // 遍历邻接节点
      const neighbors = this.map.getAdjacentNodes(currentId);
      for (const neighbor of neighbors) {
        if (closedSet.has(neighbor.nodeId)) continue;

        // 检查可通行性
        if (!neighbor.edge.attr.isAccessible) continue;

        // 轮椅模式检查
        if (options?.wheelchair && !neighbor.edge.attr.isWheelchair) continue;

        // 避免楼梯检查
        const neighborNode = this.map.getNode(neighbor.nodeId);
        if (!neighborNode) continue;
        if (options?.avoidStairs && neighborNode.type === 'stairs') continue;

        // 计算代价
        const edgeCost = neighbor.edge.attr.length / neighbor.edge.attr.speedFactor;

        // 楼层切换额外代价(模拟等电梯/走楼梯的时间)
        let floorChangeCost = 0;
        if (neighborNode.floor !== current.floor) {
          floorChangeCost = neighborNode.type === 'elevator' ? 30 : 45; // 电梯30秒,楼梯45秒
        }

        const tentativeG = current.gCost + edgeCost + floorChangeCost;

        const existingNeighbor = openSet.get(neighbor.nodeId);
        if (existingNeighbor && tentativeG >= existingNeighbor.gCost) {
          continue;
        }

        const hCost = this.heuristic(neighborNode, endNode);
        const aStarNode: AStarNode = {
          nodeId: neighbor.nodeId,
          gCost: tentativeG,
          hCost,
          fCost: tentativeG + hCost,
          parentId: currentId,
          floor: neighborNode.floor,
          building: neighborNode.building
        };

        openSet.set(neighbor.nodeId, aStarNode);
      }
    }

    console.warn('[PathPlanner] 未找到路径');
    return null;
  }

  // 启发式函数(欧氏距离)
  private heuristic(from: NavNode, to: NavNode): number {
    const dx = from.x - to.x;
    const dy = from.y - to.y;
    const floorDiff = Math.abs(from.floor - to.floor);

    // 同楼层用欧氏距离,跨楼层加上楼层距离
    const planarDist = Math.sqrt(dx * dx + dy * dy);
    const floorDist = floorDiff * 3.5; // 每层约3.5米高度

    return Math.sqrt(planarDist * planarDist + floorDist * floorDist);
  }

  // 重建路径
  private reconstructPath(
    endNode: AStarNode,
    startNodeId: string,
    endNodeId: string
  ): NavigationPath {
    const path: AStarNode[] = [];
    let current: AStarNode | undefined = endNode;

    while (current) {
      path.unshift(current);
      if (current.nodeId === startNodeId) break;
      const parentId = current.parentId;
      if (!parentId) break;
      // 需要从closedSet或openSet中找到父节点
      // 简化处理:直接从path中查找
      current = undefined; // 实际实现需要维护完整的节点映射
    }

    // 构建路径段
    const segments: PathSegment[] = [];
    const waypoints: Array<{ x: number; y: number; floor: number }> = [];
    let totalDistance = 0;
    let floorChanges = 0;

    for (let i = 0; i < path.length - 1; i++) {
      const fromNode = this.map.getNode(path[i].nodeId)!;
      const toNode = this.map.getNode(path[i + 1].nodeId)!;

      // 查找边
      const adjacents = this.map.getAdjacentNodes(fromNode.id);
      const edge = adjacents.find(a => a.nodeId === toNode.id)?.edge;

      if (edge) {
        const isFloorChange = fromNode.floor !== toNode.floor;
        if (isFloorChange) floorChanges++;

        const direction = this.calculateDirection(fromNode, toNode);
        const distance = edge.attr.length;

        segments.push({
          fromNode,
          toNode,
          edge,
          direction,
          distance,
          isFloorChange
        });

        totalDistance += distance;
      }

      waypoints.push({ x: fromNode.x, y: fromNode.y, floor: fromNode.floor });
    }

    // 添加最后一个点
    const lastNode = this.map.getNode(path[path.length - 1].nodeId)!;
    waypoints.push({ x: lastNode.x, y: lastNode.y, floor: lastNode.floor });

    const estimatedTime = totalDistance / this.averageWalkSpeed;

    return {
      segments,
      totalDistance: Math.round(totalDistance * 10) / 10,
      estimatedTime: Math.round(estimatedTime),
      floorChanges,
      startNode: this.map.getNode(startNodeId)!,
      endNode: this.map.getNode(endNodeId)!,
      waypoints
    };
  }

  // 计算方向描述
  private calculateDirection(from: NavNode, to: NavNode): string {
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    const angle = Math.atan2(dy, dx) * 180 / Math.PI;

    if (angle >= -22.5 && angle < 22.5) return '向东';
    if (angle >= 22.5 && angle < 67.5) return '向东南';
    if (angle >= 67.5 && angle < 112.5) return '向南';
    if (angle >= 112.5 && angle < 157.5) return '向西南';
    if (angle >= 157.5 || angle < -157.5) return '向西';
    if (angle >= -157.5 && angle < -112.5) return '向西北';
    if (angle >= -112.5 && angle < -67.5) return '向北';
    if (angle >= -67.5 && angle < -22.5) return '向东北';

    return '直行';
  }

  // 多楼层路径规划
  planCrossFloorPath(
    startNodeId: string,
    endNodeId: string,
    options?: { preferElevator?: boolean; wheelchair?: boolean }
  ): NavigationPath | null {
    const startNode = this.map.getNode(startNodeId);
    const endNode = this.map.getNode(endNodeId);

    if (!startNode || !endNode) return null;

    // 同楼层直接规划
    if (startNode.floor === endNode.floor && startNode.building === endNode.building) {
      return this.planPath(startNodeId, endNodeId, options);
    }

    // 跨楼层:查找楼层连接点(电梯/楼梯)
    return this.planPath(startNodeId, endNodeId, {
      ...options,
      avoidStairs: options?.preferElevator
    });
  }
}

3.3 导航状态机与指令生成

// NavigationEngine.ets - 导航状态机与指令生成
import { IndoorMap, NavNode } from './IndoorMapModel';
import { PathPlanner, NavigationPath, PathSegment } from './PathPlanner';

// 导航状态
enum NavState {
  IDLE = 'idle',
  PLANNING = 'planning',
  GUIDING = 'guiding',
  REROUTING = 'rerouting',
  PAUSED = 'paused',
  ARRIVED = 'arrived',
}

// 导航指令
interface NavigationInstruction {
  type: 'turn' | 'straight' | 'floor_change' | 'arrive' | 'reroute';
  text: string;           // 指令文本
  detailText: string;     // 详细描述
  distanceToNext: number; // 到下一指令的距离(米)
  icon: string;           // 指令图标标识
  floor: number;          // 当前楼层
  isUrgent: boolean;      // 是否需要立即执行
}

// 导航进度
interface NavigationProgress {
  state: NavState;
  currentSegmentIndex: number;
  completedDistance: number;
  remainingDistance: number;
  totalDistance: number;
  progressPercent: number;
  currentInstruction: NavigationInstruction | null;
  nextInstruction: NavigationInstruction | null;
  estimatedArrivalTime: number;  // 秒
  offRouteDistance: number;       // 偏离距离(米)
}

export class NavigationEngine {
  private map: IndoorMap;
  private planner: PathPlanner;
  private state: NavState = NavState.IDLE;
  private currentPath: NavigationPath | null = null;
  private currentSegmentIndex: number = 0;
  private userPosition: { x: number; y: number; floor: number } | null = null;
  private offRouteThreshold: number = 5.0;  // 偏离阈值(米)
  private onStateChange?: (state: NavState) => void;
  private onInstructionUpdate?: (instruction: NavigationInstruction) => void;
  private onProgressUpdate?: (progress: NavigationProgress) => void;

  constructor(map: IndoorMap) {
    this.map = map;
    this.planner = new PathPlanner(map);
  }

  // 设置回调
  setCallbacks(callbacks: {
    onStateChange?: (state: NavState) => void;
    onInstructionUpdate?: (instruction: NavigationInstruction) => void;
    onProgressUpdate?: (progress: NavigationProgress) => void;
  }): void {
    this.onStateChange = callbacks.onStateChange;
    this.onInstructionUpdate = callbacks.onInstructionUpdate;
    this.onProgressUpdate = callbacks.onProgressUpdate;
  }

  // 开始导航
  startNavigation(
    startNodeId: string,
    endNodeId: string,
    options?: { preferElevator?: boolean; wheelchair?: boolean }
  ): boolean {
    this.setState(NavState.PLANNING);

    const path = this.planner.planCrossFloorPath(startNodeId, endNodeId, options);
    if (!path) {
      this.setState(NavState.IDLE);
      console.error('[NavEngine] 路径规划失败');
      return false;
    }

    this.currentPath = path;
    this.currentSegmentIndex = 0;
    this.setState(NavState.GUIDING);

    // 发送第一条指令
    this.emitCurrentInstruction();

    console.info(`[NavEngine] 导航开始: ${path.totalDistance}m, 约${path.estimatedTime}`);
    return true;
  }

  // 更新用户位置
  updatePosition(x: number, y: number, floor: number): void {
    this.userPosition = { x, y, floor };

    if (this.state !== NavState.GUIDING || !this.currentPath) {
      return;
    }

    // 检查是否偏离路线
    const offRouteDistance = this.calculateOffRouteDistance(x, y, floor);
    if (offRouteDistance > this.offRouteThreshold) {
      this.handleOffRoute();
      return;
    }

    // 更新路径段进度
    this.updateSegmentProgress(x, y, floor);

    // 发送进度更新
    this.emitProgress(offRouteDistance);
  }

  // 计算偏离路线距离
  private calculateOffRouteDistance(x: number, y: number, floor: number): number {
    if (!this.currentPath) return Infinity;

    // 检查当前楼层是否匹配
    const currentSegment = this.currentPath.segments[this.currentSegmentIndex];
    if (currentSegment && currentSegment.toNode.floor !== floor) {
      return this.offRouteThreshold + 1; // 楼层不匹配视为偏离
    }

    // 计算到当前路径段的最短距离
    let minDist = Infinity;
    for (let i = Math.max(0, this.currentSegmentIndex - 1);
         i < Math.min(this.currentPath.segments.length, this.currentSegmentIndex + 3);
         i++) {
      const seg = this.currentPath.segments[i];
      const dist = this.pointToSegmentDistance(
        x, y,
        seg.fromNode.x, seg.fromNode.y,
        seg.toNode.x, seg.toNode.y
      );
      minDist = Math.min(minDist, dist);
    }

    return minDist;
  }

  // 点到线段的距离
  private pointToSegmentDistance(
    px: number, py: number,
    ax: number, ay: number,
    bx: number, by: number
  ): number {
    const dx = bx - ax;
    const dy = by - ay;
    const lenSq = dx * dx + dy * dy;

    if (lenSq === 0) {
      return Math.sqrt((px - ax) ** 2 + (py - ay) ** 2);
    }

    let t = ((px - ax) * dx + (py - ay) * dy) / lenSq;
    t = Math.max(0, Math.min(1, t));

    const projX = ax + t * dx;
    const projY = ay + t * dy;

    return Math.sqrt((px - projX) ** 2 + (py - projY) ** 2);
  }

  // 更新路径段进度
  private updateSegmentProgress(x: number, y: number, floor: number): void {
    if (!this.currentPath) return;

    const segment = this.currentPath.segments[this.currentSegmentIndex];
    if (!segment) return;

    // 计算到当前段终点的距离
    const distToEnd = Math.sqrt(
      (x - segment.toNode.x) ** 2 + (y - segment.toNode.y) ** 2
    );

    // 如果接近当前段终点,切换到下一段
    if (distToEnd < 3.0) { // 3米内认为已到达
      this.currentSegmentIndex++;

      if (this.currentSegmentIndex >= this.currentPath.segments.length) {
        // 到达目的地
        this.setState(NavState.ARRIVED);
        this.emitInstruction({
          type: 'arrive',
          text: '您已到达目的地',
          detailText: this.currentPath.endNode.name || '目的地',
          distanceToNext: 0,
          icon: 'flag',
          floor,
          isUrgent: true
        });
        return;
      }

      // 发送新指令
      this.emitCurrentInstruction();
    }
  }

  // 处理偏离路线
  private handleOffRoute(): void {
    if (this.state === NavState.REROUTING) return;

    this.setState(NavState.REROUTING);

    this.emitInstruction({
      type: 'reroute',
      text: '您已偏离路线,正在重新规划...',
      detailText: '请稍候',
      distanceToNext: 0,
      icon: 'reroute',
      floor: this.userPosition?.floor ?? 1,
      isUrgent: true
    });

    // 重新规划路径
    if (this.userPosition && this.currentPath) {
      const nearestNode = this.map.findNearestNode(
        this.userPosition.x,
        this.userPosition.y,
        this.userPosition.floor,
        this.currentPath.endNode.building
      );

      if (nearestNode) {
        const newPath = this.planner.planPath(
          nearestNode.id,
          this.currentPath.endNode.id
        );

        if (newPath) {
          this.currentPath = newPath;
          this.currentSegmentIndex = 0;
          this.setState(NavState.GUIDING);
          this.emitCurrentInstruction();
        }
      }
    }
  }

  // 生成当前导航指令
  private emitCurrentInstruction(): void {
    if (!this.currentPath) return;

    const segment = this.currentPath.segments[this.currentSegmentIndex];
    if (!segment) return;

    const instruction = this.generateInstruction(segment);
    this.emitInstruction(instruction);
  }

  // 生成导航指令
  private generateInstruction(segment: PathSegment): NavigationInstruction {
    if (segment.isFloorChange) {
      const facilityType = segment.toNode.type;
      const facilityName = facilityType === 'elevator' ? '电梯' :
        facilityType === 'stairs' ? '楼梯' : '扶梯';
      const targetFloor = segment.toNode.floor;
      const floorName = targetFloor < 0 ? `B${Math.abs(targetFloor)}` : `${targetFloor}`;

      return {
        type: 'floor_change',
        text: `乘坐${facilityName}前往${floorName}`,
        detailText: `到达${floorName}后继续前行`,
        distanceToNext: segment.distance,
        icon: facilityType,
        floor: segment.fromNode.floor,
        isUrgent: false
      };
    }

    // 计算转弯方向
    let turnDirection = 'straight';
    if (this.currentSegmentIndex > 0) {
      const prevSegment = this.currentPath!.segments[this.currentSegmentIndex - 1];
      turnDirection = this.calculateTurnDirection(prevSegment, segment);
    }

    const directionMap: Record<string, string> = {
      'left': '左转',
      'right': '右转',
      'slight_left': '稍向左转',
      'slight_right': '稍向右转',
      'sharp_left': '急左转',
      'sharp_right': '急右转',
      'straight': '直行',
      'uturn': '掉头'
    };

    const turnText = directionMap[turnDirection] || '直行';
    const distanceText = segment.distance >= 100 ?
      `${Math.round(segment.distance)}` :
      `${Math.round(segment.distance * 10) / 10}`;

    return {
      type: turnDirection === 'straight' ? 'straight' : 'turn',
      text: `${turnText},前行${distanceText}`,
      detailText: segment.toNode.name ?
        `前往${segment.toNode.name}` : `沿走廊前行`,
      distanceToNext: segment.distance,
      icon: turnDirection,
      floor: segment.fromNode.floor,
      isUrgent: false
    };
  }

  // 计算转弯方向
  private calculateTurnDirection(prev: PathSegment, current: PathSegment): string {
    const prevAngle = Math.atan2(
      prev.toNode.y - prev.fromNode.y,
      prev.toNode.x - prev.fromNode.x
    );
    const currAngle = Math.atan2(
      current.toNode.y - current.fromNode.y,
      current.toNode.x - current.fromNode.x
    );

    let angleDiff = (currAngle - prevAngle) * 180 / Math.PI;
    // 归一化到 -180 ~ 180
    while (angleDiff > 180) angleDiff -= 360;
    while (angleDiff < -180) angleDiff += 360;

    if (Math.abs(angleDiff) < 15) return 'straight';
    if (angleDiff >= 15 && angleDiff < 45) return 'slight_right';
    if (angleDiff >= 45 && angleDiff < 120) return 'right';
    if (angleDiff >= 120) return 'sharp_right';
    if (angleDiff <= -15 && angleDiff > -45) return 'slight_left';
    if (angleDiff <= -45 && angleDiff > -120) return 'left';
    if (angleDiff <= -120) return 'sharp_left';

    return 'straight';
  }

  // 暂停导航
  pauseNavigation(): void {
    if (this.state === NavState.GUIDING) {
      this.setState(NavState.PAUSED);
    }
  }

  // 恢复导航
  resumeNavigation(): void {
    if (this.state === NavState.PAUSED) {
      this.setState(NavState.GUIDING);
    }
  }

  // 停止导航
  stopNavigation(): void {
    this.currentPath = null;
    this.currentSegmentIndex = 0;
    this.setState(NavState.IDLE);
  }

  // 状态变更
  private setState(newState: NavState): void {
    this.state = newState;
    this.onStateChange?.(newState);
  }

  // 发送指令
  private emitInstruction(instruction: NavigationInstruction): void {
    this.onInstructionUpdate?.(instruction);
  }

  // 发送进度
  private emitProgress(offRouteDistance: number): void {
    if (!this.currentPath) return;

    const completedDistance = this.currentPath.segments
      .slice(0, this.currentSegmentIndex)
      .reduce((sum, seg) => sum + seg.distance, 0);

    const remainingDistance = this.currentPath.totalDistance - completedDistance;
    const progressPercent = (completedDistance / this.currentPath.totalDistance) * 100;

    const currentInstruction = this.currentSegmentIndex < this.currentPath.segments.length ?
      this.generateInstruction(this.currentPath.segments[this.currentSegmentIndex]) : null;

    const nextInstruction = (this.currentSegmentIndex + 1) < this.currentPath.segments.length ?
      this.generateInstruction(this.currentPath.segments[this.currentSegmentIndex + 1]) : null;

    this.onProgressUpdate?.({
      state: this.state,
      currentSegmentIndex: this.currentSegmentIndex,
      completedDistance: Math.round(completedDistance * 10) / 10,
      remainingDistance: Math.round(remainingDistance * 10) / 10,
      totalDistance: this.currentPath.totalDistance,
      progressPercent: Math.round(progressPercent),
      currentInstruction,
      nextInstruction,
      estimatedArrivalTime: Math.round(remainingDistance / 1.2),
      offRouteDistance: Math.round(offRouteDistance * 10) / 10
    });
  }

  // 获取当前状态
  getState(): NavState {
    return this.state;
  }

  // 获取当前路径
  getCurrentPath(): NavigationPath | null {
    return this.currentPath;
  }
}

3.4 导航页面UI

// pages/IndoorNavigationPage.ets - 室内导航页面
import { IndoorMap, FloorPlan, POI } from '../service/IndoorMapModel';
import { NavigationEngine, NavState, NavigationInstruction, NavigationProgress } from '../service/NavigationEngine';

@Entry
@Component
struct IndoorNavigationPage {
  @State navState: NavState = NavState.IDLE;
  @State currentInstruction: NavigationInstruction | null = null;
  @State progress: NavigationProgress | null = null;
  @State searchQuery: string = '';
  @State searchResults: POI[] = [];
  @State currentFloor: number = 1;
  @State destinationName: string = '';

  private map: IndoorMap = new IndoorMap();
  private navEngine: NavigationEngine = new NavigationEngine(this.map);

  aboutToAppear(): void {
    this.loadMapData();
    this.setupNavCallbacks();
  }

  // 加载地图数据(模拟数据)
  private loadMapData(): void {
    const plan: FloorPlan = {
      building: 'A栋',
      floor: 1,
      name: '1F',
      width: 800,
      height: 600,
      scale: 20, // 20像素/米
      nodes: [
        { id: 'n1', type: NavNodeType.WAYPOINT, x: 100, y: 300, floor: 1, building: 'A栋' },
        { id: 'n2', type: NavNodeType.INTERSECTION, x: 300, y: 300, floor: 1, building: 'A栋', name: '走廊交叉口' },
        { id: 'n3', type: NavNodeType.INTERSECTION, x: 500, y: 300, floor: 1, building: 'A栋', name: '中央大厅' },
        { id: 'n4', type: NavNodeType.POI, x: 300, y: 150, floor: 1, building: 'A栋', name: '咖啡厅', poiId: 'poi1' },
        { id: 'n5', type: NavNodeType.POI, x: 500, y: 150, floor: 1, building: 'A栋', name: '书店', poiId: 'poi2' },
        { id: 'n6', type: NavNodeType.ELEVATOR, x: 700, y: 300, floor: 1, building: 'A栋', name: '电梯', connectedFloors: [1, 2, 3] },
        { id: 'n7', type: NavNodeType.POI, x: 500, y: 450, floor: 1, building: 'A栋', name: '卫生间', poiId: 'poi3' },
      ],
      edges: [
        { fromId: 'n1', toId: 'n2', attr: { length: 10, width: 2, isAccessible: true, isWheelchair: true, speedFactor: 1.0 } },
        { fromId: 'n2', toId: 'n3', attr: { length: 10, width: 3, isAccessible: true, isWheelchair: true, speedFactor: 1.0 } },
        { fromId: 'n2', toId: 'n4', attr: { length: 7.5, width: 2, isAccessible: true, isWheelchair: true, speedFactor: 1.0 } },
        { fromId: 'n3', toId: 'n5', attr: { length: 7.5, width: 2, isAccessible: true, isWheelchair: true, speedFactor: 1.0 } },
        { fromId: 'n3', toId: 'n6', attr: { length: 10, width: 3, isAccessible: true, isWheelchair: true, speedFactor: 1.0 } },
        { fromId: 'n3', toId: 'n7', attr: { length: 7.5, width: 2, isAccessible: true, isWheelchair: true, speedFactor: 1.0 } },
      ],
      pois: [
        { id: 'poi1', name: '咖啡厅', category: 'food', x: 300, y: 150, floor: 1, building: 'A栋', navNodeId: 'n4' },
        { id: 'poi2', name: '书店', category: 'shop', x: 500, y: 150, floor: 1, building: 'A栋', navNodeId: 'n5' },
        { id: 'poi3', name: '卫生间', category: 'restroom', x: 500, y: 450, floor: 1, building: 'A栋', navNodeId: 'n7' },
      ],
      obstacles: []
    };

    this.map.addFloorPlan(plan);
  }

  // 设置导航回调
  private setupNavCallbacks(): void {
    this.navEngine.setCallbacks({
      onStateChange: (state: NavState) => {
        this.navState = state;
      },
      onInstructionUpdate: (instruction: NavigationInstruction) => {
        this.currentInstruction = instruction;
      },
      onProgressUpdate: (progress: NavigationProgress) => {
        this.progress = progress;
      }
    });
  }

  build() {
    Column() {
      // 顶部状态栏
      this.TopStatusBar()

      // 地图区域
      this.MapArea()

      // 导航指令卡片
      if (this.navState === NavState.GUIDING && this.currentInstruction) {
        this.InstructionCard()
      }

      // 搜索与目的地(非导航状态)
      if (this.navState === NavState.IDLE) {
        this.SearchAndDestination()
      }

      // 进度条(导航中)
      if (this.progress && this.navState === NavState.GUIDING) {
        this.ProgressBar()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a1a')
  }

  @Builder
  TopStatusBar() {
    Row() {
      Text('室内导航')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')
      Blank()
      // 楼层选择
      Row() {
        ForEach([1, 2, 3], (floor: number) => {
          Text(`${floor}F`)
            .fontSize(13)
            .fontColor(this.currentFloor === floor ? '#4ade80' : '#94a3b8')
            .fontWeight(this.currentFloor === floor ? FontWeight.Bold : FontWeight.Normal)
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .borderRadius(8)
            .backgroundColor(this.currentFloor === floor ? 'rgba(74, 222, 128, 0.15)' : 'transparent')
            .onClick(() => { this.currentFloor = floor; })
        })
      }
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 12, bottom: 8 })
  }

  @Builder
  MapArea() {
    Column() {
      // 地图占位(实际应使用Canvas绘制)
      Column() {
        Text('室内地图')
          .fontSize(16)
          .fontColor('#64748b')
        Text(`${this.currentFloor}F 平面图`)
          .fontSize(12)
          .fontColor('#475569')
          .margin({ top: 4 })
      }
      .width('100%')
      .height(300)
      .justifyContent(FlexAlign.Center)
      .borderRadius(16)
      .backgroundColor('rgba(30, 41, 59, 0.5)')
      .border({ width: 1, color: 'rgba(148, 163, 184, 0.1)' })
      .margin({ left: 16, right: 16 })
    }
  }

  @Builder
  InstructionCard() {
    Column() {
      Row() {
        // 指令图标
        Column() {
          Text(this.getInstructionIcon(this.currentInstruction!.icon))
            .fontSize(28)
            .fontColor('#4ade80')
        }
        .width(50)
        .justifyContent(FlexAlign.Center)

        // 指令文本
        Column() {
          Text(this.currentInstruction!.text)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#ffffff')
          Text(this.currentInstruction!.detailText)
            .fontSize(13)
            .fontColor('#94a3b8')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        // 距离
        Column() {
          Text(`${this.currentInstruction!.distanceToNext.toFixed(0)}m`)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4ade80')
        }
        .width(60)
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .margin({ left: 16, right: 16, top: 12 })
    .borderRadius(16)
    .backgroundColor('rgba(30, 41, 59, 0.9)')
    .backdropBlur(20)
    .border({ width: 1, color: 'rgba(74, 222, 128, 0.3)' })
  }

  @Builder
  SearchAndDestination() {
    Column() {
      // 搜索框
      Row() {
        TextInput({ placeholder: '搜索目的地...', text: this.searchQuery })
          .layoutWeight(1)
          .height(44)
          .fontSize(14)
          .fontColor('#ffffff')
          .backgroundColor('rgba(30, 41, 59, 0.8)')
          .borderRadius(12)
          .onChange((value: string) => {
            this.searchQuery = value;
            this.searchResults = this.map.searchPOIByName(value, 'A栋');
          })
      }
      .width('100%')
      .margin({ top: 16 })

      // 搜索结果
      if (this.searchResults.length > 0) {
        List() {
          ForEach(this.searchResults, (poi: POI) => {
            ListItem() {
              Row() {
                Text(poi.name)
                  .fontSize(14)
                  .fontColor('#e2e8f0')
                Blank()
                Text(poi.category)
                  .fontSize(12)
                  .fontColor('#64748b')
              }
              .width('100%')
              .padding(12)
              .borderRadius(10)
              .backgroundColor('rgba(30, 41, 59, 0.5)')
              .onClick(() => {
                this.startNavToPOI(poi);
              })
            }
          }, (poi: POI) => poi.id)
        }
        .width('100%')
        .height(150)
        .margin({ top: 8 })
      }

      // 快捷分类
      Row() {
        this.CategoryButton('🍽️ 餐饮', 'food')
        this.CategoryButton('🛍️ 商店', 'shop')
        this.CategoryButton('🚻 卫生间', 'restroom')
        this.CategoryButton('🛗 电梯', 'elevator')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .margin({ top: 12 })
    }
    .padding({ left: 16, right: 16 })
  }

  @Builder
  CategoryButton(label: string, category: string) {
    Column() {
      Text(label)
        .fontSize(12)
        .fontColor('#e2e8f0')
    }
    .padding({ left: 12, right: 12, top: 8, bottom: 8 })
    .borderRadius(10)
    .backgroundColor('rgba(30, 41, 59, 0.6)')
    .border({ width: 1, color: 'rgba(148, 163, 184, 0.1)' })
    .onClick(() => {
      this.searchResults = this.map.searchPOIs(category, 'A栋', this.currentFloor);
    })
  }

  @Builder
  ProgressBar() {
    Column() {
      // 进度条
      Row() {
        Row()
          .width(`${this.progress!.progressPercent}%`)
          .height(4)
          .borderRadius(2)
          .backgroundColor('#4ade80')
      }
      .width('100%')
      .height(4)
      .borderRadius(2)
      .backgroundColor('rgba(148, 163, 184, 0.2)')

      Row() {
        Text(`剩余 ${this.progress!.remainingDistance}m`)
          .fontSize(12)
          .fontColor('#94a3b8')
        Blank()
        Text(`${this.progress!.estimatedArrivalTime}`)
          .fontSize(12)
          .fontColor('#94a3b8')
      }
      .width('100%')
      .margin({ top: 6 })

      // 停止按钮
      Button('结束导航')
        .width('100%')
        .height(40)
        .fontSize(14)
        .fontColor('#ffffff')
        .backgroundColor('#dc2626')
        .borderRadius(10)
        .margin({ top: 8 })
        .onClick(() => {
          this.navEngine.stopNavigation();
        })
    }
    .padding({ left: 16, right: 16, top: 12, bottom: 16 })
  }

  // 辅助方法
  private getInstructionIcon(icon: string): string {
    const iconMap: Record<string, string> = {
      'straight': '↑',
      'left': '←',
      'right': '→',
      'slight_left': '↖',
      'slight_right': '↗',
      'sharp_left': '↩',
      'sharp_right': '↪',
      'elevator': '🛗',
      'stairs': '🪜',
      'escalator': '↗',
      'flag': '🏁',
      'reroute': '🔄'
    };
    return iconMap[icon] || '→';
  }

  // 开始导航到POI
  private startNavToPOI(poi: POI): void {
    this.destinationName = poi.name;
    // 模拟从入口节点导航到POI节点
    this.navEngine.startNavigation('n1', poi.navNodeId);
  }
}

四、踩坑与注意事项

4.1 室内地图数据获取

问题:室内地图数据是导航的基础,但获取难度大。

解决方案

  1. CAD图纸转换:将建筑CAD图纸转换为导航图
  2. 人工标注:使用地图编辑工具手动标注节点和边
  3. 自动提取:使用计算机视觉从平面图中提取可通行区域
  4. 众包更新:允许用户反馈地图错误,持续修正

4.2 地图坐标系与定位坐标系的统一

问题:地图使用像素坐标,定位输出使用米制坐标,两者必须精确对齐。

解决方案

  • 定义统一的坐标变换参数(原点、旋转角、比例尺)
  • 在地图中标注至少3个已知坐标的锚点用于校准
  • 使用仿射变换处理非正交的建筑布局

4.3 多楼层导航的复杂性

问题:楼层切换是室内导航特有的复杂场景。

注意事项

  1. 电梯等待时间不确定(高峰期可能5-10分钟)
  2. 楼梯/扶梯的通行时间与行人流量相关
  3. 某些电梯可能需要刷卡才能到达特定楼层
  4. 楼层切换时定位信号可能短暂丢失

4.4 导航指令的时机

问题:指令发出过早或过晚都会导致用户走错路。

建议

  • 转弯指令在转弯前5-8米发出
  • 楼层切换指令在到达电梯/楼梯前10米发出
  • 到达目的地指令在距离3米内发出
  • 偏离路线指令在偏离5米后发出

4.5 PDR传感器使用注意

  1. 步频检测阈值:不同人的步态差异大,需要自适应阈值
  2. 磁力计干扰:室内金属结构严重干扰磁力计,航向估计需以陀螺仪为主
  3. 手机持握方式:手持、口袋、通话等不同持握方式影响传感器数据
  4. 传感器采样率:建议加速度计50Hz以上,陀螺仪100Hz以上

五、HarmonyOS 6适配

5.1 传感器API增强

// HarmonyOS 6 传感器融合
import { sensor } from '@kit.SensorServiceKit';

// PDR步频检测
sensor.on(sensor.SensorType.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
  // 使用峰值检测算法识别步频
  processStepDetection(data.x, data.y, data.z);
}, { interval: 20000000 }); // 50Hz

// 陀螺仪航向估计
sensor.on(sensor.SensorType.GYROSCOPE, (data: sensor.GyroscopeResponse) => {
  // 积分角速度得到航向变化
  updateHeading(data.x, data.y, data.z);
}, { interval: 10000000 }); // 100Hz

5.2 地图渲染优化

HarmonyOS 6增强了Canvas渲染能力:

// HarmonyOS 6 Canvas地图渲染
import { rendering } from '@kit.ArkGraphics2D';

// 使用硬件加速Canvas
const canvas = rendering.getCanvas('navMap');
const ctx = canvas.getContext('2d');

// 绘制路径
ctx.beginPath();
ctx.strokeStyle = '#4ade80';
ctx.lineWidth = 4;
for (const waypoint of path.waypoints) {
  ctx.lineTo(waypoint.x, waypoint.y);
}
ctx.stroke();

// 绘制当前位置
ctx.beginPath();
ctx.arc(userX, userY, 8, 0, Math.PI * 2);
ctx.fillStyle = '#3b82f6';
ctx.fill();

5.3 语音导航

// HarmonyOS 6 语音播报
import { textToSpeech } from '@kit.AIKit';

async function speakInstruction(text: string): Promise<void> {
  const ttsEngine = textToSpeech.createEngine('zh-CN');
  await ttsEngine.speak(text, {
    speed: 1.0,
    pitch: 1.0,
    volume: 0.8,
  });
}

六、总结

本文系统讲解了基于HarmonyOS的室内导航与路径规划技术,核心要点回顾:

环节 关键技术 注意事项
地图建模 NavGraph导航图、POI/设施点 坐标系统一、数据获取
路径规划 A*算法、多楼层扩展 启发式函数选择、楼层切换代价
导航状态机 6种状态、偏离重规划 指令时机、偏离阈值
PDR辅助 步频检测、步长估计、航向推算 传感器噪声、持握方式
指令生成 转弯方向计算、距离描述 指令简洁明确、时机恰当

室内导航系统设计原则

  1. 鲁棒性优先:定位信号丢失时PDR兜底,偏离路线自动重规划
  2. 用户体验:指令简洁明确,语音+视觉双重反馈
  3. 可扩展性:地图数据与导航引擎解耦,支持动态加载
  4. 低功耗:传感器按需启用,后台降低采样率

室内导航是室内定位技术的"最后一公里",将冰冷的坐标数据转化为用户可感知的导航服务。下一篇文章将深入定位精度优化与多源融合,探讨如何将多种定位技术的优势整合,实现更精确、更稳定的室内定位。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。