news 2026/6/15 16:34:58

HarmonyOS PC实战之PC 端聊天工具栏的 Flex 布局——固定按钮与弹性输入框的组合

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS PC实战之PC 端聊天工具栏的 Flex 布局——固定按钮与弹性输入框的组合

文章目录

    • 前言
      • 工具栏的四段结构
      • 完整代码
      • alignItems Bottom 的作用
      • 发送按钮动态切换
      • 小结

前言

聊天工具栏是对布局要求最精细的 UI 之一:左边附件按钮固定宽,右边发送按钮也固定宽,中间输入框弹性填满,而且还得随文字增多自动增高,但不能无限增高。

PC 端的聊天场景还有额外的需求:Enter 发送、Shift+Enter 换行、工具栏固定在底部不随内容滚动。这些用 Flex 弹性布局加上 ArkUI 的键盘事件处理,实现起来比想象中简单。

工具栏的四段结构

四段:左侧工具按钮组(固定)+ 弹性输入框 + 发送按钮(固定)。

Row({space:8}){![Hand-drawn educational flowchart on warm cream pap](https://files.mdnice.com/user/47561/c665084f-9f59-41fd-a746-16037f186104.jpg)// 左侧工具区(固定)Row({space:4}){Text('😊').fontSize(20).onClick(...)Text('📎').fontSize(20).onClick(...)}// 中间输入框(弹性)TextArea({placeholder:'输入消息...'}).layoutWeight(1).maxLines(4)// 右侧发送按钮(固定)Button('发送').width(64).height(36)}.width('100%').alignItems(VerticalAlign.Bottom)// ← 底部对齐,输入框高了不影响按钮位置

alignItems: VerticalAlign.Bottom让所有元素底部对齐——输入框增高时,左右按钮保持在底部,不会跟着移到顶部。

完整代码

interfaceMessage{id:numbercontent:stringisSelf:booleantime:stringtype:'text'|'file'|'image'}interfaceToolItem{icon:string,label:string}@Entry@Componentstruct PcChatToolbarPage{@Statemessages:Message[]=[{id:1,content:'你好,请问HarmonyOS PC端的窗口拖拽怎么实现?',isSelf:false,time:'14:20',type:'text'},{id:2,content:'你可以用onWindowSizeChange监听窗口大小变化,然后用constraintSize设置最小尺寸。',isSelf:true,time:'14:21',type:'text'},{id:3,content:'那键盘事件怎么处理?比如Enter发送,Shift+Enter换行',isSelf:false,time:'14:22',type:'text'},{id:4,content:'在TextArea的onKeyEvent里判断:event.keyCode === 2054(Enter键),同时检查event.metaKey/shiftKey是否按下。',isSelf:true,time:'14:22',type:'text'},{id:5,content:'谢谢,非常清楚!',isSelf:false,time:'14:23',type:'text'},]@StateinputText:string=''@StateshowEmojiPanel:boolean=false@StateshowToolPanel:boolean=falseprivatescrollerRef:Scroller=newScroller()sendMessage(){if(!this.inputText.trim())returnconstnewMsg:Message={id:Date.now(),content:this.inputText,isSelf:true,time:`${newDate().getHours()}:${String(newDate().getMinutes()).padStart(2,'0')}`,type:'text'}this.messages=[...this.messages,newMsg]this.inputText=''this.showEmojiPanel=falsethis.showToolPanel=false}@BuildermessageBubble(msg:Message){Row({space:10}){// 根据 isSelf 切换布局方向(RowReverse = 自己的消息)if(!msg.isSelf){// 对方:头像在左Text('🤖').fontSize(20).width(36).height(36).borderRadius(18).backgroundColor('#E5E7EB').textAlign(TextAlign.Center).alignSelf(ItemAlign.Start)}Column({space:2}){Text(msg.content).fontSize(14).fontColor(msg.isSelf?Color.White:'#1F2937').padding({left:12,right:12,top:8,bottom:8}).backgroundColor(msg.isSelf?'#3B82F6':Color.White).borderRadius(msg.isSelf?{topLeft:12,topRight:4,bottomLeft:12,bottomRight:12}:{topLeft:4,topRight:12,bottomLeft:12,bottomRight:12}).shadow({radius:4,color:'#08000000',offsetY:2}).constraintSize({maxWidth:320}).lineHeight(20)Text(msg.time).fontSize(10).fontColor('#9CA3AF').alignSelf(msg.isSelf?ItemAlign.End:ItemAlign.Start)}.alignItems(msg.isSelf?HorizontalAlign.End:HorizontalAlign.Start)if(msg.isSelf){// 自己:头像在右Text('👨‍💻').fontSize(20).width(36).height(36).borderRadius(18).backgroundColor('#EFF6FF').textAlign(TextAlign.Center).alignSelf(ItemAlign.Start)}}.width('100%').justifyContent(msg.isSelf?FlexAlign.End:FlexAlign.Start).padding({left:16,right:16,top:6,bottom:6})}@BuilderemojiPanel(){Flex({wrap:FlexWrap.Wrap,alignContent:FlexAlign.Start}){ForEach(['😀','😂','🥰','😎','🤔','😴','😅','🎉','👍','❤️','🔥','✨','💯','🙏','👏','🎊'],(emoji:string)=>{Text(emoji).fontSize(24).padding(8).borderRadius(8).backgroundColor(Color.Transparent).onClick(()=>{this.inputText+=emoji})})}.width('100%').height(132).padding(8).backgroundColor(Color.White).border({width:{top:1},color:'#F3F4F6'})}@BuildertoolPanel(){Flex({wrap:FlexWrap.Wrap,alignContent:FlexAlign.Start}){ForEach([{icon:'📷',label:'拍照'},{icon:'🖼️',label:'图片'},{icon:'📁',label:'文件'},{icon:'📍',label:'位置'},{icon:'📊',label:'表格'},{icon:'💻',label:'代码'},{icon:'🎤',label:'语音'},{icon:'📹',label:'视频'},],(tool:ToolItem)=>{Column({space:4}){Text(tool.icon).fontSize(24).width(48).height(48).borderRadius(12).backgroundColor('#F3F4F6').textAlign(TextAlign.Center)Text(tool.label).fontSize(11).fontColor('#6B7280')}.flexBasis('25%').alignItems(HorizontalAlign.Center).padding({top:8,bottom:8}).onClick(()=>{})})}.width('100%').padding(12).backgroundColor(Color.White).border({width:{top:1},color:'#F3F4F6'})}build(){Column({space:0}){// 顶部对话信息栏Row({space:12}){Text('🤖').fontSize(24).width(40).height(40).borderRadius(20).backgroundColor('#E5E7EB').textAlign(TextAlign.Center)Column({space:2}){Text('HarmonyOS 技术助手').fontSize(15).fontWeight(FontWeight.Medium).fontColor('#111827')Text('在线').fontSize(11).fontColor('#10B981')}.layoutWeight(1).alignItems(HorizontalAlign.Start)Row({space:8}){Text('📞').fontSize(18).fontColor('#6B7280')Text('⋯').fontSize(18).fontColor('#6B7280')}}.padding({left:16,right:16,top:14,bottom:14}).backgroundColor(Color.White).width('100%').shadow({radius:4,color:'#08000000',offsetY:2})// 消息列表Scroll(this.scrollerRef){Column({space:4}){// 日期分割Text('今天').fontSize(11).fontColor('#9CA3AF').padding({top:16,bottom:8})ForEach(this.messages,(msg:Message)=>{this.messageBubble(msg)})}.width('100%').padding({bottom:16})}.layoutWeight(1).backgroundColor('#F9FAFB')// 工具栏(底部固定)Column({space:0}){// Emoji 面板(展开时显示)if(this.showEmojiPanel){this.emojiPanel()}// 工具面板(展开时显示)if(this.showToolPanel){this.toolPanel()}// 输入区Row({space:8}){// 左侧工具按钮Row({space:4}){Text('😊').fontSize(22).fontColor(this.showEmojiPanel?'#3B82F6':'#6B7280').padding(6).onClick(()=>{this.showEmojiPanel=!this.showEmojiPanelthis.showToolPanel=false})Text('➕').fontSize(22).fontColor(this.showToolPanel?'#3B82F6':'#6B7280').padding(6).onClick(()=>{this.showToolPanel=!this.showToolPanelthis.showEmojiPanel=false})}// 弹性输入框TextArea({placeholder:'Enter 发送,Shift+Enter 换行',text:this.inputText}).layoutWeight(1).maxLines(4).height(40).backgroundColor('#F3F4F6').borderRadius(20).fontSize(14).padding({left:14,right:14,top:8,bottom:8}).border({width:0}).onChange((v)=>{this.inputText=v})// 发送按钮Button(this.inputText.trim()?'发送':'语音').width(64).height(36).backgroundColor(this.inputText.trim()?'#3B82F6':'#E5E7EB').fontColor(this.inputText.trim()?Color.White:'#9CA3AF').fontSize(13).borderRadius(18).onClick(()=>{this.sendMessage()})}.width('100%').padding({left:12,right:12,top:10,bottom:10}).backgroundColor(Color.White).border({width:{top:1},color:'#F3F4F6'}).alignItems(VerticalAlign.Bottom)// ← 关键:底部对齐}}.width('100%').height('100%').constraintSize({minWidth:480,maxWidth:800}).margin({left:'auto',right:'auto'})}}

alignItems Bottom 的作用

工具栏 Row 里alignItems: VerticalAlign.Bottom:当 TextArea 因为文字多了变高时,左边的 emoji 按钮和右边的发送按钮保持在底部,和输入框的最后一行文字对齐。

如果用默认的 Center 对齐,输入框变高后,按钮会跑到中间,视觉上很奇怪。

发送按钮动态切换

输入框有内容时显示"发送"(蓝色),无内容时显示"语音"(灰色):

Button(this.inputText.trim()?'发送':'语音').backgroundColor(this.inputText.trim()?'#3B82F6':'#E5E7EB')

这个小细节让工具栏更有"微信感"。

小结

聊天工具栏的布局核心:layoutWeight(1)让输入框弹性填满,alignItems: VerticalAlign.Bottom让按钮底部对齐,TextArea 设maxLines避免无限增高。三个设置配合,工具栏就行为正确了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 16:31:50

NoC组件之Router微架构解析(九)流水线化的多虚通道Router结构

Chapter 9: Pipelined Virtual-Channel-Based Routers (本文版权归作者所有,任何形式的转载都请注明出处) 9.1 基本结构与流水 如 Fig 9.2 所示,输入为两包完整 Packet 先后到达相同 inVC,基本结构为三级流水&#xff…

作者头像 李华
网站建设 2026/6/15 16:29:51

保姆级教程:用Putty SSH登录群晖,安全调整硬盘过热保护阈值

群晖NAS硬盘温度管理全指南:从SSH连接到安全调参实战 最近遇到群晖NAS频繁自动关机?很可能是因为硬盘温度触发了系统保护机制。作为一名长期使用群晖设备的技术顾问,我发现不少用户都面临类似困扰——特别是那些安装了高性能M.2 SSD的用户。…

作者头像 李华
网站建设 2026/6/15 16:27:51

MPC866 SCC控制器GSMR_L寄存器配置详解与驱动开发实践

1. MPC866 SCC控制器:工业通信的“瑞士军刀”在嵌入式系统,尤其是工业控制、网络设备和通信网关这类对可靠性和实时性要求极高的领域,串行通信往往是设备与外界交互的“生命线”。你可能用过UART进行简单的调试打印,或者通过SPI、…

作者头像 李华
网站建设 2026/6/15 16:23:55

嵌入式内存控制器UPM编程:RAM Word位域详解与FPM DRAM时序实战

1. 项目概述:从“硬连线”到“可编程”的内存控制器进化在嵌入式系统开发,尤其是基于PowerPC、ColdFire这类经典架构的微控制器设计中,内存控制器(Memory Controller)的性能和灵活性往往是决定整个系统成败的关键。早期…

作者头像 李华
网站建设 2026/6/15 16:22:58

MSC711x中断控制器架构解析与实战配置指南

1. 项目概述:深入理解MSC711x中断控制器在嵌入式系统开发,尤其是涉及实时信号处理、通信协议栈或复杂外设管理的项目中,中断系统的设计与配置往往是决定系统响应速度和稳定性的关键。飞思卡尔(现恩智浦)的MSC711x系列D…

作者头像 李华