feat(report): 诊断流四项修复 + 形象深度诊断端侧——表单草稿跨页保留(AppStorage);照片预览改竖版 3:4;40920 改中性兜底+历史报告入口(initialTab 参数);重新生成 40900 弹窗引导;深度诊断卡替换预约占位(生成/查看/防套娃)+ 色板/场景/30天计划渲染 + 「深度」徽标
This commit is contained in:
@@ -115,10 +115,20 @@ export struct ReportListTab {
|
|||||||
ReportItem(item: ReportListItemWire) {
|
ReportItem(item: ReportListItemWire) {
|
||||||
Row({ space: 12 }) {
|
Row({ space: 12 }) {
|
||||||
Column({ space: 4 }) {
|
Column({ space: 4 }) {
|
||||||
Text(`形象报告 v${item.version ?? 1}`)
|
Row({ space: 6 }) {
|
||||||
|
Text(item.tier === 'full' ? `深度诊断报告 v${item.version ?? 1}` : `形象报告 v${item.version ?? 1}`)
|
||||||
.fontSize(15)
|
.fontSize(15)
|
||||||
.fontWeight(FontWeight.Medium)
|
.fontWeight(FontWeight.Medium)
|
||||||
.fontColor(TEXT_PRIMARY)
|
.fontColor(TEXT_PRIMARY)
|
||||||
|
if (item.tier === 'full') {
|
||||||
|
Text('深度')
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(ACCENT)
|
||||||
|
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
||||||
|
.borderRadius(6)
|
||||||
|
.backgroundColor('#F7E3EA')
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(this.reportTime(item))
|
Text(this.reportTime(item))
|
||||||
.fontSize(12)
|
.fontSize(12)
|
||||||
.fontColor(TEXT_SECONDARY)
|
.fontColor(TEXT_SECONDARY)
|
||||||
|
|||||||
@@ -62,15 +62,62 @@ export interface ValidationWire {
|
|||||||
degraded?: boolean // true = 规则引擎降级生成,UI 需黄条透明提示
|
degraded?: boolean // true = 规则引擎降级生成,UI 需黄条透明提示
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 色板单色(result.full.palette.{best,neutral,avoid}[];hex 为 '#RRGGBB') */
|
||||||
|
export interface PaletteColorWire {
|
||||||
|
name?: string
|
||||||
|
hex?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 十二季色彩定位(result.full.season;analysis 为 LLM 季型解析文案) */
|
||||||
|
export interface FullSeasonWire {
|
||||||
|
code?: string
|
||||||
|
detailed?: string
|
||||||
|
name_cn?: string
|
||||||
|
analysis?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullPaletteWire {
|
||||||
|
best?: PaletteColorWire[]
|
||||||
|
neutral?: PaletteColorWire[]
|
||||||
|
avoid?: PaletteColorWire[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullOutfitWire {
|
||||||
|
name?: string
|
||||||
|
points?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullSceneWire {
|
||||||
|
key?: string
|
||||||
|
title?: string
|
||||||
|
outfits?: FullOutfitWire[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanWeekWire {
|
||||||
|
week?: number
|
||||||
|
title?: string
|
||||||
|
tasks?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 深度诊断(tier=full)增量内容:色板 / 场景穿搭 / 30 天计划 */
|
||||||
|
export interface FullResultWire {
|
||||||
|
season?: FullSeasonWire
|
||||||
|
palette?: FullPaletteWire
|
||||||
|
scenes?: FullSceneWire[]
|
||||||
|
plan30?: PlanWeekWire[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReportResultWire {
|
export interface ReportResultWire {
|
||||||
diagnosis?: DiagnosisWire
|
diagnosis?: DiagnosisWire
|
||||||
copy?: CopyWire
|
copy?: CopyWire
|
||||||
validation?: ValidationWire
|
validation?: ValidationWire
|
||||||
|
full?: FullResultWire
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /api/v1/reports/{id} 的响应 */
|
/** GET /api/v1/reports/{id} 的响应 */
|
||||||
export interface ReportDetailWire {
|
export interface ReportDetailWire {
|
||||||
report_id?: string
|
report_id?: string
|
||||||
|
tier?: string // 'basic' | 'full'
|
||||||
status?: string
|
status?: string
|
||||||
version?: number
|
version?: number
|
||||||
result?: ReportResultWire
|
result?: ReportResultWire
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
* params 键名精确为 gender/birthday/height_cm/weight_kg,值全部为 string
|
* params 键名精确为 gender/birthday/height_cm/weight_kg,值全部为 string
|
||||||
* (接收端 ReportCreatePage 按 string 读取);跳转失败页内红字兜底。
|
* (接收端 ReportCreatePage 按 string 读取);跳转失败页内红字兜底。
|
||||||
* - 职业仅本地留存(AppStorage),不进提交体(需求 §3.6/验收 4)。
|
* - 职业仅本地留存(AppStorage),不进提交体(需求 §3.6/验收 4)。
|
||||||
|
* - 全部字段做 AppStorage 草稿(f3_diagnosis_*):aboutToAppear 恢复、
|
||||||
|
* 每次变更写回(write-through),离开/重进诊断流程不丢已填内容。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { router, componentUtils } from '@kit.ArkUI'
|
import { router, componentUtils } from '@kit.ArkUI'
|
||||||
@@ -50,6 +52,11 @@ const BIRTHDAY_FALLBACK_SELECTED: Date = new Date(2000, 0, 1)
|
|||||||
|
|
||||||
/** 职业为本地留存字段(后端问卷 schema 无 occupation,Phase B 随 schema 扩展提交) */
|
/** 职业为本地留存字段(后端问卷 schema 无 occupation,Phase B 随 schema 扩展提交) */
|
||||||
const OCCUPATION_STORAGE_KEY: string = 'f3_diagnosis_occupation'
|
const OCCUPATION_STORAGE_KEY: string = 'f3_diagnosis_occupation'
|
||||||
|
// 表单草稿(F3 跨页面保留):离开/重进诊断流程不丢已填内容,提交成功也不清
|
||||||
|
const DRAFT_GENDER_KEY: string = 'f3_diagnosis_gender'
|
||||||
|
const DRAFT_BIRTHDAY_KEY: string = 'f3_diagnosis_birthday'
|
||||||
|
const DRAFT_HEIGHT_KEY: string = 'f3_diagnosis_height'
|
||||||
|
const DRAFT_WEIGHT_KEY: string = 'f3_diagnosis_weight'
|
||||||
|
|
||||||
function buildRangeOptions(min: number, max: number): string[] {
|
function buildRangeOptions(min: number, max: number): string[] {
|
||||||
const arr: string[] = []
|
const arr: string[] = []
|
||||||
@@ -78,6 +85,39 @@ struct DiagnosisFormPage {
|
|||||||
@State navError: string = ''
|
@State navError: string = ''
|
||||||
private scroller: Scroller = new Scroller()
|
private scroller: Scroller = new Scroller()
|
||||||
|
|
||||||
|
aboutToAppear(): void {
|
||||||
|
// 恢复草稿:用户离开诊断流程(回首页/切页)后重进不丢已填内容
|
||||||
|
const gender = AppStorage.get<string>(DRAFT_GENDER_KEY)
|
||||||
|
if (gender !== undefined) {
|
||||||
|
this.gender = gender
|
||||||
|
}
|
||||||
|
const birthday = AppStorage.get<string>(DRAFT_BIRTHDAY_KEY)
|
||||||
|
if (birthday !== undefined) {
|
||||||
|
this.birthday = birthday
|
||||||
|
}
|
||||||
|
const height = AppStorage.get<string>(DRAFT_HEIGHT_KEY)
|
||||||
|
if (height !== undefined) {
|
||||||
|
this.heightInput = height
|
||||||
|
}
|
||||||
|
const weight = AppStorage.get<string>(DRAFT_WEIGHT_KEY)
|
||||||
|
if (weight !== undefined) {
|
||||||
|
this.weightInput = weight
|
||||||
|
}
|
||||||
|
const occupation = AppStorage.get<string>(OCCUPATION_STORAGE_KEY)
|
||||||
|
if (occupation !== undefined) {
|
||||||
|
this.occupation = occupation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 草稿写回(write-through):每次字段变更即落 AppStorage。 */
|
||||||
|
private saveDraft(): void {
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_GENDER_KEY, this.gender)
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_BIRTHDAY_KEY, this.birthday)
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_HEIGHT_KEY, this.heightInput)
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_WEIGHT_KEY, this.weightInput)
|
||||||
|
AppStorage.setOrCreate<string>(OCCUPATION_STORAGE_KEY, this.occupation)
|
||||||
|
}
|
||||||
|
|
||||||
/** 由出生日期派生只读年龄(周岁)。 */
|
/** 由出生日期派生只读年龄(周岁)。 */
|
||||||
private ageOf(): number {
|
private ageOf(): number {
|
||||||
if (this.birthday === '') {
|
if (this.birthday === '') {
|
||||||
@@ -114,6 +154,7 @@ struct DiagnosisFormPage {
|
|||||||
onDateAccept: (value: Date): void => {
|
onDateAccept: (value: Date): void => {
|
||||||
this.birthday = formatDate(value)
|
this.birthday = formatDate(value)
|
||||||
this.birthdayErr = ''
|
this.birthdayErr = ''
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -131,6 +172,7 @@ struct DiagnosisFormPage {
|
|||||||
if (idx >= 0 && idx < HEIGHT_OPTIONS.length) {
|
if (idx >= 0 && idx < HEIGHT_OPTIONS.length) {
|
||||||
this.heightInput = HEIGHT_OPTIONS[idx]
|
this.heightInput = HEIGHT_OPTIONS[idx]
|
||||||
this.heightErr = ''
|
this.heightErr = ''
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -148,6 +190,7 @@ struct DiagnosisFormPage {
|
|||||||
if (idx >= 0 && idx < WEIGHT_OPTIONS.length) {
|
if (idx >= 0 && idx < WEIGHT_OPTIONS.length) {
|
||||||
this.weightInput = WEIGHT_OPTIONS[idx]
|
this.weightInput = WEIGHT_OPTIONS[idx]
|
||||||
this.weightErr = ''
|
this.weightErr = ''
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -215,8 +258,8 @@ struct DiagnosisFormPage {
|
|||||||
this.scrollToFirstError()
|
this.scrollToFirstError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 职业仅本地留存,不进提交体(需求 §3.6/验收 4)
|
// 职业仅本地留存,不进提交体(需求 §3.6/验收 4);整份草稿一并写回
|
||||||
AppStorage.setOrCreate<string>(OCCUPATION_STORAGE_KEY, this.occupation)
|
this.saveDraft()
|
||||||
// 四个值全部为 string,接收端 ReportCreatePage 按 string 读取
|
// 四个值全部为 string,接收端 ReportCreatePage 按 string 读取
|
||||||
const params: Record<string, Object> = {
|
const params: Record<string, Object> = {
|
||||||
'gender': this.gender,
|
'gender': this.gender,
|
||||||
@@ -260,6 +303,7 @@ struct DiagnosisFormPage {
|
|||||||
if (idx >= 0 && idx < GENDER_OPTIONS.length) {
|
if (idx >= 0 && idx < GENDER_OPTIONS.length) {
|
||||||
this.gender = GENDER_OPTIONS[idx]
|
this.gender = GENDER_OPTIONS[idx]
|
||||||
this.genderErr = ''
|
this.genderErr = ''
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -278,6 +322,7 @@ struct DiagnosisFormPage {
|
|||||||
.margin({ right: 8, bottom: 8 })
|
.margin({ right: 8, bottom: 8 })
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.occupation = this.occupation === o ? '' : o
|
this.occupation = this.occupation === o ? '' : o
|
||||||
|
this.saveDraft()
|
||||||
})
|
})
|
||||||
}, (o: string) => o)
|
}, (o: string) => o)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
* (组件级复用允许 import,独立路由页无底部 Tab 会把用户锁死在页面里)
|
* (组件级复用允许 import,独立路由页无底部 Tab 会把用户锁死在页面里)
|
||||||
* - 请求只走 ApiClient;今日额度失败静默降级为 '—'(首页不打扰用户,不弹错误态)
|
* - 请求只走 ApiClient;今日额度失败静默降级为 '—'(首页不打扰用户,不弹错误态)
|
||||||
* - 响应 wire 类型用具名 interface(字段对齐后端 JSON);泛型调用显式类型参数
|
* - 响应 wire 类型用具名 interface(字段对齐后端 JSON);泛型调用显式类型参数
|
||||||
|
* - 可选路由参数 initialTab(number 0~3):外部页面可指定落地 Tab
|
||||||
|
* (如报告结果页「查看历史报告」传 1 落地「报告」Tab)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { router } from '@kit.ArkUI'
|
import { router } from '@kit.ArkUI'
|
||||||
@@ -45,14 +47,27 @@ struct HomePage {
|
|||||||
@State quotaRemaining: string = '…'
|
@State quotaRemaining: string = '…'
|
||||||
@State quotaNetError: boolean = false // 额度拉取遇网络错误(P0-6):配额卡位置换 NetworkBanner
|
@State quotaNetError: boolean = false // 额度拉取遇网络错误(P0-6):配额卡位置换 NetworkBanner
|
||||||
@State reportRefreshTick: number = 0 // 自增通知报告 Tab 刷新(N2)
|
@State reportRefreshTick: number = 0 // 自增通知报告 Tab 刷新(N2)
|
||||||
|
private tabsController: TabsController = new TabsController() // 供 initialTab 参数落地指定 Tab
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
|
// 可选参数 initialTab:外部页面(如报告结果页「查看历史报告」)可指定落地 Tab
|
||||||
|
const params = router.getParams() as Record<string, Object> | undefined
|
||||||
|
if (params !== undefined && typeof params['initialTab'] === 'number') {
|
||||||
|
const idx = params['initialTab'] as number
|
||||||
|
if (idx >= 0 && idx <= 3) {
|
||||||
|
this.currentTabIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
this.dateLine = this.buildDateLine()
|
this.dateLine = this.buildDateLine()
|
||||||
this.loadQuota()
|
this.loadQuota()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从报告结果页等返回(应用壳重新可见):当前停在报告 Tab 时刷新列表(N2)。 */
|
/** 从报告结果页等返回(应用壳重新可见):当前停在报告 Tab 时刷新列表(N2)。 */
|
||||||
onPageShow(): void {
|
onPageShow(): void {
|
||||||
|
// initialTab 落地:Tabs 已挂载后 controller 才生效(aboutToAppear 里调用是空操作)
|
||||||
|
if (this.currentTabIndex !== 0) {
|
||||||
|
this.tabsController.changeIndex(this.currentTabIndex)
|
||||||
|
}
|
||||||
if (this.currentTabIndex === 1) {
|
if (this.currentTabIndex === 1) {
|
||||||
this.reportRefreshTick += 1
|
this.reportRefreshTick += 1
|
||||||
}
|
}
|
||||||
@@ -248,7 +263,7 @@ struct HomePage {
|
|||||||
|
|
||||||
build() {
|
build() {
|
||||||
Column() {
|
Column() {
|
||||||
Tabs({ barPosition: BarPosition.End }) {
|
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
|
||||||
TabContent() {
|
TabContent() {
|
||||||
this.HomeTab()
|
this.HomeTab()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,14 @@
|
|||||||
* 身高/体重为滚轮选择器(showTextPickerDialog),不提供键盘输入
|
* 身高/体重为滚轮选择器(showTextPickerDialog),不提供键盘输入
|
||||||
* 3) POST /api/v1/reports(request_key 随机 hex;任务 FAILED 后重试换新 key,
|
* 3) POST /api/v1/reports(request_key 随机 hex;任务 FAILED 后重试换新 key,
|
||||||
* 服务端幂等重放不过滤状态,沿用旧 key 只会重放回原失败任务)
|
* 服务端幂等重放不过滤状态,沿用旧 key 只会重放回原失败任务)
|
||||||
* 4) 40920(本期已生成)→ 页内对话框:可跳最近一次报告(AppStorage 记录,
|
* 4) 40920(本期已生成)→ 页内对话框兜底:后端已不再抛 40920(重复生成改由
|
||||||
* 无记录时实时拉报告列表兜底,重启后也找得回)
|
* report_basic 配额兜底),此处仅为兼容旧端的兜底分支,可跳最近一次报告
|
||||||
|
* (AppStorage 记录,无记录时实时拉报告列表兜底,重启后也找得回)
|
||||||
* 5) 成功 → 3 秒轮询任务(TaskPollingPanel,页面销毁自动清定时器,轮询期间允许返回)
|
* 5) 成功 → 3 秒轮询任务(TaskPollingPanel,页面销毁自动清定时器,轮询期间允许返回)
|
||||||
* 6) SUCCEEDED → router.replaceUrl 结果页(params 带 reportId);FAILED → 失败态 + 新 key 重试
|
* 6) SUCCEEDED → router.replaceUrl 结果页(params 带 reportId);FAILED → 失败态 + 新 key 重试
|
||||||
* 7) 提交前弹配额确认框(report_basic 终身 1 次,产品方案设计纪律 3「禁止静默扣减」)
|
* 7) 提交前弹配额确认框(report_basic 配额,产品方案设计纪律 3「禁止静默扣减」)
|
||||||
|
* 8) 表单字段做 AppStorage 草稿(f3_report_*):aboutToAppear 恢复、变更即写回,
|
||||||
|
* 提交成功不清(用户可能再生成);photoUri 不持久化(媒体 URI 会失效)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { router, componentUtils } from '@kit.ArkUI'
|
import { router, componentUtils } from '@kit.ArkUI'
|
||||||
@@ -36,6 +39,14 @@ const GENDER_OPTIONS: string[] = ['女', '男', '不便告知']
|
|||||||
const GOAL_OPTIONS: string[] = ['显高', '显瘦', '显白', '显气质', '遮肉', '修饰肩颈']
|
const GOAL_OPTIONS: string[] = ['显高', '显瘦', '显白', '显气质', '遮肉', '修饰肩颈']
|
||||||
const STYLE_OPTIONS: string[] = ['戏剧', '睿智', '自然', '古典', '优雅', '浪漫', '前卫', '少女', '少年']
|
const STYLE_OPTIONS: string[] = ['戏剧', '睿智', '自然', '古典', '优雅', '浪漫', '前卫', '少女', '少年']
|
||||||
const LAST_REPORT_STORAGE_KEY: string = 'f3_last_report_id'
|
const LAST_REPORT_STORAGE_KEY: string = 'f3_last_report_id'
|
||||||
|
// 本页表单草稿(f3_ 前缀,与 DiagnosisFormPage 同套风格):跨页面保留已填内容,
|
||||||
|
// 提交成功不清(用户可能再生成);photoUri 不持久化(媒体 URI 会失效)。
|
||||||
|
const DRAFT_GENDER_KEY: string = 'f3_report_gender'
|
||||||
|
const DRAFT_HEIGHT_KEY: string = 'f3_report_height'
|
||||||
|
const DRAFT_WEIGHT_KEY: string = 'f3_report_weight'
|
||||||
|
const DRAFT_BIRTHDAY_KEY: string = 'f3_report_birthday'
|
||||||
|
const DRAFT_GOALS_KEY: string = 'f3_report_goals' // 多选用 '|' 拼接成串存储
|
||||||
|
const DRAFT_STYLE_KEY: string = 'f3_report_style'
|
||||||
|
|
||||||
// 身高/体重改为选择器(不允许键盘输入):数值范围与原校验一致
|
// 身高/体重改为选择器(不允许键盘输入):数值范围与原校验一致
|
||||||
const HEIGHT_MIN: number = 100
|
const HEIGHT_MIN: number = 100
|
||||||
@@ -112,28 +123,79 @@ struct ReportCreatePage {
|
|||||||
if (cached !== undefined && cached !== '') {
|
if (cached !== undefined && cached !== '') {
|
||||||
this.lastReportId = cached
|
this.lastReportId = cached
|
||||||
}
|
}
|
||||||
// 美学诊断填写页(DiagnosisFormPage)预填进入(需求 v2 §3.6):只填四字段,
|
// 先恢复本页草稿(跨页面保留已填内容),再应用路由参数:
|
||||||
// 核心诉求/心理风格仍由用户在本页补齐;无参数时行为与旧直达完全一致。
|
// 美学诊断填写页(DiagnosisFormPage)预填进入(需求 v2 §3.6)时参数优先,
|
||||||
|
// 并同步更新草稿;无参数时行为与旧直达完全一致。
|
||||||
|
this.restoreDraft()
|
||||||
const params = router.getParams() as Record<string, Object> | undefined
|
const params = router.getParams() as Record<string, Object> | undefined
|
||||||
if (params !== undefined) {
|
if (params !== undefined) {
|
||||||
|
let paramsApplied = false
|
||||||
if (typeof params['gender'] === 'string' && params['gender'].length > 0) {
|
if (typeof params['gender'] === 'string' && params['gender'].length > 0) {
|
||||||
this.gender = params['gender'] as string
|
this.gender = params['gender'] as string
|
||||||
|
paramsApplied = true
|
||||||
}
|
}
|
||||||
if (typeof params['birthday'] === 'string' && params['birthday'].length >= 8) {
|
if (typeof params['birthday'] === 'string' && params['birthday'].length >= 8) {
|
||||||
const d = new Date(params['birthday'] as string)
|
const d = new Date(params['birthday'] as string)
|
||||||
if (!isNaN(d.getTime())) {
|
if (!isNaN(d.getTime())) {
|
||||||
this.birthday = d
|
this.birthday = d
|
||||||
|
paramsApplied = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof params['height_cm'] === 'string' && (params['height_cm'] as string).length > 0) {
|
if (typeof params['height_cm'] === 'string' && (params['height_cm'] as string).length > 0) {
|
||||||
this.heightInput = params['height_cm'] as string
|
this.heightInput = params['height_cm'] as string
|
||||||
|
paramsApplied = true
|
||||||
}
|
}
|
||||||
if (typeof params['weight_kg'] === 'string' && (params['weight_kg'] as string).length > 0) {
|
if (typeof params['weight_kg'] === 'string' && (params['weight_kg'] as string).length > 0) {
|
||||||
this.weightInput = params['weight_kg'] as string
|
this.weightInput = params['weight_kg'] as string
|
||||||
|
paramsApplied = true
|
||||||
|
}
|
||||||
|
if (paramsApplied) {
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 恢复草稿(photoUri 不在草稿内:媒体 URI 会失效,每次需重选)。 */
|
||||||
|
private restoreDraft(): void {
|
||||||
|
const gender = AppStorage.get<string>(DRAFT_GENDER_KEY)
|
||||||
|
if (gender !== undefined) {
|
||||||
|
this.gender = gender
|
||||||
|
}
|
||||||
|
const height = AppStorage.get<string>(DRAFT_HEIGHT_KEY)
|
||||||
|
if (height !== undefined) {
|
||||||
|
this.heightInput = height
|
||||||
|
}
|
||||||
|
const weight = AppStorage.get<string>(DRAFT_WEIGHT_KEY)
|
||||||
|
if (weight !== undefined) {
|
||||||
|
this.weightInput = weight
|
||||||
|
}
|
||||||
|
const birthday = AppStorage.get<string>(DRAFT_BIRTHDAY_KEY)
|
||||||
|
if (birthday !== undefined && birthday.length >= 8) {
|
||||||
|
const d = new Date(birthday)
|
||||||
|
if (!isNaN(d.getTime())) {
|
||||||
|
this.birthday = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const goals = AppStorage.get<string>(DRAFT_GOALS_KEY)
|
||||||
|
if (goals !== undefined && goals !== '') {
|
||||||
|
this.coreGoals = goals.split('|')
|
||||||
|
}
|
||||||
|
const style = AppStorage.get<string>(DRAFT_STYLE_KEY)
|
||||||
|
if (style !== undefined) {
|
||||||
|
this.psychStyle = style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 草稿写回(write-through):每次字段变更即落 AppStorage;提交成功不清。 */
|
||||||
|
private saveDraft(): void {
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_GENDER_KEY, this.gender)
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_HEIGHT_KEY, this.heightInput)
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_WEIGHT_KEY, this.weightInput)
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_BIRTHDAY_KEY, formatDate(this.birthday))
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_GOALS_KEY, this.coreGoals.join('|'))
|
||||||
|
AppStorage.setOrCreate<string>(DRAFT_STYLE_KEY, this.psychStyle)
|
||||||
|
}
|
||||||
|
|
||||||
private validateForm(): boolean {
|
private validateForm(): boolean {
|
||||||
let ok = true
|
let ok = true
|
||||||
this.photoErr = this.photoUri === '' ? '请上传个人照片' : ''
|
this.photoErr = this.photoUri === '' ? '请上传个人照片' : ''
|
||||||
@@ -172,6 +234,7 @@ struct ReportCreatePage {
|
|||||||
this.coreGoals.push(g)
|
this.coreGoals.push(g)
|
||||||
}
|
}
|
||||||
this.goalsErr = ''
|
this.goalsErr = ''
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
private isSelectedGoal(g: string): boolean {
|
private isSelectedGoal(g: string): boolean {
|
||||||
@@ -196,6 +259,7 @@ struct ReportCreatePage {
|
|||||||
if (idx >= 0 && idx < HEIGHT_OPTIONS.length) {
|
if (idx >= 0 && idx < HEIGHT_OPTIONS.length) {
|
||||||
this.heightInput = HEIGHT_OPTIONS[idx]
|
this.heightInput = HEIGHT_OPTIONS[idx]
|
||||||
this.heightErr = ''
|
this.heightErr = ''
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -213,6 +277,7 @@ struct ReportCreatePage {
|
|||||||
if (idx >= 0 && idx < WEIGHT_OPTIONS.length) {
|
if (idx >= 0 && idx < WEIGHT_OPTIONS.length) {
|
||||||
this.weightInput = WEIGHT_OPTIONS[idx]
|
this.weightInput = WEIGHT_OPTIONS[idx]
|
||||||
this.weightErr = ''
|
this.weightErr = ''
|
||||||
|
this.saveDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -273,7 +338,7 @@ struct ReportCreatePage {
|
|||||||
this.openQuotaConfirm()
|
this.openQuotaConfirm()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开配额确认框并拉取当前剩余(首份报告为终身 1 次权益)。 */
|
/** 打开配额确认框并拉取当前剩余(report_basic 终身配额)。 */
|
||||||
private openQuotaConfirm(): void {
|
private openQuotaConfirm(): void {
|
||||||
this.showQuotaConfirm = true
|
this.showQuotaConfirm = true
|
||||||
this.quotaLoaded = false
|
this.quotaLoaded = false
|
||||||
@@ -403,7 +468,8 @@ struct ReportCreatePage {
|
|||||||
this.submitting = false
|
this.submitting = false
|
||||||
if (e instanceof ApiError) {
|
if (e instanceof ApiError) {
|
||||||
if (e.code === 40920) {
|
if (e.code === 40920) {
|
||||||
// 本期报告已生成:始终给「查看我的报告」入口(无本地记录也能从服务端找回)
|
// 兜底分支:后端已不再抛 40920(重复生成由 report_basic 配额兜底),
|
||||||
|
// 仅兼容旧端;始终给「查看我的报告」入口(无本地记录也能从服务端找回)
|
||||||
this.showDupDialog = true
|
this.showDupDialog = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -538,7 +604,7 @@ struct ReportCreatePage {
|
|||||||
.fontColor('#999999')
|
.fontColor('#999999')
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height(140)
|
.aspectRatio(0.75) // 竖版 3:4:与全身照取景一致,不再用横条占位
|
||||||
.justifyContent(FlexAlign.Center)
|
.justifyContent(FlexAlign.Center)
|
||||||
.backgroundColor('#F5F6F8')
|
.backgroundColor('#F5F6F8')
|
||||||
.borderRadius(10)
|
.borderRadius(10)
|
||||||
@@ -550,7 +616,7 @@ struct ReportCreatePage {
|
|||||||
Stack() {
|
Stack() {
|
||||||
Image(this.photoUri)
|
Image(this.photoUri)
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height(220)
|
.aspectRatio(0.75) // 竖版 3:4:竖屏全身照不再被上下截断
|
||||||
.objectFit(ImageFit.Cover)
|
.objectFit(ImageFit.Cover)
|
||||||
.borderRadius(10)
|
.borderRadius(10)
|
||||||
Text('重选')
|
Text('重选')
|
||||||
@@ -598,6 +664,7 @@ struct ReportCreatePage {
|
|||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.gender = g
|
this.gender = g
|
||||||
this.genderErr = ''
|
this.genderErr = ''
|
||||||
|
this.saveDraft()
|
||||||
})
|
})
|
||||||
}, (g: string) => g)
|
}, (g: string) => g)
|
||||||
}
|
}
|
||||||
@@ -682,6 +749,7 @@ struct ReportCreatePage {
|
|||||||
})
|
})
|
||||||
.onDateChange((d: Date) => {
|
.onDateChange((d: Date) => {
|
||||||
this.birthday = d
|
this.birthday = d
|
||||||
|
this.saveDraft()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -734,6 +802,7 @@ struct ReportCreatePage {
|
|||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.psychStyle = s
|
this.psychStyle = s
|
||||||
this.styleErr = ''
|
this.styleErr = ''
|
||||||
|
this.saveDraft()
|
||||||
})
|
})
|
||||||
}, (s: string) => s)
|
}, (s: string) => s)
|
||||||
}
|
}
|
||||||
@@ -879,15 +948,15 @@ struct ReportCreatePage {
|
|||||||
@Builder
|
@Builder
|
||||||
DupDialog() {
|
DupDialog() {
|
||||||
Column({ space: 14 }) {
|
Column({ space: 14 }) {
|
||||||
Text('本期报告已生成')
|
Text('已有生成过的报告')
|
||||||
.fontSize(17)
|
.fontSize(17)
|
||||||
.fontWeight(FontWeight.Bold)
|
.fontWeight(FontWeight.Bold)
|
||||||
.fontColor('#333333')
|
.fontColor('#333333')
|
||||||
Text('首份形象报告为一次性权益,已生成的报告可随时查看。')
|
Text('可直接查看最近一次的报告,也可返回修改后重新提交生成新报告。')
|
||||||
.fontSize(13)
|
.fontSize(13)
|
||||||
.fontColor('#666666')
|
.fontColor('#666666')
|
||||||
.textAlign(TextAlign.Center)
|
.textAlign(TextAlign.Center)
|
||||||
Text('将为你打开最近一次生成的报告。')
|
Text('选择「查看我的报告」将为你打开最近一次生成的报告。')
|
||||||
.fontSize(12)
|
.fontSize(12)
|
||||||
.fontColor('#999999')
|
.fontColor('#999999')
|
||||||
Row({ space: 12 }) {
|
Row({ space: 12 }) {
|
||||||
@@ -918,7 +987,7 @@ struct ReportCreatePage {
|
|||||||
.backgroundColor('#FFFFFF')
|
.backgroundColor('#FFFFFF')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提交前配额确认框(#5:终身 1 次权益禁止静默扣减)。 */
|
/** 提交前配额确认框(#5:消耗额度禁止静默扣减)。 */
|
||||||
@Builder
|
@Builder
|
||||||
QuotaConfirmDialog() {
|
QuotaConfirmDialog() {
|
||||||
Column({ space: 14 }) {
|
Column({ space: 14 }) {
|
||||||
@@ -926,7 +995,7 @@ struct ReportCreatePage {
|
|||||||
.fontSize(17)
|
.fontSize(17)
|
||||||
.fontWeight(FontWeight.Bold)
|
.fontWeight(FontWeight.Bold)
|
||||||
.fontColor('#333333')
|
.fontColor('#333333')
|
||||||
Text('首份形象报告为一次性权益,本次生成将使用 1 次。')
|
Text('生成报告将消耗 1 次报告额度。')
|
||||||
.fontSize(13)
|
.fontSize(13)
|
||||||
.fontColor('#666666')
|
.fontColor('#666666')
|
||||||
.textAlign(TextAlign.Center)
|
.textAlign(TextAlign.Center)
|
||||||
|
|||||||
@@ -16,6 +16,17 @@
|
|||||||
* (服务端幂等重放不过滤状态,同 key 只会重放回原失败任务)
|
* (服务端幂等重放不过滤状态,同 key 只会重放回原失败任务)
|
||||||
* - 导出 PDF / 重新生成均为消耗配额的操作:提交前弹确认框展示本次代价
|
* - 导出 PDF / 重新生成均为消耗配额的操作:提交前弹确认框展示本次代价
|
||||||
* 与剩余次数(产品方案设计纪律 3「禁止静默扣减」)
|
* 与剩余次数(产品方案设计纪律 3「禁止静默扣减」)
|
||||||
|
* - 重新生成遇 40900(早期种子报告无 rp_input 输入行)弹对话框引导去生成
|
||||||
|
* 新报告,其它错误维持底部红字;底部另有「查看历史报告」次级入口
|
||||||
|
* (pushUrl HomePage + initialTab=1 落地「报告」Tab)
|
||||||
|
* - 深度诊断(tier=full):basic 报告页底部的入口卡——未生成显示内容清单 +
|
||||||
|
* 「生成深度诊断报告」(配额确认框 → POST /reports/{id}/full → 同页轮询 →
|
||||||
|
* 采纳新 report_id 重拉,幂等键 FAILED 换新,模式同 doRegenerate);
|
||||||
|
* 已生成显示「查看深度诊断报告」(进页时 GET /reports?limit=20 找最新
|
||||||
|
* tier=full 且 SUCCEEDED 的一份);当前页本身是 full 报告时卡片换成
|
||||||
|
* 「查看基础版报告」,防止 full→full 套娃
|
||||||
|
* - full 报告增量渲染:result.full 存在时在 32 维诊断区之后渲染十二季色板
|
||||||
|
* (色块 chips)、4 场景穿搭(分组卡)、30 天计划(按周)三个区块
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { router, promptAction } from '@kit.ArkUI'
|
import { router, promptAction } from '@kit.ArkUI'
|
||||||
@@ -35,8 +46,14 @@ import { LoadingView } from '../components/f2/LoadingView'
|
|||||||
import {
|
import {
|
||||||
CopySectionWire,
|
CopySectionWire,
|
||||||
DimensionWire,
|
DimensionWire,
|
||||||
|
FullOutfitWire,
|
||||||
|
FullResultWire,
|
||||||
|
FullSceneWire,
|
||||||
|
PaletteColorWire,
|
||||||
|
PlanWeekWire,
|
||||||
ReportCreateResp,
|
ReportCreateResp,
|
||||||
ReportDetailWire,
|
ReportDetailWire,
|
||||||
|
ReportListItemWire,
|
||||||
ReportRouteParams,
|
ReportRouteParams,
|
||||||
formatIsoTime,
|
formatIsoTime,
|
||||||
randomHex,
|
randomHex,
|
||||||
@@ -81,22 +98,16 @@ interface ClaimWire {
|
|||||||
remaining: number
|
remaining: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /reservations 响应(T-P3-22)。created=false 表示幂等命中已有记录。 */
|
|
||||||
interface ReservationWire {
|
|
||||||
topic: string
|
|
||||||
created: boolean
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET /reservations?topic=… 响应。 */
|
|
||||||
interface ReservationListWire {
|
|
||||||
topics: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 本周期已领(与后端 errors.py 对齐)。 */
|
/** 本周期已领(与后端 errors.py 对齐)。 */
|
||||||
const CODE_ALREADY_CLAIMED: number = 40910
|
const CODE_ALREADY_CLAIMED: number = 40910
|
||||||
/** 预约主题(T-P3-22 首期唯一值)。 */
|
/** 状态冲突(与后端 errors.py 对齐):重新生成时输入缺失的早期种子报告走此码。 */
|
||||||
const RESERVE_TOPIC: string = 'full_report'
|
const CODE_STATE_CONFLICT: number = 40900
|
||||||
|
|
||||||
|
/** PaletteGroup @Builder 的引用传递参数($$ 形态,数组重赋值才能刷新)。 */
|
||||||
|
interface PaletteGroupParams {
|
||||||
|
title: string
|
||||||
|
colors: PaletteColorWire[]
|
||||||
|
}
|
||||||
|
|
||||||
@Entry
|
@Entry
|
||||||
@Component
|
@Component
|
||||||
@@ -119,6 +130,7 @@ struct ReportResultPage {
|
|||||||
@State regenerating: boolean = false
|
@State regenerating: boolean = false
|
||||||
@State regenTaskId: string = ''
|
@State regenTaskId: string = ''
|
||||||
@State regenError: string = ''
|
@State regenError: string = ''
|
||||||
|
@State showRegenMissingDialog: boolean = false // 40900:早期种子报告无 rp_input,引导去生成新报告
|
||||||
private regenRequestKey: string = '' // 任务 FAILED 后置空:重试换新 key(同 key 只会重放回失败任务)
|
private regenRequestKey: string = '' // 任务 FAILED 后置空:重试换新 key(同 key 只会重放回失败任务)
|
||||||
@State exporting: boolean = false
|
@State exporting: boolean = false
|
||||||
@State exportTaskId: string = ''
|
@State exportTaskId: string = ''
|
||||||
@@ -131,9 +143,22 @@ struct ReportResultPage {
|
|||||||
// —— 结论反馈(T-P3-21):dimension_key → verdict 回显表 ——
|
// —— 结论反馈(T-P3-21):dimension_key → verdict 回显表 ——
|
||||||
@State feedbackVerdicts: Record<string, string> = {}
|
@State feedbackVerdicts: Record<string, string> = {}
|
||||||
@State feedbackSubmitting: string = '' // 提交中的 dimension_key(防空点)
|
@State feedbackSubmitting: string = '' // 提交中的 dimension_key(防空点)
|
||||||
// —— 全面报告预约留资卡(T-P3-22)——
|
// —— 深度诊断(tier=full):入口卡 + 增量渲染 ——
|
||||||
@State reserved: boolean = false
|
@State tier: string = 'basic'
|
||||||
@State reserveLoading: boolean = false
|
@State fullReportId: string = '' // 已有 SUCCEEDED 的 full 报告(进页时列表查出)
|
||||||
|
@State basicReportId: string = '' // full 报告页「查看基础版报告」的目标
|
||||||
|
@State fullGenerating: boolean = false
|
||||||
|
@State fullTaskId: string = ''
|
||||||
|
@State fullError: string = ''
|
||||||
|
private fullRequestKey: string = '' // 任务 FAILED 后置空:重试换新 key
|
||||||
|
@State hasFull: boolean = false
|
||||||
|
@State fullSeasonName: string = ''
|
||||||
|
@State fullSeasonAnalysis: string = ''
|
||||||
|
@State paletteBest: PaletteColorWire[] = []
|
||||||
|
@State paletteNeutral: PaletteColorWire[] = []
|
||||||
|
@State paletteAvoid: PaletteColorWire[] = []
|
||||||
|
@State fullScenes: FullSceneWire[] = []
|
||||||
|
@State fullPlan: PlanWeekWire[] = []
|
||||||
private cardViewTracked: boolean = false // full_report.card_view 每进入结果页只打一次
|
private cardViewTracked: boolean = false // full_report.card_view 每进入结果页只打一次
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
@@ -159,6 +184,7 @@ struct ReportResultPage {
|
|||||||
this.loadErrorAuth = false
|
this.loadErrorAuth = false
|
||||||
try {
|
try {
|
||||||
const d = await ApiClient.get<ReportDetailWire>(`/api/v1/reports/${this.reportId}`)
|
const d = await ApiClient.get<ReportDetailWire>(`/api/v1/reports/${this.reportId}`)
|
||||||
|
this.tier = d.tier ?? 'basic'
|
||||||
this.versionLabel = d.version !== undefined && d.version !== null ? `v${d.version}` : 'v1'
|
this.versionLabel = d.version !== undefined && d.version !== null ? `v${d.version}` : 'v1'
|
||||||
// 后端报告状态为小写(succeeded/failed/…),统一转大写供下方状态门禁比较
|
// 后端报告状态为小写(succeeded/failed/…),统一转大写供下方状态门禁比较
|
||||||
this.statusRaw = (d.status ?? '').toUpperCase()
|
this.statusRaw = (d.status ?? '').toUpperCase()
|
||||||
@@ -177,6 +203,16 @@ struct ReportResultPage {
|
|||||||
this.totalLabel = total !== undefined && total !== null ? `${total}` : `${dims.length}`
|
this.totalLabel = total !== undefined && total !== null ? `${total}` : `${dims.length}`
|
||||||
const basicFull = result?.diagnosis?.coverage?.basic_full
|
const basicFull = result?.diagnosis?.coverage?.basic_full
|
||||||
this.basicFullText = basicFull === true ? '基础信息完整' : (basicFull === false ? '基础信息不完整' : '')
|
this.basicFullText = basicFull === true ? '基础信息完整' : (basicFull === false ? '基础信息不完整' : '')
|
||||||
|
// 深度诊断增量内容(tier=full 才有;basic 报告恒空,区块不渲染)
|
||||||
|
const full: FullResultWire | undefined = result?.full
|
||||||
|
this.hasFull = full !== undefined && full !== null
|
||||||
|
this.fullSeasonName = full?.season?.name_cn ?? ''
|
||||||
|
this.fullSeasonAnalysis = full?.season?.analysis ?? ''
|
||||||
|
this.paletteBest = full?.palette?.best ?? []
|
||||||
|
this.paletteNeutral = full?.palette?.neutral ?? []
|
||||||
|
this.paletteAvoid = full?.palette?.avoid ?? []
|
||||||
|
this.fullScenes = full?.scenes ?? []
|
||||||
|
this.fullPlan = full?.plan30 ?? []
|
||||||
const rid = d.report_id
|
const rid = d.report_id
|
||||||
if (rid !== undefined && rid !== '') {
|
if (rid !== undefined && rid !== '') {
|
||||||
AppStorage.setOrCreate<string>(LAST_REPORT_STORAGE_KEY, rid)
|
AppStorage.setOrCreate<string>(LAST_REPORT_STORAGE_KEY, rid)
|
||||||
@@ -186,18 +222,19 @@ struct ReportResultPage {
|
|||||||
// 一并换新,防止新报告复用旧导出键撞后端 IDEMPOTENCY_CONFLICT(#22)
|
// 一并换新,防止新报告复用旧导出键撞后端 IDEMPOTENCY_CONFLICT(#22)
|
||||||
this.regenRequestKey = ''
|
this.regenRequestKey = ''
|
||||||
this.exportRequestKey = ''
|
this.exportRequestKey = ''
|
||||||
|
this.fullRequestKey = ''
|
||||||
if (this.statusRaw === 'SUCCEEDED') {
|
if (this.statusRaw === 'SUCCEEDED') {
|
||||||
// 反馈回显 + report_feedback 任务闭环的数据源(T-P3-21)
|
// 反馈回显 + report_feedback 任务闭环的数据源(T-P3-21)
|
||||||
this.loadFeedback()
|
this.loadFeedback()
|
||||||
// first_report 任务闭环(T2b):报告成功展示的入口路径上静默 claim
|
// first_report 任务闭环(T2b):报告成功展示的入口路径上静默 claim
|
||||||
// (lifetime 周期,后端幂等;领过则 40910 静默同步已领态)
|
// (lifetime 周期,后端幂等;领过则 40910 静默同步已领态)
|
||||||
this.claimFirstReport()
|
this.claimFirstReport()
|
||||||
// 预约留资卡(T-P3-22):卡片渲染即曝光埋点,每进入结果页只打一次
|
// 深度诊断入口卡:卡片渲染即曝光埋点,每进入结果页只打一次
|
||||||
if (!this.cardViewTracked) {
|
if (!this.cardViewTracked) {
|
||||||
this.cardViewTracked = true
|
this.cardViewTracked = true
|
||||||
Tracker.track('full_report.card_view')
|
Tracker.track('full_report.card_view')
|
||||||
}
|
}
|
||||||
this.loadReservation()
|
this.loadFullReportList()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
@@ -237,6 +274,12 @@ struct ReportResultPage {
|
|||||||
this.regenerating = true // 同页复用轮询(TaskPollingPanel),SUCCEEDED 后自动重拉
|
this.regenerating = true // 同页复用轮询(TaskPollingPanel),SUCCEEDED 后自动重拉
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ApiError) {
|
if (e instanceof ApiError) {
|
||||||
|
if (e.code === CODE_STATE_CONFLICT) {
|
||||||
|
// 40900:早期种子报告没有 rp_input 输入行,无法复用照片/问卷直接重新生成。
|
||||||
|
// 一行红字感知太弱(像"点了没反应"),改弹对话框引导去生成新报告。
|
||||||
|
this.showRegenMissingDialog = true
|
||||||
|
return
|
||||||
|
}
|
||||||
this.regenError = e.hint !== undefined ? `${e.message}(${e.hint})` : e.message
|
this.regenError = e.hint !== undefined ? `${e.message}(${e.hint})` : e.message
|
||||||
} else {
|
} else {
|
||||||
this.regenError = '请求失败,请检查网络'
|
this.regenError = '请求失败,请检查网络'
|
||||||
@@ -336,9 +379,9 @@ struct ReportResultPage {
|
|||||||
this.exportRequestKey = ''
|
this.exportRequestKey = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开配额确认框(regen=report_regen 每 30 天 1 次 / export=hd_export 每日 1 次)。 */
|
/** 打开配额确认框(regen=report_regen 每 30 天 1 次 / export=hd_export 每日 1 次 / full=full_report 每日 1 次)。 */
|
||||||
private openConfirm(action: string): void {
|
private openConfirm(action: string): void {
|
||||||
if (action !== 'regen' && action !== 'export') {
|
if (action !== 'regen' && action !== 'export' && action !== 'full') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action === 'regen' && (this.regenerating || this.reportId === '')) {
|
if (action === 'regen' && (this.regenerating || this.reportId === '')) {
|
||||||
@@ -347,6 +390,9 @@ struct ReportResultPage {
|
|||||||
if (action === 'export' && (this.exporting || this.reportId === '')) {
|
if (action === 'export' && (this.exporting || this.reportId === '')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (action === 'full' && (this.fullGenerating || this.reportId === '')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.confirmAction = action
|
this.confirmAction = action
|
||||||
this.confirmLoaded = false
|
this.confirmLoaded = false
|
||||||
this.confirmRemaining = -1
|
this.confirmRemaining = -1
|
||||||
@@ -355,7 +401,9 @@ struct ReportResultPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadConfirmQuota(): Promise<void> {
|
private async loadConfirmQuota(): Promise<void> {
|
||||||
const key = this.confirmAction === 'export' ? 'hd_export' : 'report_regen'
|
const key = this.confirmAction === 'export'
|
||||||
|
? 'hd_export'
|
||||||
|
: (this.confirmAction === 'full' ? 'full_report' : 'report_regen')
|
||||||
try {
|
try {
|
||||||
const d = await ApiClient.get<QuotaWireResp>(`/api/v1/quota?quota_key=${key}`)
|
const d = await ApiClient.get<QuotaWireResp>(`/api/v1/quota?quota_key=${key}`)
|
||||||
this.confirmRemaining = d.remaining
|
this.confirmRemaining = d.remaining
|
||||||
@@ -375,6 +423,8 @@ struct ReportResultPage {
|
|||||||
this.doRegenerate()
|
this.doRegenerate()
|
||||||
} else if (action === 'export') {
|
} else if (action === 'export') {
|
||||||
this.doExport()
|
this.doExport()
|
||||||
|
} else if (action === 'full') {
|
||||||
|
this.doFullGenerate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,40 +519,88 @@ struct ReportResultPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 全面报告预约留资卡(T-P3-22) ====================
|
// ==================== 深度诊断(tier=full)入口卡 ====================
|
||||||
|
|
||||||
/** 拉预约态决定按钮;失败保持未预约态(用户可点击,服务端幂等兜底)。 */
|
/**
|
||||||
private async loadReservation(): Promise<void> {
|
* 查报告列表(limit=20 对齐首页报告 Tab):找最新一份 tier=full 且
|
||||||
|
* SUCCEEDED 的报告(已生成态 → 「查看深度诊断报告」),同时记下最新
|
||||||
|
* basic 报告 id(full 报告页「查看基础版报告」的跳转目标)。
|
||||||
|
* 失败静默:卡片按未生成态展示,生成按钮仍可用(服务端幂等兜底)。
|
||||||
|
*/
|
||||||
|
private async loadFullReportList(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const d = await ApiClient.get<ReservationListWire>(`/api/v1/reservations?topic=${RESERVE_TOPIC}`)
|
const list = await ApiClient.get<ReportListItemWire[]>('/api/v1/reports?limit=20')
|
||||||
this.reserved = d.topics.includes(RESERVE_TOPIC)
|
let fullId: string = ''
|
||||||
|
let basicId: string = ''
|
||||||
|
for (const item of list) {
|
||||||
|
const ok = (item.status ?? '').toUpperCase() === 'SUCCEEDED'
|
||||||
|
const rid = item.report_id ?? ''
|
||||||
|
if (!ok || rid === '') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (fullId === '' && item.tier === 'full') {
|
||||||
|
fullId = rid
|
||||||
|
}
|
||||||
|
if (basicId === '' && item.tier !== 'full') {
|
||||||
|
basicId = rid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.fullReportId = fullId
|
||||||
|
this.basicReportId = basicId
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
this.reserved = false
|
this.fullReportId = ''
|
||||||
|
this.basicReportId = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doReserve(): Promise<void> {
|
/** 生成深度诊断:模式同 doRegenerate(新 report_id 必须采纳,轮询成功本页重拉)。 */
|
||||||
if (this.reserved || this.reserveLoading) {
|
private async doFullGenerate(): Promise<void> {
|
||||||
|
if (this.fullGenerating || this.reportId === '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.reserveLoading = true
|
this.fullError = ''
|
||||||
|
if (this.fullRequestKey === '') {
|
||||||
|
this.fullRequestKey = randomHex(16)
|
||||||
|
}
|
||||||
|
const req: Record<string, Object> = { 'request_key': this.fullRequestKey }
|
||||||
try {
|
try {
|
||||||
const req: Record<string, Object> = { 'topic': RESERVE_TOPIC }
|
const data = await ApiClient.post<ReportCreateResp>(`/api/v1/reports/${this.reportId}/full`, req)
|
||||||
const d = await ApiClient.post<ReservationWire>('/api/v1/reservations', req)
|
const tid = data.task_id ?? ''
|
||||||
this.reserved = true // created=false(幂等命中)同样是已预约态
|
if (tid === '') {
|
||||||
Tracker.track('full_report.reserve_result', { 'ok': true })
|
this.fullError = '服务端未返回任务 ID,请稍后重试'
|
||||||
if (d.created) {
|
return
|
||||||
this.toast('预约成功,上线后第一时间告诉你')
|
|
||||||
}
|
}
|
||||||
|
// 深度诊断在后端是新建一行报告(tier=full、新 report_id):必须采纳新 id,
|
||||||
|
// 否则轮询成功后 loadReport 拉回的仍是基础版报告
|
||||||
|
const newRid = data.report_id ?? ''
|
||||||
|
if (newRid !== '') {
|
||||||
|
this.reportId = newRid
|
||||||
|
AppStorage.setOrCreate<string>(LAST_REPORT_STORAGE_KEY, newRid)
|
||||||
|
}
|
||||||
|
this.fullTaskId = tid
|
||||||
|
this.fullGenerating = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Tracker.track('full_report.reserve_result', {
|
if (e instanceof ApiError) {
|
||||||
'ok': false, 'code': e instanceof ApiError ? e.code : 50000
|
this.fullError = e.hint !== undefined ? `${e.message}(${e.hint})` : e.message
|
||||||
})
|
} else {
|
||||||
this.toast(e instanceof ApiError ? e.message : '预约失败,请稍后重试')
|
this.fullError = '请求失败,请检查网络'
|
||||||
} finally {
|
|
||||||
this.reserveLoading = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFullSucceeded(): void {
|
||||||
|
this.fullGenerating = false
|
||||||
|
this.fullError = ''
|
||||||
|
this.fullReportId = this.reportId // 本页已是 full 报告
|
||||||
|
this.loadReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFullFailed(reason: string): void {
|
||||||
|
this.fullGenerating = false
|
||||||
|
this.fullError = `深度诊断生成失败:${reason}`
|
||||||
|
// 清空幂等键:重试以新 key 创建新任务(旧失败任务的额度已由服务端退还)
|
||||||
|
this.fullRequestKey = ''
|
||||||
|
}
|
||||||
|
|
||||||
private dimKey(d: DimensionWire): string {
|
private dimKey(d: DimensionWire): string {
|
||||||
const k = d.key
|
const k = d.key
|
||||||
@@ -588,24 +686,50 @@ struct ReportResultPage {
|
|||||||
.padding({ left: 24, right: 24 })
|
.padding({ left: 24, right: 24 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private confirmTitle(): string {
|
||||||
|
if (this.confirmAction === 'export') {
|
||||||
|
return '确认导出 PDF'
|
||||||
|
}
|
||||||
|
if (this.confirmAction === 'full') {
|
||||||
|
return '确认生成深度诊断'
|
||||||
|
}
|
||||||
|
return '确认重新生成'
|
||||||
|
}
|
||||||
|
|
||||||
|
private confirmDesc(): string {
|
||||||
|
if (this.confirmAction === 'export') {
|
||||||
|
return '导出 PDF 每日可使用 1 次,本次将使用 1 次。'
|
||||||
|
}
|
||||||
|
if (this.confirmAction === 'full') {
|
||||||
|
return '深度诊断每日可生成 1 次,本次将使用 1 次。'
|
||||||
|
}
|
||||||
|
return '重新生成每 30 天可使用 1 次,本次将使用 1 次。'
|
||||||
|
}
|
||||||
|
|
||||||
|
private confirmRemainText(): string {
|
||||||
|
if (this.confirmAction === 'export') {
|
||||||
|
return `今日还可导出 ${this.confirmRemaining} 次`
|
||||||
|
}
|
||||||
|
if (this.confirmAction === 'full') {
|
||||||
|
return `今日还可生成 ${this.confirmRemaining} 次`
|
||||||
|
}
|
||||||
|
return `本周期还可重新生成 ${this.confirmRemaining} 次`
|
||||||
|
}
|
||||||
|
|
||||||
/** 消耗配额前的确认框(#5):展示本次代价与剩余次数,剩余 0 时阻止提交。 */
|
/** 消耗配额前的确认框(#5):展示本次代价与剩余次数,剩余 0 时阻止提交。 */
|
||||||
@Builder
|
@Builder
|
||||||
QuotaConfirmDialog() {
|
QuotaConfirmDialog() {
|
||||||
Column({ space: 14 }) {
|
Column({ space: 14 }) {
|
||||||
Text(this.confirmAction === 'export' ? '确认导出 PDF' : '确认重新生成')
|
Text(this.confirmTitle())
|
||||||
.fontSize(17)
|
.fontSize(17)
|
||||||
.fontWeight(FontWeight.Bold)
|
.fontWeight(FontWeight.Bold)
|
||||||
.fontColor('#333333')
|
.fontColor('#333333')
|
||||||
Text(this.confirmAction === 'export'
|
Text(this.confirmDesc())
|
||||||
? '导出 PDF 每日可使用 1 次,本次将使用 1 次。'
|
|
||||||
: '重新生成每 30 天可使用 1 次,本次将使用 1 次。')
|
|
||||||
.fontSize(13)
|
.fontSize(13)
|
||||||
.fontColor('#666666')
|
.fontColor('#666666')
|
||||||
.textAlign(TextAlign.Center)
|
.textAlign(TextAlign.Center)
|
||||||
if (this.confirmLoaded && this.confirmRemaining > 0) {
|
if (this.confirmLoaded && this.confirmRemaining > 0) {
|
||||||
Text(this.confirmAction === 'export'
|
Text(this.confirmRemainText())
|
||||||
? `今日还可导出 ${this.confirmRemaining} 次`
|
|
||||||
: `本周期还可重新生成 ${this.confirmRemaining} 次`)
|
|
||||||
.fontSize(13)
|
.fontSize(13)
|
||||||
.fontColor(COLOR_PRIMARY)
|
.fontColor(COLOR_PRIMARY)
|
||||||
} else if (this.confirmLoaded && this.confirmRemaining <= 0) {
|
} else if (this.confirmLoaded && this.confirmRemaining <= 0) {
|
||||||
@@ -645,12 +769,53 @@ struct ReportResultPage {
|
|||||||
.backgroundColor('#FFFFFF')
|
.backgroundColor('#FFFFFF')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 40900 兜底引导:早期种子报告缺输入行,无法直接重新生成 → 引导去生成新报告。 */
|
||||||
|
@Builder
|
||||||
|
RegenMissingDialog() {
|
||||||
|
Column({ space: 14 }) {
|
||||||
|
Text('无法直接重新生成')
|
||||||
|
.fontSize(17)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#333333')
|
||||||
|
Text('该报告为早期版本,缺少可复用的照片与问卷信息。可以重新拍摄照片,生成一份全新报告。')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#666666')
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
Row({ space: 12 }) {
|
||||||
|
Button('取消')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(40)
|
||||||
|
.fontSize(14)
|
||||||
|
.backgroundColor('#F3F4F6')
|
||||||
|
.fontColor('#666666')
|
||||||
|
.onClick(() => {
|
||||||
|
this.showRegenMissingDialog = false
|
||||||
|
})
|
||||||
|
Button('去生成新报告')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(40)
|
||||||
|
.fontSize(14)
|
||||||
|
.backgroundColor(COLOR_PRIMARY)
|
||||||
|
.onClick(() => {
|
||||||
|
this.showRegenMissingDialog = false
|
||||||
|
router.pushUrl({ url: 'pages/ReportCreatePage' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.margin({ top: 4 })
|
||||||
|
}
|
||||||
|
.width('82%')
|
||||||
|
.padding(20)
|
||||||
|
.borderRadius(16)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
HeaderCard() {
|
HeaderCard() {
|
||||||
Column({ space: 6 }) {
|
Column({ space: 6 }) {
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
Column({ space: 2 }) {
|
Column({ space: 2 }) {
|
||||||
Text('形象诊断报告')
|
Text(this.tier === 'full' ? '形象深度诊断报告' : '形象诊断报告')
|
||||||
.fontSize(18)
|
.fontSize(18)
|
||||||
.fontWeight(FontWeight.Bold)
|
.fontWeight(FontWeight.Bold)
|
||||||
.fontColor('#333333')
|
.fontColor('#333333')
|
||||||
@@ -824,15 +989,48 @@ struct ReportResultPage {
|
|||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 全面报告预约留资卡(T-P3-22):模糊目录 teaser,无价格字样(FR-BILL-07 纪律同 apply)。 */
|
|
||||||
@Builder
|
@Builder
|
||||||
ReservationCard() {
|
FullGenView() {
|
||||||
|
Column({ space: 16 }) {
|
||||||
|
Text('正在生成深度诊断报告')
|
||||||
|
.fontSize(20)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#333333')
|
||||||
|
Text('基于你的基础报告生成更完整的色彩、场景与计划内容,完成后自动展示')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#999999')
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
TaskPollingPanel({
|
||||||
|
taskId: this.fullTaskId,
|
||||||
|
onSucceeded: (): void => {
|
||||||
|
this.onFullSucceeded()
|
||||||
|
},
|
||||||
|
onFailed: (reason: string): void => {
|
||||||
|
this.onFullFailed(reason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.width('86%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
.padding({ left: 24, right: 24 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度诊断入口卡(内容区底部)。三态:当前页是 full →「查看基础版报告」
|
||||||
|
* (防 full→full 套娃);已有 full →「查看深度诊断报告」(本页内采纳 id
|
||||||
|
* 重拉,不压路由栈);未生成 → 内容清单 +「生成深度诊断报告」(配额确认框)。
|
||||||
|
*/
|
||||||
|
@Builder
|
||||||
|
FullReportCard() {
|
||||||
Column({ space: 10 }) {
|
Column({ space: 10 }) {
|
||||||
Text('形象深度诊断 · 即将上线')
|
Text(this.tier === 'full' ? '当前为深度诊断报告' : '形象深度诊断')
|
||||||
.fontSize(16)
|
.fontSize(16)
|
||||||
.fontWeight(FontWeight.Bold)
|
.fontWeight(FontWeight.Bold)
|
||||||
.fontColor('#333333')
|
.fontColor('#333333')
|
||||||
.width('100%')
|
.width('100%')
|
||||||
|
if (this.tier !== 'full' && this.fullReportId === '') {
|
||||||
Column({ space: 6 }) {
|
Column({ space: 6 }) {
|
||||||
Text('· 十二季色彩完整色板')
|
Text('· 十二季色彩完整色板')
|
||||||
.fontSize(13)
|
.fontSize(13)
|
||||||
@@ -849,26 +1047,45 @@ struct ReportResultPage {
|
|||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
if (this.reserved) {
|
}
|
||||||
Text('已预约,上线后第一时间告诉你')
|
if (this.tier === 'full') {
|
||||||
.fontSize(14)
|
if (this.basicReportId !== '') {
|
||||||
.fontColor('#9AA0A6')
|
Button('查看基础版报告')
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height(44)
|
.height(44)
|
||||||
.textAlign(TextAlign.Center)
|
.fontSize(15)
|
||||||
|
.fontColor(COLOR_PRIMARY)
|
||||||
|
.backgroundColor('#F7E3EA')
|
||||||
.borderRadius(22)
|
.borderRadius(22)
|
||||||
.backgroundColor('#F3F4F6')
|
.onClick(() => {
|
||||||
} else {
|
this.reportId = this.basicReportId
|
||||||
Button(this.reserveLoading ? '预约中…' : '抢先预约,上线提醒我')
|
AppStorage.setOrCreate<string>(LAST_REPORT_STORAGE_KEY, this.basicReportId)
|
||||||
|
this.loadReport()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (this.fullReportId !== '') {
|
||||||
|
Button('查看深度诊断报告')
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height(44)
|
.height(44)
|
||||||
.fontSize(15)
|
.fontSize(15)
|
||||||
.fontColor(Color.White)
|
.fontColor(Color.White)
|
||||||
.backgroundColor(COLOR_PRIMARY)
|
.backgroundColor(COLOR_PRIMARY)
|
||||||
.borderRadius(22)
|
.borderRadius(22)
|
||||||
.enabled(!this.reserveLoading)
|
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.doReserve()
|
this.reportId = this.fullReportId
|
||||||
|
AppStorage.setOrCreate<string>(LAST_REPORT_STORAGE_KEY, this.fullReportId)
|
||||||
|
this.loadReport()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Button(this.fullError !== '' ? '重试生成深度诊断报告' : '生成深度诊断报告')
|
||||||
|
.width('100%')
|
||||||
|
.height(44)
|
||||||
|
.fontSize(15)
|
||||||
|
.fontColor(Color.White)
|
||||||
|
.backgroundColor(COLOR_PRIMARY)
|
||||||
|
.borderRadius(22)
|
||||||
|
.onClick(() => {
|
||||||
|
this.openConfirm('full')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -879,6 +1096,154 @@ struct ReportResultPage {
|
|||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 深度诊断增量渲染(result.full 存在时) ====================
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
PaletteGroup($$: PaletteGroupParams) {
|
||||||
|
if ($$.colors.length > 0) {
|
||||||
|
Column({ space: 8 }) {
|
||||||
|
Text(`${$$.title}(${$$.colors.length} 色)`)
|
||||||
|
.fontSize(13)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#555555')
|
||||||
|
.width('100%')
|
||||||
|
Flex({ wrap: FlexWrap.Wrap }) {
|
||||||
|
ForEach($$.colors, (c: PaletteColorWire, idx: number) => {
|
||||||
|
Column({ space: 4 }) {
|
||||||
|
Blank()
|
||||||
|
.width(44)
|
||||||
|
.height(30)
|
||||||
|
.borderRadius(6)
|
||||||
|
.backgroundColor(c.hex ?? '#CCCCCC')
|
||||||
|
.border({ width: 1, color: '#E5E7EB' })
|
||||||
|
Text(c.name ?? '')
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor('#666666')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
.width(56)
|
||||||
|
.alignItems(HorizontalAlign.Center)
|
||||||
|
.margin({ bottom: 8 })
|
||||||
|
}, (c: PaletteColorWire, idx: number) => `${idx}-${c.name ?? ''}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
FullSeasonPaletteCard() {
|
||||||
|
Column({ space: 12 }) {
|
||||||
|
Text(this.fullSeasonName !== '' ? `十二季色彩定位 · ${this.fullSeasonName}` : '十二季色彩定位')
|
||||||
|
.fontSize(16)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#333333')
|
||||||
|
.width('100%')
|
||||||
|
if (this.fullSeasonAnalysis !== '') {
|
||||||
|
Text(this.fullSeasonAnalysis)
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#555555')
|
||||||
|
.lineHeight(21)
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
this.PaletteGroup({ title: '适合色', colors: this.paletteBest })
|
||||||
|
this.PaletteGroup({ title: '中性色', colors: this.paletteNeutral })
|
||||||
|
this.PaletteGroup({ title: '避雷色', colors: this.paletteAvoid })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(16)
|
||||||
|
.borderRadius(14)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
FullScenesCard() {
|
||||||
|
Column({ space: 12 }) {
|
||||||
|
Text('场景穿搭方案')
|
||||||
|
.fontSize(16)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#333333')
|
||||||
|
.width('100%')
|
||||||
|
ForEach(this.fullScenes, (scene: FullSceneWire, sIdx: number) => {
|
||||||
|
Column({ space: 8 }) {
|
||||||
|
Text(scene.title ?? '')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#444444')
|
||||||
|
.width('100%')
|
||||||
|
ForEach(scene.outfits ?? [], (outfit: FullOutfitWire, oIdx: number) => {
|
||||||
|
Column({ space: 4 }) {
|
||||||
|
Text(outfit.name ?? '')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor(COLOR_PRIMARY)
|
||||||
|
.width('100%')
|
||||||
|
ForEach(outfit.points ?? [], (point: string, pIdx: number) => {
|
||||||
|
Text(`· ${point}`)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#555555')
|
||||||
|
.lineHeight(19)
|
||||||
|
.width('100%')
|
||||||
|
}, (point: string, pIdx: number) => `${sIdx}-${oIdx}-${pIdx}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(10)
|
||||||
|
.borderRadius(10)
|
||||||
|
.backgroundColor('#F7F8FA')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}, (outfit: FullOutfitWire, oIdx: number) => `${sIdx}-${oIdx}-${outfit.name ?? ''}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}, (scene: FullSceneWire, sIdx: number) => `${sIdx}-${scene.key ?? ''}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(16)
|
||||||
|
.borderRadius(14)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
FullPlanCard() {
|
||||||
|
Column({ space: 12 }) {
|
||||||
|
Text('30 天形象提升计划')
|
||||||
|
.fontSize(16)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#333333')
|
||||||
|
.width('100%')
|
||||||
|
ForEach(this.fullPlan, (week: PlanWeekWire, wIdx: number) => {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Text(week.title ?? `第 ${week.week ?? wIdx + 1} 周`)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#444444')
|
||||||
|
.width('100%')
|
||||||
|
ForEach(week.tasks ?? [], (task: string, tIdx: number) => {
|
||||||
|
Text(`· ${task}`)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#555555')
|
||||||
|
.lineHeight(19)
|
||||||
|
.width('100%')
|
||||||
|
}, (task: string, tIdx: number) => `${wIdx}-${tIdx}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(10)
|
||||||
|
.borderRadius(10)
|
||||||
|
.backgroundColor('#F7F8FA')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}, (week: PlanWeekWire, wIdx: number) => `${wIdx}-${week.week ?? 0}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(16)
|
||||||
|
.borderRadius(14)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
ContentBody() {
|
ContentBody() {
|
||||||
Column() {
|
Column() {
|
||||||
@@ -890,9 +1255,15 @@ struct ReportResultPage {
|
|||||||
}
|
}
|
||||||
this.CopyCard()
|
this.CopyCard()
|
||||||
this.DiagnosisArea()
|
this.DiagnosisArea()
|
||||||
// 预约留资卡(T-P3-22):内容区底部,导出/重新生成按钮区之前
|
// 深度诊断增量区块(仅 tier=full 报告有 result.full):色板 / 场景 / 30 天计划
|
||||||
|
if (this.hasFull) {
|
||||||
|
this.FullSeasonPaletteCard()
|
||||||
|
this.FullScenesCard()
|
||||||
|
this.FullPlanCard()
|
||||||
|
}
|
||||||
|
// 深度诊断入口卡:内容区底部,导出/重新生成按钮区之前
|
||||||
if (this.statusRaw === 'SUCCEEDED') {
|
if (this.statusRaw === 'SUCCEEDED') {
|
||||||
this.ReservationCard()
|
this.FullReportCard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
@@ -915,6 +1286,13 @@ struct ReportResultPage {
|
|||||||
.width('100%')
|
.width('100%')
|
||||||
.textAlign(TextAlign.Start)
|
.textAlign(TextAlign.Start)
|
||||||
}
|
}
|
||||||
|
if (this.fullError !== '') {
|
||||||
|
Text(this.fullError)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#E5484D')
|
||||||
|
.width('100%')
|
||||||
|
.textAlign(TextAlign.Start)
|
||||||
|
}
|
||||||
// 仅成功结案的报告可导出 PDF(服务端同样校验,端侧只是提前挡)
|
// 仅成功结案的报告可导出 PDF(服务端同样校验,端侧只是提前挡)
|
||||||
if (this.statusRaw === 'SUCCEEDED') {
|
if (this.statusRaw === 'SUCCEEDED') {
|
||||||
Button(this.exportError !== '' ? '重试导出 PDF' : '导出 PDF 报告')
|
Button(this.exportError !== '' ? '重试导出 PDF' : '导出 PDF 报告')
|
||||||
@@ -935,6 +1313,17 @@ struct ReportResultPage {
|
|||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.openConfirm('regen')
|
this.openConfirm('regen')
|
||||||
})
|
})
|
||||||
|
// 次级入口:回首页「报告」Tab 查看全部历史报告(initialTab=1)
|
||||||
|
Button('查看历史报告')
|
||||||
|
.width('100%')
|
||||||
|
.height(46)
|
||||||
|
.fontSize(15)
|
||||||
|
.fontColor(COLOR_PRIMARY)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.onClick(() => {
|
||||||
|
const params: Record<string, Object> = { 'initialTab': 1 }
|
||||||
|
router.pushUrl({ url: 'pages/HomePage', params: params })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.padding({ left: 16, right: 16, top: 10, bottom: 14 })
|
.padding({ left: 16, right: 16, top: 10, bottom: 14 })
|
||||||
@@ -951,6 +1340,8 @@ struct ReportResultPage {
|
|||||||
this.ExportView()
|
this.ExportView()
|
||||||
} else if (this.regenerating) {
|
} else if (this.regenerating) {
|
||||||
this.RegenView()
|
this.RegenView()
|
||||||
|
} else if (this.fullGenerating) {
|
||||||
|
this.FullGenView()
|
||||||
} else if (this.loading) {
|
} else if (this.loading) {
|
||||||
// N5':换用 f2 通用加载态(包一层 Column 承接 layoutWeight 占位)
|
// N5':换用 f2 通用加载态(包一层 Column 承接 layoutWeight 占位)
|
||||||
Column() {
|
Column() {
|
||||||
@@ -999,6 +1390,16 @@ struct ReportResultPage {
|
|||||||
.backgroundColor('#66000000')
|
.backgroundColor('#66000000')
|
||||||
.justifyContent(FlexAlign.Center)
|
.justifyContent(FlexAlign.Center)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.showRegenMissingDialog) {
|
||||||
|
Column() {
|
||||||
|
this.RegenMissingDialog()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('#66000000')
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
|
|||||||
Reference in New Issue
Block a user