fix(app): 真机首验三连修——信封解析/DatePicker 折叠/跳转兜底

真机+模拟器注入实测发现并修复:
1. ApiClient 把一切非 200 HTTP 吞成 50000「服务开小差了」——后端业务
   错误(40920/40301/42910…)走非 200 状态码但 body 是统一信封,先验
   信封形状再判成败。这是「提交不起作用」的直接原因(用户二次提交
   =409,红字被布局挤在视野外)
2. 生日 DatePicker 默认展开高约半屏:九型必填项与提交按钮被推出视野、
   滚动手势被选择器消费。改点击展开,表单一屏可见
3. 校验失败加顶部汇总红条(字段红字可能在视野外的可见性兜底)
4. 轮询完成不自动跳结果页:@Builder 传函数给子组件在真机调用无效
   (手动按钮同一方法正常)——加「查看报告」兜底按钮 + replaceUrl
   显式 catch + 完成态文案引导,回调保留 try/catch 与日志
验证:40920 对话框正常弹出;新用户提交→轮询→查看报告→32 维结果页
完整渲染(含降级黄条、报告编号)
This commit is contained in:
xsl
2026-08-15 13:11:26 +08:00
parent dff8084bbb
commit c24924fa43
3 changed files with 91 additions and 18 deletions
@@ -81,7 +81,15 @@ export struct TaskPollingPanel {
} }
if (t.status === 'SUCCEEDED') { if (t.status === 'SUCCEEDED') {
this.stopTimer() this.stopTimer()
this.onSucceeded() this.stageText = '已完成——点击下方「查看报告」查看结果'
// 回调经 @Builder 传函数在真机上存在调用无效的坑(手动按钮同一
// 方法完全正常,真机实测轮询完成不自动跳转)。保留回调尝试,
// 失败静默并由宿主的「查看报告」兜底按钮承担导航。
try {
this.onSucceeded()
} catch (e) {
console.error(`poll onSucceeded failed: ${JSON.stringify(e)}`)
}
return return
} }
if (t.status === 'FAILED') { if (t.status === 'FAILED') {
+12 -7
View File
@@ -76,15 +76,20 @@ export class ApiClient {
connectTimeout: 10000, connectTimeout: 10000,
readTimeout: 30000 readTimeout: 30000
}) })
if (resp.responseCode !== 200 || resp.result === null || resp.result === undefined) { // 后端业务错误(40920/40301/42910…)走非 200 的 HTTP 状态码,
// 网关/代理层错误:没有信封可用,给统一话术(NFR-UX-02) // 但 body 仍是统一信封——先验信封形状再判成败;只有 body 不是
throw new ApiError(50000, '服务开小差了', '请稍后重试', resp.responseCode) // 信封(网关 502 的 HTML、字符串等)才按网关错误兜底。
} // 真机实测教训:把非 200 一律吞成 50000,会吃掉全部业务错误分支
// (40920 的"已生成过"对话框、40301 的额度提示全部失效)。
const env = resp.result as ApiEnvelope<T> const env = resp.result as ApiEnvelope<T>
if (env.code !== 0) { if (typeof env === 'object' && env !== null && typeof env.code === 'number') {
throw new ApiError(env.code, env.message, env.hint, resp.responseCode) if (env.code !== 0) {
throw new ApiError(env.code, env.message, env.hint, resp.responseCode)
}
return env.data
} }
return env.data // 网关/代理层错误:没有信封可用,给统一话术(NFR-UX-02)
throw new ApiError(50000, '服务开小差了', '请稍后重试', resp.responseCode)
} finally { } finally {
client.destroy() client.destroy()
} }
@@ -41,6 +41,9 @@ struct ReportCreatePage {
@State goalsErr: string = '' @State goalsErr: string = ''
@State styleErr: string = '' @State styleErr: string = ''
@State submitting: boolean = false @State submitting: boolean = false
@State showDatePicker: boolean = false
@State formSummary: string = ''
@State navError: string = ''
@State submitError: string = '' @State submitError: string = ''
@State failMsg: string = '' @State failMsg: string = ''
@State showDupDialog: boolean = false @State showDupDialog: boolean = false
@@ -105,7 +108,13 @@ struct ReportCreatePage {
if (this.submitting) { if (this.submitting) {
return return
} }
this.formSummary = ''
if (!this.validateForm()) { 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} 项未完成,请检查标红的字段(性别/身高/体重/核心诉求/心理风格)`
return return
} }
this.doSubmit() this.doSubmit()
@@ -176,7 +185,12 @@ struct ReportCreatePage {
private onTaskSucceeded(): void { private onTaskSucceeded(): void {
const params: Record<string, Object> = { 'reportId': this.reportId } const params: Record<string, Object> = { 'reportId': this.reportId }
// 显式 catch:replaceUrl 的 Promise 拒绝在无 catch 时完全静默(真机实测
// 卡在轮询页无任何提示),至少把失败暴露出来并由兜底按钮接管。
router.replaceUrl({ url: 'pages/ReportResultPage', params: params }) router.replaceUrl({ url: 'pages/ReportResultPage', params: params })
.catch((e: Object): void => {
this.navError = `报告已生成,但打开结果页失败:${String(e).substring(0, 80)}`
})
} }
private onTaskFailed(reason: string): void { private onTaskFailed(reason: string): void {
@@ -308,17 +322,32 @@ struct ReportCreatePage {
Column({ space: 8 }) { Column({ space: 8 }) {
this.FieldLabel('生日', '') this.FieldLabel('生日', '')
Text(`当前选择:${formatDate(this.birthday)}`) Row({ space: 12 }) {
.fontSize(12) Text(`当前选择:${formatDate(this.birthday)}`)
.fontColor('#999999') .fontSize(13)
DatePicker({ .fontColor('#333333')
start: new Date(1930, 0, 1), .layoutWeight(1)
end: new Date(), Text(this.showDatePicker ? '收起 ▲' : '修改 ▼')
selected: this.birthday .fontSize(13)
}) .fontColor('#C2185B')
.onDateChange((d: Date) => { .onClick(() => {
this.birthday = d this.showDatePicker = !this.showDatePicker
})
}
.width('100%')
// DatePicker 默认收起:展开态高约半屏,会把心理风格/提交按钮挤出
// 视野并把滚动手势吃掉(真机实测:用户看不到必填项与错误提示,
// 表现为"提交没反应")。需要改生日时才展开。
if (this.showDatePicker) {
DatePicker({
start: new Date(1930, 0, 1),
end: new Date(),
selected: this.birthday
}) })
.onDateChange((d: Date) => {
this.birthday = d
})
}
} }
.width('100%') .width('100%')
.alignItems(HorizontalAlign.Start) .alignItems(HorizontalAlign.Start)
@@ -407,6 +436,16 @@ struct ReportCreatePage {
.backgroundColor('#FFFFFF') .backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Start) .alignItems(HorizontalAlign.Start)
if (this.formSummary !== '') {
Text(this.formSummary)
.fontSize(13)
.fontColor('#E5484D')
.width('100%')
.padding(10)
.backgroundColor('#FDECEC')
.borderRadius(8)
.textAlign(TextAlign.Start)
}
if (this.submitError !== '') { if (this.submitError !== '') {
Text(this.submitError) Text(this.submitError)
.fontSize(12) .fontSize(12)
@@ -453,6 +492,27 @@ struct ReportCreatePage {
} }
}) })
.width('86%') .width('86%')
if (this.navError !== '') {
Text(this.navError)
.fontSize(12)
.fontColor('#E5484D')
.width('86%')
.textAlign(TextAlign.Center)
}
// 兜底:自动跳转若失败(或用户中途离开又回来),手动进结果页。
// reportId 在提交成功时即已保存,轮询页始终可进。
if (this.reportId !== '') {
Button('查看报告')
.fontSize(14)
.backgroundColor('#FFFFFF')
.fontColor('#C2185B')
.borderRadius(20)
.padding({ left: 24, right: 24 })
.margin({ top: 8 })
.onClick(() => {
this.onTaskSucceeded()
})
}
} }
.width('100%') .width('100%')
.layoutWeight(1) .layoutWeight(1)