HarmonyOS 6.1 PC应用实战:键鼠协同与多窗体——电商后台管理系统“上车”
系列番外·生产力工具篇。车机篇发布后有开发者问“手机、车机都写了那传说中的鸿蒙PC呢咱们这个电商Demo能不能搞个PC版的后台管理系统” 问到点子上了随着HarmonyOS NEXT向PC端延伸一次开发多端部署的价值将被最大化。今天我们将电商Demo的“后台管理端”适配到PC平台。重点攻克三大PC开发独有的难关键鼠交互范式Hover、右键、拖拽、多窗体架构主窗口子窗口弹窗、响应式布局进阶从移动端单列到PC端多列侧边栏。全程基于API23含官方文档未涉及的PC端UX设计规范。一、前言为什么PC端是鸿蒙生态的“临门一脚”在移动端我们习惯了触摸交互和全屏应用。但在PC端用户的心智模型完全不同效率优先用户使用键盘快捷键CtrlC/V、鼠标精确点击、多窗口并排来提升效率。空间充裕PC拥有大屏幕1920x1080起可以容纳更多信息密度不再需要“隐藏式菜单”。多任务并行用户希望一边看数据报表一边回客服消息一边查商品库存。我们的电商Demo之前只有“C端用户版”手机App缺少“B端商家版”后台管理。今天我们利用ArkUI的跨平台能力在不重写业务逻辑的前提下快速构建一个运行在PC上的电商后台管理系统。这将充分展示ArkUI-X和HarmonyOS NEXT的威力一套代码跑在手机、车机和PC上。二、核心概念辨析移动端思维 vs PC端思维很多开发者直接把手机UI拉伸到PC屏幕上结果惨不忍睹。必须先转变思维维度移动端手机/车机PC端桌面/笔记本交互核心触摸粗放键鼠精细 触摸二合一设备导航结构底部Tab栏 页面栈顶部菜单栏 侧边导航栏 多标签页内容布局单列流式布局多列栅格布局 分栏布局Master-Detail窗口形态强制全屏自由窗口可缩放、最小化、最大化关键事件onClick, onTouchonHover, onKeyEvent, onDrop, onDrag核心认知PC端开发不仅仅是屏幕变大了而是交互逻辑的根本改变。我们需要为鼠标悬停Hover提供视觉反馈为键盘快捷键提供响应逻辑为窗口大小变化提供动态布局。三、代码实现从“手持电商”到“桌面后台”3.1 环境准备与配置新建PC模块在DevEco Studio中右键项目 - New - Module - PC Module。设备类型声明修改module.json5声明支持PC。{ module: { deviceTypes: [pc], // 声明为PC应用 abilities: [ { name: EntryAbility, window: { designWidth: 1920, // 设计稿基准宽度 autoDesignWidth: true, maxWindowRatio: 3.0, // 最大窗口比例 minWindowRatio: 0.5 // 最小窗口比例 } } ] } }资源目录在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(adminshop.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(⌨️ CtrlK) .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交互Hover悬停.onHover()。当鼠标移到列表行上时可以改变背景色给用户即时的视觉反馈。Context Menu右键菜单.onContextMenu()。PC用户习惯右键操作这里替代了手机上的长按菜单。Keyboard Shortcuts键盘快捷键.onKeyEvent()。监听CtrlK、ESC等组合键大幅提升操作效率。四、踩坑记录官方文档没写的5个PC坑窗口尺寸监听windowSizeChange回调非常频繁如果在回调里直接做重计算如重新构建整个UI树会导致界面卡顿。解决使用防抖Debounce或节流Throttle技术限制更新频率如200ms一次。焦点管理FocusPC端有“焦点”的概念。点击一个按钮后焦点可能还在按钮上此时按空格键会再次触发点击。在输入框失去焦点时需要手动管理焦点状态。拖拽文件上传PC端常见需求是拖拽文件到窗口上传。onDrop事件需要处理DragEvent从中提取文件的URIevent.items[0].uri然后通过文件管理API读取。官方文档对此流的介绍比较分散。多窗口层级PC端可能有多个窗口如主窗口、设置窗口、弹窗。promptAction.showDialog默认是模态的会阻塞主窗口。对于非阻塞通知应该使用Toast或自定义的Overlay。字体渲染PC端屏幕PPI差异巨大从普通显示器到4K屏。使用fp单位字体像素而不是px确保字体在不同DPI下物理大小一致。