780 lines
25 KiB
Plaintext
780 lines
25 KiB
Plaintext
/**
|
||
* ReportCreatePage —— 报告创建(F3):问卷六字段 + 模拟照片键 + 提交 + 任务轮询。
|
||
*
|
||
* 流程:
|
||
* 1) 问卷表单(性别/身高/体重/生日/核心诉求多选/心理风格九型),提交前前端校验;
|
||
* 身高/体重为滚轮选择器(showTextPickerDialog),不提供键盘输入
|
||
* 2) 照片键为模拟:V1 骨架 COS 直传未接入,留空自动生成 report_photo/{userId}/{随机}.jpg
|
||
* 3) POST /api/v1/reports(request_key 随机 hex;失败重试沿用同一 key,服务端幂等重放)
|
||
* 4) 40920(本期已生成)→ 页内对话框:可跳最近一次报告(AppStorage 记录),
|
||
* 无记录则提示稍后在任务中心查看
|
||
* 5) 成功 → 3 秒轮询任务(TaskPollingPanel,页面销毁自动清定时器,轮询期间允许返回)
|
||
* 6) SUCCEEDED → router.replaceUrl 结果页(params 带 reportId);FAILED → 失败态 + 幂等重试
|
||
*/
|
||
|
||
import { router, componentUtils } from '@kit.ArkUI'
|
||
import { ApiClient } from '../core/ApiClient'
|
||
import { ApiError } from '../core/ApiTypes'
|
||
import { AuthStore } from '../core/AuthStore'
|
||
import { TaskPollingPanel } from '../components/f3/TaskPollingPanel'
|
||
import { ReportCreateResp, buildMockPhotoKey, formatDate, randomHex } from '../components/f3/ReportTypes'
|
||
|
||
const COLOR_PRIMARY: string = '#C2185B'
|
||
const GENDER_OPTIONS: string[] = ['女', '男', '不便告知']
|
||
const GOAL_OPTIONS: string[] = ['显高', '显瘦', '显白', '显气质', '遮肉', '修饰肩颈']
|
||
const STYLE_OPTIONS: string[] = ['戏剧', '睿智', '自然', '古典', '优雅', '浪漫', '前卫', '少女', '少年']
|
||
const LAST_REPORT_STORAGE_KEY: string = 'f3_last_report_id'
|
||
|
||
// 身高/体重改为选择器(不允许键盘输入):数值范围与原校验一致
|
||
const HEIGHT_MIN: number = 100
|
||
const HEIGHT_MAX: number = 250
|
||
const HEIGHT_DEFAULT: number = 165
|
||
const WEIGHT_MIN: number = 25
|
||
const WEIGHT_MAX: number = 300
|
||
const WEIGHT_DEFAULT: number = 52
|
||
|
||
function buildRangeOptions(min: number, max: number): string[] {
|
||
const arr: string[] = []
|
||
for (let v = min; v <= max; v++) {
|
||
arr.push(`${v}`)
|
||
}
|
||
return arr
|
||
}
|
||
|
||
const HEIGHT_OPTIONS: string[] = buildRangeOptions(HEIGHT_MIN, HEIGHT_MAX)
|
||
const WEIGHT_OPTIONS: string[] = buildRangeOptions(WEIGHT_MIN, WEIGHT_MAX)
|
||
|
||
interface FieldLabelParams {
|
||
label: string
|
||
err: string
|
||
required: boolean
|
||
}
|
||
|
||
@Entry
|
||
@Component
|
||
struct ReportCreatePage {
|
||
@State phase: string = 'form' // form | polling | failed
|
||
@State gender: string = ''
|
||
@State heightInput: string = ''
|
||
@State weightInput: string = ''
|
||
@State birthday: Date = new Date(2000, 0, 15)
|
||
@State coreGoals: string[] = []
|
||
@State psychStyle: string = ''
|
||
@State photoKeyInput: string = ''
|
||
@State genderErr: string = ''
|
||
@State heightErr: string = ''
|
||
@State weightErr: string = ''
|
||
@State goalsErr: string = ''
|
||
@State styleErr: string = ''
|
||
@State submitting: boolean = false
|
||
@State showDatePicker: boolean = false
|
||
@State formSummary: string = ''
|
||
@State navError: string = ''
|
||
@State submitError: string = ''
|
||
@State failMsg: string = ''
|
||
@State showDupDialog: boolean = false
|
||
@State lastReportId: string = ''
|
||
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)
|
||
if (cached !== undefined && cached !== '') {
|
||
this.lastReportId = cached
|
||
}
|
||
}
|
||
|
||
private validateForm(): boolean {
|
||
let ok = true
|
||
this.genderErr = this.gender === '' ? '请选择性别' : ''
|
||
if (this.genderErr !== '') {
|
||
ok = false
|
||
}
|
||
// 选择器产出必然在合法区间,未选才是唯一错误态
|
||
this.heightErr = this.heightInput === '' ? '请选择身高' : ''
|
||
if (this.heightErr !== '') {
|
||
ok = false
|
||
}
|
||
this.weightErr = this.weightInput === '' ? '请选择体重' : ''
|
||
if (this.weightErr !== '') {
|
||
ok = false
|
||
}
|
||
this.goalsErr = this.coreGoals.length === 0 ? '至少选择 1 项' : ''
|
||
if (this.goalsErr !== '') {
|
||
ok = false
|
||
}
|
||
this.styleErr = this.psychStyle === '' ? '请选择心理风格' : ''
|
||
if (this.styleErr !== '') {
|
||
ok = false
|
||
}
|
||
return ok
|
||
}
|
||
|
||
private toggleGoal(g: string): void {
|
||
const idx = this.coreGoals.indexOf(g)
|
||
if (idx >= 0) {
|
||
this.coreGoals.splice(idx, 1)
|
||
} else {
|
||
this.coreGoals.push(g)
|
||
}
|
||
this.goalsErr = ''
|
||
}
|
||
|
||
private isSelectedGoal(g: string): boolean {
|
||
return this.coreGoals.indexOf(g) >= 0
|
||
}
|
||
|
||
/** TextPickerResult.index 是联合类型(多列选择器为数组),单列取数字下标。 */
|
||
private pickIndex(v: TextPickerResult): number {
|
||
return typeof v.index === 'number' ? v.index : -1
|
||
}
|
||
|
||
/** 已有值从当前值滚起,未选过从常见默认值(165cm)滚起。 */
|
||
private openHeightPicker(): void {
|
||
const cur = this.heightInput !== '' ? Number(this.heightInput) : HEIGHT_DEFAULT
|
||
const clamped = Math.min(Math.max(cur - HEIGHT_MIN, 0), HEIGHT_OPTIONS.length - 1)
|
||
this.getUIContext().showTextPickerDialog({
|
||
range: HEIGHT_OPTIONS,
|
||
selected: clamped,
|
||
canLoop: false,
|
||
onAccept: (value: TextPickerResult) => {
|
||
const idx = this.pickIndex(value)
|
||
if (idx >= 0 && idx < HEIGHT_OPTIONS.length) {
|
||
this.heightInput = HEIGHT_OPTIONS[idx]
|
||
this.heightErr = ''
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
private openWeightPicker(): void {
|
||
const cur = this.weightInput !== '' ? Number(this.weightInput) : WEIGHT_DEFAULT
|
||
const clamped = Math.min(Math.max(cur - WEIGHT_MIN, 0), WEIGHT_OPTIONS.length - 1)
|
||
this.getUIContext().showTextPickerDialog({
|
||
range: WEIGHT_OPTIONS,
|
||
selected: clamped,
|
||
canLoop: false,
|
||
onAccept: (value: TextPickerResult) => {
|
||
const idx = this.pickIndex(value)
|
||
if (idx >= 0 && idx < WEIGHT_OPTIONS.length) {
|
||
this.weightInput = WEIGHT_OPTIONS[idx]
|
||
this.weightErr = ''
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
private onSubmitTap(): void {
|
||
if (this.submitting) {
|
||
return
|
||
}
|
||
this.formSummary = ''
|
||
if (!this.validateForm()) {
|
||
// 字段级红字可能滚在视野外(真机实测的"点了没反应"就是这种情况):
|
||
// 汇总条点名缺哪几项,并自动滚到第一个缺失字段。
|
||
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 = ''
|
||
if (this.requestKey === '') {
|
||
// 随机 hex;FAILED 后的重试复用同一 key(见 retrySubmit 注释)
|
||
this.requestKey = randomHex(16)
|
||
}
|
||
const heightVal: number = Number(this.heightInput)
|
||
const weightVal: number = Number(this.weightInput)
|
||
const goalsObj: Object = this.coreGoals
|
||
const questionnaire: Record<string, Object> = {
|
||
'gender': this.gender,
|
||
'height_cm': heightVal,
|
||
'weight_kg': weightVal,
|
||
'birthday': formatDate(this.birthday),
|
||
'core_goals': goalsObj,
|
||
'psych_style': this.psychStyle
|
||
}
|
||
const qObj: Object = questionnaire
|
||
const photoInput = this.photoKeyInput.trim()
|
||
const photoKey = photoInput !== '' ? photoInput : buildMockPhotoKey(AuthStore.getUserId() ?? 'anonymous')
|
||
const req: Record<string, Object> = {
|
||
'questionnaire': qObj,
|
||
'photo_object_key': photoKey,
|
||
'request_key': this.requestKey
|
||
}
|
||
try {
|
||
const data = await ApiClient.post<ReportCreateResp>('/api/v1/reports', req)
|
||
this.reportId = data.report_id ?? ''
|
||
this.taskId = data.task_id ?? ''
|
||
if (this.reportId !== '') {
|
||
this.lastReportId = this.reportId
|
||
AppStorage.setOrCreate<string>(LAST_REPORT_STORAGE_KEY, this.reportId)
|
||
}
|
||
if (this.taskId === '') {
|
||
this.submitError = '服务端未返回任务 ID,请稍后重试'
|
||
this.submitting = false
|
||
return
|
||
}
|
||
this.submitting = false
|
||
this.phase = 'polling'
|
||
} catch (e) {
|
||
this.submitting = false
|
||
if (e instanceof ApiError) {
|
||
if (e.code === 40920) {
|
||
// 本期报告已生成:给“查看我的报告”入口(无记录则提示稍后在任务中心看)
|
||
this.showDupDialog = true
|
||
return
|
||
}
|
||
this.submitError = e.hint !== undefined ? `${e.message}(${e.hint})` : e.message
|
||
return
|
||
}
|
||
this.submitError = '请求失败,请检查网络'
|
||
}
|
||
}
|
||
|
||
private retrySubmit(): void {
|
||
// 幂等重试:沿用同一 request_key 重新提交,服务端按幂等重放处理,
|
||
// 不会重复生成报告或重复扣减额度
|
||
this.failMsg = ''
|
||
this.doSubmit()
|
||
}
|
||
|
||
private onTaskSucceeded(): void {
|
||
const params: Record<string, Object> = { 'reportId': this.reportId }
|
||
// 显式 catch:replaceUrl 的 Promise 拒绝在无 catch 时完全静默(真机实测
|
||
// 卡在轮询页无任何提示),至少把失败暴露出来并由兜底按钮接管。
|
||
router.replaceUrl({ url: 'pages/ReportResultPage', params: params })
|
||
.catch((e: Object): void => {
|
||
this.navError = `报告已生成,但打开结果页失败:${String(e).substring(0, 80)}`
|
||
})
|
||
}
|
||
|
||
private onTaskFailed(reason: string): void {
|
||
this.phase = 'failed'
|
||
this.failMsg = reason
|
||
}
|
||
|
||
private goLastReport(): void {
|
||
this.showDupDialog = false
|
||
if (this.lastReportId === '') {
|
||
return
|
||
}
|
||
const params: Record<string, Object> = { 'reportId': this.lastReportId }
|
||
router.pushUrl({ url: 'pages/ReportResultPage', params: params })
|
||
}
|
||
|
||
@Builder
|
||
TopBar() {
|
||
Row({ space: 8 }) {
|
||
Text('←')
|
||
.fontSize(20)
|
||
.fontColor('#333333')
|
||
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
|
||
.onClick(() => {
|
||
router.back()
|
||
})
|
||
Text('生成报告')
|
||
.fontSize(17)
|
||
.fontWeight(FontWeight.Bold)
|
||
.fontColor('#333333')
|
||
}
|
||
.width('100%')
|
||
.padding({ left: 12, right: 16, top: 10, bottom: 6 })
|
||
}
|
||
|
||
// 按引用传递($$):@Builder 按值传参时 @State 变化不会触发刷新,
|
||
// 字段级红字将永远停在初始空值(真机实测复现)
|
||
@Builder
|
||
FieldLabel($$: FieldLabelParams) {
|
||
Row({ space: 6 }) {
|
||
if ($$.required) {
|
||
Text('*')
|
||
.fontSize(14)
|
||
.fontColor('#E5484D')
|
||
}
|
||
Text($$.label)
|
||
.fontSize(13)
|
||
.fontColor('#666666')
|
||
if ($$.err !== '') {
|
||
Text($$.err)
|
||
.fontSize(11)
|
||
.fontColor('#E5484D')
|
||
.layoutWeight(1)
|
||
.maxLines(1)
|
||
}
|
||
}
|
||
.width('100%')
|
||
}
|
||
|
||
@Builder
|
||
FormBody() {
|
||
Scroll(this.scroller) {
|
||
Column({ space: 12 }) {
|
||
Column({ space: 4 }) {
|
||
Text('生成你的形象诊断报告')
|
||
.fontSize(20)
|
||
.fontWeight(FontWeight.Bold)
|
||
.fontColor('#333333')
|
||
Text('填写 6 项基础信息,约 1~3 分钟生成 32 维报告')
|
||
.fontSize(12)
|
||
.fontColor('#999999')
|
||
}
|
||
.width('100%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
.padding({ top: 4, bottom: 4 })
|
||
|
||
Column({ space: 14 }) {
|
||
Text('基础信息')
|
||
.fontSize(15)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor('#333333')
|
||
Column({ space: 8 }) {
|
||
this.FieldLabel({ label: '性别', err: this.genderErr, required: true })
|
||
Row({ space: 8 }) {
|
||
ForEach(GENDER_OPTIONS, (g: string) => {
|
||
Text(g)
|
||
.fontSize(14)
|
||
.fontColor(this.gender === g ? '#FFFFFF' : '#444444')
|
||
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
|
||
.backgroundColor(this.gender === g ? COLOR_PRIMARY : '#F3F4F6')
|
||
.borderRadius(14)
|
||
.onClick(() => {
|
||
this.gender = g
|
||
this.genderErr = ''
|
||
})
|
||
}, (g: string) => g)
|
||
}
|
||
}
|
||
.width('100%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
.id('field_gender')
|
||
|
||
Row({ space: 12 }) {
|
||
Column({ space: 8 }) {
|
||
this.FieldLabel({ label: '身高 (cm)', err: this.heightErr, required: true })
|
||
Row() {
|
||
Text(this.heightInput === '' ? '请选择' : `${this.heightInput} cm`)
|
||
.fontSize(14)
|
||
.fontColor(this.heightInput === '' ? '#AAAAAA' : '#333333')
|
||
.layoutWeight(1)
|
||
Text('▼')
|
||
.fontSize(10)
|
||
.fontColor('#999999')
|
||
}
|
||
.height(44)
|
||
.padding({ left: 12, right: 12 })
|
||
.backgroundColor('#F5F6F8')
|
||
.borderRadius(10)
|
||
.alignItems(VerticalAlign.Center)
|
||
.onClick(() => {
|
||
this.openHeightPicker()
|
||
})
|
||
}
|
||
.layoutWeight(1)
|
||
.alignItems(HorizontalAlign.Start)
|
||
.id('field_height')
|
||
|
||
Column({ space: 8 }) {
|
||
this.FieldLabel({ label: '体重 (kg)', err: this.weightErr, required: true })
|
||
Row() {
|
||
Text(this.weightInput === '' ? '请选择' : `${this.weightInput} kg`)
|
||
.fontSize(14)
|
||
.fontColor(this.weightInput === '' ? '#AAAAAA' : '#333333')
|
||
.layoutWeight(1)
|
||
Text('▼')
|
||
.fontSize(10)
|
||
.fontColor('#999999')
|
||
}
|
||
.height(44)
|
||
.padding({ left: 12, right: 12 })
|
||
.backgroundColor('#F5F6F8')
|
||
.borderRadius(10)
|
||
.alignItems(VerticalAlign.Center)
|
||
.onClick(() => {
|
||
this.openWeightPicker()
|
||
})
|
||
}
|
||
.layoutWeight(1)
|
||
.alignItems(HorizontalAlign.Start)
|
||
}
|
||
.width('100%')
|
||
|
||
Column({ space: 8 }) {
|
||
this.FieldLabel({ label: '生日', err: '', required: false })
|
||
Row({ space: 12 }) {
|
||
Text(`当前选择:${formatDate(this.birthday)}`)
|
||
.fontSize(13)
|
||
.fontColor('#333333')
|
||
.layoutWeight(1)
|
||
Text(this.showDatePicker ? '收起 ▲' : '修改 ▼')
|
||
.fontSize(13)
|
||
.fontColor('#C2185B')
|
||
.onClick(() => {
|
||
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%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
}
|
||
.width('100%')
|
||
.padding(16)
|
||
.borderRadius(14)
|
||
.backgroundColor('#FFFFFF')
|
||
|
||
Column({ space: 14 }) {
|
||
Text('风格偏好')
|
||
.fontSize(15)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor('#333333')
|
||
Column({ space: 8 }) {
|
||
this.FieldLabel({ label: '核心诉求(多选)', err: this.goalsErr, required: true })
|
||
Flex({ wrap: FlexWrap.Wrap }) {
|
||
ForEach(GOAL_OPTIONS, (g: string) => {
|
||
Text(g)
|
||
.fontSize(13)
|
||
.fontColor(this.isSelectedGoal(g) ? '#FFFFFF' : '#444444')
|
||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||
.backgroundColor(this.isSelectedGoal(g) ? COLOR_PRIMARY : '#F3F4F6')
|
||
.borderRadius(13)
|
||
.margin({ right: 8, bottom: 8 })
|
||
.onClick(() => {
|
||
this.toggleGoal(g)
|
||
})
|
||
}, (g: string) => g)
|
||
}
|
||
.width('100%')
|
||
}
|
||
.width('100%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
.id('field_goals')
|
||
|
||
Column({ space: 8 }) {
|
||
this.FieldLabel({ label: '心理风格(九型·单选)', err: this.styleErr, required: true })
|
||
Flex({ wrap: FlexWrap.Wrap }) {
|
||
ForEach(STYLE_OPTIONS, (s: string) => {
|
||
Text(s)
|
||
.fontSize(13)
|
||
.fontColor(this.psychStyle === s ? '#FFFFFF' : '#444444')
|
||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||
.backgroundColor(this.psychStyle === s ? COLOR_PRIMARY : '#F3F4F6')
|
||
.borderRadius(13)
|
||
.margin({ right: 8, bottom: 8 })
|
||
.onClick(() => {
|
||
this.psychStyle = s
|
||
this.styleErr = ''
|
||
})
|
||
}, (s: string) => s)
|
||
}
|
||
.width('100%')
|
||
}
|
||
.width('100%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
.id('field_style')
|
||
}
|
||
.width('100%')
|
||
.padding(16)
|
||
.borderRadius(14)
|
||
.backgroundColor('#FFFFFF')
|
||
|
||
Column({ space: 8 }) {
|
||
Text('照片(模拟)')
|
||
.fontSize(15)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor('#333333')
|
||
Text('V1 骨架:COS 直传未接入,此处模拟对象键')
|
||
.fontSize(11)
|
||
.fontColor('#999999')
|
||
TextInput({ placeholder: 'report_photo/xxx.jpg(选填)', text: this.photoKeyInput })
|
||
.height(44)
|
||
.fontSize(13)
|
||
.backgroundColor('#F5F6F8')
|
||
.borderRadius(10)
|
||
.onChange((v: string) => {
|
||
this.photoKeyInput = v
|
||
})
|
||
Text('留空时自动生成 report_photo/{userId}/{随机}.jpg')
|
||
.fontSize(11)
|
||
.fontColor('#AAAAAA')
|
||
}
|
||
.width('100%')
|
||
.padding(16)
|
||
.borderRadius(14)
|
||
.backgroundColor('#FFFFFF')
|
||
.alignItems(HorizontalAlign.Start)
|
||
|
||
if (this.formSummary !== '') {
|
||
Text(this.formSummary)
|
||
.fontSize(13)
|
||
.fontColor('#E5484D')
|
||
.width('100%')
|
||
.padding(10)
|
||
.backgroundColor('#FDECEC')
|
||
.borderRadius(8)
|
||
.textAlign(TextAlign.Start)
|
||
.onClick(() => {
|
||
this.scrollToFirstError()
|
||
})
|
||
}
|
||
if (this.submitError !== '') {
|
||
Text(this.submitError)
|
||
.fontSize(12)
|
||
.fontColor('#E5484D')
|
||
.width('100%')
|
||
.textAlign(TextAlign.Start)
|
||
}
|
||
|
||
Button(this.submitting ? '提交中…' : '提交并生成报告')
|
||
.width('100%')
|
||
.height(48)
|
||
.fontSize(16)
|
||
.backgroundColor(COLOR_PRIMARY)
|
||
.enabled(!this.submitting)
|
||
.margin({ bottom: 24 })
|
||
.onClick(() => {
|
||
this.onSubmitTap()
|
||
})
|
||
}
|
||
.padding({ left: 16, right: 16 })
|
||
.id('form_root')
|
||
}
|
||
.layoutWeight(1)
|
||
.width('100%')
|
||
}
|
||
|
||
@Builder
|
||
PollingBody() {
|
||
Column({ space: 16 }) {
|
||
Text('正在生成你的专属报告')
|
||
.fontSize(20)
|
||
.fontWeight(FontWeight.Bold)
|
||
.fontColor('#333333')
|
||
Text('32 维形象诊断通常需要 1~3 分钟,生成完成后自动进入结果页')
|
||
.fontSize(13)
|
||
.fontColor('#999999')
|
||
.textAlign(TextAlign.Center)
|
||
TaskPollingPanel({
|
||
taskId: this.taskId,
|
||
onSucceeded: (): void => {
|
||
this.onTaskSucceeded()
|
||
},
|
||
onFailed: (reason: string): void => {
|
||
this.onTaskFailed(reason)
|
||
}
|
||
})
|
||
.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%')
|
||
.layoutWeight(1)
|
||
.justifyContent(FlexAlign.Center)
|
||
.padding({ left: 24, right: 24 })
|
||
}
|
||
|
||
@Builder
|
||
FailedBody() {
|
||
Column({ space: 12 }) {
|
||
Text('生成失败')
|
||
.fontSize(20)
|
||
.fontWeight(FontWeight.Bold)
|
||
.fontColor('#333333')
|
||
Text(this.failMsg)
|
||
.fontSize(13)
|
||
.fontColor('#999999')
|
||
.textAlign(TextAlign.Center)
|
||
Text('重试将沿用同一 request_key 重新提交,服务端按幂等重放处理,不会重复消耗额度。')
|
||
.fontSize(11)
|
||
.fontColor('#AAAAAA')
|
||
.textAlign(TextAlign.Center)
|
||
Button('重试提交')
|
||
.width('62%')
|
||
.height(44)
|
||
.fontSize(15)
|
||
.backgroundColor(COLOR_PRIMARY)
|
||
.margin({ top: 8 })
|
||
.onClick(() => {
|
||
this.retrySubmit()
|
||
})
|
||
Button('返回修改问卷')
|
||
.width('62%')
|
||
.height(44)
|
||
.fontSize(15)
|
||
.backgroundColor('#F3F4F6')
|
||
.fontColor('#666666')
|
||
.onClick(() => {
|
||
this.phase = 'form'
|
||
})
|
||
}
|
||
.width('100%')
|
||
.layoutWeight(1)
|
||
.justifyContent(FlexAlign.Center)
|
||
.padding({ left: 32, right: 32 })
|
||
}
|
||
|
||
@Builder
|
||
DupDialog() {
|
||
Column({ space: 14 }) {
|
||
Text('本期报告已生成')
|
||
.fontSize(17)
|
||
.fontWeight(FontWeight.Bold)
|
||
.fontColor('#333333')
|
||
Text('每个周期仅可生成一次完整报告,无需重复提交。')
|
||
.fontSize(13)
|
||
.fontColor('#666666')
|
||
.textAlign(TextAlign.Center)
|
||
if (this.lastReportId !== '') {
|
||
Text('将为你打开最近一次生成的报告。')
|
||
.fontSize(12)
|
||
.fontColor('#999999')
|
||
} else {
|
||
Text('本期报告稍后可在任务中心查看。')
|
||
.fontSize(12)
|
||
.fontColor('#999999')
|
||
}
|
||
Row({ space: 12 }) {
|
||
Button('我知道了')
|
||
.layoutWeight(1)
|
||
.height(40)
|
||
.fontSize(14)
|
||
.backgroundColor('#F3F4F6')
|
||
.fontColor('#666666')
|
||
.onClick(() => {
|
||
this.showDupDialog = false
|
||
})
|
||
if (this.lastReportId !== '') {
|
||
Button('查看我的报告')
|
||
.layoutWeight(1)
|
||
.height(40)
|
||
.fontSize(14)
|
||
.backgroundColor(COLOR_PRIMARY)
|
||
.onClick(() => {
|
||
this.goLastReport()
|
||
})
|
||
}
|
||
}
|
||
.width('100%')
|
||
.margin({ top: 4 })
|
||
}
|
||
.width('82%')
|
||
.padding(20)
|
||
.borderRadius(16)
|
||
.backgroundColor('#FFFFFF')
|
||
}
|
||
|
||
build() {
|
||
Stack() {
|
||
Column() {
|
||
this.TopBar()
|
||
if (this.phase === 'form') {
|
||
this.FormBody()
|
||
} else if (this.phase === 'polling') {
|
||
this.PollingBody()
|
||
} else {
|
||
this.FailedBody()
|
||
}
|
||
}
|
||
.width('100%')
|
||
.height('100%')
|
||
|
||
if (this.showDupDialog) {
|
||
Column() {
|
||
this.DupDialog()
|
||
}
|
||
.width('100%')
|
||
.height('100%')
|
||
.backgroundColor('#66000000')
|
||
.justifyContent(FlexAlign.Center)
|
||
}
|
||
}
|
||
.width('100%')
|
||
.height('100%')
|
||
.backgroundColor('#F5F6F8')
|
||
}
|
||
}
|