F3: 表单校验反馈修复——点名缺失字段+自动滚动+@Builder按引用传参
用户真机反馈"还有 2 项未完成,点击不了提交并生成报告",排查出两层问题:
1. 表层(可见性):汇总条只报数量不报字段名,缺失字段在视野外。
- 汇总条改为点名缺失字段:"请完成:核心诉求、心理风格"
- 校验失败自动滚动到第一个缺失字段(componentUtils.getRectangleById
取 windowOffset 相对 form_root 的内容坐标,Scroller.scrollTo 动画滚动)
- 汇总条本身可点击,点击重新跳到第一个缺失字段
- 必填项标签加红色 * 标记
2. 根因(@Builder 陷阱):字段级红字从未渲染过。FieldLabel 按值传参
(label/err/required),ArkUI 规定 @Builder 按值传参不随 @State 变化
刷新——err 停在初始空串。改为 $$ 按引用传参(FieldLabelParams),
真机 dumpLayout 验证红字已随校验结果出现。
真机验证(uitest 注入 + dumpLayout + 截图):
- 空表单提交:红字四项齐现、汇总条点名、自动滚到首个错误
- 完整表单提交:进 40920 对话框(本期已生成),链路正常
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
* 6) SUCCEEDED → router.replaceUrl 结果页(params 带 reportId);FAILED → 失败态 + 幂等重试
|
||||
*/
|
||||
|
||||
import { router } from '@kit.ArkUI'
|
||||
import { router, componentUtils } from '@kit.ArkUI'
|
||||
import { ApiClient } from '../core/ApiClient'
|
||||
import { ApiError } from '../core/ApiTypes'
|
||||
import { AuthStore } from '../core/AuthStore'
|
||||
@@ -24,6 +24,12 @@ const GOAL_OPTIONS: string[] = ['显高', '显瘦', '显白', '显气质', '遮
|
||||
const STYLE_OPTIONS: string[] = ['戏剧', '睿智', '自然', '古典', '优雅', '浪漫', '前卫', '少女', '少年']
|
||||
const LAST_REPORT_STORAGE_KEY: string = 'f3_last_report_id'
|
||||
|
||||
interface FieldLabelParams {
|
||||
label: string
|
||||
err: string
|
||||
required: boolean
|
||||
}
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct ReportCreatePage {
|
||||
@@ -51,6 +57,7 @@ struct ReportCreatePage {
|
||||
private requestKey: string = '' // 失败重试沿用同一 key:重新提交即服务端幂等重放,不重复消耗额度
|
||||
private taskId: string = ''
|
||||
private reportId: string = ''
|
||||
private scroller: Scroller = new Scroller()
|
||||
|
||||
aboutToAppear(): void {
|
||||
const cached = AppStorage.get<string>(LAST_REPORT_STORAGE_KEY)
|
||||
@@ -111,15 +118,61 @@ struct ReportCreatePage {
|
||||
this.formSummary = ''
|
||||
if (!this.validateForm()) {
|
||||
// 字段级红字可能滚在视野外(真机实测的"点了没反应"就是这种情况):
|
||||
// 顶部再放一条汇总,让失败永远可见。
|
||||
const errs: string[] = [this.genderErr, this.heightErr, this.weightErr, this.goalsErr, this.styleErr]
|
||||
const n = errs.filter((e: string): boolean => e !== '').length
|
||||
this.formSummary = `还有 ${n} 项未完成,请检查标红的字段(性别/身高/体重/核心诉求/心理风格)`
|
||||
// 汇总条点名缺哪几项,并自动滚到第一个缺失字段。
|
||||
const labels: string[] = this.missingLabels()
|
||||
this.formSummary = labels.length > 0 ? `请完成:${labels.join('、')}` : '请检查标红的字段'
|
||||
this.scrollToFirstError()
|
||||
return
|
||||
}
|
||||
this.doSubmit()
|
||||
}
|
||||
|
||||
private missingLabels(): string[] {
|
||||
const labels: string[] = []
|
||||
if (this.genderErr !== '') {
|
||||
labels.push('性别')
|
||||
}
|
||||
if (this.heightErr !== '') {
|
||||
labels.push('身高')
|
||||
}
|
||||
if (this.weightErr !== '') {
|
||||
labels.push('体重')
|
||||
}
|
||||
if (this.goalsErr !== '') {
|
||||
labels.push('核心诉求')
|
||||
}
|
||||
if (this.styleErr !== '') {
|
||||
labels.push('心理风格')
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
private scrollToFirstError(): void {
|
||||
let firstId = ''
|
||||
if (this.genderErr !== '') {
|
||||
firstId = 'field_gender'
|
||||
} else if (this.heightErr !== '' || this.weightErr !== '') {
|
||||
firstId = 'field_height'
|
||||
} else if (this.goalsErr !== '') {
|
||||
firstId = 'field_goals'
|
||||
} else if (this.styleErr !== '') {
|
||||
firstId = 'field_style'
|
||||
}
|
||||
if (firstId === '') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// windowOffset 随滚动同步移动,两处相减即为内容坐标差,
|
||||
// 与当前滚动位置无关
|
||||
const root = componentUtils.getRectangleById('form_root')
|
||||
const target = componentUtils.getRectangleById(firstId)
|
||||
const dy = target.windowOffset.y - root.windowOffset.y - 8
|
||||
this.scroller.scrollTo({ xOffset: 0, yOffset: Math.max(0, dy), animation: { duration: 250, curve: Curve.EaseOut } })
|
||||
} catch (e) {
|
||||
console.error(`scrollTo ${firstId} failed: ${String(e).substring(0, 80)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async doSubmit(): Promise<void> {
|
||||
this.submitting = true
|
||||
this.submitError = ''
|
||||
@@ -226,14 +279,21 @@ struct ReportCreatePage {
|
||||
.padding({ left: 12, right: 16, top: 10, bottom: 6 })
|
||||
}
|
||||
|
||||
// 按引用传递($$):@Builder 按值传参时 @State 变化不会触发刷新,
|
||||
// 字段级红字将永远停在初始空值(真机实测复现)
|
||||
@Builder
|
||||
FieldLabel(label: string, err: string) {
|
||||
FieldLabel($$: FieldLabelParams) {
|
||||
Row({ space: 6 }) {
|
||||
Text(label)
|
||||
if ($$.required) {
|
||||
Text('*')
|
||||
.fontSize(14)
|
||||
.fontColor('#E5484D')
|
||||
}
|
||||
Text($$.label)
|
||||
.fontSize(13)
|
||||
.fontColor('#666666')
|
||||
if (err !== '') {
|
||||
Text(err)
|
||||
if ($$.err !== '') {
|
||||
Text($$.err)
|
||||
.fontSize(11)
|
||||
.fontColor('#E5484D')
|
||||
.layoutWeight(1)
|
||||
@@ -245,7 +305,7 @@ struct ReportCreatePage {
|
||||
|
||||
@Builder
|
||||
FormBody() {
|
||||
Scroll() {
|
||||
Scroll(this.scroller) {
|
||||
Column({ space: 12 }) {
|
||||
Column({ space: 4 }) {
|
||||
Text('生成你的形象诊断报告')
|
||||
@@ -266,7 +326,7 @@ struct ReportCreatePage {
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#333333')
|
||||
Column({ space: 8 }) {
|
||||
this.FieldLabel('性别', this.genderErr)
|
||||
this.FieldLabel({ label: '性别', err: this.genderErr, required: true })
|
||||
Row({ space: 8 }) {
|
||||
ForEach(GENDER_OPTIONS, (g: string) => {
|
||||
Text(g)
|
||||
@@ -284,10 +344,11 @@ struct ReportCreatePage {
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.id('field_gender')
|
||||
|
||||
Row({ space: 12 }) {
|
||||
Column({ space: 8 }) {
|
||||
this.FieldLabel('身高 (cm)', this.heightErr)
|
||||
this.FieldLabel({ label: '身高 (cm)', err: this.heightErr, required: true })
|
||||
TextInput({ placeholder: '如 165', text: this.heightInput })
|
||||
.type(InputType.Number)
|
||||
.height(44)
|
||||
@@ -301,9 +362,10 @@ struct ReportCreatePage {
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.id('field_height')
|
||||
|
||||
Column({ space: 8 }) {
|
||||
this.FieldLabel('体重 (kg)', this.weightErr)
|
||||
this.FieldLabel({ label: '体重 (kg)', err: this.weightErr, required: true })
|
||||
TextInput({ placeholder: '如 52', text: this.weightInput })
|
||||
.type(InputType.Number)
|
||||
.height(44)
|
||||
@@ -321,7 +383,7 @@ struct ReportCreatePage {
|
||||
.width('100%')
|
||||
|
||||
Column({ space: 8 }) {
|
||||
this.FieldLabel('生日', '')
|
||||
this.FieldLabel({ label: '生日', err: '', required: false })
|
||||
Row({ space: 12 }) {
|
||||
Text(`当前选择:${formatDate(this.birthday)}`)
|
||||
.fontSize(13)
|
||||
@@ -363,7 +425,7 @@ struct ReportCreatePage {
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#333333')
|
||||
Column({ space: 8 }) {
|
||||
this.FieldLabel('核心诉求(多选)', this.goalsErr)
|
||||
this.FieldLabel({ label: '核心诉求(多选)', err: this.goalsErr, required: true })
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(GOAL_OPTIONS, (g: string) => {
|
||||
Text(g)
|
||||
@@ -382,9 +444,10 @@ struct ReportCreatePage {
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.id('field_goals')
|
||||
|
||||
Column({ space: 8 }) {
|
||||
this.FieldLabel('心理风格(九型·单选)', this.styleErr)
|
||||
this.FieldLabel({ label: '心理风格(九型·单选)', err: this.styleErr, required: true })
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(STYLE_OPTIONS, (s: string) => {
|
||||
Text(s)
|
||||
@@ -404,6 +467,7 @@ struct ReportCreatePage {
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.id('field_style')
|
||||
}
|
||||
.width('100%')
|
||||
.padding(16)
|
||||
@@ -445,6 +509,9 @@ struct ReportCreatePage {
|
||||
.backgroundColor('#FDECEC')
|
||||
.borderRadius(8)
|
||||
.textAlign(TextAlign.Start)
|
||||
.onClick(() => {
|
||||
this.scrollToFirstError()
|
||||
})
|
||||
}
|
||||
if (this.submitError !== '') {
|
||||
Text(this.submitError)
|
||||
@@ -466,6 +533,7 @@ struct ReportCreatePage {
|
||||
})
|
||||
}
|
||||
.padding({ left: 16, right: 16 })
|
||||
.id('form_root')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
|
||||
Reference in New Issue
Block a user