HarmonyOS PC 实战之注册表单的状态设计——四个 @State 如何驱动完整的表单交互
文章目录
前言
一个注册表单,看起来就是几个输入框,但要做到"该提示的时候提示、不该提示的时候不打扰、注册按钮灰色时不可点",背后需要好几个状态变量分工合作。
HarmonyOS PC 端的注册表单用四个 @State 变量就能把所有交互逻辑覆盖掉。这篇把这四个变量各自负责什么、怎么联动说清楚。

四个核心状态
@State formData: FormData = { // 存所有输入框的值
username: '', email: '',
password: '', confirmPassword: ''
}
@State agreeTerms: boolean = false // 用户协议是否勾选
@State showPassword: boolean = false // 密码是否可见
@State attempted: boolean = false // 用户是否点过提交
一个状态,一个职责,互不重叠。
formData:一个对象管所有字段

不要给每个输入框单独定义 @State username、@State email……六七个字段就六七个状态,冗余。
用一个接口对象统一管理:
interface FormData {
username: string
email: string
password: string
confirmPassword: string
}
@State formData: 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 = true
if (this.isFormValid) {
this.submitForm()
}
})
isFormValid:用 get 派生,不用 @State
表单是否填写完整、是否可提交,这不是一个需要存储的状态,而是从 formData 派生的计算结果:
get isFormValid(): 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 自动重新计算,注册按钮的禁用状态跟着变。
完整代码
enum FormFieldKey {
None,
Username,
Email,
Password,
ConfirmPassword
}
interface FieldConfig {
key: FormFieldKey
label: string
placeholder: string
required: boolean
type: 'text' | 'email' | 'password'
iconLeft: string
hint: string
errorMsg: string
}
@Entry
@Component
struct PcFormLayoutPage {
@State username: string = ''
@State email: string = ''
@State password: string = ''
@State confirmPassword: string = ''
@State showPassword: boolean = false
@State showConfirmPassword: boolean = false
@State attempted: boolean = false
@State focusedField: 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) return this.username
if (key === FormFieldKey.Email) return this.email
if (key === FormFieldKey.Password) return this.password
return this.confirmPassword
}
hasError(key: FormFieldKey, val: string): boolean {
if (!this.attempted) return false
if (!val || val.trim() === '') return true
if (key === FormFieldKey.Email && !val.includes('@')) return true
if (key === FormFieldKey.Password && val.length < 8) return true
if (key === FormFieldKey.ConfirmPassword && val !== this.password) return true
return false
}
getErrorMsg(field: FieldConfig, val: string): string {
if (!this.hasError(field.key, val)) return ''
if (!val || val.trim() === '') return field.errorMsg
if (field.key === FormFieldKey.Email) return '请输入有效的邮箱地址'
if (field.key === FormFieldKey.Password) return '密码至少8位'
if (field.key === FormFieldKey.ConfirmPassword) return '两次密码不一致'
return field.errorMsg
}
get isFormValid(): boolean {
return !!(
this.username.trim() &&
this.email.includes('@') &&
this.password.length >= 8 &&
this.confirmPassword === this.password
)
}
@Builder
formField(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
} else if (field.key === FormFieldKey.Email) {
this.email = v
} else if (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.showPassword
else this.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 派生:
get strengthLevel(): number {
const p = this.formData.password
let score = 0
if (p.length >= 8) score++
if (/[A-Z]/.test(p)) score++
if (/[0-9]/.test(p)) score++
if (/[^A-Za-z0-9]/.test(p)) score++
return score // 0~4
}
四条规则每满足一条 +1 分,满分 4 分。密码变化时 strengthLevel 自动重算,强度条跟着变色。
agreeTerms 和 isFormValid 的关系
勾选协议是注册的必要条件。在 isFormValid 里加上 && this.agreeTerms,协议没勾选时注册按钮始终灰色:
get isFormValid(): boolean {
return fieldRulesAllValid && this.agreeTerms
}
用户没有勾选协议就点注册,attempted 设为 true,错误提示出来了,但协议那行没有专门的错误提示——让协议 Checkbox 本身颜色变红就够了,不需要额外的提示文字。
小结
四个状态:formData 存数据、agreeTerms 存协议状态、showPassword 控制密码可见性、attempted 控制错误显示时机。派生计算不用状态存:isFormValid、strengthLevel 都是 get 属性,自动跟着状态变化。
更多推荐



所有评论(0)