【HarmonyOS 7 平行视界深度实战】03 购物模式怎么实现连续浏览和左右推挤

前言

商品列表进入详情,再从详情进入规格页,这类连续浏览经常需要把上一层内容留作参照。用户可能刚刚从几十件商品中选中一把键盘,进入详情以后又想确认连接方式;也可能在房源列表里打开一套房子,查看户型后继续进入周边信息。手机窗口一次只能容纳一个完整页面,所以常见做法是整页进入详情,再依靠返回按钮找回列表。宽窗口已经能同时放下相邻两层,页面组织方式也可以随之改变。

连续浏览关心的是用户刚才从哪里进入、当前看到哪一项,以及返回以后能不能接着原来的位置继续浏览。列表滚动到第十几项、筛选条件已经生效、当前条目已经选中,这些信息共同组成浏览上下文。每次打开详情都覆盖列表时,用户返回以后还要重新确认位置;相邻页面并排显示后,来路和当前内容会同时留在视野中,列表、详情和下一层信息之间的关系也更容易理解。

平行视界的购物模式会读取 Navigation 路由栈,把栈顶页面与次栈顶页面组织到左右区域,开发者仍然维护一套普通跳转链路。用户继续进入下一层时,原来的右侧页面移动到左侧,新页面出现在右侧,屏幕上的位置变化与路由前进保持同一个方向。商品、资讯和图片素材都可能采用这种结构,因为这些业务经常重复打开一项、查看内容、返回并继续下一项。当前 Demo 选择商品列表、商品详情和规格页,三层关系足以把推进与返回讲清楚,又不会把支付、库存等业务变量带进实验。

一、购物模式怎样选择左右页面

要让上一层持续成为参照,系统需要从路由栈中稳定选出相邻两页,当前配置把 navigationSplitOptions.mode 设为 0,由系统启用购物模式。宽窗口进入平行视界后,当前路由栈的栈顶页面显示在右侧,次栈顶页面显示在左侧。每次 pushPathByName() 都会产生新的栈顶,原右侧页面随之成为次栈顶并进入左侧,因此页面会呈现连续向前推挤的关系。这个过程沿用现有路由顺序,页面代码没有另外维护一份左右位置表。

我们可以把两栏理解成路由栈顶部的一个观察窗口。栈里可能保存了更多页面,屏幕当前只呈现最靠近顶部的两层。路由从 A 进入 B 时,A 与 B 会在两栏中并排显示,供用户保留上一层参照;继续进入 C 后,B 与 C 并排,A 仍留在栈中等待返回。购物模式中的左栏会跟着相邻层级变化,所以它可能先显示商品列表,随后显示商品详情。开发者如果把左栏当成固定列表,再额外编写一套列表常驻逻辑,系统推进和应用布局就可能同时作用于画面,排查时也很难确认页面位置由谁决定。

操作状态 路由栈 左侧区域 右侧区域
打开应用 商品列表 商品列表 暂无详情页
打开商品 商品列表 → 商品详情 商品列表 商品详情
打开规格 商品列表 → 商品详情 → 规格页 商品详情 规格页
从规格页返回 商品列表 → 商品详情 商品列表 商品详情

这组状态说明左侧来源会跟随相邻层级变化。商品详情进入规格页以后,列表仍留在栈中,却已经离开当前两栏;规格页返回以后,列表重新成为左侧参照。右侧进入、原页左移的空间变化给出了明确的前进方向。某个按钮如果绕开当前 NavPathStack 打开新窗口,或者子页面重新创建另一条栈,屏幕位置就无法继续对应原来的进入顺序。

Navigation 已经保存列表、详情和规格的进入顺序,EasyGo 还要知道主页区域、第一层关联页以及宽窗口采用的推进方式。homePage 指向 Navigation 的主页区域,relatedPage 填写稳定的详情页名称,mode: 0 让系统按照购物模式组织相邻页面。主页第一次进入详情时,系统据此建立列表与详情的关系;详情继续进入规格页时,顺序仍然来自同一条 NavPathStack

relatedPage 保存的是页面名,不能替每件商品携带业务数据。商品名称、价格或生产项目中的商品 ID 仍然要作为 Navigation 参数进入目标页面。这样,同一个 ProductDetailPage 可以显示不同商品,EasyGo 配置也不需要随着商品内容反复变化。

json 复制代码
"navigationSplitOptions": {
  "homePage": "navBar",
  "relatedPage": "ProductDetailPage",
  "mode": 0
}

这段配置只说明系统怎样选择两栏,没有替业务创建新的页面顺序。代码仍然要把详情和规格依次压入同一条路由栈。接入时可以先运行列表进入详情的最短路径,确认列表与详情的位置,再加入规格页;如果三层同时接入后位置异常,开发者仍能沿着页面名、栈变化和 mode 三处逐项检查。

二、三层跳转只维护一套路由栈

显示规则确定以后,代码需要保证三层页面处在同一条路由栈中。首页把商品详情压入路由栈,详情页再把规格页压入同一个路由栈。页面代码没有创建 Row 或两个固定容器,系统可以根据 EasyGo 规则组织宽窗口中的页面。窗口不满足平行视界条件时,NavigationMode.Stack 仍然按照手机上熟悉的方式整页跳转,所以同一条业务链可以同时接受宽窗口和窄窗口验证。

路由参数只需要携带目标页识别当前业务对象所需的信息。示例传商品名和价格,方便读者直接观察两个商品是否打开了各自的详情;生产项目通常更适合传商品 ID,再由详情页加载最新数据。如果两个详情页都读取同一个没有按 ID 区分的全局对象,后加载的数据可能同时覆盖左右页面。参数跟随路由节点保存,可以避免页面位置正确、内容却指向另一件商品。

ts 复制代码
private openProduct(name: string, price: string): void {
  let param: ProductParam = new ProductParam()
  param.name = name
  param.price = price
  this.pageStack.pushPathByName('ProductDetailPage', param)
}

目标页需要真正读取参数,传递才形成完整路径。示例在 NavDestination.onReady 中取得当前 pathInfo.param,再把商品名和价格写入页面状态。点击触控笔时,详情页因此显示触控笔与对应价格,轻薄键盘内容不会继续残留。

ts 复制代码
.onReady((context: NavDestinationContext) => {
  const param = context.pathInfo.param as ProductParam
  this.productName = param.name
  this.productPrice = param.price
})

从触控笔卡片进入详情时,列表页先创建一份 ProductParam,然后把它与 ProductDetailPage 一起压入栈。目标页开始构建时读取这份参数,页面状态随即得到触控笔和价格。用户在详情页继续打开规格,代码又创建一份属于 SpecPage 的参数。两次读取都依附各自的路由节点,页面名负责找到组件,参数负责让组件显示当前业务对象。

这一步也给人工观察增加了内容判断。只看左右位置,开发者只能确认页面发生了推进;再看详情标题与价格,才能知道新栈顶读取的是本次点击携带的数据。先点轻薄键盘、返回列表、再点触控笔,可以直接暴露详情页仍然写死内容或错误复用上一份参数的问题。当前示例没有接入数据仓库,因此结论只覆盖路由参数传递与读取。

每次调用 pushPathByName() 都会生成新的栈顶,这段普通路由代码也就提供了购物模式的页面推进顺序。规格页返回时继续操作同一条栈:pop() 移除规格页,左右区域重新显示商品列表和商品详情;clear() 移除全部 NavDestination,Navigation 首页重新成为当前页面。两个动作承担的返回范围不同,按钮文案需要让用户预先知道自己会回到详情还是列表。

pop()clear() 只改变路由节点,不会替业务决定筛选条件、滚动位置或未提交内容怎样保存。当前 Demo 使用静态商品数据,只观察左右页面如何恢复。真实项目需要另外定义业务状态的保存位置,这个问题属于路由与状态恢复,将在本专题第 06 篇集中处理。

把推进和返回连起来看,当前主路径只有四个动作:列表打开详情,详情打开规格,规格返回详情,详情返回列表。前两个动作增加栈顶,第三个动作移除一层,第四个动作清空目标页。每次操作以后都能在前面的状态表找到对应左右页面。如果实际画面无法对应某一行,先检查本次栈操作,不需要立刻引入新的页面模式解释。

ts 复制代码
Button('返回商品详情')
  .onClick(() => {
    this.pageStack.pop()
  })

Button('返回商品列表')
  .onClick(() => {
    this.pageStack.clear()
  })

购物模式还提供 transPages 处理不应改变主要左右层级的过渡页,但当前 Demo 没有配置它。开发者应当先确认默认 push、pop 和 clear 的连续关系,再为真实业务单独验证过渡页面;登录、安全与事务流程不能只凭页面名称直接套用。这里保留这一项边界,是为了说明默认推进并非所有页面的唯一选择,不继续展开它的配置组合。

三、适用场景和人工观察点

页面推进和返回都交给同一条 Navigation 路由栈以后,页面顺序已经稳定。产品体验还要回答另一个问题:用户进入下一层以后,屏幕上应该继续保留哪一个页面。这个判断会直接影响购物模式是否适合当前业务。商品详情进入规格页时,左侧详情能够帮助用户核对名称、价格和当前选择;图片素材进入预览或授权信息时,左侧缩略图详情也能保留刚才的选择线索。

稳定目录长期占据左侧的业务更适合导航模式,因为两种模式对左侧区域的来源定义不同。邮件分类、设置目录或知识库树形导航中,用户更频繁的动作是切换同级入口,左侧应该持续提供定位。购物模式会随着路由推进更换左侧页面,用在这类场景中容易让分类入口离开屏幕,用户每次切换栏目都要先返回。模式选择需要跟随下一步操作,而不能只看页面是否采用列表加详情。

可以用一个问题判断链路是否适合:用户进入下一页后,最想保留的是刚才那一页,还是长期不变的分类入口。商品详情进入规格时,刚才的详情有参考价值;设置页切换网络和显示时,左侧分类列表更有价值。全屏视频、沉浸阅读和复杂编辑器需要更集中的空间,两个页面长期并排反而会压缩主要内容,这类任务可以继续整页显示。

连续比较和逐层进入代表两种不同的用户任务。用户在商品 A 与商品 B 之间反复切换时,产品可能更需要固定列表和右侧替换,这更接近导航模式;用户从商品详情继续查看规格、安装说明和兼容设备时,相邻层级会持续提供有用参照,购物模式的推进关系更清楚。同一个电商应用可以在不同页面采用不同组织策略,判断依据来自用户当前任务和页面层级,不能只根据业务名称统一选择一种模式。

判断模式时,需要直接观察用户下一步需要哪一页作为参照。资讯列表进入文章、文章继续进入作者资料时,刚刚阅读的文章仍可能帮助用户理解作者信息,这条链路可以评估购物模式。用户频繁在多个栏目之间切换时,长期保留栏目入口更有帮助。开发者先画出三次连续操作,再标出每一步最需要留在视野里的页面,通常就能判断左侧应该随层级变化还是保持固定。

初次接入时,问题通常集中在三个位置。子页面重新创建 NavPathStack,下一层会进入另一条返回链;应用保留手写双栏,画面会同时受到两套页面组织规则影响;EasyGo、pushPathByName() 和 Builder 使用的页面名不一致,系统也无法建立预期关系。开发者可以先查看当前栈和参数,再核对页面名与 mode,确认页面关系以后才需要检查其他配置。

从商品列表依次进入轻薄键盘和继续查看规格,观察商品详情是否被推到左侧、规格页是否出现在右侧;再点返回商品详情,观察左右区域是否恢复为商品列表和商品详情。

后面如果页面位置不对,可以先确认所有页面是否仍在同一个 NavPathStack 中,再核对 EasyGo、pushPathByName() 和 Builder 使用的页面名称。名称一致以后继续检查 mode: 0 是否仍位于 navigationSplitOptions 中。页面关系没有问题,再到模拟器里观察 push、pop 和 clear 之后的左右页面,这样每一步都能排除一组具体原因。

完成三层推进与返回以后,再把窗口缩窄一次,确认普通单页仍然沿同一条栈工作。这个动作可以排除宽窗口页面关系正确、窄窗口返回顺序却已经改变的问题。更复杂的同名页面、重复入栈和状态恢复我会放到这个专栏后面的文章给大家讲解,当前我们观察只围绕三层相邻页面。

总结

用户从商品列表进入详情,再打开规格页时,如果下一步判断还依赖刚刚离开的页面,购物模式适合保留这种连续关系。Navigation 继续记录页面进入和返回的顺序,系统在宽窗口中显示最接近栈顶的两层。用户从规格页返回以后,商品详情重新出现在右侧,列表也回到左侧,画面变化仍然符合原来的访问路径。

当前 Demo 还让详情页与规格页读取各自的路由参数,用页面内容确认参数跟随节点前进。如果用户进入下一层后仍然需要上一页作为参照,而且普通 push、pop 和 clear 能够保持正确顺序,这条链路就适合继续采用购物模式。

我目前手里还没有可以测试 HarmonyOS 7 的真机,所以相关内容现阶段主要通过 HarmonyOS 7 模拟器进行验证,真机上的系统表现、设备差异和实际体验,后面有条件再继续补测,最终还是以实际设备运行结果为准。

完整代码

Index.ets

ts 复制代码
class ProductCatalog {
  static getName(productId: string): string {
    if (productId === 'keyboard') {
      return '轻薄键盘'
    }

    if (productId === 'pen') {
      return '触控笔'
    }

    return '请选择商品'
  }

  static getPrice(productId: string): string {
    if (productId === 'keyboard') {
      return '¥399'
    }

    if (productId === 'pen') {
      return '¥299'
    }

    return ''
  }

  static getDescription(productId: string): string {
    if (productId === 'keyboard') {
      return '轻薄设计,支持多种连接方式'
    }

    if (productId === 'pen') {
      return '支持书写、批注和快捷操作'
    }

    return ''
  }

  static getSpecTitle(productId: string): string {
    if (productId === 'keyboard') {
      return '轻薄键盘规格'
    }

    if (productId === 'pen') {
      return '触控笔规格'
    }

    return '商品规格'
  }
}

@Entry
@ComponentV2
struct Index {
  @Provider('pageStack')
  pageStack: NavPathStack = new NavPathStack()

  /**
   * 当前项目启用了严格 ArkTSCheck,
   * 所以这里不显式声明 any 或 unknown。
   *
   * PageMap 只负责按照页面名称创建目标页面。
   * 页面参数在对应 NavDestination 的 onReady 中读取。
   */
  @Builder
  pageMap(name: string) {
    if (name === 'ProductDetailPage') {
      ProductDetailPage()
    } else if (name === 'SpecPage') {
      SpecPage()
    }
  }

  @Builder
  productCard(productId: string, name: string, price: string) {
    Column({ space: 10 }) {
      Text(name)
        .fontSize(20)
        .fontWeight(FontWeight.Medium)
        .width('100%')

      Text(price)
        .fontSize(16)
        .fontColor('#3F5BD8')
        .width('100%')

      Button('查看详情')
        .width('100%')
        .height(46)
        .onClick(() => {
          this.pageStack.pushPathByName(
            'ProductDetailPage',
            productId
          )
        })
    }
    .width('100%')
    .padding(18)
    .backgroundColor('#F2F4FA')
    .borderRadius(18)
    .alignItems(HorizontalAlign.Start)
  }

  build() {
    Navigation(this.pageStack) {
      Column({ space: 16 }) {
        Text('商品列表')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .width('100%')

        Text(
          '选择一个商品进入详情,然后继续打开规格页,' +
            '可以观察购物模式下页面怎样连续推进。'
        )
          .fontSize(15)
          .fontColor('#5F6678')
          .lineHeight(22)
          .width('100%')

        this.productCard(
          'keyboard',
          '轻薄键盘',
          '¥399'
        )

        this.productCard(
          'pen',
          '触控笔',
          '¥299'
        )
      }
      .width('100%')
      .height('100%')
      .padding(24)
      .alignItems(HorizontalAlign.Start)
    }
    .mode(NavigationMode.Stack)
    .title('商品列表')
    .navDestination(this.pageMap)
    .width('100%')
    .height('100%')
  }
}

@ComponentV2
struct ProductDetailPage {
  @Consumer('pageStack')
  pageStack: NavPathStack = new NavPathStack()

  /**
   * 空字符串表示系统提前创建了 relatedPage,
   * 此时用户还没有从左侧选择具体商品。
   */
  @Local
  productId: string = ''

  private hasProduct(): boolean {
    return this.productId.length > 0
  }

  private getProductName(): string {
    return ProductCatalog.getName(this.productId)
  }

  private getProductPrice(): string {
    return ProductCatalog.getPrice(this.productId)
  }

  private getProductDescription(): string {
    return ProductCatalog.getDescription(this.productId)
  }

  build() {
    NavDestination() {
      Column({ space: 18 }) {
        Text('商品详情')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .width('100%')

        if (this.hasProduct()) {
          Text(
            '当前商品已经进入详情页。继续打开规格以后,' +
              '可以观察详情页和规格页在购物模式中的位置变化。'
          )
            .fontSize(15)
            .fontColor('#5F6678')
            .lineHeight(22)
            .width('100%')

          Column({ space: 10 }) {
            Text(this.getProductName())
              .fontSize(22)
              .fontWeight(FontWeight.Medium)
              .width('100%')

            Text(this.getProductPrice())
              .fontSize(18)
              .fontColor('#3F5BD8')
              .width('100%')

            Text(this.getProductDescription())
              .fontSize(15)
              .fontColor('#667085')
              .lineHeight(22)
              .width('100%')
          }
          .width('100%')
          .padding(20)
          .backgroundColor('#F2F4FA')
          .borderRadius(18)
          .alignItems(HorizontalAlign.Start)

          Button('继续查看规格')
            .width('100%')
            .height(50)
            .onClick(() => {
              this.pageStack.pushPathByName(
                'SpecPage',
                this.productId
              )
            })

          Button('返回商品列表')
            .width('100%')
            .height(46)
            .buttonStyle(ButtonStyleMode.TEXTUAL)
            .onClick(() => {
              this.pageStack.clear()
            })
        } else {
          Column({ space: 14 }) {
            Text('请选择一个商品')
              .fontSize(22)
              .fontWeight(FontWeight.Medium)
              .width('100%')

            Text(
              '从左侧商品列表选择一个商品后,' +
                '这里会显示对应的详情内容。'
            )
              .fontSize(15)
              .fontColor('#667085')
              .lineHeight(22)
              .width('100%')
          }
          .width('100%')
          .padding(20)
          .backgroundColor('#F2F4FA')
          .borderRadius(18)
          .alignItems(HorizontalAlign.Start)
        }
      }
      .width('100%')
      .height('100%')
      .padding(24)
      .alignItems(HorizontalAlign.Start)
    }
    .title('商品详情')
    .onReady((context: NavDestinationContext) => {
      /**
       * Navigation 的框架类型中 param 使用 unknown,
       * 业务代码这里直接转换成明确的 string 类型。
       *
       * relatedPage 由系统提前创建时 param 可能为空,
       * 因此这里必须先检查。
       */
      let routeParam: string | undefined | null =
        context.pathInfo.param as string | undefined | null

      if (routeParam === undefined || routeParam === null) {
        this.productId = ''
        return
      }

      this.productId = routeParam
    })
  }
}

@ComponentV2
struct SpecPage {
  @Consumer('pageStack')
  pageStack: NavPathStack = new NavPathStack()

  @Local
  productId: string = ''

  private hasProduct(): boolean {
    return this.productId.length > 0
  }

  private getProductName(): string {
    return ProductCatalog.getName(this.productId)
  }

  private getSpecTitle(): string {
    return ProductCatalog.getSpecTitle(this.productId)
  }

  build() {
    NavDestination() {
      Column({ space: 18 }) {
        Text(this.getSpecTitle())
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .width('100%')

        if (this.hasProduct()) {
          Text(
            '规格页进入路由栈以后,可以继续观察返回商品详情时,' +
              '左右页面怎样恢复到上一层状态。'
          )
            .fontSize(15)
            .fontColor('#5F6678')
            .lineHeight(22)
            .width('100%')

          Column({ space: 10 }) {
            Text('当前商品:' + this.getProductName())
              .fontSize(18)
              .fontWeight(FontWeight.Medium)
              .width('100%')

            if (this.productId === 'keyboard') {
              Text('连接方式:蓝牙 / USB-C / 接收器')
                .fontSize(16)
                .width('100%')

              Text('续航:约 30 天')
                .fontSize(16)
                .width('100%')

              Text('重量:320g')
                .fontSize(16)
                .width('100%')
            } else if (this.productId === 'pen') {
              Text('连接方式:蓝牙')
                .fontSize(16)
                .width('100%')

              Text('压感:支持多级压感')
                .fontSize(16)
                .width('100%')

              Text('充电方式:磁吸充电')
                .fontSize(16)
                .width('100%')
            }
          }
          .width('100%')
          .padding(20)
          .backgroundColor('#F2F4FA')
          .borderRadius(18)
          .alignItems(HorizontalAlign.Start)

          Button('返回商品详情')
            .width('100%')
            .height(50)
            .onClick(() => {
              this.pageStack.pop()
            })

          Button('返回商品列表')
            .width('100%')
            .height(46)
            .buttonStyle(ButtonStyleMode.TEXTUAL)
            .onClick(() => {
              this.pageStack.clear()
            })
        } else {
          Text('当前没有可显示的商品规格。')
            .fontSize(16)
            .fontColor('#667085')
            .width('100%')
        }
      }
      .width('100%')
      .height('100%')
      .padding(24)
      .alignItems(HorizontalAlign.Start)
    }
    .title('商品规格')
    .onReady((context: NavDestinationContext) => {
      let routeParam: string | undefined | null =
        context.pathInfo.param as string | undefined | null

      if (routeParam === undefined || routeParam === null) {
        this.productId = ''
        return
      }

      this.productId = routeParam
    })
  }
}

module.json5

json5 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "easyGo": "$profile:easy_go",
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "icon": "$media:layered_image",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "ohos.want.action.home"
            ]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ],
      }
    ]
  }
}

easy_go.json

json 复制代码
{
  "common": {
    "displayModeOptions": {
      "wideWindowMode": "navigationSplit",
      "squareWindowMode": "navigationSplit",
      "navigationSplitOptions": {
        "homePage": "navBar",
        "relatedPage": "ProductDetailPage",
        "mode": 0
      }
    }
  }
}
相关推荐
SuperHeroWu73 小时前
HarmonyOS Dev Assistant (HarmonyOS开发助手)如何打通元服务开发全流程
ai·agent·harmonyos·vs code·元服务·hbuilderx·assistant
熊猫钓鱼>_>3 小时前
用 OHAudioSuite 空间渲染节点搭一座「3D 有声博物馆」——从三种模式到 FreeBuds 头追的实战记录
c++·3d·ai·harmonyos·arkts·audio·ohaudiosuite
OH_TPC4 小时前
HarmonyOS APP开发---"心动卡"社交匹配App,需要用到这个库
harmonyos
光锥智能4 小时前
HUAWEI WATCH 6系列正式发布:鸿蒙AI手表,智慧健康旗舰
人工智能·华为·harmonyos
光锥智能5 小时前
HUAWEI WATCH Ultimate 2 非凡探索推出滑雪登山功能,专业数据助力探索雪域奇境
华为
上学的小垃圾5 小时前
华为eNSP防火墙USG6000V登录管理方式
网络·网络安全·华为
OH_TPC6 小时前
【鸿蒙优选三方库】@react-native-ohos/react-native-gesture-handler:原生级手势识别与处理框架
华为·harmonyos·鸿蒙
嘴贱欠吻!6 小时前
Flutter 触觉反馈插件 Gaimon三方库鸿蒙端教程
flutter·华为·harmonyos
Francek Chen6 小时前
【HarmonyOS 7】新品发布:华为Mate XT 2携麒麟9050 Pro与鸿蒙7重磅登场
人工智能·华为·harmonyos·鸿蒙·小艺