HarmonyOS PC 实战之注册表单的状态设计——四个 @State 如何驱动完整的表单交互

📅 2026/6/16 0:03:15
HarmonyOS PC 实战之注册表单的状态设计——四个 @State 如何驱动完整的表单交互
文章目录前言四个核心状态formData一个对象管所有字段attempted控制错误提示的显示时机isFormValid用 get 派生不用 State完整代码密码强度的派生计算agreeTerms 和 isFormValid 的关系小结前言一个注册表单看起来就是几个输入框但要做到该提示的时候提示、不该提示的时候不打扰、注册按钮灰色时不可点背后需要好几个状态变量分工合作。HarmonyOS PC 端的注册表单用四个State变量就能把所有交互逻辑覆盖掉。这篇把这四个变量各自负责什么、怎么联动说清楚。四个核心状态StateformData:FormData{// 存所有输入框的值username:,email:,password:,confirmPassword:}StateagreeTerms:booleanfalse// 用户协议是否勾选StateshowPassword:booleanfalse// 密码是否可见Stateattempted:booleanfalse// 用户是否点过提交一个状态一个职责互不重叠。formData一个对象管所有字段不要给每个输入框单独定义State username、State email……六七个字段就六七个状态冗余。用一个接口对象统一管理interfaceFormData{username:stringemail:stringpassword:stringconfirmPassword:string}StateformData:FormData{username:,email:,password:,confirmPassword:}更新时展开赋值this.formData{...this.formData,username:newValue}这样不会丢掉其他字段也能触发响应式刷新。attempted控制错误提示的显示时机用户没有点提交就显示错误提示是一个很差的体验——用户刚打开表单还没输任何东西就一片红这让人很不舒服。attempted就是解决这个问题的// 错误提示只在 attempted 为 true 后才显示if(this.attemptedthis.hasError(field.key)){Text(this.getErrorMsg(field)).fontColor(#EF4444)}点击注册按钮时设attempted true这时才开始显示错误提示Button(注册账号).onClick((){this.attemptedtrueif(this.isFormValid){this.submitForm()}})isFormValid用 get 派生不用 State表单是否填写完整、是否可提交这不是一个需要存储的状态而是从formData派生的计算结果getisFormValid():boolean{return!!(this.formData.username.trim().length2this.formData.email.includes()this.formData.password.length8this.formData.confirmPasswordthis.formData.passwordthis.agreeTerms)}formData或agreeTerms任意一个变化isFormValid自动重新计算注册按钮的禁用状态跟着变。完整代码enumFormFieldKey{None,Username,Email,Password,ConfirmPassword}interfaceFieldConfig{key:FormFieldKey label:stringplaceholder:stringrequired:booleantype:text|email|passwordiconLeft:stringhint:stringerrorMsg:string}EntryComponentstruct PcFormLayoutPage{Stateusername:stringStateemail:stringStatepassword:stringStateconfirmPassword:stringStateshowPassword:booleanfalseStateshowConfirmPassword:booleanfalseStateattempted:booleanfalseStatefocusedField:FormFieldKeyFormFieldKey.None fields:FieldConfig[][{key:FormFieldKey.Username,label:用户名,placeholder:4~20个字符支持中英文,required:true,type:text,iconLeft:,hint:,errorMsg:用户名不能为空},{key:FormFieldKey.Email,label:电子邮箱,placeholder:exampleemail.com,required:true,type:email,iconLeft:,hint:,errorMsg:请输入有效的邮箱地址},{key:FormFieldKey.Password,label:密码,placeholder:至少8位包含字母和数字,required:true,type:password,iconLeft:,hint:忘记密码,errorMsg:密码至少8位},{key:FormFieldKey.ConfirmPassword,label:确认密码,placeholder:再次输入密码,required:true,type:password,iconLeft:,hint:,errorMsg:两次密码不一致},]getFieldValue(key:FormFieldKey):string{if(keyFormFieldKey.Username)returnthis.usernameif(keyFormFieldKey.Email)returnthis.emailif(keyFormFieldKey.Password)returnthis.passwordreturnthis.confirmPassword}hasError(key:FormFieldKey,val:string):boolean{if(!this.attempted)returnfalseif(!val||val.trim())returntrueif(keyFormFieldKey.Email!val.includes())returntrueif(keyFormFieldKey.Passwordval.length8)returntrueif(keyFormFieldKey.ConfirmPasswordval!this.password)returntruereturnfalse}getErrorMsg(field:FieldConfig,val:string):string{if(!this.hasError(field.key,val))returnif(!val||val.trim())returnfield.errorMsgif(field.keyFormFieldKey.Email)return请输入有效的邮箱地址if(field.keyFormFieldKey.Password)return密码至少8位if(field.keyFormFieldKey.ConfirmPassword)return两次密码不一致returnfield.errorMsg}getisFormValid():boolean{return!!(this.username.trim()this.email.includes()this.password.length8this.confirmPasswordthis.password)}BuilderformField(field:FieldConfig,value:string){Column({space:6}){// Label 行Row({space:4}){if(field.required){Text(*).fontSize(13).fontColor(#EF4444)}Text(field.label).fontSize(13).fontColor(#374151).fontWeight(FontWeight.Medium)Blank()if(field.hint){Text(field.hint).fontSize(11).fontColor(#3B82F6)}}.width(100%).alignItems(VerticalAlign.Center)// 输入区行Row({space:8}){Text(field.iconLeft).fontSize(16).fontColor(this.hasError(field.key,value)?#EF4444:#9CA3AF).width(20).textAlign(TextAlign.Center)TextInput({placeholder:field.placeholder,text:value}).layoutWeight(1).backgroundColor(Color.Transparent).border({width:0}).fontSize(14).placeholderColor(#C4C9D4).type(field.typepassword?(field.keyFormFieldKey.Password?(this.showPassword?InputType.Normal:InputType.Password):(this.showConfirmPassword?InputType.Normal:InputType.Password)):(field.typeemail?InputType.Email:InputType.Normal)).onChange((v){if(field.keyFormFieldKey.Username){this.usernamev}elseif(field.keyFormFieldKey.Email){this.emailv}elseif(field.keyFormFieldKey.Password){this.passwordv}else{this.confirmPasswordv}}).onFocus((){this.focusedFieldfield.key}).onBlur((){this.focusedFieldFormFieldKey.None})if(field.typepassword){Text(field.keyFormFieldKey.Password?(this.showPassword?:):(this.showConfirmPassword?:)).fontSize(16).fontColor(#9CA3AF).onClick((){if(field.keyFormFieldKey.Password)this.showPassword!this.showPasswordelsethis.showConfirmPassword!this.showConfirmPassword})}// 校验状态图标if(this.attempted){Text(this.hasError(field.key,value)?❌:✅).fontSize(14)}}.height(48).padding({left:14,right:14}).backgroundColor(this.hasError(field.key,value)?#FEF2F2:#F9FAFB).borderRadius(10).border({width:1.5,color:this.hasError(field.key,value)?#EF4444:this.focusedFieldfield.key?#3B82F6:#E5E7EB}).animation({duration:150})// 错误提示if(this.attemptedthis.hasError(field.key,value)){Row({space:4}){Text(⚠).fontSize(11).fontColor(#EF4444)Text(this.getErrorMsg(field,value)).fontSize(11).fontColor(#EF4444)}}}.width(100%).alignItems(HorizontalAlign.Start)}build(){Scroll(){Column({space:0}){// 卡片容器Column({space:24}){// 标题Column({space:6}){Text(创建账号).fontSize(24).fontWeight(FontWeight.Bold).fontColor(#111827)Text(加入 HarmonyOS 开发者社区).fontSize(14).fontColor(#6B7280)}.alignItems(HorizontalAlign.Start).width(100%)// 表单字段ForEach(this.fields,(field:FieldConfig){this.formField(field,this.getFieldValue(field.key))})// 提交按钮Column({space:12}){Button(注册账号).width(100%).height(48).backgroundColor(this.isFormValid?#3B82F6:#9CA3AF).borderRadius(10).fontSize(15).fontWeight(FontWeight.Medium).onClick((){this.attemptedtrue})Row({space:6}){Text(已有账号).fontSize(13).fontColor(#6B7280)Text(立即登录).fontSize(13).fontColor(#3B82F6).fontWeight(FontWeight.Medium)}.justifyContent(FlexAlign.Center)}.width(100%)}.padding({left:40,right:40,top:40,bottom:40}).backgroundColor(Color.White).borderRadius(20).shadow({radius:24,color:#10000000,offsetY:8}).width(100%).constraintSize({maxWidth:480}).margin({left:auto,right:auto})}.padding({left:24,right:24,top:48,bottom:48})}.width(100%).height(100%).backgroundColor(#F9FAFB)}}密码强度的派生计算密码强度是一个纯计算结果不需要State用get派生getstrengthLevel():number{constpthis.formData.passwordletscore0if(p.length8)scoreif(/[A-Z]/.test(p))scoreif(/[0-9]/.test(p))scoreif(/[^A-Za-z0-9]/.test(p))scorereturnscore// 0~4}四条规则每满足一条 1 分满分 4 分。密码变化时strengthLevel自动重算强度条跟着变色。agreeTerms 和 isFormValid 的关系勾选协议是注册的必要条件。在isFormValid里加上 this.agreeTerms协议没勾选时注册按钮始终灰色getisFormValid():boolean{returnfieldRulesAllValidthis.agreeTerms}用户没有勾选协议就点注册attempted设为 true错误提示出来了但协议那行没有专门的错误提示——让协议 Checkbox 本身颜色变红就够了不需要额外的提示文字。小结四个状态formData存数据、agreeTerms存协议状态、showPassword控制密码可见性、attempted控制错误显示时机。派生计算不用状态存isFormValid、strengthLevel都是get属性自动跟着状态变化。