文章目录
- 前言
- 页面基本结构
- 完整实现:PC端登录页
- TextInput 要显式设 width('100%')
- "忘记密码"的 alignSelf
- 写在最后
前言
登录页是 Column 居中布局最典型的场景:Logo、标题、输入框、按钮,从上往下堆叠,整体居中显示。
但真做起来会发现不少细节问题:为什么 TextInput 加了width('100%')之后反而更对?"忘记密码"为什么要单独alignSelf(End)?按钮组的左右分布怎么做?
这篇文章拿 PC 端登录页做案例,把这些细节一一解决。
页面基本结构
PC 端登录页通常不是全屏表单,而是一个白色卡片居中显示在深色或浅灰背景上。外层是全屏 Column 垂直居中,内层是卡片 Column 包裹表单元素。
// 外层:全屏垂直居中Column(){// 内层:表单卡片Column({space:20}){// Logo、标题、输入框、按钮...}.width(400).padding(40).backgroundColor(Color.White).borderRadius(16).shadow({radius:20,color:'rgba(0,0,0,0.08)',offsetY:4})}.width('100%').height('100%').justifyContent(FlexAlign.Center)// 垂直居中.alignItems(HorizontalAlign.Center)// 水平居中外层 Column 同时设justifyContent: Center和alignItems: Center,内层卡片就自然居中。
完整实现:PC端登录页
完整示例:PcLoginPage.ets
import{router}from'@kit.ArkUI'@Entry@Componentstruct PcLoginPage{@Stateusername:string=''@Statepassword:string=''@StateshowPassword:boolean=false@StateisLoading:boolean=false@StateerrorMsg:string=''@StaterememberMe:boolean=false@StateactiveTab:'password'|'code'='password'@StateverifyCode:string=''@StatecodeSent:boolean=false@Statecountdown:number=0getcanSubmit():boolean{if(this.activeTab==='password'){returnthis.username.trim().length>0&&this.password.length>=6}returnthis.username.trim().length>0&&this.verifyCode.length===6}handleLogin(){if(!this.canSubmit){this.errorMsg=this.activeTab==='password'?'请输入账号和密码(至少6位)':'请输入完整的验证码'return}this.isLoading=truethis.errorMsg=''// 模拟登录请求setTimeout(()=>{this.isLoading=falseif(this.username==='admin'&&this.password==='123456'){// 登录成功}else{this.errorMsg='账号或密码错误,请重试'}},1500)}sendCode(){if(!this.username.trim()){this.errorMsg='请先输入手机号或邮箱'return}this.codeSent=truethis.countdown=60consttimer=setInterval(()=>{if(this.countdown<=0){clearInterval(timer)this.codeSent=false}else{this.countdown-=1}},1000)}build(){Column(){// 表单卡片Column({space:0}){// Logo 区Column({space:10}){Stack(){Circle({width:56,height:56}).fill('#1D4ED8')Text('H').fontSize(24).fontColor(Color.White).fontWeight(FontWeight.Bold)}Text('HarmonyOS 开发者平台').fontSize(18).fontColor('#111827').fontWeight(FontWeight.Bold)Text('登录您的开发者账号以继续').fontSize(13).fontColor('#6B7280')}.alignItems(HorizontalAlign.Center).padding({bottom:28}).width('100%').border({width:{bottom:1},color:'#F3F4F6'})Column({space:20}){// 登录方式 TabRow({space:0}){ForEach(['password','code']asstring[],(tab:string)=>{Text(tab==='password'?'密码登录':'验证码登录').fontSize(14).fontColor(this.activeTab===tab?'#3B82F6':'#6B7280').fontWeight(this.activeTab===tab?FontWeight.Medium:FontWeight.Normal).layoutWeight(1).textAlign(TextAlign.Center).padding({top:12,bottom:12}).border({width:{bottom:2},color:this.activeTab===tab?'#3B82F6':Color.Transparent}).onClick(()=>{this.activeTab=tabas'password'|'code'this.errorMsg=''})})}.width('100%').border({width:{bottom:1},color:'#F3F4F6'})// 账号输入Column({space:6}){Text('账号').fontSize(13).fontColor('#374151').fontWeight(FontWeight.Medium).alignSelf(ItemAlign.Start)TextInput({placeholder:'手机号 / 邮箱 / 用户名',text:this.username}).width('100%').height(44).fontSize(14).backgroundColor('#F9FAFB').borderRadius(8).border({width:1,color:'#E5E7EB'}).padding({left:12,right:12}).onChange((v)=>{this.username=vthis.errorMsg=''})}// 密码输入(仅密码登录时显示)if(this.activeTab==='password'){Column({space:6}){Row(){Text('密码').fontSize(13).fontColor('#374151').fontWeight(FontWeight.Medium)Blank()Text('忘记密码?').fontSize(12).fontColor('#3B82F6').onClick(()=>{})}.width('100%')Row(){TextInput({placeholder:'请输入密码',text:this.password}).type(this.showPassword?InputType.Normal:InputType.Password).layoutWeight(1).height(42).fontSize(14).backgroundColor(Color.Transparent).padding({left:12}).onChange((v)=>{this.password=vthis.errorMsg=''})Text(this.showPassword?'🙈':'👁').fontSize(16).padding({right:12}).onClick(()=>this.showPassword=!this.showPassword)}.width('100%').height(44).backgroundColor('#F9FAFB').borderRadius(8).border({width:1,color:'#E5E7EB'})}}// 验证码输入(仅验证码登录时显示)if(this.activeTab==='code'){Column({space:6}){Text('验证码').fontSize(13).fontColor('#374151').fontWeight(FontWeight.Medium).alignSelf(ItemAlign.Start)Row({space:10}){TextInput({placeholder:'请输入6位验证码',text:this.verifyCode}).layoutWeight(1).height(44).fontSize(14).backgroundColor('#F9FAFB').borderRadius(8).border({width:1,color:'#E5E7EB'}).padding({left:12}).onChange((v)=>this.verifyCode=v)Button(this.codeSent?`${this.countdown}s`:'发送验证码').fontSize(13).height(44).padding({left:16,right:16}).backgroundColor(this.codeSent?'#F3F4F6':'#3B82F6').fontColor(this.codeSent?'#9CA3AF':Color.White).borderRadius(8).enabled(!this.codeSent).onClick(()=>this.sendCode())}}}// 记住我Row({space:8}){Toggle({type:ToggleType.Checkbox,isOn:this.rememberMe}).width(16).height(16).onChange((v)=>this.rememberMe=v)Text('记住我').fontSize(13).fontColor('#374151')}.alignSelf(ItemAlign.Start)// 错误提示if(this.errorMsg){Row({space:6}){Text('⚠️').fontSize(13)Text(this.errorMsg).fontSize(13).fontColor('#EF4444')}.width('100%').padding({left:10,right:10,top:8,bottom:8}).backgroundColor('#FEF2F2').borderRadius(6)}// 登录按钮Button(this.isLoading?'登录中...':'登录').width('100%').height(44).fontSize(15).fontWeight(FontWeight.Medium).backgroundColor(this.canSubmit?'#3B82F6':'#E5E7EB').fontColor(this.canSubmit?Color.White:'#9CA3AF').borderRadius(8).enabled(!this.isLoading).onClick(()=>this.handleLogin())// 注册提示Row({space:4}){Text('还没有账号?').fontSize(13).fontColor('#6B7280')Text('立即注册').fontSize(13).fontColor('#3B82F6').fontWeight(FontWeight.Medium)}.justifyContent(FlexAlign.Center).width('100%')}.padding({top:24})}.width(400).padding({left:40,right:40,top:40,bottom:40}).backgroundColor(Color.White).borderRadius(16).shadow({radius:24,color:'rgba(0,0,0,0.08)',offsetY:8})}.width('100%').height('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).linearGradient({direction:GradientDirection.RightBottom,colors:[['#EFF6FF',0],['#F5F3FF',1]]})}}TextInput 要显式设 width(‘100%’)
Column 默认alignItems: Center,但 TextInput 不会因此自动变成父容器宽度的一半——它有自己的默认宽度(通常是 300vp 左右)。要让它填满父容器,必须显式写width('100%')。
这是很多新手踩的坑:以为 Column 内子项会自动居中并填满,但实际上子项的宽度是独立的,只有对齐位置受alignItems控制。
"忘记密码"的 alignSelf
密码行的 Label 区用 Row 做三段式——左边"密码"文字,右边"忘记密码?"。这是最直接的方式:
Row(){Text('密码').fontSize(13).fontColor('#374151')Blank()// 撑开中间空间Text('忘记密码?').fontSize(12).fontColor('#3B82F6')}.width('100%')Blank()把"忘记密码"推到右边,不需要alignSelf。
写在最后
登录页没什么特别复杂的布局,Column +justifyContent: Center+alignItems: Center就能做出卡片居中的效果。细节上:TextInput 要给width('100%'),Label 行用 Row+Blank 做三段式,按钮禁用状态要同时改颜色和.enabled(false)。这几点处理好,登录页基本上就完整了。