系列番外·生产力工具篇。车机篇发布后,有开发者问:“手机、车机都写了,那传说中的鸿蒙PC呢?咱们这个电商Demo能不能搞个PC版的后台管理系统?” 问到点子上了!随着HarmonyOS NEXT向PC端延伸,一次开发,多端部署的价值将被最大化。今天我们将电商Demo的“后台管理端”适配到PC平台。重点攻克三大PC开发独有的难关:键鼠交互范式(Hover、右键、拖拽)、多窗体架构(主窗口+子窗口+弹窗)、响应式布局进阶(从移动端单列到PC端多列+侧边栏)。全程基于API23,含官方文档未涉及的PC端UX设计规范。


一、前言:为什么PC端是鸿蒙生态的“临门一脚”?

在移动端,我们习惯了触摸交互和全屏应用。但在PC端,用户的心智模型完全不同:

  • 效率优先:用户使用键盘快捷键(Ctrl+C/V)、鼠标精确点击、多窗口并排来提升效率。

  • 空间充裕:PC拥有大屏幕(1920x1080起),可以容纳更多信息密度,不再需要“隐藏式菜单”。

  • 多任务并行:用户希望一边看数据报表,一边回客服消息,一边查商品库存。

我们的电商Demo之前只有“C端用户版”(手机App),缺少“B端商家版”(后台管理)。今天,我们利用ArkUI的跨平台能力,在不重写业务逻辑的前提下,快速构建一个运行在PC上的电商后台管理系统。这将充分展示ArkUI-X和HarmonyOS NEXT的威力:一套代码,跑在手机、车机和PC上


二、核心概念辨析(移动端思维 vs PC端思维)

很多开发者直接把手机UI拉伸到PC屏幕上,结果惨不忍睹。必须先转变思维:

维度

移动端(手机/车机)

PC端(桌面/笔记本)

交互核心

触摸(粗放)

键鼠(精细)​ + 触摸(二合一设备)

导航结构

底部Tab栏 + 页面栈

顶部菜单栏​ + 侧边导航栏 + 多标签页

内容布局

单列流式布局

多列栅格布局​ + 分栏布局(Master-Detail)

窗口形态

强制全屏

自由窗口(可缩放、最小化、最大化)

关键事件

onClick, onTouch

onHover, onKeyEvent, onDrop, onDrag

💡 核心认知:PC端开发不仅仅是屏幕变大了,而是交互逻辑的根本改变。我们需要为鼠标悬停(Hover)提供视觉反馈,为键盘快捷键提供响应逻辑,为窗口大小变化提供动态布局。


三、代码实现:从“手持电商”到“桌面后台”

3.1 环境准备与配置

  1. 新建PC模块:在DevEco Studio中,右键项目 -> New -> Module -> PC Module。

  2. 设备类型声明:修改module.json5,声明支持PC。

    
      
    
      
    {
      "module": {
        "deviceTypes": ["pc"], // 声明为PC应用
        "abilities": [
          {
            "name": "EntryAbility",
            "window": {
              "designWidth": 1920, // 设计稿基准宽度
              "autoDesignWidth": true,
              "maxWindowRatio": 3.0, // 最大窗口比例
              "minWindowRatio": 0.5  // 最小窗口比例
            }
          }
        ]
      }
    }
  3. 资源目录:在resources下新建layout-pc目录,存放PC专属布局。

3.2 构建PC端经典布局(侧边栏+主内容区)

创建entry_pc/src/main/ets/pages/AdminIndex.ets。采用经典的“主-详”布局。



import { router } from '@kit.ArkUI'
import { CommonConstants } from '../common/Constants'

// 定义菜单项接口
interface MenuItem {
  id: string
  icon: Resource
  label: string
  route: string
}

@Entry
@Component
struct AdminIndex {
  @State currentMenu: string = 'dashboard'
  @State windowWidth: number = 1920 // 监听窗口宽度
  private menus: MenuItem[] = [
    { id: 'dashboard', icon: $r('app.media.ic_dashboard'), label: '数据看板', route: 'pages/admin/Dashboard' },
    { id: 'products', icon: $r('app.media.ic_goods'), label: '商品管理', route: 'pages/admin/ProductList' },
    { id: 'orders', icon: $r('app.media.ic_order'), label: '订单管理', route: 'pages/admin/OrderList' },
    { id: 'settings', icon: $r('app.media.ic_settings'), label: '系统设置', route: 'pages/admin/Settings' }
  ]

  aboutToAppear(): void {
    // 监听窗口尺寸变化,实现响应式布局
    window.getLastWindow(getContext(this)).then((win) => {
      win.on('windowSizeChange', (size) => {
        this.windowWidth = size.width
      })
    })
  }

  build() {
    Row() {
      // 左侧:固定导航栏(PC专属)
      Column() {
        // Logo
        Text('Shop Admin')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0A59F7')
          .margin({ bottom: 40 })

        // 菜单列表
        List() {
          ForEach(this.menus, (menu: MenuItem) => {
            Row() {
              Image(menu.icon)
                .width(24)
                .height(24)
                .fillColor(this.currentMenu === menu.id ? '#0A59F7' : '#666')
              if (this.windowWidth > 1200) { // 宽屏显示文字,窄屏只显示图标
                Text(menu.label)
                  .fontSize(16)
                  .fontColor(this.currentMenu === menu.id ? '#0A59F7' : '#333')
                  .margin({ left: 12 })
              }
            }
            .width('100%')
            .padding(16)
            .backgroundColor(this.currentMenu === menu.id ? '#E8F0FE' : Color.Transparent)
            .borderRadius(12)
            .onClick(() => {
              this.currentMenu = menu.id
              // PC端通常是单页应用(SPA),这里用路由替换内容区,而不是打开新页面
              router.replacePath({ url: menu.route })
            })
            .onHover((isHover) => {
              // 鼠标悬停效果(PC核心交互)
              if (isHover && this.currentMenu !== menu.id) {
                // 可以在这里改变背景色,提供反馈
              }
            })
          })
        }
        .layoutWeight(1)

        // 底部用户信息
        Row() {
          Image($r('app.media.avatar'))
            .width(40)
            .height(40)
            .borderRadius(20)
          if (this.windowWidth > 1200) {
            Column() {
              Text('Admin')
                .fontSize(14)
                .fontWeight(FontWeight.Medium)
              Text('admin@shop.com')
                .fontSize(12)
                .fontColor('#999')
            }
            .margin({ left: 12 })
            .layoutWeight(1)
          }
        }
        .padding(16)
      }
      .width(this.windowWidth > 1200 ? 240 : 80) // 响应式宽度
      .height('100%')
      .backgroundColor('#F8F9FA')
      .padding(16)
      .transition(TransitionEffect.OPACITY.animation({ duration: 300 })) // 平滑过渡

      // 右侧:主内容区
      Column() {
        // 顶部工具栏(面包屑+搜索+快捷键提示)
        Row() {
          Text(this.menus.find(m => m.id === this.currentMenu)?.label || '')
            .fontSize(20)
            .fontWeight(FontWeight.Medium)
          Blank()
          TextInput({ placeholder: '搜索商品/订单...' })
            .width(300)
            .height(40)
            .backgroundColor('#F0F2F5')
            .borderRadius(20)
            .padding({ left: 16 })
          Text('⌨️ Ctrl+K')
            .fontSize(12)
            .fontColor('#999')
            .margin({ left: 10 })
        }
        .width('100%')
        .padding(16)
        .border({ width: { bottom: 1 }, color: '#EEE' })

        // 内容区(根据菜单切换)
        Stack() {
          // 这里根据currentMenu加载不同的Component
          // 例如:Dashboard, ProductList等
          this.ContentArea()
        }
        .layoutWeight(1)
        .backgroundColor('#FFFFFF')
      }
      .layoutWeight(1)
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .onKeyEvent((event: KeyEvent) => {
      // PC端键盘快捷键支持
      if (event.type === KeyType.Down) {
        // Ctrl + K 聚焦搜索框
        if (event.ctrlKey && event.keyCode === KeyCode.KEY_K) {
          // 实际开发中,这里需要拿到TextInput的Controller并调用focus()
          promptAction.showToast({ message: '搜索快捷键触发' })
          return true // 阻止事件冒泡
        }
        // ESC 返回仪表盘
        if (event.keyCode === KeyCode.KEY_ESCAPE) {
          this.currentMenu = 'dashboard'
          return true
        }
      }
      return false
    })
  }

  /**
   * 根据当前菜单渲染内容区
   */
  @Builder
  ContentArea() {
    if (this.currentMenu === 'products') {
      // 复用手机端的商品列表逻辑,但改变UI布局
      this.ProductManagementTable()
    } else {
      // 其他页面的占位符
      Text('内容加载中...')
        .fontSize(16)
        .fontColor('#999')
    }
  }

  /**
   * PC端商品管理表格(替代手机端的List)
   */
  @Builder
  ProductManagementTable() {
    List() {
      // 表头
      ListItem() {
        Row() {
          Text('商品名称').layoutWeight(3)
          Text('价格').layoutWeight(1)
          Text('库存').layoutWeight(1)
          Text('状态').layoutWeight(1)
          Text('操作').layoutWeight(2)
        }
        .padding(16)
        .backgroundColor('#F8F9FA')
        .fontWeight(FontWeight.Medium)
      }

      // 数据行
      ForEach(this.getProducts(), (item: GoodsBean) => {
        ListItem() {
          Row() {
            // 商品名+图
            Row() {
              Image(item.image).width(48).height(48).borderRadius(8).objectFit(ImageFit.Cover)
              Text(item.name).margin({ left: 12 }).maxLines(1)
            }.layoutWeight(3)

            Text(`¥${item.price}`).layoutWeight(1)
            Text(item.stock.toString()).layoutWeight(1)
            Text(item.status === 1 ? '在售' : '下架')
              .layoutWeight(1)
              .fontColor(item.status === 1 ? '#4CAF50' : '#F44336')
            
            // 操作按钮组
            Row() {
              Button('编辑').height(32).backgroundColor('#E8F0FE').fontColor('#0A59F7')
              Button('下架').height(32).backgroundColor('#FFF3E0').fontColor('#FF9800').margin({ left: 8 })
            }.layoutWeight(2)
          }
          .padding(16)
          .border({ width: { bottom: 1 }, color: '#EEE' })
          .onHover((isHover) => {
            // 鼠标悬停高亮行
            // 实际开发中,可以通过状态变量控制背景色
          })
          // PC端支持右键菜单
          .onContextMenu((event: ContextMenuEvent) => {
            this.showContextMenu(event.x, event.y, item)
          })
        }
      })
    }
    .sticky(StickyStyle.Header) // 表头吸顶
    .width('100%')
    .height('100%')
  }

  /**
   * 显示右键菜单(PC专属)
   */
  showContextMenu(x: number, y: number, item: GoodsBean): void {
    ContextMenu.show({
      x: x,
      y: y,
      menuItems: [
        { title: '复制商品ID', icon: $r('app.media.ic_copy'), action: () => console.log(item.id) },
        { title: '查看详情', icon: $r('app.media.ic_view'), action: () => router.pushUrl({ url: 'pages/admin/ProductDetail', params: { id: item.id } }) },
        { divider: true },
        { title: '删除商品', icon: $r('app.media.ic_delete'), fontColor: '#F44336', action: () => this.deleteProduct(item.id) }
      ]
    })
  }

  // 模拟数据
  getProducts(): GoodsBean[] {
    return [
      { id: 1, name: 'HarmonyOS 6.1 定制款手机', price: 5999, stock: 99, status: 1, image: 'phone.jpg' },
      { id: 2, name: '鸿蒙智行 车载充电器', price: 199, stock: 500, status: 1, image: 'charger.jpg' },
      { id: 3, name: '星闪无线鼠标', price: 299, stock: 0, status: 0, image: 'mouse.jpg' }
    ]
  }

  deleteProduct(id: number): void {
    promptAction.showDialog({
      title: '确认删除',
      message: `确定要删除商品ID: ${id} 吗?`,
      buttons: [{ text: '取消' }, { text: '删除', color: '#F44336' }]
    }).then((result) => {
      if (result.index === 1) {
        // 执行删除逻辑
        promptAction.showToast({ message: '删除成功' })
      }
    })
  }
}

3.3 关键PC交互实现

上面的代码展示了三个关键的PC交互:

  1. Hover(悬停).onHover()。当鼠标移到列表行上时,可以改变背景色,给用户即时的视觉反馈。

  2. Context Menu(右键菜单).onContextMenu()。PC用户习惯右键操作,这里替代了手机上的长按菜单。

  3. Keyboard Shortcuts(键盘快捷键).onKeyEvent()。监听Ctrl+KESC等组合键,大幅提升操作效率。


四、踩坑记录(官方文档没写的5个PC坑)

  1. 窗口尺寸监听windowSizeChange回调非常频繁,如果在回调里直接做重计算(如重新构建整个UI树),会导致界面卡顿。解决:使用防抖(Debounce)或节流(Throttle)技术,限制更新频率(如200ms一次)。

  2. 焦点管理(Focus):PC端有“焦点”的概念。点击一个按钮后,焦点可能还在按钮上,此时按空格键会再次触发点击。在输入框失去焦点时,需要手动管理焦点状态。

  3. 拖拽文件上传:PC端常见需求是拖拽文件到窗口上传。onDrop事件需要处理DragEvent,从中提取文件的URI(event.items[0].uri),然后通过文件管理API读取。官方文档对此流的介绍比较分散。

  4. 多窗口层级:PC端可能有多个窗口(如主窗口、设置窗口、弹窗)。promptAction.showDialog默认是模态的,会阻塞主窗口。对于非阻塞通知,应该使用Toast或自定义的Overlay

  5. 字体渲染:PC端屏幕PPI差异巨大(从普通显示器到4K屏)。使用fp单位(字体像素)而不是px,确保字体在不同DPI下物理大小一致。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐