feat(report): 美学诊断照片必选——本地直传链路 + 报告创建照片卡 + 授权文案改属实
- ReportCreatePage:必选照片卡(人脸授权→相册/拍照→预览/重选),未选红字+汇总条拦截;提交先上传拿真 object_key 再建报告(上传失败不碰配额可重试);删除 buildMockPhotoKey 模拟链路;照片引导按需求文档 §3.4/§5.1(正面全身照) - core/UploadApi.ets:POST /uploads/direct 原始字节上传(绕开真机 multiFormDataList 0 字节 bug),带鉴权信封解包 - FacePhotoConsent:加可选 usageText(诊断页传报告口径文案,试穿/发型页零影响);处理方/保存两行按本地磁盘降级路线改属实(去「第三方」「生成后即删除」),CONSENT_VERSION v1→v2 全体重询一次 - 后端配套:meida_backend a1d0171(本地直传+存在性校验);联调 E2E 已验证(真 key 全链路 succeeded / 假 key 404 零流水)
This commit is contained in:
@@ -22,7 +22,7 @@ import { COLOR_PRIMARY } from '../../core/Theme'
|
|||||||
const CONSENT_STORAGE_KEY: string = 'face_photo_consent_granted'
|
const CONSENT_STORAGE_KEY: string = 'face_photo_consent_granted'
|
||||||
const CONSENT_PREF_NAME: string = 'meida_face_consent'
|
const CONSENT_PREF_NAME: string = 'meida_face_consent'
|
||||||
const CONSENT_RECORD_KEY: string = 'record'
|
const CONSENT_RECORD_KEY: string = 'record'
|
||||||
const CONSENT_VERSION: string = 'v1'
|
const CONSENT_VERSION: string = 'v2' // v2:处理方/保存文案按本地磁盘降级路线改为属实表述(2026-09-12)
|
||||||
const TEXT_PRIMARY: string = '#222222'
|
const TEXT_PRIMARY: string = '#222222'
|
||||||
const TEXT_SECONDARY: string = '#8A9099'
|
const TEXT_SECONDARY: string = '#8A9099'
|
||||||
const ACCENT: string = COLOR_PRIMARY
|
const ACCENT: string = COLOR_PRIMARY
|
||||||
@@ -103,6 +103,8 @@ export class FaceConsentStore {
|
|||||||
export struct FacePhotoConsent {
|
export struct FacePhotoConsent {
|
||||||
onAgree: () => void = () => {}
|
onAgree: () => void = () => {}
|
||||||
onRefuse: () => void = () => {}
|
onRefuse: () => void = () => {}
|
||||||
|
/** 用途说明行:默认试穿/发型场景口径;诊断报告等场景传入自己的文案。 */
|
||||||
|
usageText: string = '用途:照片仅用于为你生成试穿/发型效果图,不用于识别你的身份,也不做其他用途的分析。'
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
ConsentLine(bullet: string, text: string) {
|
ConsentLine(bullet: string, text: string) {
|
||||||
@@ -128,9 +130,9 @@ export struct FacePhotoConsent {
|
|||||||
.fontWeight(FontWeight.Bold)
|
.fontWeight(FontWeight.Bold)
|
||||||
.fontColor(TEXT_PRIMARY)
|
.fontColor(TEXT_PRIMARY)
|
||||||
Column({ space: 8 }) {
|
Column({ space: 8 }) {
|
||||||
this.ConsentLine('·', '用途:照片仅用于为你生成试穿/发型效果图,不用于识别你的身份,也不做其他用途的分析。')
|
this.ConsentLine('·', this.usageText)
|
||||||
this.ConsentLine('·', '处理方:照片将上传至第三方 AI 图像服务完成本次生成,不会共享给其他方。')
|
this.ConsentLine('·', '处理方:照片将上传至美搭的图像服务完成本次生成,不会共享给其他方。')
|
||||||
this.ConsentLine('·', '保存:原图在生成完成后即删除;你保存到相册的效果图可随时自行删除。')
|
this.ConsentLine('·', '保存:上传的原图不会公开展示;你保存到相册的效果图可随时自行删除。')
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
|||||||
@@ -264,12 +264,6 @@ export function randomHex(len: number): string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模拟照片对象键:report_photo/{userId}/{随机}.jpg(V1 骨架,COS 直传未接入) */
|
|
||||||
export function buildMockPhotoKey(userId: string): string {
|
|
||||||
const uid = userId === '' ? 'anonymous' : userId
|
|
||||||
return `report_photo/${uid}/${randomHex(12)}.jpg`
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 32 维按 status 分两组:已分析(ok)与未覆盖(skipped,含 status 缺失的兜底) */
|
/** 32 维按 status 分两组:已分析(ok)与未覆盖(skipped,含 status 缺失的兜底) */
|
||||||
export function splitDimensions(dims: DimensionWire[]): DimensionGroups {
|
export function splitDimensions(dims: DimensionWire[]): DimensionGroups {
|
||||||
const groups: DimensionGroups = { analyzed: [], skipped: [] }
|
const groups: DimensionGroups = { analyzed: [], skipped: [] }
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* UploadApi:诊断照片直传本机后端(无 COS 四件套的本地磁盘降级路线)。
|
||||||
|
*
|
||||||
|
* 后端 POST /api/v1/uploads/direct 收原始字节(后端仓库未引入
|
||||||
|
* python-multipart,不收 multipart 表单;这也正好绕开真机
|
||||||
|
* http.multiFormDataList 的 0 字节 bug):
|
||||||
|
* - body 即图片字节(ImageCodec.toJpegBuffer 压缩后的 JPEG)
|
||||||
|
* - Content-Type 头申报类型;scene 走 query(默认 report_photo)
|
||||||
|
* - 响应走统一信封,data.object_key 回传给 POST /api/v1/reports
|
||||||
|
*
|
||||||
|
* 错误一律抛 ApiError(与 ApiClient 约定一致,UI 层只 catch ApiError)。
|
||||||
|
* 鉴权头从 AuthStore 取;401 自动刷新不在此处做(与 TryonApi 同口径,
|
||||||
|
* 令牌过期由下一次 ApiClient 请求兜底刷新)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http from '@ohos.net.http'
|
||||||
|
import { hilog } from '@kit.PerformanceAnalysisKit'
|
||||||
|
import { ApiError } from './ApiTypes'
|
||||||
|
import { AuthStore } from './AuthStore'
|
||||||
|
|
||||||
|
const UPLOAD_DOMAIN: number = 0x0201
|
||||||
|
const UPLOAD_TAG: string = 'MEIDA_UPLOAD'
|
||||||
|
const UPLOAD_READ_TIMEOUT_MS: number = 60000 // 压缩后单张 ~300KB,60s 覆盖弱网
|
||||||
|
|
||||||
|
/** POST /uploads/direct 响应的 data 段(字段名对齐后端 JSON)。 */
|
||||||
|
export interface DirectUploadData {
|
||||||
|
object_key?: string
|
||||||
|
size_bytes?: number
|
||||||
|
content_type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 直传响应信封(同 ApiEnvelope 形状,data 可选以防御后端缺省)。 */
|
||||||
|
interface DirectUploadWire {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data?: DirectUploadData
|
||||||
|
hint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UploadApi {
|
||||||
|
/**
|
||||||
|
* 上传诊断照片(已压缩 JPEG 字节),返回真实 object_key。
|
||||||
|
* 失败抛 ApiError(message/hint 用服务端信封原文,可直接展示重试)。
|
||||||
|
*/
|
||||||
|
static async uploadReportPhoto(imageBuf: ArrayBuffer): Promise<string> {
|
||||||
|
const token = AuthStore.getAccessToken()
|
||||||
|
if (token === undefined) {
|
||||||
|
// 与 ApiClient 同一纪律:未登录直接短路,省一次必然 401 的往返
|
||||||
|
throw new ApiError(40100, '请先登录', '登录后即可继续')
|
||||||
|
}
|
||||||
|
const base = await AuthStore.getBaseUrl()
|
||||||
|
const url = `${base}/api/v1/uploads/direct?scene=report_photo`
|
||||||
|
const client = http.createHttp()
|
||||||
|
try {
|
||||||
|
const header: Record<string, string> = {
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
hilog.info(UPLOAD_DOMAIN, UPLOAD_TAG, 'direct post %{public}s, %{public}d bytes', url, imageBuf.byteLength)
|
||||||
|
const resp = await client.request(url, {
|
||||||
|
method: http.RequestMethod.POST,
|
||||||
|
header: header,
|
||||||
|
extraData: imageBuf,
|
||||||
|
expectDataType: http.HttpDataType.STRING,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
readTimeout: UPLOAD_READ_TIMEOUT_MS
|
||||||
|
})
|
||||||
|
const bodyText: string = (resp.result ?? '') as string
|
||||||
|
hilog.info(UPLOAD_DOMAIN, UPLOAD_TAG, 'direct resp code=%{public}d body=%{public}s',
|
||||||
|
resp.responseCode, bodyText.substring(0, 300))
|
||||||
|
let wire: DirectUploadWire
|
||||||
|
try {
|
||||||
|
wire = JSON.parse(bodyText) as DirectUploadWire
|
||||||
|
} catch (_p) {
|
||||||
|
// 网关/代理层错误:body 不是信封 JSON,给统一话术(对齐 ApiClient)
|
||||||
|
throw new ApiError(50000, '服务开小差了', '请稍后重试', resp.responseCode)
|
||||||
|
}
|
||||||
|
const key = wire.data?.object_key ?? ''
|
||||||
|
if (wire.code === 0 && key !== '') {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
if (wire.code !== 0) {
|
||||||
|
throw new ApiError(wire.code, wire.message, wire.hint, resp.responseCode)
|
||||||
|
}
|
||||||
|
// code=0 但没有 object_key:契约破损,按服务端异常处理
|
||||||
|
throw new ApiError(50000, '照片上传结果异常', '请稍后重试', resp.responseCode)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
hilog.error(UPLOAD_DOMAIN, UPLOAD_TAG, 'direct request failed: %{public}s', String(e))
|
||||||
|
throw new ApiError(50000, '照片上传失败,请检查网络', '请稍后重试')
|
||||||
|
} finally {
|
||||||
|
client.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* ReportCreatePage —— 报告创建(F3):问卷六字段 + 模拟照片键 + 提交 + 任务轮询。
|
* ReportCreatePage —— 报告创建(F3):必选照片 + 问卷六字段 + 提交 + 任务轮询。
|
||||||
*
|
*
|
||||||
* 流程:
|
* 流程:
|
||||||
* 1) 问卷表单(性别/身高/体重/生日/核心诉求多选/心理风格九型),提交前前端校验;
|
* 1) 个人照片(必选,正面全身照):先走 FacePhotoConsent 人脸单独授权(CR-01),
|
||||||
|
* 再相册/拍照;提交时先上传(POST /uploads/direct,本地磁盘降级路线)
|
||||||
|
* 拿真实 object_key,再建报告——模拟键已被服务端存在性校验封死
|
||||||
|
* 2) 问卷表单(性别/身高/体重/生日/核心诉求多选/心理风格九型),提交前前端校验;
|
||||||
* 身高/体重为滚轮选择器(showTextPickerDialog),不提供键盘输入
|
* 身高/体重为滚轮选择器(showTextPickerDialog),不提供键盘输入
|
||||||
* 2) 照片键为模拟:V1 骨架 COS 直传未接入,留空自动生成 report_photo/{userId}/{随机}.jpg
|
|
||||||
* 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(本期已生成)→ 页内对话框:可跳最近一次报告(AppStorage 记录,
|
||||||
@@ -17,14 +19,15 @@
|
|||||||
import { router, componentUtils } from '@kit.ArkUI'
|
import { router, componentUtils } from '@kit.ArkUI'
|
||||||
import { ApiClient } from '../core/ApiClient'
|
import { ApiClient } from '../core/ApiClient'
|
||||||
import { ApiError } from '../core/ApiTypes'
|
import { ApiError } from '../core/ApiTypes'
|
||||||
import { AuthStore } from '../core/AuthStore'
|
import { ImageCodec } from '../core/ImageCodec'
|
||||||
|
import { UploadApi } from '../core/UploadApi'
|
||||||
import { Tracker } from '../core/Tracker'
|
import { Tracker } from '../core/Tracker'
|
||||||
import { COLOR_PRIMARY, COLOR_TEXT_SECONDARY } from '../core/Theme'
|
import { COLOR_PRIMARY, COLOR_TEXT_SECONDARY } from '../core/Theme'
|
||||||
import { TaskPollingPanel } from '../components/f3/TaskPollingPanel'
|
import { TaskPollingPanel } from '../components/f3/TaskPollingPanel'
|
||||||
|
import { FacePhotoConsent, FaceConsentStore } from '../components/f2/FacePhotoConsent'
|
||||||
import {
|
import {
|
||||||
ReportCreateResp,
|
ReportCreateResp,
|
||||||
ReportListItemWire,
|
ReportListItemWire,
|
||||||
buildMockPhotoKey,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
randomHex
|
randomHex
|
||||||
} from '../components/f3/ReportTypes'
|
} from '../components/f3/ReportTypes'
|
||||||
@@ -79,7 +82,10 @@ struct ReportCreatePage {
|
|||||||
@State birthday: Date = new Date(2000, 0, 15)
|
@State birthday: Date = new Date(2000, 0, 15)
|
||||||
@State coreGoals: string[] = []
|
@State coreGoals: string[] = []
|
||||||
@State psychStyle: string = ''
|
@State psychStyle: string = ''
|
||||||
@State photoKeyInput: string = ''
|
@State photoUri: string = '' // 已选照片(媒体 URI);空 = 未选,提交必校验
|
||||||
|
@State photoErr: string = ''
|
||||||
|
@State showFaceConsent: boolean = false
|
||||||
|
@State showPhotoSource: boolean = false
|
||||||
@State genderErr: string = ''
|
@State genderErr: string = ''
|
||||||
@State heightErr: string = ''
|
@State heightErr: string = ''
|
||||||
@State weightErr: string = ''
|
@State weightErr: string = ''
|
||||||
@@ -130,6 +136,10 @@ struct ReportCreatePage {
|
|||||||
|
|
||||||
private validateForm(): boolean {
|
private validateForm(): boolean {
|
||||||
let ok = true
|
let ok = true
|
||||||
|
this.photoErr = this.photoUri === '' ? '请上传个人照片' : ''
|
||||||
|
if (this.photoErr !== '') {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
this.genderErr = this.gender === '' ? '请选择性别' : ''
|
this.genderErr = this.gender === '' ? '请选择性别' : ''
|
||||||
if (this.genderErr !== '') {
|
if (this.genderErr !== '') {
|
||||||
ok = false
|
ok = false
|
||||||
@@ -208,6 +218,44 @@ struct ReportCreatePage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 照片入口:人脸属敏感个人信息,上传前须单独同意(CR-01 / FR-ACC-08);
|
||||||
|
* 已授权则直接弹来源选择(相册/拍照)。
|
||||||
|
*/
|
||||||
|
private onPhotoTap(): void {
|
||||||
|
this.photoErr = ''
|
||||||
|
if (!FaceConsentStore.isGranted()) {
|
||||||
|
this.showFaceConsent = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.showPhotoSource = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private onConsentAgree(): void {
|
||||||
|
FaceConsentStore.grant()
|
||||||
|
this.showFaceConsent = false
|
||||||
|
this.showPhotoSource = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private onConsentRefuse(): void {
|
||||||
|
this.showFaceConsent = false
|
||||||
|
this.photoErr = '需同意照片授权后才能上传'
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doPickPhoto(fromCamera: boolean): Promise<void> {
|
||||||
|
this.showPhotoSource = false
|
||||||
|
try {
|
||||||
|
const picked = fromCamera ? await ImageCodec.takePhoto() : await ImageCodec.pickFromGallery()
|
||||||
|
if (picked === undefined) {
|
||||||
|
return // 用户取消:保持原状
|
||||||
|
}
|
||||||
|
this.photoUri = picked.uri
|
||||||
|
this.photoErr = ''
|
||||||
|
} catch (e) {
|
||||||
|
this.photoErr = e instanceof ApiError ? e.message : '读取照片失败,请重试'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onSubmitTap(): void {
|
private onSubmitTap(): void {
|
||||||
if (this.submitting) {
|
if (this.submitting) {
|
||||||
return
|
return
|
||||||
@@ -247,6 +295,9 @@ struct ReportCreatePage {
|
|||||||
|
|
||||||
private missingLabels(): string[] {
|
private missingLabels(): string[] {
|
||||||
const labels: string[] = []
|
const labels: string[] = []
|
||||||
|
if (this.photoErr !== '') {
|
||||||
|
labels.push('个人照片')
|
||||||
|
}
|
||||||
if (this.genderErr !== '') {
|
if (this.genderErr !== '') {
|
||||||
labels.push('性别')
|
labels.push('性别')
|
||||||
}
|
}
|
||||||
@@ -267,7 +318,9 @@ struct ReportCreatePage {
|
|||||||
|
|
||||||
private scrollToFirstError(): void {
|
private scrollToFirstError(): void {
|
||||||
let firstId = ''
|
let firstId = ''
|
||||||
if (this.genderErr !== '') {
|
if (this.photoErr !== '') {
|
||||||
|
firstId = 'field_photo'
|
||||||
|
} else if (this.genderErr !== '') {
|
||||||
firstId = 'field_gender'
|
firstId = 'field_gender'
|
||||||
} else if (this.heightErr !== '' || this.weightErr !== '') {
|
} else if (this.heightErr !== '' || this.weightErr !== '') {
|
||||||
firstId = 'field_height'
|
firstId = 'field_height'
|
||||||
@@ -310,8 +363,21 @@ struct ReportCreatePage {
|
|||||||
'psych_style': this.psychStyle
|
'psych_style': this.psychStyle
|
||||||
}
|
}
|
||||||
const qObj: Object = questionnaire
|
const qObj: Object = questionnaire
|
||||||
const photoInput = this.photoKeyInput.trim()
|
// 先上传照片拿真实 object_key(本机后端磁盘直传),成功后再建报告;
|
||||||
const photoKey = photoInput !== '' ? photoInput : buildMockPhotoKey(AuthStore.getUserId() ?? 'anonymous')
|
// 上传失败不扣报告额度(服务端存在性校验也挡在配额预扣之前)。
|
||||||
|
let photoKey: string = ''
|
||||||
|
try {
|
||||||
|
const jpegBuf = await ImageCodec.toJpegBuffer(this.photoUri)
|
||||||
|
photoKey = await UploadApi.uploadReportPhoto(jpegBuf)
|
||||||
|
} catch (e) {
|
||||||
|
this.submitting = false
|
||||||
|
if (e instanceof ApiError) {
|
||||||
|
this.submitError = e.hint !== undefined ? `${e.message}(${e.hint})` : e.message
|
||||||
|
} else {
|
||||||
|
this.submitError = '照片处理失败,请重试'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
const req: Record<string, Object> = {
|
const req: Record<string, Object> = {
|
||||||
'questionnaire': qObj,
|
'questionnaire': qObj,
|
||||||
'photo_object_key': photoKey,
|
'photo_object_key': photoKey,
|
||||||
@@ -447,7 +513,7 @@ struct ReportCreatePage {
|
|||||||
.fontSize(20)
|
.fontSize(20)
|
||||||
.fontWeight(FontWeight.Bold)
|
.fontWeight(FontWeight.Bold)
|
||||||
.fontColor('#333333')
|
.fontColor('#333333')
|
||||||
Text('填写 6 项基础信息,约 1~3 分钟生成 32 维报告')
|
Text('上传 1 张正面全身照 + 填写 6 项基础信息,约 1~3 分钟生成 32 维报告')
|
||||||
.fontSize(12)
|
.fontSize(12)
|
||||||
.fontColor('#999999')
|
.fontColor('#999999')
|
||||||
}
|
}
|
||||||
@@ -455,6 +521,65 @@ struct ReportCreatePage {
|
|||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
.padding({ top: 4, bottom: 4 })
|
.padding({ top: 4, bottom: 4 })
|
||||||
|
|
||||||
|
Column({ space: 14 }) {
|
||||||
|
Text('个人照片')
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#333333')
|
||||||
|
Column({ space: 8 }) {
|
||||||
|
this.FieldLabel({ label: '正面全身照', err: this.photoErr, required: true })
|
||||||
|
if (this.photoUri === '') {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Text('+')
|
||||||
|
.fontSize(28)
|
||||||
|
.fontColor('#999999')
|
||||||
|
Text('上传个人照片')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#999999')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(140)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
.backgroundColor('#F5F6F8')
|
||||||
|
.borderRadius(10)
|
||||||
|
.border({ width: 1, color: '#DDDDDD', style: BorderStyle.Dashed })
|
||||||
|
.onClick(() => {
|
||||||
|
this.onPhotoTap()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Stack() {
|
||||||
|
Image(this.photoUri)
|
||||||
|
.width('100%')
|
||||||
|
.height(220)
|
||||||
|
.objectFit(ImageFit.Cover)
|
||||||
|
.borderRadius(10)
|
||||||
|
Text('重选')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
|
||||||
|
.backgroundColor('#80000000')
|
||||||
|
.borderRadius(12)
|
||||||
|
.margin(8)
|
||||||
|
.onClick(() => {
|
||||||
|
this.onPhotoTap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.alignContent(Alignment.BottomEnd)
|
||||||
|
}
|
||||||
|
Text('请用本人正面全身照:正面站立、双手自然下垂、纯色背景、自然光,头到脚完整入镜')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor('#999999')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(16)
|
||||||
|
.borderRadius(14)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
.id('field_photo')
|
||||||
|
|
||||||
Column({ space: 14 }) {
|
Column({ space: 14 }) {
|
||||||
Text('基础信息')
|
Text('基础信息')
|
||||||
.fontSize(15)
|
.fontSize(15)
|
||||||
@@ -623,23 +748,6 @@ struct ReportCreatePage {
|
|||||||
.borderRadius(14)
|
.borderRadius(14)
|
||||||
.backgroundColor('#FFFFFF')
|
.backgroundColor('#FFFFFF')
|
||||||
|
|
||||||
// 照片区(#6):上传能力未开放,只做预告;对象键输入框对用户隐藏
|
|
||||||
// (后端本就自动生成 report_photo/{userId}/{随机}.jpg,输入无用户价值)
|
|
||||||
Column({ space: 8 }) {
|
|
||||||
Text('照片(选填)')
|
|
||||||
.fontSize(15)
|
|
||||||
.fontWeight(FontWeight.Medium)
|
|
||||||
.fontColor('#333333')
|
|
||||||
Text('上传照片可以让报告更准,即将开放')
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor('#999999')
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.padding(16)
|
|
||||||
.borderRadius(14)
|
|
||||||
.backgroundColor('#FFFFFF')
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
|
|
||||||
if (this.formSummary !== '') {
|
if (this.formSummary !== '') {
|
||||||
Text(this.formSummary)
|
Text(this.formSummary)
|
||||||
.fontSize(13)
|
.fontSize(13)
|
||||||
@@ -862,6 +970,50 @@ struct ReportCreatePage {
|
|||||||
.backgroundColor('#FFFFFF')
|
.backgroundColor('#FFFFFF')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 照片来源选择(相册/拍照):授权通过后弹出。 */
|
||||||
|
@Builder
|
||||||
|
PhotoSourceDialog() {
|
||||||
|
Column({ space: 14 }) {
|
||||||
|
Text('上传个人照片')
|
||||||
|
.fontSize(17)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#333333')
|
||||||
|
Text('请上传本人正面全身照,照片仅用于本次形象诊断')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#666666')
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
Button('相册选择')
|
||||||
|
.width('100%')
|
||||||
|
.height(42)
|
||||||
|
.fontSize(15)
|
||||||
|
.backgroundColor(COLOR_PRIMARY)
|
||||||
|
.onClick(() => {
|
||||||
|
this.doPickPhoto(false)
|
||||||
|
})
|
||||||
|
Button('拍照')
|
||||||
|
.width('100%')
|
||||||
|
.height(42)
|
||||||
|
.fontSize(15)
|
||||||
|
.backgroundColor(COLOR_PRIMARY)
|
||||||
|
.onClick(() => {
|
||||||
|
this.doPickPhoto(true)
|
||||||
|
})
|
||||||
|
Button('取消')
|
||||||
|
.width('100%')
|
||||||
|
.height(42)
|
||||||
|
.fontSize(14)
|
||||||
|
.backgroundColor('#F3F4F6')
|
||||||
|
.fontColor('#666666')
|
||||||
|
.onClick(() => {
|
||||||
|
this.showPhotoSource = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('82%')
|
||||||
|
.padding(20)
|
||||||
|
.borderRadius(16)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
Stack() {
|
Stack() {
|
||||||
Column() {
|
Column() {
|
||||||
@@ -877,6 +1029,28 @@ struct ReportCreatePage {
|
|||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
|
|
||||||
|
if (this.showFaceConsent) {
|
||||||
|
FacePhotoConsent({
|
||||||
|
usageText: '用途:照片仅用于为你生成形象诊断报告,不用于识别你的身份,也不做其他用途的分析。',
|
||||||
|
onAgree: (): void => {
|
||||||
|
this.onConsentAgree()
|
||||||
|
},
|
||||||
|
onRefuse: (): void => {
|
||||||
|
this.onConsentRefuse()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showPhotoSource) {
|
||||||
|
Column() {
|
||||||
|
this.PhotoSourceDialog()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('#66000000')
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.showDupDialog) {
|
if (this.showDupDialog) {
|
||||||
Column() {
|
Column() {
|
||||||
this.DupDialog()
|
this.DupDialog()
|
||||||
|
|||||||
+1
-1
Submodule meida_backend updated: 7bcbb8d40b...a1d0171fc5
Reference in New Issue
Block a user