文章目录
- 前言
- 四个核心状态
- formData:一个对象管所有字段
- attempted:控制错误提示的显示时机
- isFormValid:用 get 派生,不用 @State
- 完整代码
- 密码强度的派生计算
- agreeTerms 和 isFormValid 的关系
- 小结
前言
一个注册表单,看起来就是几个输入框,但要做到"该提示的时候提示、不该提示的时候不打扰、注册按钮灰色时不可点",背后需要好几个状态变量分工合作。
HarmonyOS PC 端的注册表单用四个@State变量就能把所有交互逻辑覆盖掉。这篇把这四个变量各自负责什么、怎么联动说清楚。
四个核心状态
@StateformData:FormData={// 存所有输入框的值username:'',email:'',password:'',confirmPassword:''}@StateagreeTerms:boolean=false// 用户协议是否勾选@StateshowPassword:boolean=false// 密码是否可见@Stateattempted:boolean=false// 用户是否点过提交一个状态,一个职责,互不重叠。
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.attempted&&this.hasError(field.key)){Text(this.getErrorMsg(field)).fontColor('#EF4444')}点击注册按钮时设attempted = true,这时才开始显示错误提示:
Button('注册账号').onClick(()=>{this.attempted=trueif(this.isFormValid){this.submitForm()}})isFormValid:用 get 派生,不用 @State
表单是否填写完整、是否可提交,这不是一个需要存储的状态,而是从formData派生的计算结果:
getisFormValid():boolean{return!!(this.formData.username.trim().length>=2&&this.formData.email.includes('@')&&this.formData.password.length>=8&&this.formData.confirmPassword===this.formData.password&&this.agreeTerms)}formData或agreeTerms任意一个变化,isFormValid自动重新计算,注册按钮的禁用状态跟着变。
完整代码
enumFormFieldKey{None,Username,Email,Password,ConfirmPassword}interfaceFieldConfig{key:FormFieldKey label:stringplaceholder:stringrequired:booleantype:'text'|'email'|'password'iconLeft:stringhint:stringerrorMsg:string}@Entry@Componentstruct PcFormLayoutPage{@Stateusername:string=''@Stateemail:string=''@Statepassword:string=''@StateconfirmPassword:string=''@StateshowPassword:boolean=false@StateshowConfirmPassword:boolean=false@Stateattempted:boolean=false@StatefocusedField:FormFieldKey=FormFieldKey.None fields:FieldConfig[]=[{key:FormFieldKey.Username,label:'用户名',placeholder:'4~20个字符,支持中英文',required:true,type:'text',iconLeft:'👤',hint:'',errorMsg:'用户名不能为空'},{key:FormFieldKey.Email,label:'电子邮箱',placeholder:'example@email.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(key===FormFieldKey.Username)returnthis.usernameif(key===FormFieldKey.Email)returnthis.emailif(key===FormFieldKey.Password)returnthis.passwordreturnthis.confirmPassword}hasError(key:FormFieldKey,val:string):boolean{if(!this.attempted)returnfalseif(!val||val.trim()==='')returntrueif(key===FormFieldKey.Email&&!val.includes('@'))returntrueif(key===FormFieldKey.Password&&val.length<8)returntrueif(key===FormFieldKey.ConfirmPassword&&val!==this.password)returntruereturnfalse}getErrorMsg(field:FieldConfig,val:string):string{if(!this.hasError(field.key,val))return''if(!val||val.trim()==='')returnfield.errorMsgif(field.key===FormFieldKey.Email)return'请输入有效的邮箱地址'if(field.key===FormFieldKey.Password)return'密码至少8位'if(field.key===FormFieldKey.ConfirmPassword)return'两次密码不一致'returnfield.errorMsg}getisFormValid():boolean{return!!(this.username.trim()&&this.email.includes('@')&&this.password.length>=8&&this.confirmPassword===this.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.type==='password'?(field.key===FormFieldKey.Password?(this.showPassword?InputType.Normal:InputType.Password):(this.showConfirmPassword?InputType.Normal:InputType.Password)):(field.type==='email'?InputType.Email:InputType.Normal)).onChange((v)=>{if(field.key===FormFieldKey.Username){this.username=v}elseif(field.key===FormFieldKey.Email){this.email=v}elseif(field.key===FormFieldKey.Password){this.password=v}else{this.confirmPassword=v}}).onFocus(()=>{this.focusedField=field.key}).onBlur(()=>{this.focusedField=FormFieldKey.None})if(field.type==='password'){Text(field.key===FormFieldKey.Password?(this.showPassword?'🙈':'👁'):(this.showConfirmPassword?'🙈':'👁')).fontSize(16).fontColor('#9CA3AF').onClick(()=>{if(field.key===FormFieldKey.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.focusedField===field.key?'#3B82F6':'#E5E7EB'}).animation({duration:150})// 错误提示if(this.attempted&&this.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.attempted=true})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{constp=this.formData.passwordletscore=0if(p.length>=8)score++if(/[A-Z]/.test(p))score++if(/[0-9]/.test(p))score++if(/[^A-Za-z0-9]/.test(p))score++returnscore// 0~4}四条规则每满足一条 +1 分,满分 4 分。密码变化时strengthLevel自动重算,强度条跟着变色。
agreeTerms 和 isFormValid 的关系
勾选协议是注册的必要条件。在isFormValid里加上&& this.agreeTerms,协议没勾选时注册按钮始终灰色:
getisFormValid():boolean{returnfieldRulesAllValid&&this.agreeTerms}用户没有勾选协议就点注册,attempted设为 true,错误提示出来了,但协议那行没有专门的错误提示——让协议 Checkbox 本身颜色变红就够了,不需要额外的提示文字。
小结
四个状态:formData存数据、agreeTerms存协议状态、showPassword控制密码可见性、attempted控制错误显示时机。派生计算不用状态存:isFormValid、strengthLevel都是get属性,自动跟着状态变化。