diff --git a/design/ui-20261009/README.md b/design/ui-20261009/README.md new file mode 100644 index 0000000..7deb87d --- /dev/null +++ b/design/ui-20261009/README.md @@ -0,0 +1,42 @@ +# UI 外发版 10.09 图片资源 + +来源:用户提供的 `UI外发版10.09.psd`。按 Photoshop 图层原样导出,未重新生成或重绘素材。 + +## 文件位置 + +- `pages/`:10 个画板的原尺寸 PNG,宽 1080px。包含登录、启动、授权弹窗、我的报告、档案填写、拍照弹窗、三视图拍照及长报告。 +- `pages-overview.jpg`:全部页面缩略总览,按 PSD 原始画板顺序排列。 +- `assets-overview.jpg`:独立切图总览。 +- `manifest.json`:页面与切图的尺寸、文件名、图层索引、边界、可见状态及 SHA-256;同时保存完整图层树及文字,便于后续还原。 +- `../../h5/assets/img/ui-20261009/`:新版页面使用的独立图片,与旧版素材分开存放。H5 使用去除原生顶栏的 `*-h5.jpg` 衍生背景。 + +## 前端素材对应 + +| 文件 | 用途 | +| --- | --- | +| `bg-splash.jpg` | 启动页原始照片,不含独立标题、Logo 与按钮 | +| `bg-profile.jpg` / `bg-login.jpg` | 档案页 / 登录页背景,按画板裁剪,保留设计里的叠色 | +| `bg-report.jpg` / `bg-history.jpg` | 长报告 / 我的报告页纯背景,按画板裁剪 | +| `skin-0-dry.png` … `skin-4-mixedoily.png` | 五档肤质插画;也导出了隐藏状态中的变体 | +| `product-25.png` / `product-35.png` / `product-50.png` | 三档产品图,分别取对应产品组中的主视觉图层 | +| `brand-*.png` / `product-badge.png` | 品牌标志与产品标志 | +| `icon-*.png` | 关闭、返回、下拉、相册、切换相机、快门、微信、游客图标 | +| `score-ring-*.png` | 设计稿中的评分环参考素材;实际评分仍应动态绘制 | +| `camera-mask-front.png` / `camera-mask-right.png` / `camera-mask-left.png` | 三步拍照轮廓蒙版,保留画板坐标与透明区域 | +| `camera-guide-front.png` / `camera-guide-right.png` / `camera-guide-left.png` | 对应三步的眼睛与鼻梁虚线定位叠层,与蒙版使用相同画板坐标 | +| `sample-*.png` | 设计稿内检测示意图,仅供视觉参考,不代替用户真实检测图 | + +照片使用高质量渐进 JPG;其他素材使用 PNG 保留透明通道。切图按原图层尺寸输出,不强行放大,不将文字与按钮烘焙进背景。整页预览保留设计稿原有系统状态栏和重复/隐藏图层状态。 + +`sample-lines.png` 来自隐藏的备选示意图层,设计文件内各分类存在重复示意图,文件名不表示图片已经过该项目评分引擎的真实检测。产品图按主视觉独立导出,卡片圆角和裁剪需由前端容器实现。 + +注意:PSD 的画板名「左面」实际文案为第 2 步「右脸出镜」,「右面」实际文案为第 3 步「左脸出镜」。切图按文案中的用户脸部方向命名,清单保存了对应图层路径。 + +## 重新导出 + +```bash +python -m pip install psd-tools Pillow aggdraw +python tools/export_psd_assets.py '/absolute/path/UI外发版10.09.psd' +``` + +重新导出会更新本版本目录内的导出文件,不会覆盖 `h5/assets/img/` 下原有同名线上图片。 diff --git a/design/ui-20261009/assets-overview.jpg b/design/ui-20261009/assets-overview.jpg new file mode 100644 index 0000000..33b1bfe Binary files /dev/null and b/design/ui-20261009/assets-overview.jpg differ diff --git a/design/ui-20261009/audit-current/01-splash.png b/design/ui-20261009/audit-current/01-splash.png new file mode 100644 index 0000000..8bee610 Binary files /dev/null and b/design/ui-20261009/audit-current/01-splash.png differ diff --git a/design/ui-20261009/audit-current/02-privacy.png b/design/ui-20261009/audit-current/02-privacy.png new file mode 100644 index 0000000..c5432e6 Binary files /dev/null and b/design/ui-20261009/audit-current/02-privacy.png differ diff --git a/design/ui-20261009/audit-current/03-profile.png b/design/ui-20261009/audit-current/03-profile.png new file mode 100644 index 0000000..786455f Binary files /dev/null and b/design/ui-20261009/audit-current/03-profile.png differ diff --git a/design/ui-20261009/audit-current/04-guide.png b/design/ui-20261009/audit-current/04-guide.png new file mode 100644 index 0000000..e4fec45 Binary files /dev/null and b/design/ui-20261009/audit-current/04-guide.png differ diff --git a/design/ui-20261009/audit-current/05-camera-front.png b/design/ui-20261009/audit-current/05-camera-front.png new file mode 100644 index 0000000..86e3926 Binary files /dev/null and b/design/ui-20261009/audit-current/05-camera-front.png differ diff --git a/design/ui-20261009/audit-current/06-camera-right.png b/design/ui-20261009/audit-current/06-camera-right.png new file mode 100644 index 0000000..a893901 Binary files /dev/null and b/design/ui-20261009/audit-current/06-camera-right.png differ diff --git a/design/ui-20261009/audit-current/07-camera-left.png b/design/ui-20261009/audit-current/07-camera-left.png new file mode 100644 index 0000000..df144e1 Binary files /dev/null and b/design/ui-20261009/audit-current/07-camera-left.png differ diff --git a/design/ui-20261009/audit-current/08-report-top.png b/design/ui-20261009/audit-current/08-report-top.png new file mode 100644 index 0000000..6671a24 Binary files /dev/null and b/design/ui-20261009/audit-current/08-report-top.png differ diff --git a/design/ui-20261009/audit-current/09-report-tab-0.png b/design/ui-20261009/audit-current/09-report-tab-0.png new file mode 100644 index 0000000..19f4f6e Binary files /dev/null and b/design/ui-20261009/audit-current/09-report-tab-0.png differ diff --git a/design/ui-20261009/audit-current/09-report-tab-1.png b/design/ui-20261009/audit-current/09-report-tab-1.png new file mode 100644 index 0000000..307c357 Binary files /dev/null and b/design/ui-20261009/audit-current/09-report-tab-1.png differ diff --git a/design/ui-20261009/audit-current/09-report-tab-2.png b/design/ui-20261009/audit-current/09-report-tab-2.png new file mode 100644 index 0000000..a51aecf Binary files /dev/null and b/design/ui-20261009/audit-current/09-report-tab-2.png differ diff --git a/design/ui-20261009/audit-current/10-report-full.png b/design/ui-20261009/audit-current/10-report-full.png new file mode 100644 index 0000000..eb60ea3 Binary files /dev/null and b/design/ui-20261009/audit-current/10-report-full.png differ diff --git a/design/ui-20261009/audit-current/comparison-flow.jpg b/design/ui-20261009/audit-current/comparison-flow.jpg new file mode 100644 index 0000000..a5cd99c Binary files /dev/null and b/design/ui-20261009/audit-current/comparison-flow.jpg differ diff --git a/design/ui-20261009/audit-current/comparison-report.jpg b/design/ui-20261009/audit-current/comparison-report.jpg new file mode 100644 index 0000000..dbe2d19 Binary files /dev/null and b/design/ui-20261009/audit-current/comparison-report.jpg differ diff --git a/design/ui-20261009/audit-current/evidence.json b/design/ui-20261009/audit-current/evidence.json new file mode 100644 index 0000000..3d92b7e --- /dev/null +++ b/design/ui-20261009/audit-current/evidence.json @@ -0,0 +1,780 @@ +{ + "viewport": { + "width": 375, + "height": 726 + }, + "pages": { + "01-splash": { + "screen": "s-splash", + "text": "Life in Full\n为更好的生命表现\n\n肌肤状态检测\n开始检测", + "overlays": [], + "rects": [ + { + "class": "splash-slogan", + "rect": { + "x": 0, + "y": 77.671875, + "width": 375, + "height": 63.46875, + "top": 77.671875, + "right": 375, + "bottom": 141.140625, + "left": 0 + } + }, + { + "class": "splash-bottom", + "rect": { + "x": 0, + "y": 551.984375, + "width": 375, + "height": 84, + "top": 551.984375, + "right": 375, + "bottom": 635.984375, + "left": 0 + } + } + ] + }, + "02-privacy": { + "screen": "s-splash", + "text": "Life in Full\n为更好的生命表现\n\n肌肤状态检测\n开始检测", + "overlays": [ + { + "id": "p-privacy", + "text": "欢迎体验1'me\n肌肤状态评估服务!\n\n我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。\n\n我已阅读并同意相关隐私协议\n继续" + } + ], + "rects": [ + { + "class": "splash-slogan", + "rect": { + "x": 0, + "y": 77.671875, + "width": 375, + "height": 63.46875, + "top": 77.671875, + "right": 375, + "bottom": 141.140625, + "left": 0 + } + }, + { + "class": "splash-bottom", + "rect": { + "x": 0, + "y": 551.984375, + "width": 375, + "height": 84, + "top": 551.984375, + "right": 375, + "bottom": 635.984375, + "left": 0 + } + } + ] + }, + "03-profile": { + "screen": "s-profile", + "text": "您的肌肤状态\n您的出生年月\n您的性别\n您的所在地区\n您的日常肤质\n偏干\n偏油\n水油平衡\n干性肌肤\n\n全脸干燥,容易紧绷、缺乏光泽\n\n混合偏干肌肤\n\n两颊偏干,T区油脂较少或偶尔出油\n\n中性肌肤\n\n水油平衡,肤感舒适,无明显干燥出油\n\n油性肌肤\n\n全脸易出油,容易泛油光、毛孔明显\n\n混合偏油肌肤\n\nT区易出油,两颊相对清爽或轻微干燥\n\n提交", + "overlays": [], + "rects": [ + { + "class": "profile-body", + "rect": { + "x": 0, + "y": 0, + "width": 375, + "height": 726, + "top": 0, + "right": 375, + "bottom": 726, + "left": 0 + } + }, + { + "class": "page-title", + "rect": { + "x": 64, + "y": 34, + "width": 247, + "height": 31, + "top": 34, + "right": 311, + "bottom": 65, + "left": 64 + } + } + ] + }, + "04-guide": { + "screen": "s-profile", + "text": "您的肌肤状态\n您的出生年月\n1996年1月\n您的性别\n女\n您的所在地区\n北京市\n您的日常肤质\n偏干\n偏油\n水油平衡\n干性肌肤\n\n全脸干燥,容易紧绷、缺乏光泽\n\n混合偏干肌肤\n\n两颊偏干,T区油脂较少或偶尔出油\n\n中性肌肤\n\n水油平衡,肤感舒适,无明显干燥出油\n\n油性肌肤\n\n全脸易出油,容易泛油光、毛孔明显\n\n混合偏油肌肤\n\nT区易出油,两颊相对清爽或轻微干燥\n\n提交", + "overlays": [ + { + "id": "p-photoguide", + "text": "Life in Full\n为更好的生命表现\n\n请拍摄或上传\n正面及左右侧面照片\n开始" + } + ], + "rects": [ + { + "class": "profile-body", + "rect": { + "x": 0, + "y": 0, + "width": 375, + "height": 726, + "top": 0, + "right": 375, + "bottom": 726, + "left": 0 + } + }, + { + "class": "page-title", + "rect": { + "x": 64, + "y": 34, + "width": 247, + "height": 31, + "top": 34, + "right": 311, + "bottom": 65, + "left": 64 + } + } + ] + }, + "05-camera-front": { + "screen": "s-cam", + "text": "1/3\n请正对屏幕\n\n检测引擎加载中…\n\n识别不到?请正对镜头,并到光线充足处\n\n正脸拍摄完成\n准备拍摄下一张\n正脸居中\n取下眼镜\n光线充足", + "overlays": [], + "rects": [ + { + "class": "cam-step", + "rect": { + "x": 0, + "y": 51.53125, + "width": 375, + "height": 64.5, + "top": 51.53125, + "right": 375, + "bottom": 116.03125, + "left": 0 + } + }, + { + "class": "cam-guide", + "rect": { + "x": 0, + "y": 542.390625, + "width": 375, + "height": 21, + "top": 542.390625, + "right": 375, + "bottom": 563.390625, + "left": 0 + } + }, + { + "class": "cam-controls", + "rect": { + "x": 0, + "y": 626.25, + "width": 375, + "height": 62, + "top": 626.25, + "right": 375, + "bottom": 688.25, + "left": 0 + } + } + ] + }, + "06-camera-right": { + "screen": "s-cam", + "text": "1/3\n请正对屏幕\n\n检测引擎加载中…\n\n识别不到?请正对镜头,并到光线充足处\n\n正脸拍摄完成\n准备拍摄下一张\n正脸居中\n取下眼镜\n光线充足", + "overlays": [], + "rects": [ + { + "class": "cam-step", + "rect": { + "x": 0, + "y": 51.53125, + "width": 375, + "height": 64.5, + "top": 51.53125, + "right": 375, + "bottom": 116.03125, + "left": 0 + } + }, + { + "class": "cam-guide", + "rect": { + "x": 0, + "y": 542.390625, + "width": 375, + "height": 21, + "top": 542.390625, + "right": 375, + "bottom": 563.390625, + "left": 0 + } + }, + { + "class": "cam-controls", + "rect": { + "x": 0, + "y": 626.25, + "width": 375, + "height": 62, + "top": 626.25, + "right": 375, + "bottom": 688.25, + "left": 0 + } + } + ] + }, + "07-camera-left": { + "screen": "s-cam", + "text": "2/3\n请右脸出镜\n\n检测引擎加载中…\n\n识别不到?请正对镜头,并到光线充足处\n\n正脸拍摄完成\n下一张:右脸出镜\n请向左转头\n右脸居中\n取下眼镜\n光线充足", + "overlays": [], + "rects": [ + { + "class": "cam-step", + "rect": { + "x": 0, + "y": 51.53125, + "width": 375, + "height": 64.5, + "top": 51.53125, + "right": 375, + "bottom": 116.03125, + "left": 0 + } + }, + { + "class": "cam-guide", + "rect": { + "x": 0, + "y": 542.390625, + "width": 375, + "height": 21, + "top": 542.390625, + "right": 375, + "bottom": 563.390625, + "left": 0 + } + }, + { + "class": "cam-controls", + "rect": { + "x": 0, + "y": 626.25, + "width": 375, + "height": 62, + "top": 626.25, + "right": 375, + "bottom": 688.25, + "left": 0 + } + } + ] + }, + "08-report-top": { + "screen": "s-report", + "text": "您的肌肤状态表现\n70分\n综合评分\n肌肤年龄\n27岁\n比实际年龄年轻1岁\n紧塑力\n7.7\n优先关注\n活力感\n8.7\n稳定力\n9.1\n近期优先关注:紧塑力\n紧塑提拉力,轮廓上扬不松垮。\n紧塑力\n活力感\n稳定力\n轮廓\n\n下颌线角度左104.54°/右103.75°(标准区间116–120°,双侧均偏离标准,提示下颌缘支撑力下降);眼袋1级、黑眼圈1级;中面部支撑与轮廓清晰度处于预警水平。建议加强轮廓紧致护理:重点针对下颌线与下颌缘区域使用紧致类精华,配合由下向上的提拉按摩手法,同时加强眼周支撑护理,将紧致防线前置。\n\n皱纹\n\n全脸皱纹159条、细纹150条,最差区域为左嘴角纹(分区分77,最高严重度3级),部分区域已现静态纹趋势。建议针对眼周、法令纹等高发区使用视黄醇/胜肽类淡纹精华,遏制静态纹转化。\n\n*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。\n*本次检测仅代表当前肌肤状态参考。\n\n护理建议\n\n1、你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。\n\n2、加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。\n\n3、加强细腻度管理,避免过度清洁破坏角质屏障,重点关注毛孔显著区域,配合低浓度酸类(如水杨酸)周期护理与柔润精华。\n\n4、建议搭配使用35+支撑精华:专为轻熟肌打造,提升弹韧表现、维持面部轮廓清晰度,改善疲态并延缓衰老速度。\n\n5、保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳\n\n好状态,是内外同调的长期节奏。\n\n精华推荐\n\n35+支撑精华\n\n推荐依据:皱纹得分偏低\n\n感受高光时刻\n从日常的好状态开始\n以我自在有势挺括\n双仓精华液\n\n参透皮下根源 28天校准皮肤状态\n\n了解更多", + "overlays": [], + "rects": [ + { + "class": "report-title", + "rect": { + "x": 18, + "y": 0, + "width": 339, + "height": 86, + "top": 0, + "right": 357, + "bottom": 86, + "left": 18 + } + }, + { + "class": "card card-summary", + "rect": { + "x": 18, + "y": 86, + "width": 339, + "height": 140, + "top": 86, + "right": 357, + "bottom": 226, + "left": 18 + } + }, + { + "class": "card card-dims", + "rect": { + "x": 18, + "y": 238, + "width": 339, + "height": 201.1875, + "top": 238, + "right": 357, + "bottom": 439.1875, + "left": 18 + } + }, + { + "class": "card card-detail", + "rect": { + "x": 18, + "y": 451.1875, + "width": 339, + "height": 334.96875, + "top": 451.1875, + "right": 357, + "bottom": 786.15625, + "left": 18 + } + }, + { + "class": "card card-care", + "rect": { + "x": 18, + "y": 852.15625, + "width": 339, + "height": 510, + "top": 852.15625, + "right": 357, + "bottom": 1362.15625, + "left": 18 + } + }, + { + "class": "card card-essence", + "rect": { + "x": 18, + "y": 1374.15625, + "width": 339, + "height": 119.5, + "top": 1374.15625, + "right": 357, + "bottom": 1493.65625, + "left": 18 + } + }, + { + "class": "card card-product", + "rect": { + "x": 18, + "y": 1608.34375, + "width": 339, + "height": 215.671875, + "top": 1608.34375, + "right": 357, + "bottom": 1824.015625, + "left": 18 + } + } + ] + }, + "09-report-tab-0": { + "screen": "s-report", + "text": "您的肌肤状态表现\n70分\n综合评分\n肌肤年龄\n27岁\n比实际年龄年轻1岁\n紧塑力\n7.7\n优先关注\n活力感\n8.7\n稳定力\n9.1\n近期优先关注:紧塑力\n紧塑提拉力,轮廓上扬不松垮。\n紧塑力\n活力感\n稳定力\n轮廓\n\n下颌线角度左104.54°/右103.75°(标准区间116–120°,双侧均偏离标准,提示下颌缘支撑力下降);眼袋1级、黑眼圈1级;中面部支撑与轮廓清晰度处于预警水平。建议加强轮廓紧致护理:重点针对下颌线与下颌缘区域使用紧致类精华,配合由下向上的提拉按摩手法,同时加强眼周支撑护理,将紧致防线前置。\n\n皱纹\n\n全脸皱纹159条、细纹150条,最差区域为左嘴角纹(分区分77,最高严重度3级),部分区域已现静态纹趋势。建议针对眼周、法令纹等高发区使用视黄醇/胜肽类淡纹精华,遏制静态纹转化。\n\n*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。\n*本次检测仅代表当前肌肤状态参考。\n\n护理建议\n\n1、你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。\n\n2、加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。\n\n3、加强细腻度管理,避免过度清洁破坏角质屏障,重点关注毛孔显著区域,配合低浓度酸类(如水杨酸)周期护理与柔润精华。\n\n4、建议搭配使用35+支撑精华:专为轻熟肌打造,提升弹韧表现、维持面部轮廓清晰度,改善疲态并延缓衰老速度。\n\n5、保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳\n\n好状态,是内外同调的长期节奏。\n\n精华推荐\n\n35+支撑精华\n\n推荐依据:皱纹得分偏低\n\n感受高光时刻\n从日常的好状态开始\n以我自在有势挺括\n双仓精华液\n\n参透皮下根源 28天校准皮肤状态\n\n了解更多", + "overlays": [], + "rects": [ + { + "class": "report-title", + "rect": { + "x": 18, + "y": -60, + "width": 339, + "height": 86, + "top": -60, + "right": 357, + "bottom": 26, + "left": 18 + } + }, + { + "class": "card card-summary", + "rect": { + "x": 18, + "y": 26, + "width": 339, + "height": 140, + "top": 26, + "right": 357, + "bottom": 166, + "left": 18 + } + }, + { + "class": "card card-dims", + "rect": { + "x": 18, + "y": 178, + "width": 339, + "height": 201.1875, + "top": 178, + "right": 357, + "bottom": 379.1875, + "left": 18 + } + }, + { + "class": "card card-detail", + "rect": { + "x": 18, + "y": 391.1875, + "width": 339, + "height": 334.96875, + "top": 391.1875, + "right": 357, + "bottom": 726.15625, + "left": 18 + } + }, + { + "class": "card card-care", + "rect": { + "x": 18, + "y": 792.15625, + "width": 339, + "height": 510, + "top": 792.15625, + "right": 357, + "bottom": 1302.15625, + "left": 18 + } + }, + { + "class": "card card-essence", + "rect": { + "x": 18, + "y": 1314.15625, + "width": 339, + "height": 119.5, + "top": 1314.15625, + "right": 357, + "bottom": 1433.65625, + "left": 18 + } + }, + { + "class": "card card-product", + "rect": { + "x": 18, + "y": 1548.34375, + "width": 339, + "height": 215.671875, + "top": 1548.34375, + "right": 357, + "bottom": 1764.015625, + "left": 18 + } + } + ] + }, + "09-report-tab-1": { + "screen": "s-report", + "text": "您的肌肤状态表现\n70分\n综合评分\n肌肤年龄\n27岁\n比实际年龄年轻1岁\n紧塑力\n7.7\n优先关注\n活力感\n8.7\n稳定力\n9.1\n近期优先关注:紧塑力\n紧塑提拉力,轮廓上扬不松垮。\n紧塑力\n活力感\n稳定力\n光滑\n\n皮肤表面平整度较低,最差毛孔区位于额头(分区毛孔分32),粗大毛孔1041颗、黑头113颗,T区出油面积57%、左右脸颊20%/2%,纹理分仅82,整体细腻度不足。建议避免过度清洁,重点关注额头,配合低浓度酸类周期护理与柔润精华。\n\n肤色水润\n\n肤色ITA 58.6(极白,HA 56.2),色素分92,整体稳定但存在局部暗沉倾向(未见明显色斑标记,检出褐色斑37处);水分分78,缺水面积19.8%、严重度22。建议加强抗氧化与光泽管理,做好基础防晒与补水。\n\n*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。\n*本次检测仅代表当前肌肤状态参考。\n\n护理建议\n\n1、你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。\n\n2、加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。\n\n3、加强细腻度管理,避免过度清洁破坏角质屏障,重点关注毛孔显著区域,配合低浓度酸类(如水杨酸)周期护理与柔润精华。\n\n4、建议搭配使用35+支撑精华:专为轻熟肌打造,提升弹韧表现、维持面部轮廓清晰度,改善疲态并延缓衰老速度。\n\n5、保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳\n\n好状态,是内外同调的长期节奏。\n\n精华推荐\n\n35+支撑精华\n\n推荐依据:皱纹得分偏低\n\n感受高光时刻\n从日常的好状态开始\n以我自在有势挺括\n双仓精华液\n\n参透皮下根源 28天校准皮肤状态\n\n了解更多", + "overlays": [], + "rects": [ + { + "class": "report-title", + "rect": { + "x": 18, + "y": -60, + "width": 339, + "height": 86, + "top": -60, + "right": 357, + "bottom": 26, + "left": 18 + } + }, + { + "class": "card card-summary", + "rect": { + "x": 18, + "y": 26, + "width": 339, + "height": 140, + "top": 26, + "right": 357, + "bottom": 166, + "left": 18 + } + }, + { + "class": "card card-dims", + "rect": { + "x": 18, + "y": 178, + "width": 339, + "height": 201.1875, + "top": 178, + "right": 357, + "bottom": 379.1875, + "left": 18 + } + }, + { + "class": "card card-detail", + "rect": { + "x": 18, + "y": 391.1875, + "width": 339, + "height": 306.53125, + "top": 391.1875, + "right": 357, + "bottom": 697.71875, + "left": 18 + } + }, + { + "class": "card card-care", + "rect": { + "x": 18, + "y": 763.71875, + "width": 339, + "height": 510, + "top": 763.71875, + "right": 357, + "bottom": 1273.71875, + "left": 18 + } + }, + { + "class": "card card-essence", + "rect": { + "x": 18, + "y": 1285.71875, + "width": 339, + "height": 119.5, + "top": 1285.71875, + "right": 357, + "bottom": 1405.21875, + "left": 18 + } + }, + { + "class": "card card-product", + "rect": { + "x": 18, + "y": 1519.90625, + "width": 339, + "height": 215.671875, + "top": 1519.90625, + "right": 357, + "bottom": 1735.578125, + "left": 18 + } + } + ] + }, + "09-report-tab-2": { + "screen": "s-report", + "text": "您的肌肤状态表现\n70分\n综合评分\n肌肤年龄\n27岁\n比实际年龄年轻1岁\n紧塑力\n7.7\n优先关注\n活力感\n8.7\n稳定力\n9.1\n近期优先关注:紧塑力\n紧塑提拉力,轮廓上扬不松垮。\n紧塑力\n活力感\n稳定力\n敏感\n\n泛红面积0.6%,红区分99,屏障状态稳定,未见明显敏感信号。建议维持现有温和护理,避免过度去角质。\n\n痘痘\n\n检出9处皮损(闭口粉刺6处、丘疹3处)。建议尽快进行针对性抗痘护理,避免挤压与刺激,必要时寻求皮肤科专业处理。\n\n*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。\n*本次检测仅代表当前肌肤状态参考。\n\n护理建议\n\n1、你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。\n\n2、加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。\n\n3、加强细腻度管理,避免过度清洁破坏角质屏障,重点关注毛孔显著区域,配合低浓度酸类(如水杨酸)周期护理与柔润精华。\n\n4、建议搭配使用35+支撑精华:专为轻熟肌打造,提升弹韧表现、维持面部轮廓清晰度,改善疲态并延缓衰老速度。\n\n5、保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳\n\n好状态,是内外同调的长期节奏。\n\n精华推荐\n\n35+支撑精华\n\n推荐依据:皱纹得分偏低\n\n感受高光时刻\n从日常的好状态开始\n以我自在有势挺括\n双仓精华液\n\n参透皮下根源 28天校准皮肤状态\n\n了解更多", + "overlays": [], + "rects": [ + { + "class": "report-title", + "rect": { + "x": 18, + "y": -60, + "width": 339, + "height": 86, + "top": -60, + "right": 357, + "bottom": 26, + "left": 18 + } + }, + { + "class": "card card-summary", + "rect": { + "x": 18, + "y": 26, + "width": 339, + "height": 140, + "top": 26, + "right": 357, + "bottom": 166, + "left": 18 + } + }, + { + "class": "card card-dims", + "rect": { + "x": 18, + "y": 178, + "width": 339, + "height": 201.1875, + "top": 178, + "right": 357, + "bottom": 379.1875, + "left": 18 + } + }, + { + "class": "card card-detail", + "rect": { + "x": 18, + "y": 391.1875, + "width": 339, + "height": 303, + "top": 391.1875, + "right": 357, + "bottom": 694.1875, + "left": 18 + } + }, + { + "class": "card card-care", + "rect": { + "x": 18, + "y": 760.1875, + "width": 339, + "height": 510, + "top": 760.1875, + "right": 357, + "bottom": 1270.1875, + "left": 18 + } + }, + { + "class": "card card-essence", + "rect": { + "x": 18, + "y": 1282.1875, + "width": 339, + "height": 119.5, + "top": 1282.1875, + "right": 357, + "bottom": 1401.6875, + "left": 18 + } + }, + { + "class": "card card-product", + "rect": { + "x": 18, + "y": 1516.375, + "width": 339, + "height": 215.671875, + "top": 1516.375, + "right": 357, + "bottom": 1732.046875, + "left": 18 + } + } + ] + }, + "splash-375x844": { + "screen": "s-splash", + "text": "Life in Full\n为更好的生命表现\n\n肌肤状态检测\n开始检测", + "overlays": [], + "rects": [ + { + "class": "splash-slogan", + "rect": { + "x": 0, + "y": 90.296875, + "width": 375, + "height": 63.46875, + "top": 90.296875, + "right": 375, + "bottom": 153.765625, + "left": 0 + } + }, + { + "class": "splash-bottom", + "rect": { + "x": 0, + "y": 655.359375, + "width": 375, + "height": 84, + "top": 655.359375, + "right": 375, + "bottom": 739.359375, + "left": 0 + } + } + ] + }, + "splash-390x844": { + "screen": "s-splash", + "text": "Life in Full\n为更好的生命表现\n\n肌肤状态检测\n开始检测", + "overlays": [], + "rects": [ + { + "class": "splash-slogan", + "rect": { + "x": 0, + "y": 90.296875, + "width": 390, + "height": 66, + "top": 90.296875, + "right": 390, + "bottom": 156.296875, + "left": 0 + } + }, + { + "class": "splash-bottom", + "rect": { + "x": 0, + "y": 652.265625, + "width": 390, + "height": 87.09375, + "top": 652.265625, + "right": 390, + "bottom": 739.359375, + "left": 0 + } + } + ] + }, + "splash-414x844": { + "screen": "s-splash", + "text": "Life in Full\n为更好的生命表现\n\n肌肤状态检测\n开始检测", + "overlays": [], + "rects": [ + { + "class": "splash-slogan", + "rect": { + "x": 0, + "y": 90.296875, + "width": 414, + "height": 70.0625, + "top": 90.296875, + "right": 414, + "bottom": 160.359375, + "left": 0 + } + }, + { + "class": "splash-bottom", + "rect": { + "x": 0, + "y": 645.859375, + "width": 414, + "height": 93.5, + "top": 645.859375, + "right": 414, + "bottom": 739.359375, + "left": 0 + } + } + ] + } + }, + "errors": [] +} \ No newline at end of file diff --git a/design/ui-20261009/audit-current/splash-375x844.png b/design/ui-20261009/audit-current/splash-375x844.png new file mode 100644 index 0000000..7fbfd24 Binary files /dev/null and b/design/ui-20261009/audit-current/splash-375x844.png differ diff --git a/design/ui-20261009/audit-current/splash-390x844.png b/design/ui-20261009/audit-current/splash-390x844.png new file mode 100644 index 0000000..7170122 Binary files /dev/null and b/design/ui-20261009/audit-current/splash-390x844.png differ diff --git a/design/ui-20261009/audit-current/splash-414x844.png b/design/ui-20261009/audit-current/splash-414x844.png new file mode 100644 index 0000000..c90fcce Binary files /dev/null and b/design/ui-20261009/audit-current/splash-414x844.png differ diff --git a/design/ui-20261009/decisions/01-header.jpg b/design/ui-20261009/decisions/01-header.jpg new file mode 100644 index 0000000..d860fcb Binary files /dev/null and b/design/ui-20261009/decisions/01-header.jpg differ diff --git a/design/ui-20261009/decisions/02-login.jpg b/design/ui-20261009/decisions/02-login.jpg new file mode 100644 index 0000000..6335d86 Binary files /dev/null and b/design/ui-20261009/decisions/02-login.jpg differ diff --git a/design/ui-20261009/decisions/03-history.jpg b/design/ui-20261009/decisions/03-history.jpg new file mode 100644 index 0000000..de4fe80 Binary files /dev/null and b/design/ui-20261009/decisions/03-history.jpg differ diff --git a/design/ui-20261009/decisions/04-score.jpg b/design/ui-20261009/decisions/04-score.jpg new file mode 100644 index 0000000..82d5a0c Binary files /dev/null and b/design/ui-20261009/decisions/04-score.jpg differ diff --git a/design/ui-20261009/decisions/05-tabs.jpg b/design/ui-20261009/decisions/05-tabs.jpg new file mode 100644 index 0000000..8a05fa1 Binary files /dev/null and b/design/ui-20261009/decisions/05-tabs.jpg differ diff --git a/design/ui-20261009/decisions/06-content.jpg b/design/ui-20261009/decisions/06-content.jpg new file mode 100644 index 0000000..06046ec Binary files /dev/null and b/design/ui-20261009/decisions/06-content.jpg differ diff --git a/design/ui-20261009/decisions/07-products.jpg b/design/ui-20261009/decisions/07-products.jpg new file mode 100644 index 0000000..eb0bdc6 Binary files /dev/null and b/design/ui-20261009/decisions/07-products.jpg differ diff --git a/design/ui-20261009/decisions/08-camera.jpg b/design/ui-20261009/decisions/08-camera.jpg new file mode 100644 index 0000000..afea522 Binary files /dev/null and b/design/ui-20261009/decisions/08-camera.jpg differ diff --git a/design/ui-20261009/decisions/09-font.jpg b/design/ui-20261009/decisions/09-font.jpg new file mode 100644 index 0000000..26080f0 Binary files /dev/null and b/design/ui-20261009/decisions/09-font.jpg differ diff --git a/design/ui-20261009/decisions/10-proofreading.jpg b/design/ui-20261009/decisions/10-proofreading.jpg new file mode 100644 index 0000000..9e4c84f Binary files /dev/null and b/design/ui-20261009/decisions/10-proofreading.jpg differ diff --git a/design/ui-20261009/manifest.json b/design/ui-20261009/manifest.json new file mode 100644 index 0000000..714f7f4 --- /dev/null +++ b/design/ui-20261009/manifest.json @@ -0,0 +1,6475 @@ +{ + "source": "UI外发版10.09.psd", + "document_size": [ + 6150, + 16411 + ], + "pages": [ + { + "file": "design/ui-20261009/pages/01-启动页.png", + "name": "启动页", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 1237, + 0, + 2317, + 2339 + ] + }, + { + "file": "design/ui-20261009/pages/02-启动页弹窗.png", + "name": "启动页弹窗", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 2474, + 0, + 3554, + 2339 + ] + }, + { + "file": "design/ui-20261009/pages/03-我的报告.png", + "name": "我的报告", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 3654, + 0, + 4734, + 2339 + ] + }, + { + "file": "design/ui-20261009/pages/04-档案填写.png", + "name": "档案填写", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 1237, + 2626, + 2317, + 4965 + ] + }, + { + "file": "design/ui-20261009/pages/05-拍照弹窗.png", + "name": "拍照弹窗", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 1237, + 5252, + 2317, + 7591 + ] + }, + { + "file": "design/ui-20261009/pages/06-正面.png", + "name": "正面", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 2522, + 5252, + 3602, + 7591 + ] + }, + { + "file": "design/ui-20261009/pages/07-左面.png", + "name": "左面", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 3796, + 5252, + 4876, + 7591 + ] + }, + { + "file": "design/ui-20261009/pages/08-右面.png", + "name": "右面", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 5070, + 5252, + 6150, + 7591 + ] + }, + { + "file": "design/ui-20261009/pages/09-报告页.png", + "name": "报告页", + "size": [ + 1080, + 7765 + ], + "bbox": [ + 1237, + 8646, + 2317, + 16411 + ] + }, + { + "file": "design/ui-20261009/pages/10-登录.png", + "name": "登录", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 0, + 0, + 1080, + 2339 + ] + } + ], + "assets": [ + { + "file": "h5/assets/img/ui-20261009/bg-splash.jpg", + "source_layer": "0/0", + "layer_name": "背景", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 1237, + 0, + 2317, + 2339 + ], + "bytes": 726591, + "sha256": "99d175ca53070007b71ac5637cf2398a27d9819b9823929a344b6e99c71a612b", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/bg-profile.jpg", + "source_layer": "3/0/0", + "layer_name": "背景", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 1237, + 2626, + 2317, + 4965 + ], + "bytes": 420618, + "sha256": "50cb52ab24d120c1dba02a685ec6e2366ead91b1f5f2a90f1df2fe24eabb557d", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/bg-login.jpg", + "source_layer": "9/0/0", + "layer_name": "背景", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 0, + 0, + 1080, + 2339 + ], + "bytes": 420618, + "sha256": "50cb52ab24d120c1dba02a685ec6e2366ead91b1f5f2a90f1df2fe24eabb557d", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/bg-report.jpg", + "source_layer": "8/0", + "layer_name": "背景", + "size": [ + 1080, + 7765 + ], + "bbox": [ + 1237, + 8646, + 2317, + 16411 + ], + "bytes": 50162, + "sha256": "007d7be20cf7d1d1b19a68fe02d8c1b4855c5d5b0f167674210e681ba734bc5b", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/bg-history.jpg", + "source_layer": "2/0", + "layer_name": "图层 15", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 3654, + 0, + 4734, + 2339 + ], + "bytes": 15536, + "sha256": "7d1f47dcde4a625d8d282298faf0ab8546b815007dfcad033d729f701e7eab8d", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/brand-logo.png", + "source_layer": "0/1/0", + "layer_name": "LOGO", + "size": [ + 132, + 59 + ], + "bbox": [ + 1515, + 1861, + 1647, + 1920 + ], + "bytes": 1627, + "sha256": "adf0a0c8bd0a16c457d88d7e993e8d39b0b0fa5a8b4d7fea4b89449e9d859a8d", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/brand-login.png", + "source_layer": "9/7/1", + "layer_name": "图层 550", + "size": [ + 419, + 89 + ], + "bbox": [ + 330, + 513, + 749, + 602 + ], + "bytes": 7882, + "sha256": "b978f081165d2b88115705ec94b4ded58789baf63ed9f453c6158580a7bd8a67", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-close.png", + "source_layer": "1/6", + "layer_name": "关闭", + "size": [ + 51, + 50 + ], + "bbox": [ + 3356, + 770, + 3407, + 820 + ], + "bytes": 544, + "sha256": "577bd0b2595284c121279b31cc92bf3e6226a652259c3849186074275f4e7983", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-dropdown.png", + "source_layer": "3/1/2", + "layer_name": "下拉", + "size": [ + 33, + 18 + ], + "bbox": [ + 2060, + 3447, + 2093, + 3465 + ], + "bytes": 303, + "sha256": "bdcf4c8f4774d41f0b3b9b500c8e3b6b60175e9438d7ba475f580e26940ba758", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-back.png", + "source_layer": "3/6/0", + "layer_name": "下拉", + "size": [ + 18, + 33 + ], + "bbox": [ + 1292, + 2802, + 1310, + 2835 + ], + "bytes": 331, + "sha256": "b6eaa69c75675fd22819b96e8baf2b89b76a160656756f48985fc6217a02ceb4", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-album.png", + "source_layer": "5/1/1", + "layer_name": "相册", + "size": [ + 82, + 70 + ], + "bbox": [ + 2684, + 7351, + 2766, + 7421 + ], + "bytes": 1134, + "sha256": "aa71bc5ca610ad2d4c169b0740dfab781ed47c66ed2a8b5714c76b0e52f0d9e3", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-switch-camera.png", + "source_layer": "5/1/0", + "layer_name": "left", + "size": [ + 80, + 79 + ], + "bbox": [ + 3365, + 7346, + 3445, + 7425 + ], + "bytes": 1653, + "sha256": "95ef236b7339bc6c90ffde0f95499778df054ca4821f00186899135a871ce314", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-wechat.png", + "source_layer": "9/7/6", + "layer_name": "9d8063d8012f839a7c5ab1f0366ca5d7717c9f9a905-seG7pC_fw1200webp", + "size": [ + 65, + 52 + ], + "bbox": [ + 380, + 1724, + 445, + 1776 + ], + "bytes": 1414, + "sha256": "33df09092c4f727274a979c265481bb2a9a3acade62490a26db349639be41d03", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-guest.png", + "source_layer": "9/7/7", + "layer_name": "52e66a7d0a88f23305ddc3618535c77ee3b79bbe30c-n21Vnn_fw1200webp", + "size": [ + 63, + 50 + ], + "bbox": [ + 381, + 1868, + 444, + 1918 + ], + "bytes": 1104, + "sha256": "76df2058907a4ca91fd5e90f17ecde1df5ea68cff4e945f413c7b45961f0aeac", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/icon-shutter.png", + "source_layer": "5/1/2", + "layer_name": "拍照", + "size": [ + 178, + 178 + ], + "bbox": [ + 2973, + 7297, + 3151, + 7475 + ], + "bytes": 6588, + "sha256": "c4039a6b0d0b12744cd5ee5aae1753d1ee563a345ba33be528831d7fd6e8c57f", + "original_visible": false, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/skin-0-dry.png", + "source_layer": "3/3/1/0/2", + "layer_name": "矢量智能对象", + "size": [ + 316, + 316 + ], + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "bytes": 21537, + "sha256": "455a4da833b7a057e07dfb3ca5b45cfe26754b3bc5c7174d5da4f4c8d6c34c5b", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/skin-1-mixeddry.png", + "source_layer": "3/3/1/1/2", + "layer_name": "矢量智能对象", + "size": [ + 316, + 316 + ], + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "bytes": 21391, + "sha256": "4d3dee68411589c1128f34e6ce54499010a4ca16260a6a84ba3a03a52204270b", + "original_visible": false, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/skin-2-neutral.png", + "source_layer": "3/3/1/2/2", + "layer_name": "矢量智能对象", + "size": [ + 316, + 316 + ], + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "bytes": 21349, + "sha256": "c92d6e6c31a6cc413014f0e0dc0426ade6480e13720b192ac0659df9a1d65d8f", + "original_visible": false, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/skin-3-oily.png", + "source_layer": "3/3/1/3/2", + "layer_name": "矢量智能对象", + "size": [ + 316, + 316 + ], + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "bytes": 21349, + "sha256": "7524fa1c7986f7561a38878dbd5b29f434ecd4c6ca2c2e4c7a165e8754ab2769", + "original_visible": false, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/skin-4-mixedoily.png", + "source_layer": "3/3/1/4/2", + "layer_name": "矢量智能对象", + "size": [ + 316, + 316 + ], + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "bytes": 21370, + "sha256": "446e84fc00ddf78aff4d56e5b584302f33e579dc980aef2a95ec86c79f95b80e", + "original_visible": false, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/product-25.png", + "source_layer": "8/1/0/1", + "layer_name": "图片", + "size": [ + 522, + 652 + ], + "bbox": [ + 1765, + 14655, + 2287, + 15307 + ], + "bytes": 574827, + "sha256": "78e34166c190c9454c1768b4f79e96f0c6c83c85a43cb3b646da84e53182af72", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/product-35.png", + "source_layer": "8/1/1/2", + "layer_name": "ChatGPT Image 2026年9月17日 19_01_25", + "size": [ + 521, + 694 + ], + "bbox": [ + 1772, + 15191, + 2293, + 15885 + ], + "bytes": 562341, + "sha256": "d5c2270a15c15fb0be68df006c93c7a97623cc76a46a8743a0513d61428e0f9a", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/product-50.png", + "source_layer": "8/1/2/4", + "layer_name": "50岁KV", + "size": [ + 502, + 669 + ], + "bbox": [ + 1765, + 15731, + 2267, + 16400 + ], + "bytes": 669968, + "sha256": "f913845228d59c04cdb65d2e943e2f62987b9a74768a52a1404a1e080e9be37a", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/product-badge.png", + "source_layer": "8/1/0/3", + "layer_name": "图层 566", + "size": [ + 101, + 77 + ], + "bbox": [ + 1364, + 15039, + 1465, + 15116 + ], + "bytes": 2291, + "sha256": "1cdb0a878ac6a0d1c0b44392208e4567d4384f32df4e3bbaa1e11ef3725af3cd", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/score-ring-base.png", + "source_layer": "8/12/5", + "layer_name": "图层 18", + "size": [ + 312, + 312 + ], + "bbox": [ + 1394, + 9160, + 1706, + 9472 + ], + "bytes": 8968, + "sha256": "a52300f5b45b61c9d729837645a0a086d62e66e5f875807fa101e6179f405e7d", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/score-ring-value.png", + "source_layer": "8/12/6", + "layer_name": "图层 19", + "size": [ + 310, + 312 + ], + "bbox": [ + 1395, + 9160, + 1705, + 9472 + ], + "bytes": 8049, + "sha256": "b19d4bab0acc0cbae7d51b4b7dd7e8b45d548fcde51f208739dffadf8be30a46", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/sample-contour.png", + "source_layer": "8/9/8", + "layer_name": "图层 9", + "size": [ + 642, + 833 + ], + "bbox": [ + 1467, + 10113, + 2109, + 10946 + ], + "bytes": 627146, + "sha256": "8c1fdaf9aa17f3a19e340e7ee20b5da58952b2976ae7672878b9405652acc4eb", + "original_visible": true, + "usage": "reference-only" + }, + { + "file": "h5/assets/img/ui-20261009/sample-lines.png", + "source_layer": "8/9/10", + "layer_name": "图层 11", + "size": [ + 264, + 351 + ], + "bbox": [ + 1471, + 10877, + 1735, + 11228 + ], + "bytes": 242182, + "sha256": "37e35010addaf422f31de480d91654816abc35aa0874040b38d4743a0e1ec4ef", + "original_visible": true, + "usage": "reference-only" + }, + { + "file": "h5/assets/img/ui-20261009/sample-tone.png", + "source_layer": "8/10/10", + "layer_name": "图层 13", + "size": [ + 630, + 838 + ], + "bbox": [ + 1462, + 11428, + 2092, + 12266 + ], + "bytes": 963862, + "sha256": "41a2352f28f51e74778d49b4c9648788b1a705c6ce993d4694f938bfffd93de9", + "original_visible": true, + "usage": "reference-only" + }, + { + "file": "h5/assets/img/ui-20261009/sample-redness.png", + "source_layer": "8/11/8", + "layer_name": "图层 9", + "size": [ + 642, + 833 + ], + "bbox": [ + 1467, + 12794, + 2109, + 13627 + ], + "bytes": 627146, + "sha256": "8c1fdaf9aa17f3a19e340e7ee20b5da58952b2976ae7672878b9405652acc4eb", + "original_visible": true, + "usage": "reference-only" + }, + { + "file": "h5/assets/img/ui-20261009/camera-mask-front.png", + "source_layer": "5/0", + "layer_name": "矩形 7", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 2522, + 5252, + 3602, + 7591 + ], + "bytes": 28116, + "sha256": "de62e3c83352ae7df19bbdf2fc42e6002b51df7ab7d01cfcec3e895c42151b25", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/camera-mask-right.png", + "source_layer": "6/0", + "layer_name": "矩形 7", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 3796, + 5252, + 4876, + 7591 + ], + "bytes": 28185, + "sha256": "4c5d74f1f19527eadd541f73630c828789dc54c72f02225df22135937bdf568b", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/camera-mask-left.png", + "source_layer": "7/0", + "layer_name": "矩形 7", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 5070, + 5252, + 6150, + 7591 + ], + "bytes": 28629, + "sha256": "5a51b88470094c83693e5a82927a32f886c273ba74cf5335a17dc6a397fb6d5d", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/camera-guide-front.png", + "source_layer": [ + "5/8", + "5/9", + "5/10" + ], + "layer_name": "形状 559", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 2522, + 5252, + 3602, + 7591 + ], + "bytes": 11853, + "sha256": "992907eb88f6cd44e76959d54d2d198382289f8ff5693d398e9684a6925e7258", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/camera-guide-right.png", + "source_layer": [ + "6/8", + "6/9", + "6/10" + ], + "layer_name": "形状 559", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 3796, + 5252, + 4876, + 7591 + ], + "bytes": 14277, + "sha256": "a0a8a1fee241706a8535145d47b31bd10504d3cead336350c3d2812a4f000c04", + "original_visible": true, + "usage": "frontend-asset" + }, + { + "file": "h5/assets/img/ui-20261009/camera-guide-left.png", + "source_layer": [ + "7/8", + "7/9", + "7/10" + ], + "layer_name": "形状 559", + "size": [ + 1080, + 2339 + ], + "bbox": [ + 5070, + 5252, + 6150, + 7591 + ], + "bytes": 14436, + "sha256": "9e6954d4b41bbcbe0888ebb9222741aaa7c2046232578fc7fc9eb399c935bbdc", + "original_visible": true, + "usage": "frontend-asset" + } + ], + "layers": [ + { + "path": "0", + "name": "启动页", + "kind": "artboard", + "bbox": [ + 1237, + 0, + 2317, + 2339 + ], + "visible": true + }, + { + "path": "0/0", + "name": "背景", + "kind": "smartobject", + "bbox": [ + 1067, + -10, + 2481, + 2413 + ], + "visible": true + }, + { + "path": "0/1", + "name": "1‘me 肌肤状态检测", + "kind": "group", + "bbox": [ + 1515, + 1861, + 2032, + 1920 + ], + "visible": true + }, + { + "path": "0/1/0", + "name": "LOGO", + "kind": "smartobject", + "bbox": [ + 1515, + 1861, + 1647, + 1920 + ], + "visible": true + }, + { + "path": "0/1/1", + "name": "肌肤状态检测", + "kind": "type", + "bbox": [ + 1664, + 1862, + 2032, + 1920 + ], + "visible": true, + "text": "肌肤状态检测" + }, + { + "path": "0/2", + "name": "按钮框开始检测 拷贝 2", + "kind": "group", + "bbox": [ + 1516, + 2104, + 2038, + 2291 + ], + "visible": true + }, + { + "path": "0/2/0", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 1516, + 2104, + 2038, + 2216 + ], + "visible": false + }, + { + "path": "0/2/1", + "name": "开始检测", + "kind": "type", + "bbox": [ + 1696, + 2138, + 1857, + 2178 + ], + "visible": false, + "text": "开始检测" + }, + { + "path": "0/2/2", + "name": "按钮框开始检测 拷贝", + "kind": "group", + "bbox": [ + 1516, + 2104, + 2038, + 2291 + ], + "visible": true + }, + { + "path": "0/2/2/0", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 1516, + 2104, + 2038, + 2216 + ], + "visible": true + }, + { + "path": "0/2/2/1", + "name": "我的报告", + "kind": "type", + "bbox": [ + 1696, + 2138, + 1859, + 2177 + ], + "visible": true, + "text": "我的报告" + }, + { + "path": "0/2/2/2", + "name": "我的报告 拷贝", + "kind": "type", + "bbox": [ + 1696, + 2252, + 1859, + 2291 + ], + "visible": true, + "text": "我的报告" + }, + { + "path": "0/3", + "name": "按钮框开始检测", + "kind": "group", + "bbox": [ + 1516, + 1971, + 2039, + 2083 + ], + "visible": true + }, + { + "path": "0/3/0", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 1516, + 1971, + 2038, + 2083 + ], + "visible": true + }, + { + "path": "0/3/1", + "name": "开始检测", + "kind": "type", + "bbox": [ + 1696, + 2005, + 1857, + 2045 + ], + "visible": true, + "text": "开始检测" + }, + { + "path": "0/3/2", + "name": "按钮框开始检测 拷贝", + "kind": "group", + "bbox": [ + 1517, + 1971, + 2039, + 2083 + ], + "visible": true + }, + { + "path": "0/3/2/0", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 1517, + 1971, + 2039, + 2083 + ], + "visible": true + }, + { + "path": "0/3/2/1", + "name": "开始检测", + "kind": "type", + "bbox": [ + 1696, + 2005, + 1857, + 2045 + ], + "visible": true, + "text": "开始检测" + }, + { + "path": "0/4", + "name": "以我为始 自有表现", + "kind": "type", + "bbox": [ + 1502, + 545, + 2045, + 608 + ], + "visible": true, + "text": "以我为始 自有表现" + }, + { + "path": "0/5", + "name": " ONE ME, FROM THE INSIDE OUT", + "kind": "type", + "bbox": [ + 1415, + 391, + 2133, + 520 + ], + "visible": true, + "text": " ONE ME,\rFROM THE INSIDE OUT" + }, + { + "path": "0/6", + "name": "顶部占位", + "kind": "group", + "bbox": [ + 1234, + -6, + 2320, + 248 + ], + "visible": true + }, + { + "path": "0/6/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 1234, + -6, + 2320, + 248 + ], + "visible": true + }, + { + "path": "0/6/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 1237, + 0, + 2317, + 247 + ], + "visible": true + }, + { + "path": "0/6/2", + "name": "1'me肌肤状态检测 ", + "kind": "type", + "bbox": [ + 1609, + 173, + 1943, + 213 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "1", + "name": "启动页弹窗", + "kind": "artboard", + "bbox": [ + 2474, + 0, + 3554, + 2339 + ], + "visible": true + }, + { + "path": "1/0", + "name": "背景", + "kind": "group", + "bbox": [ + 2304, + -79, + 3718, + 2413 + ], + "visible": true + }, + { + "path": "1/0/0", + "name": "背景 拷贝", + "kind": "smartobject", + "bbox": [ + 2304, + -10, + 3718, + 2413 + ], + "visible": true + }, + { + "path": "1/0/1", + "name": "矩形 3", + "kind": "shape", + "bbox": [ + 2428, + -79, + 3623, + 2382 + ], + "visible": true + }, + { + "path": "1/1", + "name": "弹窗框", + "kind": "shape", + "bbox": [ + 2571, + 719, + 3458, + 1621 + ], + "visible": true + }, + { + "path": "1/2", + "name": "按钮框", + "kind": "group", + "bbox": [ + 2753, + 1410, + 3275, + 1522 + ], + "visible": true + }, + { + "path": "1/2/0", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 2753, + 1410, + 3275, + 1522 + ], + "visible": true + }, + { + "path": "1/2/1", + "name": "继续", + "kind": "type", + "bbox": [ + 2974, + 1444, + 3054, + 1483 + ], + "visible": true, + "text": "继续" + }, + { + "path": "1/3", + "name": "欢迎文案", + "kind": "group", + "bbox": [ + 2663, + 832, + 3361, + 1177 + ], + "visible": true + }, + { + "path": "1/3/0", + "name": "欢迎体验1'me 肌肤状态评估服务!", + "kind": "type", + "bbox": [ + 2664, + 832, + 3218, + 979 + ], + "visible": true, + "text": "欢迎体验1'me\r肌肤状态评估服务!" + }, + { + "path": "1/3/1", + "name": "我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。", + "kind": "type", + "bbox": [ + 2663, + 1045, + 3361, + 1177 + ], + "visible": true, + "text": "我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。" + }, + { + "path": "1/4", + "name": "隐私未选中", + "kind": "group", + "bbox": [ + 2657, + 1253, + 3098, + 1290 + ], + "visible": true + }, + { + "path": "1/4/0", + "name": "椭圆 1", + "kind": "shape", + "bbox": [ + 2657, + 1253, + 2691, + 1288 + ], + "visible": true + }, + { + "path": "1/4/1", + "name": "我已阅读并同意相关隐私协议", + "kind": "type", + "bbox": [ + 2720, + 1257, + 3098, + 1290 + ], + "visible": true, + "text": "我已阅读并同意相关隐私协议" + }, + { + "path": "1/5", + "name": "隐私选中", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "1/5/0", + "name": "椭圆 1", + "kind": "shape", + "bbox": [ + 2657, + 1256, + 2691, + 1291 + ], + "visible": true + }, + { + "path": "1/5/1", + "name": "椭圆 1 拷贝", + "kind": "shape", + "bbox": [ + 2662, + 1263, + 2685, + 1286 + ], + "visible": true + }, + { + "path": "1/5/2", + "name": "我已阅读并同意相关隐私协议", + "kind": "type", + "bbox": [ + 2720, + 1257, + 3098, + 1290 + ], + "visible": true, + "text": "我已阅读并同意相关隐私协议" + }, + { + "path": "1/6", + "name": "关闭", + "kind": "smartobject", + "bbox": [ + 3356, + 770, + 3407, + 820 + ], + "visible": true + }, + { + "path": "1/7", + "name": "屏幕占位", + "kind": "group", + "bbox": [ + 2471, + -6, + 3557, + 248 + ], + "visible": true + }, + { + "path": "1/7/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 2471, + -6, + 3557, + 248 + ], + "visible": true + }, + { + "path": "1/7/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 2474, + 0, + 3554, + 247 + ], + "visible": true + }, + { + "path": "1/7/2", + "name": "1'me在线肌肤测试", + "kind": "type", + "bbox": [ + 2846, + 173, + 3180, + 213 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "2", + "name": "我的报告", + "kind": "artboard", + "bbox": [ + 3654, + 0, + 4734, + 2339 + ], + "visible": true + }, + { + "path": "2/0", + "name": "图层 15", + "kind": "pixel", + "bbox": [ + 3654, + 0, + 8569, + 15799 + ], + "visible": true + }, + { + "path": "2/1", + "name": "图层 14", + "kind": "pixel", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "2/2", + "name": "Life in Full 为更好的生命表现 拷贝 2", + "kind": "type", + "bbox": [ + 3910, + 477, + 4435, + 627 + ], + "visible": false, + "text": "Life in Full\r为更好的生命表现" + }, + { + "path": "2/3", + "name": "屏幕占位", + "kind": "group", + "bbox": [ + 3651, + -7, + 4737, + 247 + ], + "visible": true + }, + { + "path": "2/3/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 3651, + -7, + 4737, + 247 + ], + "visible": true + }, + { + "path": "2/3/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 3654, + -1, + 4734, + 246 + ], + "visible": true + }, + { + "path": "2/3/2", + "name": "1'me在线肌肤测试", + "kind": "type", + "bbox": [ + 4026, + 172, + 4360, + 212 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "2/4", + "name": "您的报告", + "kind": "type", + "bbox": [ + 4074, + 334, + 4334, + 397 + ], + "visible": true, + "text": "您的报告" + }, + { + "path": "2/5", + "name": "测评 时间", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "2/6", + "name": "测评", + "kind": "group", + "bbox": [ + 3702, + 456, + 4685, + 1139 + ], + "visible": true + }, + { + "path": "2/6/0", + "name": "测试数量", + "kind": "group", + "bbox": [ + 3703, + 456, + 4685, + 689 + ], + "visible": true + }, + { + "path": "2/6/0/0", + "name": "按钮框 拷贝", + "kind": "shape", + "bbox": [ + 3703, + 456, + 4685, + 689 + ], + "visible": true + }, + { + "path": "2/6/0/1", + "name": "您最近经进行过4次面部问题检测", + "kind": "type", + "bbox": [ + 3995, + 596, + 4416, + 624 + ], + "visible": true, + "text": "您最近经进行过4次面部问题检测" + }, + { + "path": "2/6/0/2", + "name": "测试数量", + "kind": "type", + "bbox": [ + 4137, + 500, + 4251, + 528 + ], + "visible": true, + "text": "测试数量" + }, + { + "path": "2/6/0/3", + "name": "形状 3 拷贝 4", + "kind": "shape", + "bbox": [ + 4144, + 560, + 4245, + 565 + ], + "visible": true + }, + { + "path": "2/6/1", + "name": "时间 拷贝", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "2/6/1/0", + "name": "按钮框 拷贝 4", + "kind": "shape", + "bbox": [ + 3702, + 972, + 4685, + 1153 + ], + "visible": true + }, + { + "path": "2/6/1/1", + "name": "2026-10-07", + "kind": "type", + "bbox": [ + 3900, + 1016, + 4056, + 1039 + ], + "visible": true, + "text": "2026-10-07" + }, + { + "path": "2/6/1/2", + "name": "按钮框 拷贝 4", + "kind": "shape", + "bbox": [ + 4448, + 1086, + 4629, + 1122 + ], + "visible": true + }, + { + "path": "2/6/1/3", + "name": "查看详情", + "kind": "type", + "bbox": [ + 4482, + 1091, + 4596, + 1118 + ], + "visible": true, + "text": "查看详情" + }, + { + "path": "2/6/1/4", + "name": "测试时间", + "kind": "type", + "bbox": [ + 3758, + 1014, + 3871, + 1041 + ], + "visible": true, + "text": "测试时间" + }, + { + "path": "2/6/1/5", + "name": "形状 3 拷贝 6", + "kind": "shape", + "bbox": [ + 3755, + 1060, + 4646, + 1065 + ], + "visible": true + }, + { + "path": "2/6/2", + "name": "时间 拷贝 2", + "kind": "group", + "bbox": [ + 3702, + 958, + 4685, + 1139 + ], + "visible": true + }, + { + "path": "2/6/2/0", + "name": "按钮框 拷贝 4", + "kind": "shape", + "bbox": [ + 3702, + 958, + 4685, + 1139 + ], + "visible": true + }, + { + "path": "2/6/2/1", + "name": "2026-10-08", + "kind": "type", + "bbox": [ + 3900, + 1002, + 4059, + 1025 + ], + "visible": true, + "text": "2026-10-08" + }, + { + "path": "2/6/2/2", + "name": "按钮框 拷贝 4", + "kind": "shape", + "bbox": [ + 4392, + 1067, + 4622, + 1112 + ], + "visible": true + }, + { + "path": "2/6/2/3", + "name": "查看详情", + "kind": "type", + "bbox": [ + 4439, + 1073, + 4570, + 1105 + ], + "visible": true, + "text": "查看详情" + }, + { + "path": "2/6/2/4", + "name": "测试时间", + "kind": "type", + "bbox": [ + 3758, + 1000, + 3871, + 1027 + ], + "visible": true, + "text": "测试时间" + }, + { + "path": "2/6/2/5", + "name": "形状 3 拷贝 6", + "kind": "shape", + "bbox": [ + 3755, + 1046, + 4646, + 1051 + ], + "visible": true + }, + { + "path": "2/6/3", + "name": "时间", + "kind": "group", + "bbox": [ + 3702, + 766, + 4685, + 947 + ], + "visible": true + }, + { + "path": "2/6/3/0", + "name": "按钮框 拷贝 4", + "kind": "shape", + "bbox": [ + 3702, + 766, + 4685, + 947 + ], + "visible": true + }, + { + "path": "2/6/3/1", + "name": "2026-10-08", + "kind": "type", + "bbox": [ + 3900, + 810, + 4059, + 833 + ], + "visible": true, + "text": "2026-10-08" + }, + { + "path": "2/6/3/2", + "name": "按钮框 拷贝 4", + "kind": "shape", + "bbox": [ + 4392, + 875, + 4622, + 920 + ], + "visible": true + }, + { + "path": "2/6/3/3", + "name": "查看详情", + "kind": "type", + "bbox": [ + 4439, + 881, + 4570, + 913 + ], + "visible": true, + "text": "查看详情" + }, + { + "path": "2/6/3/4", + "name": "测试时间", + "kind": "type", + "bbox": [ + 3758, + 808, + 3871, + 835 + ], + "visible": true, + "text": "测试时间" + }, + { + "path": "2/6/3/5", + "name": "形状 3 拷贝 6", + "kind": "shape", + "bbox": [ + 3755, + 854, + 4646, + 859 + ], + "visible": true + }, + { + "path": "3", + "name": "档案填写", + "kind": "artboard", + "bbox": [ + 1237, + 2626, + 2317, + 4965 + ], + "visible": true + }, + { + "path": "3/0", + "name": "年龄", + "kind": "group", + "bbox": [ + 1067, + 2547, + 2481, + 5039 + ], + "visible": true + }, + { + "path": "3/0/0", + "name": "背景", + "kind": "group", + "bbox": [ + 1067, + 2547, + 2481, + 5039 + ], + "visible": true + }, + { + "path": "3/0/0/0", + "name": "图层 1", + "kind": "pixel", + "bbox": [ + 1237, + 2626, + 2317, + 4965 + ], + "visible": true + }, + { + "path": "3/0/0/1", + "name": "背景 拷贝 3", + "kind": "smartobject", + "bbox": [ + 1067, + 2616, + 2481, + 5039 + ], + "visible": true + }, + { + "path": "3/0/0/2", + "name": "矩形 3", + "kind": "shape", + "bbox": [ + 1191, + 2547, + 2386, + 5008 + ], + "visible": true + }, + { + "path": "3/0/1", + "name": "按钮", + "kind": "group", + "bbox": [ + 1516, + 4502, + 2038, + 4614 + ], + "visible": true + }, + { + "path": "3/0/1/0", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 1516, + 4502, + 2038, + 4614 + ], + "visible": true + }, + { + "path": "3/0/1/1", + "name": "提交", + "kind": "type", + "bbox": [ + 1737, + 4536, + 1817, + 4576 + ], + "visible": true, + "text": "提交" + }, + { + "path": "3/0/2", + "name": "您的出生年月chusyheng ", + "kind": "type", + "bbox": [ + 1460, + 3098, + 1700, + 3139 + ], + "visible": true, + "text": "您的出生年月chusyheng " + }, + { + "path": "3/0/3", + "name": "矩形 5", + "kind": "shape", + "bbox": [ + 1421, + 3164, + 1746, + 3276 + ], + "visible": true + }, + { + "path": "3/1", + "name": "地区", + "kind": "group", + "bbox": [ + 1421, + 3335, + 2133, + 3513 + ], + "visible": true + }, + { + "path": "3/1/0", + "name": "您的所在地区", + "kind": "type", + "bbox": [ + 1654, + 3335, + 1900, + 3376 + ], + "visible": true, + "text": "您的所在地区" + }, + { + "path": "3/1/1", + "name": "矩形 5 拷贝", + "kind": "shape", + "bbox": [ + 1421, + 3400, + 2133, + 3513 + ], + "visible": true + }, + { + "path": "3/1/2", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 2060, + 3447, + 2093, + 3465 + ], + "visible": true + }, + { + "path": "3/2", + "name": "性别", + "kind": "group", + "bbox": [ + 1675, + 3098, + 2132, + 3276 + ], + "visible": true + }, + { + "path": "3/2/0", + "name": "矩形 5 拷贝 2", + "kind": "shape", + "bbox": [ + 1807, + 3164, + 2132, + 3276 + ], + "visible": true + }, + { + "path": "3/2/1", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 2061, + 3211, + 2094, + 3229 + ], + "visible": true + }, + { + "path": "3/2/2", + "name": "下拉 拷贝 2", + "kind": "smartobject", + "bbox": [ + 1675, + 3211, + 1708, + 3229 + ], + "visible": true + }, + { + "path": "3/2/3", + "name": "您的性别", + "kind": "type", + "bbox": [ + 1888, + 3098, + 2048, + 3138 + ], + "visible": true, + "text": "您的性别" + }, + { + "path": "3/3", + "name": "肤质", + "kind": "group", + "bbox": [ + 1420, + 3571, + 2133, + 4443 + ], + "visible": true + }, + { + "path": "3/3/0", + "name": "矩形 5 拷贝 3", + "kind": "shape", + "bbox": [ + 1420, + 3635, + 2133, + 4443 + ], + "visible": true + }, + { + "path": "3/3/1", + "name": "肤质选择", + "kind": "group", + "bbox": [ + 1619, + 3781, + 1935, + 4379 + ], + "visible": true + }, + { + "path": "3/3/1/0", + "name": "干性", + "kind": "group", + "bbox": [ + 1619, + 3781, + 1935, + 4278 + ], + "visible": true + }, + { + "path": "3/3/1/0/0", + "name": "干性肌肤", + "kind": "type", + "bbox": [ + 1692, + 4148, + 1839, + 4183 + ], + "visible": true, + "text": "干性肌肤" + }, + { + "path": "3/3/1/0/1", + "name": "全脸干燥,容易紧绷、 缺乏光泽", + "kind": "type", + "bbox": [ + 1632, + 4208, + 1905, + 4278 + ], + "visible": true, + "text": "全脸干燥,容易紧绷、\r缺乏光泽" + }, + { + "path": "3/3/1/0/2", + "name": "矢量智能对象", + "kind": "smartobject", + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "visible": true + }, + { + "path": "3/3/1/1", + "name": "混干", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "3/3/1/1/0", + "name": "混合偏干肌肤", + "kind": "type", + "bbox": [ + 1666, + 4148, + 1888, + 4183 + ], + "visible": true, + "text": "混合偏干肌肤" + }, + { + "path": "3/3/1/1/1", + "name": "两颊偏干,T区油脂较少 或偶尔出油", + "kind": "type", + "bbox": [ + 1624, + 4208, + 1930, + 4277 + ], + "visible": true, + "text": "两颊偏干,T区油脂较少\r或偶尔出油" + }, + { + "path": "3/3/1/1/2", + "name": "矢量智能对象", + "kind": "smartobject", + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "visible": true + }, + { + "path": "3/3/1/2", + "name": "中性", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "3/3/1/2/0", + "name": "中性肌肤", + "kind": "type", + "bbox": [ + 1706, + 4148, + 1850, + 4183 + ], + "visible": true, + "text": "中性肌肤" + }, + { + "path": "3/3/1/2/1", + "name": "水油平衡,肤感舒适, 无明显干燥出油", + "kind": "type", + "bbox": [ + 1632, + 4208, + 1903, + 4278 + ], + "visible": true, + "text": "水油平衡,肤感舒适,\r无明显干燥出油" + }, + { + "path": "3/3/1/2/2", + "name": "矢量智能对象", + "kind": "smartobject", + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "visible": true + }, + { + "path": "3/3/1/3", + "name": "油性", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "3/3/1/3/0", + "name": "油性肌肤", + "kind": "type", + "bbox": [ + 1704, + 4148, + 1850, + 4183 + ], + "visible": true, + "text": "油性肌肤" + }, + { + "path": "3/3/1/3/1", + "name": "全脸易出油,容易泛油 光、毛孔明显", + "kind": "type", + "bbox": [ + 1632, + 4208, + 1920, + 4278 + ], + "visible": true, + "text": "全脸易出油,容易泛油\r光、毛孔明显" + }, + { + "path": "3/3/1/3/2", + "name": "矢量智能对象", + "kind": "smartobject", + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "visible": true + }, + { + "path": "3/3/1/4", + "name": "混合偏油肌肤", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "3/3/1/4/0", + "name": "混合偏油肌肤", + "kind": "type", + "bbox": [ + 1666, + 4148, + 1888, + 4183 + ], + "visible": true, + "text": "混合偏油肌肤" + }, + { + "path": "3/3/1/4/1", + "name": "T区易出油,两颊相对清爽 或轻微干燥", + "kind": "type", + "bbox": [ + 1608, + 4208, + 1944, + 4277 + ], + "visible": true, + "text": "T区易出油,两颊相对清爽\r或轻微干燥" + }, + { + "path": "3/3/1/4/2", + "name": "矢量智能对象", + "kind": "smartobject", + "bbox": [ + 1619, + 3781, + 1935, + 4097 + ], + "visible": true + }, + { + "path": "3/3/1/5", + "name": "轮播标识", + "kind": "group", + "bbox": [ + 1696, + 4351, + 1858, + 4379 + ], + "visible": true + }, + { + "path": "3/3/1/5/0", + "name": "椭圆 2", + "kind": "shape", + "bbox": [ + 1730, + 4351, + 1757, + 4379 + ], + "visible": true + }, + { + "path": "3/3/1/5/1", + "name": "椭圆 2 拷贝 4", + "kind": "shape", + "bbox": [ + 1696, + 4351, + 1722, + 4379 + ], + "visible": true + }, + { + "path": "3/3/1/5/2", + "name": "椭圆 2 拷贝", + "kind": "shape", + "bbox": [ + 1764, + 4351, + 1791, + 4379 + ], + "visible": true + }, + { + "path": "3/3/1/5/3", + "name": "椭圆 2 拷贝 2", + "kind": "shape", + "bbox": [ + 1797, + 4351, + 1825, + 4379 + ], + "visible": true + }, + { + "path": "3/3/1/5/4", + "name": "椭圆 2 拷贝 3", + "kind": "shape", + "bbox": [ + 1831, + 4351, + 1858, + 4379 + ], + "visible": true + }, + { + "path": "3/3/2", + "name": "解释", + "kind": "group", + "bbox": [ + 1597, + 3693, + 1953, + 3718 + ], + "visible": true + }, + { + "path": "3/3/2/0", + "name": "偏干", + "kind": "type", + "bbox": [ + 1636, + 3693, + 1685, + 3717 + ], + "visible": true, + "text": "偏干" + }, + { + "path": "3/3/2/1", + "name": "矩形 11", + "kind": "shape", + "bbox": [ + 1597, + 3693, + 1622, + 3718 + ], + "visible": true + }, + { + "path": "3/3/2/2", + "name": "偏油", + "kind": "type", + "bbox": [ + 1745, + 3693, + 1792, + 3717 + ], + "visible": true, + "text": "偏油" + }, + { + "path": "3/3/2/3", + "name": "矩形 11 拷贝", + "kind": "shape", + "bbox": [ + 1706, + 3693, + 1731, + 3718 + ], + "visible": true + }, + { + "path": "3/3/2/4", + "name": "水油平衡", + "kind": "type", + "bbox": [ + 1855, + 3693, + 1953, + 3717 + ], + "visible": true, + "text": "水油平衡" + }, + { + "path": "3/3/2/5", + "name": "矩形 11 拷贝 2", + "kind": "shape", + "bbox": [ + 1815, + 3693, + 1840, + 3718 + ], + "visible": true + }, + { + "path": "3/3/3", + "name": "您的日常肤质", + "kind": "type", + "bbox": [ + 1654, + 3571, + 1901, + 3611 + ], + "visible": true, + "text": "您的日常肤质" + }, + { + "path": "3/4", + "name": "您的肌肤状态", + "kind": "type", + "bbox": [ + 1591, + 2966, + 1962, + 3026 + ], + "visible": true, + "text": "您的肌肤状态" + }, + { + "path": "3/5", + "name": "屏幕占位", + "kind": "group", + "bbox": [ + 1234, + 2620, + 2320, + 2874 + ], + "visible": true + }, + { + "path": "3/5/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 1234, + 2620, + 2320, + 2874 + ], + "visible": true + }, + { + "path": "3/5/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 1237, + 2626, + 2317, + 2873 + ], + "visible": true + }, + { + "path": "3/5/2", + "name": "1'me肌肤状态检测 ", + "kind": "type", + "bbox": [ + 1609, + 2799, + 1943, + 2839 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "3/6", + "name": "返回", + "kind": "group", + "bbox": [ + 1261, + 2778, + 1341, + 2858 + ], + "visible": true + }, + { + "path": "3/6/0", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 1292, + 2802, + 1310, + 2835 + ], + "visible": true + }, + { + "path": "3/6/1", + "name": "矩形 6", + "kind": "shape", + "bbox": [ + 1261, + 2778, + 1341, + 2858 + ], + "visible": true + }, + { + "path": "3/7", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 2082, + 4023, + 2100, + 4056 + ], + "visible": true + }, + { + "path": "3/8", + "name": "下拉 拷贝", + "kind": "smartobject", + "bbox": [ + 1454, + 4024, + 1472, + 4057 + ], + "visible": true + }, + { + "path": "4", + "name": "拍照弹窗", + "kind": "artboard", + "bbox": [ + 1237, + 5252, + 2317, + 7591 + ], + "visible": true + }, + { + "path": "4/0", + "name": "背景", + "kind": "group", + "bbox": [ + 1067, + 5173, + 2481, + 7665 + ], + "visible": true + }, + { + "path": "4/0/0", + "name": "图层 1", + "kind": "pixel", + "bbox": [ + 1237, + 5252, + 2317, + 7591 + ], + "visible": true + }, + { + "path": "4/0/1", + "name": "背景 拷贝 2", + "kind": "smartobject", + "bbox": [ + 1067, + 5242, + 2481, + 7665 + ], + "visible": true + }, + { + "path": "4/0/2", + "name": "以我为始 自有表现", + "kind": "type", + "bbox": [ + 1502, + 5797, + 2045, + 5860 + ], + "visible": true, + "text": "以我为始 自有表现" + }, + { + "path": "4/0/3", + "name": " ONE ME, FROM THE INSIDE OUT", + "kind": "type", + "bbox": [ + 1415, + 5643, + 2133, + 5772 + ], + "visible": true, + "text": " ONE ME,\rFROM THE INSIDE OUT" + }, + { + "path": "4/0/4", + "name": "矩形 3", + "kind": "shape", + "bbox": [ + 1191, + 5173, + 2386, + 7634 + ], + "visible": true + }, + { + "path": "4/1", + "name": "弹窗", + "kind": "shape", + "bbox": [ + 1334, + 5971, + 2221, + 6873 + ], + "visible": true + }, + { + "path": "4/2", + "name": "按钮框", + "kind": "group", + "bbox": [ + 1517, + 6604, + 2039, + 6716 + ], + "visible": true + }, + { + "path": "4/2/0", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 1517, + 6604, + 2039, + 6716 + ], + "visible": true + }, + { + "path": "4/2/1", + "name": "开始", + "kind": "type", + "bbox": [ + 1737, + 6638, + 1817, + 6678 + ], + "visible": true, + "text": "开始" + }, + { + "path": "4/3", + "name": "欢迎文案", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "4/4", + "name": "关闭", + "kind": "smartobject", + "bbox": [ + 2120, + 6022, + 2171, + 6072 + ], + "visible": true + }, + { + "path": "4/5", + "name": "屏幕占位", + "kind": "group", + "bbox": [ + 1234, + 5246, + 2320, + 5500 + ], + "visible": true + }, + { + "path": "4/5/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 1234, + 5246, + 2320, + 5500 + ], + "visible": true + }, + { + "path": "4/5/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 1237, + 5252, + 2317, + 5499 + ], + "visible": true + }, + { + "path": "4/5/2", + "name": "1'me在线肌肤测试", + "kind": "type", + "bbox": [ + 1609, + 5425, + 1943, + 5465 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "4/6", + "name": "返回", + "kind": "group", + "bbox": [ + 1261, + 5404, + 1341, + 5484 + ], + "visible": true + }, + { + "path": "4/6/0", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 1292, + 5428, + 1310, + 5461 + ], + "visible": true + }, + { + "path": "4/6/1", + "name": "矩形 6", + "kind": "shape", + "bbox": [ + 1261, + 5404, + 1341, + 5484 + ], + "visible": true + }, + { + "path": "4/7", + "name": "请拍摄或上传 正面及左右侧面照片", + "kind": "type", + "bbox": [ + 1442, + 6160, + 2036, + 6307 + ], + "visible": true, + "text": "请拍摄或上传\r正面及左右侧面照片" + }, + { + "path": "4/8", + "name": "为使测试结果更准确,请关闭美颜/滤镜、并确保光线充足均匀。", + "kind": "type", + "bbox": [ + 1443, + 6372, + 2119, + 6454 + ], + "visible": true, + "text": "为使测试结果更准确,请关闭美颜/滤镜、并确保光线充足均匀。" + }, + { + "path": "5", + "name": "正面", + "kind": "artboard", + "bbox": [ + 2522, + 5252, + 3602, + 7591 + ], + "visible": true + }, + { + "path": "5/0", + "name": "矩形 7", + "kind": "shape", + "bbox": [ + 2520, + 5244, + 3604, + 7596 + ], + "visible": true + }, + { + "path": "5/1", + "name": "拍照icon", + "kind": "group", + "bbox": [ + 2684, + 7346, + 3445, + 7425 + ], + "visible": true + }, + { + "path": "5/1/0", + "name": "left", + "kind": "smartobject", + "bbox": [ + 3365, + 7346, + 3445, + 7425 + ], + "visible": true + }, + { + "path": "5/1/1", + "name": "相册", + "kind": "smartobject", + "bbox": [ + 2684, + 7351, + 2766, + 7421 + ], + "visible": true + }, + { + "path": "5/1/2", + "name": "拍照", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "5/1/2/0", + "name": "椭圆 3 拷贝", + "kind": "shape", + "bbox": [ + 2973, + 7297, + 3151, + 7475 + ], + "visible": true + }, + { + "path": "5/1/2/1", + "name": "椭圆 3 拷贝 2", + "kind": "shape", + "bbox": [ + 2996, + 7320, + 3129, + 7453 + ], + "visible": true + }, + { + "path": "5/2", + "name": "拍照指示", + "kind": "group", + "bbox": [ + 2682, + 7098, + 3442, + 7145 + ], + "visible": true + }, + { + "path": "5/2/0", + "name": "组 8", + "kind": "group", + "bbox": [ + 2682, + 7101, + 3442, + 7141 + ], + "visible": true + }, + { + "path": "5/2/0/0", + "name": "正脸居中", + "kind": "type", + "bbox": [ + 2682, + 7101, + 2842, + 7140 + ], + "visible": true, + "text": "正脸居中" + }, + { + "path": "5/2/0/1", + "name": "取下眼镜", + "kind": "type", + "bbox": [ + 2980, + 7101, + 3143, + 7140 + ], + "visible": true, + "text": "取下眼镜" + }, + { + "path": "5/2/0/2", + "name": "光线充足", + "kind": "type", + "bbox": [ + 3279, + 7101, + 3442, + 7141 + ], + "visible": true, + "text": "光线充足" + }, + { + "path": "5/2/1", + "name": "形状 1", + "kind": "shape", + "bbox": [ + 2914, + 7098, + 2918, + 7145 + ], + "visible": true + }, + { + "path": "5/2/2", + "name": "形状 1 拷贝 2", + "kind": "shape", + "bbox": [ + 3212, + 7098, + 3216, + 7145 + ], + "visible": true + }, + { + "path": "5/3", + "name": "1/3 请正脸出境", + "kind": "type", + "bbox": [ + 2898, + 5656, + 3226, + 5804 + ], + "visible": true, + "text": "1/3\r请正脸出境" + }, + { + "path": "5/4", + "name": "屏幕占位", + "kind": "group", + "bbox": [ + 2519, + 5246, + 3605, + 5500 + ], + "visible": true + }, + { + "path": "5/4/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 2519, + 5246, + 3605, + 5500 + ], + "visible": true + }, + { + "path": "5/4/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 2522, + 5252, + 3602, + 5499 + ], + "visible": true + }, + { + "path": "5/4/2", + "name": "1'me在线肌肤测试", + "kind": "type", + "bbox": [ + 2894, + 5425, + 3228, + 5465 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "5/5", + "name": "返回", + "kind": "group", + "bbox": [ + 2546, + 5404, + 2626, + 5484 + ], + "visible": true + }, + { + "path": "5/5/0", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 2577, + 5428, + 2595, + 5461 + ], + "visible": true + }, + { + "path": "5/5/1", + "name": "矩形 6", + "kind": "shape", + "bbox": [ + 2546, + 5404, + 2626, + 5484 + ], + "visible": true + }, + { + "path": "5/6", + "name": "扫描", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "5/6/0", + "name": "扫描", + "kind": "pixel", + "bbox": [ + 2429, + 5499, + 3718, + 7256 + ], + "visible": false + }, + { + "path": "5/6/1", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 2321, + 5340, + 3804, + 7220 + ], + "visible": true + }, + { + "path": "5/7", + "name": "img_v3_0216a_c5b652b0-5aee-4c92-b6c7-8a356b48119g", + "kind": "smartobject", + "bbox": [ + 2633, + 5916, + 3502, + 7099 + ], + "visible": false + }, + { + "path": "5/8", + "name": "形状 559", + "kind": "shape", + "bbox": [ + 2809, + 6325, + 2997, + 6364 + ], + "visible": true + }, + { + "path": "5/9", + "name": "形状 559 拷贝 2", + "kind": "shape", + "bbox": [ + 3134, + 6325, + 3322, + 6364 + ], + "visible": true + }, + { + "path": "5/10", + "name": "形状 562", + "kind": "shape", + "bbox": [ + 3059, + 6441, + 3071, + 6593 + ], + "visible": true + }, + { + "path": "5/11", + "name": "形状 559 拷贝", + "kind": "shape", + "bbox": [ + 2909, + 6307, + 2909, + 6307 + ], + "visible": true + }, + { + "path": "6", + "name": "左面", + "kind": "artboard", + "bbox": [ + 3796, + 5252, + 4876, + 7591 + ], + "visible": true + }, + { + "path": "6/0", + "name": "矩形 7", + "kind": "shape", + "bbox": [ + 3794, + 5244, + 4878, + 7596 + ], + "visible": true + }, + { + "path": "6/1", + "name": "拍照icon", + "kind": "group", + "bbox": [ + 3958, + 7346, + 4719, + 7425 + ], + "visible": true + }, + { + "path": "6/1/0", + "name": "left", + "kind": "smartobject", + "bbox": [ + 4639, + 7346, + 4719, + 7425 + ], + "visible": true + }, + { + "path": "6/1/1", + "name": "相册", + "kind": "smartobject", + "bbox": [ + 3958, + 7351, + 4040, + 7421 + ], + "visible": true + }, + { + "path": "6/1/2", + "name": "拍照", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "6/1/2/0", + "name": "椭圆 3 拷贝", + "kind": "shape", + "bbox": [ + 4247, + 7297, + 4425, + 7475 + ], + "visible": true + }, + { + "path": "6/1/2/1", + "name": "椭圆 3 拷贝 2", + "kind": "shape", + "bbox": [ + 4270, + 7320, + 4403, + 7453 + ], + "visible": true + }, + { + "path": "6/2", + "name": "2/3 请右脸出镜", + "kind": "type", + "bbox": [ + 4172, + 5673, + 4500, + 5822 + ], + "visible": true, + "text": "2/3\r请右脸出镜" + }, + { + "path": "6/3", + "name": "拍照指示", + "kind": "group", + "bbox": [ + 3956, + 7096, + 4716, + 7143 + ], + "visible": true + }, + { + "path": "6/3/0", + "name": "组 8", + "kind": "group", + "bbox": [ + 3956, + 7100, + 4716, + 7140 + ], + "visible": true + }, + { + "path": "6/3/0/0", + "name": "右脸居中", + "kind": "type", + "bbox": [ + 3956, + 7100, + 4116, + 7139 + ], + "visible": true, + "text": "右脸居中" + }, + { + "path": "6/3/0/1", + "name": "取下眼镜", + "kind": "type", + "bbox": [ + 4254, + 7100, + 4417, + 7139 + ], + "visible": true, + "text": "取下眼镜" + }, + { + "path": "6/3/0/2", + "name": "光线充足", + "kind": "type", + "bbox": [ + 4553, + 7100, + 4716, + 7140 + ], + "visible": true, + "text": "光线充足" + }, + { + "path": "6/3/1", + "name": "形状 1", + "kind": "shape", + "bbox": [ + 4188, + 7096, + 4192, + 7143 + ], + "visible": true + }, + { + "path": "6/3/2", + "name": "形状 1 拷贝 2", + "kind": "shape", + "bbox": [ + 4486, + 7096, + 4490, + 7143 + ], + "visible": true + }, + { + "path": "6/4", + "name": "屏幕占位", + "kind": "group", + "bbox": [ + 3793, + 5246, + 4879, + 5500 + ], + "visible": true + }, + { + "path": "6/4/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 3793, + 5246, + 4879, + 5500 + ], + "visible": true + }, + { + "path": "6/4/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 3796, + 5252, + 4876, + 5499 + ], + "visible": true + }, + { + "path": "6/4/2", + "name": "1'me在线肌肤测试", + "kind": "type", + "bbox": [ + 4168, + 5425, + 4502, + 5465 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "6/5", + "name": "返回", + "kind": "group", + "bbox": [ + 3820, + 5404, + 3900, + 5484 + ], + "visible": true + }, + { + "path": "6/5/0", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 3851, + 5428, + 3869, + 5461 + ], + "visible": true + }, + { + "path": "6/5/1", + "name": "矩形 6", + "kind": "shape", + "bbox": [ + 3820, + 5404, + 3900, + 5484 + ], + "visible": true + }, + { + "path": "6/6", + "name": "扫描", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "6/6/0", + "name": "扫描", + "kind": "pixel", + "bbox": [ + 3711, + 5500, + 5000, + 7257 + ], + "visible": false + }, + { + "path": "6/6/1", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 3603, + 5342, + 5087, + 7221 + ], + "visible": true + }, + { + "path": "6/7", + "name": "img_v3_0216a_c5b652b0-5aee-4c92-b6c7-8a356b48119g", + "kind": "smartobject", + "bbox": [ + 3910, + 5702, + 4779, + 6885 + ], + "visible": false + }, + { + "path": "6/8", + "name": "形状 559", + "kind": "shape", + "bbox": [ + 3979, + 6351, + 4129, + 6390 + ], + "visible": true + }, + { + "path": "6/9", + "name": "形状 559 拷贝 2", + "kind": "shape", + "bbox": [ + 4227, + 6363, + 4415, + 6402 + ], + "visible": true + }, + { + "path": "6/10", + "name": "形状 562", + "kind": "shape", + "bbox": [ + 4106, + 6455, + 4170, + 6643 + ], + "visible": true + }, + { + "path": "7", + "name": "右面", + "kind": "artboard", + "bbox": [ + 5070, + 5252, + 6150, + 7591 + ], + "visible": true + }, + { + "path": "7/0", + "name": "矩形 7", + "kind": "shape", + "bbox": [ + 5068, + 5244, + 6152, + 7596 + ], + "visible": true + }, + { + "path": "7/1", + "name": "扫描", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "7/1/0", + "name": "扫描", + "kind": "pixel", + "bbox": [ + 4965, + 5747, + 6254, + 6575 + ], + "visible": true + }, + { + "path": "7/1/1", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 4874, + 5498, + 6331, + 7343 + ], + "visible": true + }, + { + "path": "7/2", + "name": "3/3 请左脸出镜", + "kind": "type", + "bbox": [ + 5446, + 5674, + 5774, + 5822 + ], + "visible": true, + "text": "3/3\r请左脸出镜" + }, + { + "path": "7/3", + "name": "屏幕", + "kind": "group", + "bbox": [ + 5067, + 5246, + 6153, + 5500 + ], + "visible": true + }, + { + "path": "7/3/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 5067, + 5246, + 6153, + 5500 + ], + "visible": true + }, + { + "path": "7/3/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 5070, + 5252, + 6150, + 5499 + ], + "visible": true + }, + { + "path": "7/3/2", + "name": "1'me在线肌肤测试", + "kind": "type", + "bbox": [ + 5442, + 5425, + 5776, + 5465 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "7/4", + "name": "返回", + "kind": "group", + "bbox": [ + 5094, + 5404, + 5174, + 5484 + ], + "visible": true + }, + { + "path": "7/4/0", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 5125, + 5428, + 5143, + 5461 + ], + "visible": true + }, + { + "path": "7/4/1", + "name": "矩形 6", + "kind": "shape", + "bbox": [ + 5094, + 5404, + 5174, + 5484 + ], + "visible": true + }, + { + "path": "7/5", + "name": "拍照icon", + "kind": "group", + "bbox": [ + 5232, + 7346, + 5993, + 7425 + ], + "visible": true + }, + { + "path": "7/5/0", + "name": "left", + "kind": "smartobject", + "bbox": [ + 5913, + 7346, + 5993, + 7425 + ], + "visible": true + }, + { + "path": "7/5/1", + "name": "相册", + "kind": "smartobject", + "bbox": [ + 5232, + 7351, + 5314, + 7421 + ], + "visible": true + }, + { + "path": "7/5/2", + "name": "拍照", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "7/5/2/0", + "name": "椭圆 3 拷贝", + "kind": "shape", + "bbox": [ + 5521, + 7297, + 5699, + 7475 + ], + "visible": true + }, + { + "path": "7/5/2/1", + "name": "椭圆 3 拷贝 2", + "kind": "shape", + "bbox": [ + 5544, + 7320, + 5677, + 7453 + ], + "visible": true + }, + { + "path": "7/6", + "name": "拍照指示", + "kind": "group", + "bbox": [ + 5230, + 7098, + 5990, + 7145 + ], + "visible": true + }, + { + "path": "7/6/0", + "name": "组 8", + "kind": "group", + "bbox": [ + 5230, + 7101, + 5990, + 7141 + ], + "visible": true + }, + { + "path": "7/6/0/0", + "name": "左脸居中", + "kind": "type", + "bbox": [ + 5230, + 7101, + 5390, + 7140 + ], + "visible": true, + "text": "左脸居中" + }, + { + "path": "7/6/0/1", + "name": "取下眼镜", + "kind": "type", + "bbox": [ + 5528, + 7101, + 5691, + 7140 + ], + "visible": true, + "text": "取下眼镜" + }, + { + "path": "7/6/0/2", + "name": "光线充足", + "kind": "type", + "bbox": [ + 5827, + 7101, + 5990, + 7141 + ], + "visible": true, + "text": "光线充足" + }, + { + "path": "7/6/1", + "name": "形状 1", + "kind": "shape", + "bbox": [ + 5462, + 7098, + 5466, + 7145 + ], + "visible": true + }, + { + "path": "7/6/2", + "name": "形状 1 拷贝 2", + "kind": "shape", + "bbox": [ + 5760, + 7098, + 5764, + 7145 + ], + "visible": true + }, + { + "path": "7/7", + "name": "扫描", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "7/7/0", + "name": "扫描", + "kind": "pixel", + "bbox": [ + 5064, + 5748, + 6353, + 7505 + ], + "visible": false + }, + { + "path": "7/7/1", + "name": "按钮框", + "kind": "shape", + "bbox": [ + 4978, + 5589, + 6461, + 7468 + ], + "visible": true + }, + { + "path": "7/8", + "name": "形状 559", + "kind": "shape", + "bbox": [ + 5810, + 6360, + 5960, + 6399 + ], + "visible": true + }, + { + "path": "7/9", + "name": "形状 559 拷贝 2", + "kind": "shape", + "bbox": [ + 5524, + 6364, + 5713, + 6403 + ], + "visible": true + }, + { + "path": "7/10", + "name": "形状 562", + "kind": "shape", + "bbox": [ + 5777, + 6450, + 5841, + 6638 + ], + "visible": true + }, + { + "path": "8", + "name": "报告页", + "kind": "artboard", + "bbox": [ + 1237, + 8646, + 2317, + 16411 + ], + "visible": true + }, + { + "path": "8/0", + "name": "背景", + "kind": "group", + "bbox": [ + 1177, + 577, + 6208, + 16749 + ], + "visible": true + }, + { + "path": "8/0/0", + "name": "图层 1", + "kind": "pixel", + "bbox": [ + 1177, + 577, + 6208, + 16749 + ], + "visible": true + }, + { + "path": "8/1", + "name": "产品推荐", + "kind": "group", + "bbox": [ + 1285, + 14707, + 2269, + 16311 + ], + "visible": true + }, + { + "path": "8/1/0", + "name": "产品25+", + "kind": "group", + "bbox": [ + 1285, + 14707, + 2269, + 15210 + ], + "visible": true + }, + { + "path": "8/1/0/0", + "name": "矩形 5 拷贝 19", + "kind": "shape", + "bbox": [ + 1285, + 14707, + 2269, + 15210 + ], + "visible": true + }, + { + "path": "8/1/0/1", + "name": "图片", + "kind": "smartobject", + "bbox": [ + 1765, + 14655, + 2287, + 15307 + ], + "visible": true + }, + { + "path": "8/1/0/2", + "name": "图层 16", + "kind": "pixel", + "bbox": [ + 1765, + 14512, + 2267, + 15404 + ], + "visible": false + }, + { + "path": "8/1/0/3", + "name": "图层 566", + "kind": "smartobject", + "bbox": [ + 1364, + 15039, + 1465, + 15116 + ], + "visible": true + }, + { + "path": "8/1/0/4", + "name": "矩形 569", + "kind": "shape", + "bbox": [ + 1381, + 15067, + 1455, + 15090 + ], + "visible": true + }, + { + "path": "8/1/0/5", + "name": "以我本然有光蓄活 双仓精华液", + "kind": "type", + "bbox": [ + 1362, + 14880, + 1627, + 14962 + ], + "visible": true, + "text": "以我本然有光蓄活\r双仓精华液" + }, + { + "path": "8/1/0/6", + "name": "1'me 主场精华 I", + "kind": "type", + "bbox": [ + 1363, + 14809, + 1653, + 14850 + ], + "visible": true, + "text": "1'me 主场精华 I" + }, + { + "path": "8/1/1", + "name": "产品35+", + "kind": "group", + "bbox": [ + 1285, + 15258, + 2269, + 15761 + ], + "visible": true + }, + { + "path": "8/1/1/0", + "name": "矩形 5 拷贝 19", + "kind": "shape", + "bbox": [ + 1285, + 15258, + 2269, + 15761 + ], + "visible": true + }, + { + "path": "8/1/1/1", + "name": "图片", + "kind": "smartobject", + "bbox": [ + 1772, + 15181, + 2292, + 15831 + ], + "visible": true + }, + { + "path": "8/1/1/2", + "name": "ChatGPT Image 2026年9月17日 19_01_25", + "kind": "smartobject", + "bbox": [ + 1772, + 15191, + 2293, + 15885 + ], + "visible": true + }, + { + "path": "8/1/1/3", + "name": "图层 16", + "kind": "pixel", + "bbox": [ + 1765, + 15063, + 2267, + 15955 + ], + "visible": true + }, + { + "path": "8/1/1/4", + "name": "图层 3", + "kind": "pixel", + "bbox": [ + 1755, + 12458, + 2241, + 13106 + ], + "visible": false + }, + { + "path": "8/1/1/5", + "name": "图层 4", + "kind": "pixel", + "bbox": [ + 1750, + 12435, + 2255, + 13110 + ], + "visible": false + }, + { + "path": "8/1/1/6", + "name": "生物可视年龄 35+岁", + "kind": "type", + "bbox": [ + 1395, + 15673, + 1582, + 15693 + ], + "visible": false, + "text": "生物可视年龄 35+岁" + }, + { + "path": "8/1/1/7", + "name": "图层 566", + "kind": "smartobject", + "bbox": [ + 1364, + 15590, + 1465, + 15667 + ], + "visible": true + }, + { + "path": "8/1/1/8", + "name": "矩形 569", + "kind": "shape", + "bbox": [ + 1381, + 15618, + 1455, + 15641 + ], + "visible": true + }, + { + "path": "8/1/1/9", + "name": "以我自在有势挺括 双仓精华液", + "kind": "type", + "bbox": [ + 1362, + 15431, + 1627, + 15513 + ], + "visible": true, + "text": "以我自在有势挺括\r双仓精华液" + }, + { + "path": "8/1/1/10", + "name": "1'me 主场精华 II", + "kind": "type", + "bbox": [ + 1363, + 15360, + 1665, + 15401 + ], + "visible": true, + "text": "1'me 主场精华 II" + }, + { + "path": "8/1/2", + "name": "产品50+", + "kind": "group", + "bbox": [ + 1285, + 15808, + 2269, + 16311 + ], + "visible": true + }, + { + "path": "8/1/2/0", + "name": "矩形 5 拷贝 19", + "kind": "shape", + "bbox": [ + 1285, + 15808, + 2269, + 16311 + ], + "visible": true + }, + { + "path": "8/1/2/1", + "name": "图片", + "kind": "smartobject", + "bbox": [ + 1772, + 15731, + 2292, + 16381 + ], + "visible": true + }, + { + "path": "8/1/2/2", + "name": "ChatGPT Image 2026年9月17日 19_01_25", + "kind": "smartobject", + "bbox": [ + 1772, + 15741, + 2293, + 16435 + ], + "visible": true + }, + { + "path": "8/1/2/3", + "name": "图层 16", + "kind": "pixel", + "bbox": [ + 1765, + 15613, + 2267, + 16505 + ], + "visible": true + }, + { + "path": "8/1/2/4", + "name": "50岁KV", + "kind": "smartobject", + "bbox": [ + 1765, + 15731, + 2267, + 16400 + ], + "visible": true + }, + { + "path": "8/1/2/5", + "name": "图层 566", + "kind": "smartobject", + "bbox": [ + 1364, + 16140, + 1465, + 16217 + ], + "visible": true + }, + { + "path": "8/1/2/6", + "name": "矩形 569", + "kind": "shape", + "bbox": [ + 1381, + 16168, + 1455, + 16191 + ], + "visible": true + }, + { + "path": "8/1/2/7", + "name": "以我从容有态构塑 双仓精华液", + "kind": "type", + "bbox": [ + 1362, + 15981, + 1627, + 16063 + ], + "visible": true, + "text": "以我从容有态构塑\r双仓精华液" + }, + { + "path": "8/1/2/8", + "name": "1'me 主场精华 III", + "kind": "type", + "bbox": [ + 1363, + 15910, + 1677, + 15951 + ], + "visible": true, + "text": "1'me 主场精华 III" + }, + { + "path": "8/1/3", + "name": "感受高光时刻 从日常的好状态开始", + "kind": "type", + "bbox": [ + 1492, + 14492, + 2086, + 14647 + ], + "visible": false, + "text": "感受高光时刻\r从日常的好状态开始" + }, + { + "path": "8/2", + "name": "护理建议", + "kind": "group", + "bbox": [ + 1286, + 13961, + 2269, + 14414 + ], + "visible": true + }, + { + "path": "8/2/0", + "name": "护理建议", + "kind": "group", + "bbox": [ + 1286, + 13961, + 2269, + 14414 + ], + "visible": true + }, + { + "path": "8/2/0/0", + "name": "矩形 5 拷贝 18", + "kind": "shape", + "bbox": [ + 1286, + 13961, + 2269, + 14414 + ], + "visible": true + }, + { + "path": "8/2/0/1", + "name": "文案", + "kind": "group", + "bbox": [ + 1400, + 14018, + 2153, + 14350 + ], + "visible": true + }, + { + "path": "8/2/0/1/0", + "name": "护理建议", + "kind": "type", + "bbox": [ + 1401, + 14018, + 1564, + 14058 + ], + "visible": true, + "text": "护理建议" + }, + { + "path": "8/2/0/1/1", + "name": "好状态,是内外同调的长期节奏。", + "kind": "type", + "bbox": [ + 1400, + 10978, + 1880, + 11014 + ], + "visible": false, + "text": "好状态,是内外同调的长期节奏。" + }, + { + "path": "8/2/0/1/2", + "name": "1、面部支撑力明显下降,软组织有下移趋势。护肤时请避免用力向下拉扯皮肤,涂抹护肤品时配合由下向上的提拉手法,重点照顾下颌线与苹", + "kind": "type", + "bbox": [ + 1400, + 14089, + 2153, + 14350 + ], + "visible": true, + "text": "1、面部支撑力明显下降,软组织有下移趋势。护肤时请避免用力向下拉扯皮肤,涂抹护肤品时配合由下向上的提拉手法,重点照顾下颌线与苹果肌位置。\r\r2、肤色不均,局部有色素沉积。建议严格防晒,避免紫外线加重色沉;护肤上可尝试温和的美白精华,加速代谢。" + }, + { + "path": "8/3", + "name": "具体描述", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/3/0", + "name": "图层 10", + "kind": "pixel", + "bbox": [ + 1253, + 10133, + 2053, + 10833 + ], + "visible": false + }, + { + "path": "8/4", + "name": "备注小字", + "kind": "group", + "bbox": [ + 1405, + 13866, + 1942, + 13915 + ], + "visible": true + }, + { + "path": "8/4/0", + "name": "矩形 5 拷贝 17", + "kind": "shape", + "bbox": [ + 1309, + 8884, + 2246, + 9302 + ], + "visible": false + }, + { + "path": "8/4/1", + "name": "组 5", + "kind": "group", + "bbox": [ + 1405, + 13866, + 1942, + 13915 + ], + "visible": true + }, + { + "path": "8/4/1/0", + "name": "评分", + "kind": "group", + "bbox": [ + 1405, + 13866, + 1942, + 13915 + ], + "visible": true + }, + { + "path": "8/4/1/0/0", + "name": "评分", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/4/1/0/1", + "name": "年龄", + "kind": "group", + "bbox": [ + 1405, + 13866, + 1942, + 13915 + ], + "visible": true + }, + { + "path": "8/4/1/0/1/0", + "name": "组 10", + "kind": "group", + "bbox": [ + 1405, + 13866, + 1942, + 13915 + ], + "visible": true + }, + { + "path": "8/4/1/0/1/0/0", + "name": "组 17", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/4/1/0/1/0/1", + "name": "组 22", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/4/1/0/1/0/2", + "name": "形状 2", + "kind": "shape", + "bbox": [ + 1449, + 9413, + 2105, + 9417 + ], + "visible": false + }, + { + "path": "8/4/1/0/1/0/3", + "name": "*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。 *本次检测仅代表当前肌肤状态参考。", + "kind": "type", + "bbox": [ + 1405, + 13866, + 1942, + 13915 + ], + "visible": true, + "text": "*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。\r*本次检测仅代表当前肌肤状态参考。" + }, + { + "path": "8/4/1/0/2", + "name": "组 21", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/4/1/0/2/0", + "name": "组 19", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/4/1/0/2/1", + "name": "组 20", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/4/1/0/2/2", + "name": "组 18", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/4/1/1", + "name": "*状态是动态流动的,和作息、压力、季节都有关系,仅代表此刻的状态参考。", + "kind": "type", + "bbox": [ + 1503, + 9232, + 2207, + 9253 + ], + "visible": false, + "text": "*状态是动态流动的,和作息、压力、季节都有关系,仅代表此刻的状态参考。" + }, + { + "path": "8/4/1/2", + "name": "我们从6个维度观察了你的肌肤,当下优先关注:紧致、光泽、纹路、耐受、细腻、水润", + "kind": "type", + "bbox": [ + 1502, + 9129, + 2308, + 9204 + ], + "visible": false, + "text": "我们从6个维度观察了你的肌肤,当下优先关注:紧致、光泽、纹路、耐受、细腻、水润" + }, + { + "path": "8/5", + "name": "3个维度拆解", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "8/6", + "name": "总分", + "kind": "group", + "bbox": [ + 1805, + 8977, + 1810, + 9253 + ], + "visible": true + }, + { + "path": "8/6/0", + "name": "中上水平", + "kind": "type", + "bbox": [ + 1417, + 9644, + 1482, + 9661 + ], + "visible": false, + "text": "中上水平" + }, + { + "path": "8/6/1", + "name": "优秀", + "kind": "type", + "bbox": [ + 1678, + 9644, + 1711, + 9661 + ], + "visible": false, + "text": "优秀" + }, + { + "path": "8/6/2", + "name": "形状 3 拷贝 4", + "kind": "shape", + "bbox": [ + 1425, + 9145, + 1553, + 9149 + ], + "visible": false + }, + { + "path": "8/6/3", + "name": "形状 3 拷贝 5", + "kind": "shape", + "bbox": [ + 1805, + 8977, + 1810, + 9253 + ], + "visible": true + }, + { + "path": "8/7", + "name": "返回", + "kind": "group", + "bbox": [ + 1261, + 8602, + 1341, + 8682 + ], + "visible": true + }, + { + "path": "8/7/0", + "name": "矩形 6", + "kind": "shape", + "bbox": [ + 1261, + 8602, + 1341, + 8682 + ], + "visible": true + }, + { + "path": "8/8", + "name": "组 23", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": false + }, + { + "path": "8/8/0", + "name": "1me以我旗舰店", + "kind": "type", + "bbox": [ + 1636, + 15322, + 1833, + 15350 + ], + "visible": true, + "text": "1me以我旗舰店" + }, + { + "path": "8/8/1", + "name": "图层 12", + "kind": "pixel", + "bbox": [ + 1544, + 15302, + 2011, + 15371 + ], + "visible": true + }, + { + "path": "8/9", + "name": "紧塑力", + "kind": "group", + "bbox": [ + 1233, + 5243, + 6146, + 16669 + ], + "visible": true + }, + { + "path": "8/9/0", + "name": "Life in Full 为更好的生命表现", + "kind": "smartobject", + "bbox": [ + 1233, + 5243, + 6146, + 16669 + ], + "visible": true + }, + { + "path": "8/9/1", + "name": "矩形 5 拷贝 20", + "kind": "shape", + "bbox": [ + 1286, + 9976, + 2269, + 11153 + ], + "visible": true + }, + { + "path": "8/9/2", + "name": "形状 3 拷贝 2", + "kind": "shape", + "bbox": [ + 1403, + 10076, + 2154, + 10081 + ], + "visible": true + }, + { + "path": "8/9/3", + "name": "形状 3 拷贝 3", + "kind": "shape", + "bbox": [ + 1403, + 10944, + 2154, + 10948 + ], + "visible": true + }, + { + "path": "8/9/4", + "name": "紧 致 ", + "kind": "type", + "bbox": [ + 1401, + 10277, + 1432, + 10358 + ], + "visible": false, + "text": "紧\r致 " + }, + { + "path": "8/9/5", + "name": "纹路", + "kind": "type", + "bbox": [ + 1401, + 11095, + 1432, + 11177 + ], + "visible": false, + "text": "纹路" + }, + { + "path": "8/9/6", + "name": "矩形 9", + "kind": "shape", + "bbox": [ + 1471, + 10112, + 2090, + 10914 + ], + "visible": true + }, + { + "path": "8/9/7", + "name": "图层 7", + "kind": "pixel", + "bbox": [ + 1467, + 9682, + 1743, + 10083 + ], + "visible": false + }, + { + "path": "8/9/8", + "name": "图层 9", + "kind": "pixel", + "bbox": [ + 1467, + 10113, + 2109, + 10946 + ], + "visible": true + }, + { + "path": "8/9/9", + "name": "矩形 9 拷贝", + "kind": "shape", + "bbox": [ + 1469, + 11006, + 1737, + 11284 + ], + "visible": false + }, + { + "path": "8/9/10", + "name": "图层 11", + "kind": "pixel", + "bbox": [ + 1471, + 10877, + 1735, + 11228 + ], + "visible": true + }, + { + "path": "8/9/11", + "name": "面部轮廓饱满舒展,下颌线清晰立体,中面部容积充足,肌肤回弹速度快;这种紧致状态,通常和内在胶原合成充足、整体作息节奏平稳直接相", + "kind": "type", + "bbox": [ + 1404, + 10979, + 2135, + 11094 + ], + "visible": true, + "text": "面部轮廓饱满舒展,下颌线清晰立体,中面部容积充足,肌肤回弹速度快;这种紧致状态,通常和内在胶原合成充足、整体作息节奏平稳直接相关。" + }, + { + "path": "8/9/12", + "name": "面部各区域平整舒展,没有明显的纹路沟壑,只有做夸张表情时会出现极浅的动态纹路;纹路的多少,和内在胶原支撑力、日常面部肌肉活动习", + "kind": "type", + "bbox": [ + 1762, + 11040, + 2167, + 11235 + ], + "visible": false, + "text": "面部各区域平整舒展,没有明显的纹路沟壑,只有做夸张表情时会出现极浅的动态纹路;纹路的多少,和内在胶原支撑力、日常面部肌肉活动习惯都有关系。" + }, + { + "path": "8/9/13", + "name": "轮廓", + "kind": "type", + "bbox": [ + 1489, + 10027, + 1553, + 10059 + ], + "visible": true, + "text": "轮廓" + }, + { + "path": "8/9/14", + "name": "皱纹", + "kind": "type", + "bbox": [ + 1989, + 10026, + 2053, + 10058 + ], + "visible": true, + "text": "皱纹" + }, + { + "path": "8/9/15", + "name": "矩形 12 拷贝 2", + "kind": "shape", + "bbox": [ + 1402, + 10068, + 1628, + 10089 + ], + "visible": true + }, + { + "path": "8/9/16", + "name": "稳定力", + "kind": "type", + "bbox": [ + 1990, + 10050, + 2074, + 10078 + ], + "visible": false, + "text": "稳定力" + }, + { + "path": "8/9/17", + "name": "紧塑力", + "kind": "type", + "bbox": [ + 1681, + 9875, + 1871, + 9939 + ], + "visible": true, + "text": "紧塑力" + }, + { + "path": "8/10", + "name": "紧塑力 拷贝", + "kind": "group", + "bbox": [ + 1286, + 5439, + 3273, + 12487 + ], + "visible": true + }, + { + "path": "8/10/0", + "name": "Life in Full 为更好的生命表现", + "kind": "smartobject", + "bbox": [ + 2748, + 5439, + 3273, + 5589 + ], + "visible": true + }, + { + "path": "8/10/1", + "name": "矩形 5 拷贝 20", + "kind": "shape", + "bbox": [ + 1286, + 11310, + 2269, + 12487 + ], + "visible": true + }, + { + "path": "8/10/2", + "name": "形状 3 拷贝 2", + "kind": "shape", + "bbox": [ + 1403, + 11411, + 2154, + 11415 + ], + "visible": true + }, + { + "path": "8/10/3", + "name": "形状 3 拷贝 3", + "kind": "shape", + "bbox": [ + 1403, + 12277, + 2154, + 12282 + ], + "visible": true + }, + { + "path": "8/10/4", + "name": "紧 致 ", + "kind": "type", + "bbox": [ + 1401, + 11683, + 1432, + 11764 + ], + "visible": false, + "text": "紧\r致 " + }, + { + "path": "8/10/5", + "name": "纹路", + "kind": "type", + "bbox": [ + 1401, + 12501, + 1432, + 12583 + ], + "visible": false, + "text": "纹路" + }, + { + "path": "8/10/6", + "name": "矩形 9", + "kind": "shape", + "bbox": [ + 1471, + 11446, + 2090, + 12248 + ], + "visible": true + }, + { + "path": "8/10/7", + "name": "矩形 9 拷贝 2", + "kind": "shape", + "bbox": [ + 1471, + 11446, + 2090, + 12248 + ], + "visible": true + }, + { + "path": "8/10/8", + "name": "图层 7", + "kind": "pixel", + "bbox": [ + 1439, + 11379, + 2113, + 12358 + ], + "visible": true + }, + { + "path": "8/10/9", + "name": "图层 9", + "kind": "pixel", + "bbox": [ + 1467, + 11447, + 2109, + 12280 + ], + "visible": true + }, + { + "path": "8/10/10", + "name": "图层 13", + "kind": "pixel", + "bbox": [ + 1462, + 11428, + 2092, + 12266 + ], + "visible": true + }, + { + "path": "8/10/11", + "name": "矩形 9 拷贝", + "kind": "shape", + "bbox": [ + 1469, + 12412, + 1737, + 12690 + ], + "visible": false + }, + { + "path": "8/10/12", + "name": "图层 11", + "kind": "pixel", + "bbox": [ + 1471, + 12280, + 1735, + 12631 + ], + "visible": true + }, + { + "path": "8/10/13", + "name": "面部轮廓饱满舒展,下颌线清晰立体,中面部容积充足,肌肤回弹速度快;这种紧致状态,通常和内在胶原合成充足、整体作息节奏平稳直接相", + "kind": "type", + "bbox": [ + 1404, + 12313, + 2135, + 12428 + ], + "visible": true, + "text": "面部轮廓饱满舒展,下颌线清晰立体,中面部容积充足,肌肤回弹速度快;这种紧致状态,通常和内在胶原合成充足、整体作息节奏平稳直接相关。" + }, + { + "path": "8/10/14", + "name": "面部各区域平整舒展,没有明显的纹路沟壑,只有做夸张表情时会出现极浅的动态纹路;纹路的多少,和内在胶原支撑力、日常面部肌肉活动习", + "kind": "type", + "bbox": [ + 1762, + 12446, + 2167, + 12641 + ], + "visible": false, + "text": "面部各区域平整舒展,没有明显的纹路沟壑,只有做夸张表情时会出现极浅的动态纹路;纹路的多少,和内在胶原支撑力、日常面部肌肉活动习惯都有关系。" + }, + { + "path": "8/10/15", + "name": " 肤色", + "kind": "type", + "bbox": [ + 1494, + 11361, + 1558, + 11393 + ], + "visible": true, + "text": " 肤色" + }, + { + "path": "8/10/16", + "name": "水分", + "kind": "type", + "bbox": [ + 1990, + 11360, + 2053, + 11392 + ], + "visible": true, + "text": "水分" + }, + { + "path": "8/10/17", + "name": "平滑度", + "kind": "type", + "bbox": [ + 1734, + 11360, + 1831, + 11392 + ], + "visible": true, + "text": "平滑度" + }, + { + "path": "8/10/18", + "name": "矩形 12 拷贝 2", + "kind": "shape", + "bbox": [ + 1402, + 11401, + 1628, + 11423 + ], + "visible": true + }, + { + "path": "8/10/19", + "name": "稳定力", + "kind": "type", + "bbox": [ + 1990, + 11456, + 2074, + 11484 + ], + "visible": false, + "text": "稳定力" + }, + { + "path": "8/10/20", + "name": "透亮感", + "kind": "type", + "bbox": [ + 1681, + 11215, + 1875, + 11279 + ], + "visible": true, + "text": "透亮感" + }, + { + "path": "8/11", + "name": "紧塑力 拷贝 2", + "kind": "group", + "bbox": [ + 1286, + 6827, + 3273, + 14643 + ], + "visible": true + }, + { + "path": "8/11/0", + "name": "Life in Full 为更好的生命表现", + "kind": "smartobject", + "bbox": [ + 2748, + 6827, + 3273, + 6977 + ], + "visible": true + }, + { + "path": "8/11/1", + "name": "矩形 5 拷贝 20", + "kind": "shape", + "bbox": [ + 1286, + 12657, + 2269, + 13834 + ], + "visible": true + }, + { + "path": "8/11/2", + "name": "形状 3 拷贝 2", + "kind": "shape", + "bbox": [ + 1403, + 12758, + 2154, + 12762 + ], + "visible": true + }, + { + "path": "8/11/3", + "name": "形状 3 拷贝 3", + "kind": "shape", + "bbox": [ + 1403, + 13624, + 2154, + 13629 + ], + "visible": true + }, + { + "path": "8/11/4", + "name": "紧 致 ", + "kind": "type", + "bbox": [ + 1401, + 13071, + 1432, + 13152 + ], + "visible": false, + "text": "紧\r致 " + }, + { + "path": "8/11/5", + "name": "纹路", + "kind": "type", + "bbox": [ + 1401, + 13889, + 1432, + 13971 + ], + "visible": false, + "text": "纹路" + }, + { + "path": "8/11/6", + "name": "矩形 9", + "kind": "shape", + "bbox": [ + 1471, + 12793, + 2090, + 13595 + ], + "visible": true + }, + { + "path": "8/11/7", + "name": "图层 7", + "kind": "pixel", + "bbox": [ + 1467, + 12476, + 1743, + 12877 + ], + "visible": false + }, + { + "path": "8/11/8", + "name": "图层 9", + "kind": "pixel", + "bbox": [ + 1467, + 12794, + 2109, + 13627 + ], + "visible": true + }, + { + "path": "8/11/9", + "name": "矩形 9 拷贝", + "kind": "shape", + "bbox": [ + 1469, + 13800, + 1737, + 14078 + ], + "visible": false + }, + { + "path": "8/11/10", + "name": "图层 11", + "kind": "pixel", + "bbox": [ + 1471, + 13677, + 1735, + 14028 + ], + "visible": true + }, + { + "path": "8/11/11", + "name": "面部轮廓饱满舒展,下颌线清晰立体,中面部容积充足,肌肤回弹速度快;这种紧致状态,通常和内在胶原合成充足、整体作息节奏平稳直接相", + "kind": "type", + "bbox": [ + 1404, + 13660, + 2135, + 13775 + ], + "visible": true, + "text": "面部轮廓饱满舒展,下颌线清晰立体,中面部容积充足,肌肤回弹速度快;这种紧致状态,通常和内在胶原合成充足、整体作息节奏平稳直接相关。" + }, + { + "path": "8/11/12", + "name": "面部各区域平整舒展,没有明显的纹路沟壑,只有做夸张表情时会出现极浅的动态纹路;纹路的多少,和内在胶原支撑力、日常面部肌肉活动习", + "kind": "type", + "bbox": [ + 1762, + 13834, + 2167, + 14029 + ], + "visible": false, + "text": "面部各区域平整舒展,没有明显的纹路沟壑,只有做夸张表情时会出现极浅的动态纹路;纹路的多少,和内在胶原支撑力、日常面部肌肉活动习惯都有关系。" + }, + { + "path": "8/11/13", + "name": "泛红", + "kind": "type", + "bbox": [ + 1489, + 12708, + 1553, + 12739 + ], + "visible": true, + "text": "泛红" + }, + { + "path": "8/11/14", + "name": "痘痘", + "kind": "type", + "bbox": [ + 1989, + 12707, + 2053, + 12739 + ], + "visible": true, + "text": "痘痘" + }, + { + "path": "8/11/15", + "name": "矩形 12 拷贝 2", + "kind": "shape", + "bbox": [ + 1402, + 12748, + 1628, + 12770 + ], + "visible": true + }, + { + "path": "8/11/16", + "name": "稳定力", + "kind": "type", + "bbox": [ + 1990, + 12844, + 2074, + 12872 + ], + "visible": false, + "text": "稳定力" + }, + { + "path": "8/11/17", + "name": "稳韧力", + "kind": "type", + "bbox": [ + 1680, + 12559, + 1871, + 12623 + ], + "visible": true, + "text": "稳韧力" + }, + { + "path": "8/11/18", + "name": "感受高光时刻 从日常的好状态开始", + "kind": "type", + "bbox": [ + 1480, + 14492, + 2075, + 14643 + ], + "visible": true, + "text": "感受高光时刻\r从日常的好状态开始" + }, + { + "path": "8/12", + "name": "评分", + "kind": "group", + "bbox": [ + 1286, + 9114, + 2268, + 9810 + ], + "visible": true + }, + { + "path": "8/12/0", + "name": "按钮框 拷贝", + "kind": "shape", + "bbox": [ + 1286, + 9114, + 2268, + 9810 + ], + "visible": true + }, + { + "path": "8/12/1", + "name": "综合评分", + "kind": "type", + "bbox": [ + 1483, + 9352, + 1614, + 9384 + ], + "visible": true, + "text": "综合评分" + }, + { + "path": "8/12/2", + "name": "82分", + "kind": "type", + "bbox": [ + 1472, + 9253, + 1635, + 9338 + ], + "visible": true, + "text": "82分" + }, + { + "path": "8/12/3", + "name": "28岁", + "kind": "type", + "bbox": [ + 1910, + 9283, + 2152, + 9411 + ], + "visible": true, + "text": "28岁" + }, + { + "path": "8/12/4", + "name": "生物可视年龄", + "kind": "type", + "bbox": [ + 1907, + 9229, + 2103, + 9261 + ], + "visible": true, + "text": "生物可视年龄" + }, + { + "path": "8/12/5", + "name": "图层 18", + "kind": "smartobject", + "bbox": [ + 1394, + 9160, + 1706, + 9472 + ], + "visible": true + }, + { + "path": "8/12/6", + "name": "图层 19", + "kind": "smartobject", + "bbox": [ + 1395, + 9160, + 1705, + 9472 + ], + "visible": true + }, + { + "path": "8/12/7", + "name": "组 24", + "kind": "group", + "bbox": [ + 1349, + 9158, + 2205, + 9765 + ], + "visible": true + }, + { + "path": "8/12/7/0", + "name": "矩形 14 拷贝", + "kind": "shape", + "bbox": [ + 2001, + 9724, + 2148, + 9765 + ], + "visible": true + }, + { + "path": "8/12/7/1", + "name": "优先关注", + "kind": "type", + "bbox": [ + 2033, + 9734, + 2116, + 9754 + ], + "visible": true, + "text": "优先关注" + }, + { + "path": "8/12/7/2", + "name": "7.2", + "kind": "type", + "bbox": [ + 1350, + 9590, + 1471, + 9672 + ], + "visible": true, + "text": "7.2" + }, + { + "path": "8/12/7/3", + "name": "形状 3", + "kind": "shape", + "bbox": [ + 1616, + 9519, + 1621, + 9729 + ], + "visible": true + }, + { + "path": "8/12/7/4", + "name": "形状 3 拷贝 7", + "kind": "shape", + "bbox": [ + 1803, + 9158, + 1808, + 9473 + ], + "visible": true + }, + { + "path": "8/12/7/5", + "name": "形状 3 拷贝", + "kind": "shape", + "bbox": [ + 1942, + 9519, + 1947, + 9729 + ], + "visible": true + }, + { + "path": "8/12/7/6", + "name": "紧塑力", + "kind": "type", + "bbox": [ + 1350, + 9537, + 1444, + 9569 + ], + "visible": true, + "text": "紧塑力" + }, + { + "path": "8/12/7/7", + "name": "矩形 14", + "kind": "shape", + "bbox": [ + 1349, + 9691, + 1551, + 9710 + ], + "visible": true + }, + { + "path": "8/12/7/8", + "name": "矩形 14 拷贝", + "kind": "shape", + "bbox": [ + 1349, + 9691, + 1491, + 9710 + ], + "visible": true + }, + { + "path": "8/12/7/9", + "name": "8.4", + "kind": "type", + "bbox": [ + 1677, + 9589, + 1810, + 9674 + ], + "visible": true, + "text": "8.4" + }, + { + "path": "8/12/7/10", + "name": "透亮感", + "kind": "type", + "bbox": [ + 1678, + 9537, + 1775, + 9569 + ], + "visible": true, + "text": "透亮感" + }, + { + "path": "8/12/7/11", + "name": "矩形 14", + "kind": "shape", + "bbox": [ + 1677, + 9691, + 1879, + 9710 + ], + "visible": true + }, + { + "path": "8/12/7/12", + "name": "矩形 14 拷贝", + "kind": "shape", + "bbox": [ + 1677, + 9691, + 1849, + 9710 + ], + "visible": true + }, + { + "path": "8/12/7/13", + "name": "5.1", + "kind": "type", + "bbox": [ + 2004, + 9590, + 2113, + 9674 + ], + "visible": true, + "text": "5.1" + }, + { + "path": "8/12/7/14", + "name": "稳韧力", + "kind": "type", + "bbox": [ + 2004, + 9537, + 2099, + 9569 + ], + "visible": true, + "text": "稳韧力" + }, + { + "path": "8/12/7/15", + "name": "矩形 14", + "kind": "shape", + "bbox": [ + 2004, + 9691, + 2205, + 9710 + ], + "visible": true + }, + { + "path": "8/12/7/16", + "name": "矩形 14 拷贝", + "kind": "shape", + "bbox": [ + 2004, + 9691, + 2110, + 9710 + ], + "visible": true + }, + { + "path": "8/13", + "name": "您的肌肤状态表现)", + "kind": "type", + "bbox": [ + 1528, + 8991, + 2055, + 9055 + ], + "visible": true, + "text": "您的肌肤状态表现)" + }, + { + "path": "8/14", + "name": "屏幕", + "kind": "group", + "bbox": [ + 1234, + 8645, + 2320, + 8899 + ], + "visible": true + }, + { + "path": "8/14/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + 1234, + 8645, + 2320, + 8899 + ], + "visible": true + }, + { + "path": "8/14/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 1237, + 8651, + 2317, + 8898 + ], + "visible": true + }, + { + "path": "8/14/2", + "name": "1'me肌肤状态检测 ", + "kind": "type", + "bbox": [ + 1609, + 8824, + 1943, + 8864 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "8/14/3", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 1292, + 8826, + 1310, + 8859 + ], + "visible": true + }, + { + "path": "9", + "name": "登录", + "kind": "artboard", + "bbox": [ + 0, + 0, + 1080, + 2339 + ], + "visible": true + }, + { + "path": "9/0", + "name": "年龄", + "kind": "group", + "bbox": [ + -170, + -93, + 4926, + 11581 + ], + "visible": true + }, + { + "path": "9/0/0", + "name": "背景", + "kind": "group", + "bbox": [ + -170, + -93, + 4926, + 11581 + ], + "visible": true + }, + { + "path": "9/0/0/0", + "name": "图层 526", + "kind": "pixel", + "bbox": [ + 0, + 0, + 1080, + 2339 + ], + "visible": true + }, + { + "path": "9/0/0/1", + "name": "背景 拷贝 3", + "kind": "smartobject", + "bbox": [ + -170, + -10, + 4926, + 11581 + ], + "visible": true + }, + { + "path": "9/0/0/2", + "name": "矩形 3", + "kind": "shape", + "bbox": [ + -50, + -93, + 1145, + 2368 + ], + "visible": true + }, + { + "path": "9/0/1", + "name": "按钮", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "9/1", + "name": "地区", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "9/2", + "name": "性别", + "kind": "group", + "bbox": [ + 0, + 0, + 0, + 0 + ], + "visible": true + }, + { + "path": "9/3", + "name": "椭圆 1", + "kind": "shape", + "bbox": [ + 239, + 2023, + 274, + 2058 + ], + "visible": true + }, + { + "path": "9/4", + "name": "同意用户协议、隐私政策和儿童保护政策", + "kind": "type", + "bbox": [ + 304, + 2026, + 826, + 2059 + ], + "visible": true, + "text": "同意用户协议、隐私政策和儿童保护政策" + }, + { + "path": "9/5", + "name": "屏幕占位", + "kind": "group", + "bbox": [ + -3, + -6, + 1083, + 248 + ], + "visible": true + }, + { + "path": "9/5/0", + "name": "矩形 2", + "kind": "shape", + "bbox": [ + -3, + -6, + 1083, + 248 + ], + "visible": true + }, + { + "path": "9/5/1", + "name": "图层 4 拷贝", + "kind": "pixel", + "bbox": [ + 0, + 0, + 1080, + 247 + ], + "visible": true + }, + { + "path": "9/5/2", + "name": "1'me肌肤状态检测 ", + "kind": "type", + "bbox": [ + 372, + 173, + 706, + 213 + ], + "visible": true, + "text": "1'me肌肤状态检测\r" + }, + { + "path": "9/6", + "name": "返回", + "kind": "group", + "bbox": [ + 24, + 152, + 104, + 232 + ], + "visible": true + }, + { + "path": "9/6/0", + "name": "下拉", + "kind": "smartobject", + "bbox": [ + 55, + 176, + 73, + 209 + ], + "visible": true + }, + { + "path": "9/6/1", + "name": "矩形 6", + "kind": "shape", + "bbox": [ + 24, + 152, + 104, + 232 + ], + "visible": true + }, + { + "path": "9/7", + "name": "组 1", + "kind": "group", + "bbox": [ + 155, + 513, + 925, + 1953 + ], + "visible": true + }, + { + "path": "9/7/0", + "name": "矩形 5", + "kind": "shape", + "bbox": [ + 155, + 1688, + 925, + 1804 + ], + "visible": true + }, + { + "path": "9/7/1", + "name": "图层 550", + "kind": "smartobject", + "bbox": [ + 330, + 513, + 749, + 602 + ], + "visible": true + }, + { + "path": "9/7/2", + "name": "图层 551", + "kind": "smartobject", + "bbox": [ + 443, + 1333, + 637, + 1483 + ], + "visible": false + }, + { + "path": "9/7/3", + "name": "矩形 5 拷贝", + "kind": "shape", + "bbox": [ + 155, + 1835, + 925, + 1953 + ], + "visible": true + }, + { + "path": "9/7/4", + "name": "微信登录", + "kind": "type", + "bbox": [ + 479, + 1728, + 643, + 1767 + ], + "visible": true, + "text": "微信登录" + }, + { + "path": "9/7/5", + "name": "游客登录", + "kind": "type", + "bbox": [ + 480, + 1876, + 643, + 1915 + ], + "visible": true, + "text": "游客登录" + }, + { + "path": "9/7/6", + "name": "9d8063d8012f839a7c5ab1f0366ca5d7717c9f9a905-seG7pC_fw1200webp", + "kind": "smartobject", + "bbox": [ + 380, + 1724, + 445, + 1776 + ], + "visible": true + }, + { + "path": "9/7/7", + "name": "52e66a7d0a88f23305ddc3618535c77ee3b79bbe30c-n21Vnn_fw1200webp", + "kind": "smartobject", + "bbox": [ + 381, + 1868, + 444, + 1918 + ], + "visible": true + } + ] +} diff --git a/design/ui-20261009/pages-overview.jpg b/design/ui-20261009/pages-overview.jpg new file mode 100644 index 0000000..888de6c Binary files /dev/null and b/design/ui-20261009/pages-overview.jpg differ diff --git a/design/ui-20261009/pages/01-启动页.png b/design/ui-20261009/pages/01-启动页.png new file mode 100644 index 0000000..528fe56 Binary files /dev/null and b/design/ui-20261009/pages/01-启动页.png differ diff --git a/design/ui-20261009/pages/02-启动页弹窗.png b/design/ui-20261009/pages/02-启动页弹窗.png new file mode 100644 index 0000000..e01a535 Binary files /dev/null and b/design/ui-20261009/pages/02-启动页弹窗.png differ diff --git a/design/ui-20261009/pages/03-我的报告.png b/design/ui-20261009/pages/03-我的报告.png new file mode 100644 index 0000000..8d2e75b Binary files /dev/null and b/design/ui-20261009/pages/03-我的报告.png differ diff --git a/design/ui-20261009/pages/04-档案填写.png b/design/ui-20261009/pages/04-档案填写.png new file mode 100644 index 0000000..34c4601 Binary files /dev/null and b/design/ui-20261009/pages/04-档案填写.png differ diff --git a/design/ui-20261009/pages/05-拍照弹窗.png b/design/ui-20261009/pages/05-拍照弹窗.png new file mode 100644 index 0000000..6cd6548 Binary files /dev/null and b/design/ui-20261009/pages/05-拍照弹窗.png differ diff --git a/design/ui-20261009/pages/06-正面.png b/design/ui-20261009/pages/06-正面.png new file mode 100644 index 0000000..40b097e Binary files /dev/null and b/design/ui-20261009/pages/06-正面.png differ diff --git a/design/ui-20261009/pages/07-左面.png b/design/ui-20261009/pages/07-左面.png new file mode 100644 index 0000000..91bb41e Binary files /dev/null and b/design/ui-20261009/pages/07-左面.png differ diff --git a/design/ui-20261009/pages/08-右面.png b/design/ui-20261009/pages/08-右面.png new file mode 100644 index 0000000..ee5338f Binary files /dev/null and b/design/ui-20261009/pages/08-右面.png differ diff --git a/design/ui-20261009/pages/09-报告页.png b/design/ui-20261009/pages/09-报告页.png new file mode 100644 index 0000000..1a28e2f Binary files /dev/null and b/design/ui-20261009/pages/09-报告页.png differ diff --git a/design/ui-20261009/pages/10-登录.png b/design/ui-20261009/pages/10-登录.png new file mode 100644 index 0000000..1c847c8 Binary files /dev/null and b/design/ui-20261009/pages/10-登录.png differ diff --git a/design/ui-20261009/verification/01-login.png b/design/ui-20261009/verification/01-login.png new file mode 100644 index 0000000..3c1876d Binary files /dev/null and b/design/ui-20261009/verification/01-login.png differ diff --git a/design/ui-20261009/verification/02-home.png b/design/ui-20261009/verification/02-home.png new file mode 100644 index 0000000..de133a3 Binary files /dev/null and b/design/ui-20261009/verification/02-home.png differ diff --git a/design/ui-20261009/verification/03-history-empty.png b/design/ui-20261009/verification/03-history-empty.png new file mode 100644 index 0000000..5d6d5a1 Binary files /dev/null and b/design/ui-20261009/verification/03-history-empty.png differ diff --git a/design/ui-20261009/verification/04-privacy.png b/design/ui-20261009/verification/04-privacy.png new file mode 100644 index 0000000..1ff9938 Binary files /dev/null and b/design/ui-20261009/verification/04-privacy.png differ diff --git a/design/ui-20261009/verification/05-profile.png b/design/ui-20261009/verification/05-profile.png new file mode 100644 index 0000000..5d35874 Binary files /dev/null and b/design/ui-20261009/verification/05-profile.png differ diff --git a/design/ui-20261009/verification/06-report-top.png b/design/ui-20261009/verification/06-report-top.png new file mode 100644 index 0000000..c0bcef5 Binary files /dev/null and b/design/ui-20261009/verification/06-report-top.png differ diff --git a/design/ui-20261009/verification/07-report-full.png b/design/ui-20261009/verification/07-report-full.png new file mode 100644 index 0000000..bb0f76e Binary files /dev/null and b/design/ui-20261009/verification/07-report-full.png differ diff --git a/design/ui-20261009/verification/08-camera-front-layout.png b/design/ui-20261009/verification/08-camera-front-layout.png new file mode 100644 index 0000000..663b119 Binary files /dev/null and b/design/ui-20261009/verification/08-camera-front-layout.png differ diff --git a/design/ui-20261009/verification/08-camera-left-layout.png b/design/ui-20261009/verification/08-camera-left-layout.png new file mode 100644 index 0000000..ee9ce51 Binary files /dev/null and b/design/ui-20261009/verification/08-camera-left-layout.png differ diff --git a/design/ui-20261009/verification/08-camera-right-layout.png b/design/ui-20261009/verification/08-camera-right-layout.png new file mode 100644 index 0000000..52d6eb3 Binary files /dev/null and b/design/ui-20261009/verification/08-camera-right-layout.png differ diff --git a/docs/10.09新版H5实施与验收.md b/docs/10.09新版H5实施与验收.md new file mode 100644 index 0000000..cc9773d --- /dev/null +++ b/docs/10.09新版H5实施与验收.md @@ -0,0 +1,55 @@ +# 10.09 新版 H5 实施与验收 + +更新:2026-10-10。 + +## 按确认项实现 + +- 保持 H5,无额外网页导航栏,不仿造系统/微信顶部栏。 +- 登录在首页之前;复用现有协议弹层。微信网页授权链路已实现,当前内网使用本机浏览器身份,公网限制与部署见《微信登录接入与公网部署说明》。 +- 首页更新口号、背景和品牌图,增加「我的报告」。修正重复入口、出镜错字。 +- 我的报告独立页面,数量、日期、查看详情、空/失败状态;读取当前账号最近 5 份后端报告。 +- 档案页换用新版背景和五类肤质插画,保留原选择器和校验。 +- 相机轮廓与眼鼻定位虚线从 PSD 素材提取,前→右→左流程、自动抓拍、降级、语音和前后摄像头逻辑保持。 +- 汇总卡合并,名称为「生物可视年龄」「紧塑力」「透亮感」「稳韧力」,不照抄设计样例中的数值。 +- 七项视图分成三个独立 Tab 组:轮廓/皱纹;肤色/平滑度/水分;泛红/痘痘。使用真实检测图层或用户照片,不把设计里的模特检测图当成检测结果。 +- 根据 `reference/AI测肤报告页话术修改.md` 生成新结论和护理建议。无效字段不冒充 0,无图层明确说明。 +- 只显示原算法选中的一款主场精华 I / II / III,不可点击,移除「了解更多」占位入口。 +- MiSans 官方原始 Web 字体本地提供,未改造字体文件;「我的报告」底部注明字体使用。 + +## 文档未定义部分的实施口径 + +话术文档没有新增计分公式或阈值,因此原六分类/三维度评分、肌龄计算及精华推荐规则不变。旧分档「预警/改善/良好」按低/中/高顺序映射为新文案的「改善/良好/优秀」。肤色和水分拆分展示,但继续共享原 `tone` 分类的分档,**不是新增独立评分算法**。 + +痘痘话术按真实脓包/结节计数决定是否输出中重度皮损说明;没有计数不宣称「未检出」。当真实计数与高分模板冲突时,展示层选择可反映计数的文案,不改变原分数。 + +护理建议按原六分类得分选取最需要关注的两项,拼接文档对应的「护理 + 生活方式」;若命中肤色水润,分别展示肤色与水分建议,可能为三段。设计里的示例两段不作为截断真实文本的硬限制。 + +历史详情读取保存时的 `vm`、`presentation`、照片、图层,不用当前评分/话术重新生成历史数据。图像合成有渲染版本防护,切换报告时不会串用上一份图片。 + +## 验证命令 + +```bash +node h5/js/skin-score.js +node h5/test/regression.js +node h5/test/accounts.js +node h5/test/report-persistence.mjs +node h5/test/wechat-browser.mjs +node h5/test/ui-20261009.mjs +node h5/test/e2e-browser.mjs +``` + +浏览器测试需要全局 Playwright 与 Chromium,沿用项目既有环境。`ui-20261009.mjs` 默认访问本地 8080,可用 `H5_BASE_URL` 覆盖;`report-persistence.mjs` 单独启用 18089 临时服务和临时数据目录,不写入日常报告,不调用收费 Face++。 + +本轮结果:评分模块自测 83 条、真值回归 86 条通过;账号隔离、恢复登录、重启持久化、最近 5 条、OAuth state 防重放/子路径检查通过;模拟上游的完整 HTTP 链路验证七张图片及文案刷新回看完全一致;三连拍与语音端到端检查通过;320/390/480px 宽度无横向溢出,浏览器无脚本异常。 + +截图位于 `design/ui-20261009/verification/`。这些是本地 Chromium/样本数据检查,不等于真实手机摄像头或微信公网账号验收。本轮没有触发真实 Face++ 收费请求。 + +## 素材再生成 + +```bash +python tools/export_psd_assets.py /你的路径/UI外发版10.09.psd +python tools/prepare_h5_assets.py +python tools/build_report_copy.py +``` + +PSD 导出依赖 psd-tools/Pillow/aggdraw;H5 裁剪与相机轮廓提取还使用 numpy/opencv-python。原始导出保留,`*-h5.jpg` 是去除原生顶栏后的衍生背景;相机几何写入 `h5/js/camera-geometry.js`。 diff --git a/docs/2026-10-11提交前Review.md b/docs/2026-10-11提交前Review.md new file mode 100644 index 0000000..03a6f7d --- /dev/null +++ b/docs/2026-10-11提交前Review.md @@ -0,0 +1,25 @@ +# 提交前 Review(2026-10-11) + +范围:10.09 H5 UI、浏览器/微信身份、后端报告快照、部署说明;重点检查公网调试前的访问控制和回调路径。 + +## 已发现并修复 + +1. **P1:静态敏感路径可被重复/编码斜杠绕过。** 原先先按原始路径检查 `/data`、`/lib`、`/server.js`,文件读取时才规范化;`/%2fserver.js` 实测返回 200。现统一斜杠并规范化后再授权,额外禁止访问 `JUZI_DATA_ROOT`。新增原始 HTTP 路径测试,覆盖双斜杠、编码斜杠、反斜杠和父目录段;修复后源码路径返回 404,数据目录返回拒绝响应。 +2. **P2:nginx 后的用户共用登录限流。** 原限流只看 TCP 对端,代理后全站共用回环地址的每分钟 60 次额度。新增显式 `TRUST_LOOPBACK_PROXY=1`,仅信任回环代理覆盖的合法 `X-Real-IP`;文档同步 nginx 配置。已测试不同 IP 额度独立,以及未启用信任时伪造请求头不能绕过限流。 + +## 验证 + +- 评分模块自测:83 通过;真值回归:86 通过。 +- 账号/报告:权限隔离、最近 5 份、重启恢复、浏览器凭据恢复、会话退出、OAuth state 绑定和重放检查通过。 +- 完整检测 API→保存→回看:使用模拟上游,不调用收费 Face++;七张图片和话术刷新后完全一致。 +- UI 与三连拍:协议校验、登录回退、移动端布局、三视角流程、语音均通过。 +- `wechat-browser.mjs`:使用临时 HTTPS 证书和 `/state/` 反代路径,验证前端微信按钮→模拟授权站点→服务端回调→Secure/HttpOnly/SameSite Cookie→首页/报告。两个独立浏览器上下文关联同一模拟 OpenID。此测试需要本地 OpenSSL 和 Playwright。 + +## 仍需公网/业务确认 + +- 未使用真实微信 AppID/AppSecret 联调,公众号权限、网页授权域名及真实手机回调需在公网验收。 +- Node 管理接口仍依赖 nginx Basic Auth,公网只能反代访问,不能暴露 Node 端口。文件存储仅支持单进程。 +- 浏览器与微信账号不自动合并;先用本机身份产生的报告不会自动出现在微信账号中。 +- 已有协议是 Face++ 隐私政策,品牌方账号关联、报告保留/删除范围仍需业务确认;不应将当前版本视作已完成正式发布合规审核。 + +提交范围不含真实密钥、`h5/data`、PSD 原稿及 Windows Zone.Identifier 元数据。代码、运行素材、文案来源、测试和部署说明一起提交,便于服务器复现。 diff --git a/docs/UI对齐10.09设计稿-调研与实施计划.md b/docs/UI对齐10.09设计稿-调研与实施计划.md new file mode 100644 index 0000000..de99e9a --- /dev/null +++ b/docs/UI对齐10.09设计稿-调研与实施计划.md @@ -0,0 +1,195 @@ +# UI 对齐 10.09 设计稿:调研与实施计划 + +调研日期:2026-10-10。阶段:调研与计划,尚未修改业务页面。 + +## 1. 结论和依据 + +这次更新包括三个层面:既有页面的视觉对齐、报告页的结构重做、新增登录与报告列表流程。仅替换图片不能达到设计稿效果。 + +依据: + +- 最新 PSD:`UI外发版10.09.psd`,10 个画板;采用 `design/ui-20261009/manifest.json` 的图层与文本信息,不再把旧 `design/设计规范.md` 当作新版尺寸依据。 +- 实际运行页面:`http://127.0.0.1:8080/?mock=1`。在独立 Chromium 上下文采集启动、授权、档案、拍照引导、三个拍照步骤和报告三组 Tab;另检查 375/390/414px 宽启动页。 +- 实测记录:`design/ui-20261009/audit-current/evidence.json`,本轮 15 张截图,未捕获到 JS 页面异常。拍照截图使用 mock/模拟摄像头,不能据此认定手机真实摄像头已通过验证。 +- 源码:`h5/index.html`、`h5/css/style.css`、`h5/js/app.js`、`h5/js/skin-score.js`、`h5/server.js` 和浏览器回归脚本。 +- 视觉对比:`design/ui-20261009/audit-current/comparison-flow.jpg` 与 `comparison-report.jpg`。每组左侧当前页面、右侧新设计;报告采用不同示例数据,只比较结构与样式,不比较数值与照片内容。 + +当前技术栈是原生 HTML/CSS/JS + Node 服务。继续沿用即可,没有必要为此次还原引入前端框架。 + +## 2. 统一比较口径 + +PSD 标准画板为 1080×2339,报告长页为 1080×7765。375px 宽页面的换算系数为 2.88。 + +设计顶部约 248px 是系统状态栏与微信标题/胶囊区域。当前 H5 将这块交给浏览器容器,业务页面没有顶部导航。因此本次常规页面对比裁掉该区域,统一为约 375×726 的业务视口;报告顶部按其自身画板标题栏边界约 253px 处理。 + +建议以「H5 业务内容还原」验收,不在网页里重复绘制电量、时间、信号和小程序胶囊。是否增加网页自己的标题/返回栏需要单独确定;新增一条导航栏会改变所有页面的纵向坐标,不能等页面完成后再决定。 + +设计实测尺寸(375px 宽基准): + +| 元素 | PSD 尺寸 | 换算后尺寸 | 更新要点 | +| --- | --- | --- | --- | +| 首页主/次按钮 | 522×112 | 181.3×38.9 | 当前主按钮高 37px,需统一到新版 | +| 弹窗 | 887×902 | 308.0×313.2 | 统一位置、内边距与文案行距 | +| 弹窗关闭图标 | 51×50 | 17.7×17.4 | 图形按设计大小,触摸区域单独扩大 | +| 档案单列输入框 | 325×112 | 112.8×38.9 | 对齐双列间距、标签间距 | +| 肤质选择卡 | 713×808 | 247.6×280.6 | 配合新插画和文字宽度 | +| 肤质插画 | 316×316 | 109.7×109.7 | 五种状态均已导出 | +| 报告汇总卡 | 982×696 | 341.0×241.7 | 合并当前两张卡 | +| 报告分类卡 | 983×1177 | 341.3×408.7 | 改为大图在上、正文在下 | +| 分类展示图片框 | 619×802 | 214.9×278.5 | 真实照片按比例呈现,标注坐标保持一致 | +| 产品卡 | 984×503 | 341.7×174.7 | 三张卡统一规格 | + +这些是基准几何,不代表真实数据页面一律固定高度。长护理建议、缺数据提示等内容应自然撑高,避免截断。 + +PSD 字体包含 MiSans Regular / Semibold / Demibold;当前 CSS 使用 Noto Sans SC / PingFang / 系统字体,项目未发现配套字体文件。字体会影响换行、数字宽度和字重。实施时优先准备可用于 Web 的对应字体文件,或明确接受系统字体近似。PSD 文本字号还要结合图层变换矩阵计算,不能只读原始 FontSize 就直接换算。 + +## 3. 逐页差异和调整内容 + +| 页面 | 当前实现 | 最新设计 | 具体工作 | +| --- | --- | --- | --- | +| 登录 | 无登录页面、无身份状态 | 品牌标志、微信登录、游客登录、协议勾选 | 新增页面与入口状态;游客通路和真实微信认证分别验收 | +| 启动 | 旧口号、单个「开始检测」按钮 | 英文两行 + 中文口号;新增白色「我的报告」按钮 | 更新文案、背景构图、Logo/按钮间距,增加历史入口 | +| 隐私授权 | 直接覆盖当前首页,仍露出首页口号和按钮 | 遮罩底图没有首页交互内容 | 采用独立背景层;校准弹窗位置、字号、关闭图标和勾选行 | +| 档案填写 | 结构基本完整,旧肤质插画;表单整体偏下,背景构图不同 | 相同字段与轮播,但几何、字重、背景不同 | 复用选择器与字段校验,重新标定间距、卡片、五种插画和底部按钮 | +| 拍照引导 | 旧口号;标题居中;没有环境要求说明 | 新口号;标题左对齐;补充关闭美颜/滤镜和均匀光线说明 | 更新背景、标题与说明,校准按钮位置 | +| 三步拍照 | 旧脸型轮廓,无眼鼻虚线;首步「请正对屏幕」 | 新脸型、眼鼻虚线;首步「请正脸出境」 | 更新蒙版及定位线,文案建议纠正为「出镜」;复核前后镜头方向 | +| 我的报告 | 存在隐藏历史弹层逻辑,没有可点击入口 | 独立页面,数量卡、日期列表、查看详情 | 将历史展示升级为页面,接通首页入口、返回和详情打开;补空列表状态 | +| 报告汇总 | 总分/肌龄一张卡,三维度另一张;额外肌龄差、关注说明 | 总分、年龄、三维度在同一张卡 | 合并 DOM 与间距;显示名改为紧塑力/透亮感/稳韧力,处理设计未显示的说明 | +| 报告详情 | 一个全局三维度 Tab,每页两个小图+侧边文字 | 三个纵向区块,每块自有分类 Tab,大图+下方文字 | 重构为独立区块及 Tab 状态,增加肤色/水分两个展示视图 | +| 护理建议 | 五段动态文案和额外结尾句 | 设计只示意两段 | 对齐卡片和排版,保留动态文案;内容长度不能照示例硬截成两段 | +| 产品推荐 | 额外精华推荐卡;固定一张 CORE II 产品图;「了解更多」占位按钮 | 主场精华 I/II/III 三张产品卡 | 三档素材/商品文案配置化;保留真实推荐逻辑,收敛重复模块 | + +实测的报告上半部分:当前总分卡 339×140,三维度卡约 339×201,中间有 12px 间隔;设计是一张约 341×242 的卡。这是明显的结构差异,靠缩字体和调颜色无法解决。 + +当前首页和档案页的背景使用同一张图加 CSS 叠色;新版已导出独立背景,但不能把带叠色的 JPG 再叠一遍旧渐变,否则会过暗。背景和相机蒙版还需根据业务视口裁剪,不能直接把全画板 2339px 高图片拉伸到去掉顶部栏后的 2091px 区域。 + +## 4. 报告数据如何接到新 UI + +建议增加展示适配层,例如 `h5/js/report-ui.js`。评分模块继续产出已有计算结果,适配层负责新版名称、分类视图、图片和商品展示,不把显示名写回数据库或评分公式。 + +| 新 UI | 已有数据 | 展示适配 | +| --- | --- | --- | +| 紧塑力 | `vm.dims.firm` | 保留该维度评分 | +| 透亮感 | `vm.dims.vita` | 将「活力感」映射为新显示名,包括相关关注提示 | +| 稳韧力 | `vm.dims.stab` | 将「稳定力」映射为新显示名 | +| 轮廓 | `vm.cats.contour` + 正脸照 | 大图及该分类文案 | +| 皱纹 | `vm.cats.wrinkles` + `texture_enhanced_lines` | 保留增强线与原照片合成 | +| 肤色 | `vm.cats.tone` 中肤色相关数据 + `brown_area` | 从合并分类中独立展示,须明确专属文案来源 | +| 平滑度 | `vm.cats.smooth` + 已有标记/图层 | 改名并复用图像合成 | +| 水分 | `vm.cats.tone` 中水分相关数据 + `water_area` | 独立图像/说明,不能声称新增了一项独立评分 | +| 泛红 | `vm.cats.sens` + `red_area` | 显示名与红区合成图更新 | +| 痘痘 | `vm.cats.acne` + 痤疮标记 | 复用坐标叠加 | + +当前服务端已经请求并保留 `brown_area` 和 `water_area`。当前 UI 的 `tone` 只展示水分图,因此图库接口不是主要阻碍;需要增加图片容器、展示配置及缺失图层状态。 + +现有 `tone` 评分合并肤色与水分,话术也是合并话术。新版的七个图片视图,不应自动变成七分类评分。肤色和水分的独立文字应由确定的字段/模板产生;不能把同一段合并文案复制到两个 Tab 后视为完成。 + +产品映射建议使用明确的配置表:已有「25+束紧精华 / 35+支撑精华 / 50+重塑精华」对应主场精华 I / II / III 的名称、图片和说明。实际推荐由已有多项判据决定,不能只按年龄选择。三张卡可以按设计全部展示;是否强调推荐卡应另定,避免擅自在设计上增加标签。 + +重构图片异步加载时增加报告/视图版本标记,避免快速切 Tab 或历史报告后,较早的图片合成回调覆盖新页面。 + +## 5. 登录与“我的报告”的真实能力缺口 + +目前只有静态页面、检测 API、管理 API 与健康检查,没有面向用户的登录、会话或个人报告接口。管理后台记录不等于个人报告库,不能直接将后台查询接口接到“我的报告”。 + +本机历史:`juzi.reports.v1`,最多 5 条,存储三张照片与结果,主动删除 `face_maps` 以减少容量。代码可打开旧报告,但图层类图片回看会降级。因而“恢复入口”可以复用较多代码,“跨设备完整回看”则需要新增服务端能力。 + +建议分两个可验收阶段: + +1. **游客闭环**:登录页游客入口 → 首页 → 检测 / 我的报告 → 详情。本地报告列表如实表达当前保留数量,补空态、损坏记录、存储失败和返回行为。图层不全时明确缺失,不能用设计示例人脸补齐。 +2. **账号闭环**:明确微信公众号/H5 实际运行容器及可用配置后接入真实登录;建立会话、报告 owner 归属、用户范围内列表/详情/图片读取、退出及游客数据衔接。设计稿自身不能提供这些接口契约。 + +第二阶段尚未落地前,不能把微信按钮接成“点击即登录成功”。希望完整实现设计中的微信登录功能,需要纳入后端和账号配置工作,而不只是 UI 工期。 + +若要求游客也能完整回看,需要另选客户端持久化渲染缩略图/IndexedDB,或以受限凭据查询服务端结果;在明确存储策略前,不把大幅 base64 图层直接塞回现有 localStorage。 + +## 6. 原稿中需要整理的内容 + +以下来自对 PSD 可见图层及代码契约的核对,不能无条件当成正式需求: + +- 首页底部存在重复的「我的报告」文字图层,建议只保留一枚有效按钮。 +- 首步「请正脸出境」建议修正为「请正脸出镜」;报告标题多余的右括号、历史数量文案中的重复字也需校正。 +- 画板名「左面」文案是第 2 步右脸,「右面」文案是第 3 步左脸。以用户脸部语义和送检字段为准,不按画板名机械替换。 +- PSD 范例里有 5.1 分,但现行 S = 7 + 3×P/100 的正常 0–100 输入对应 7–10 分。5.1 只是设计示例;若它代表新评分规则,需独立修改业务契约。 +- 设计复用了检测示意图与描述,不能据此让所有真实分类显示同一张图或同一段话。 +- 两段护理建议与三张商品卡属于展示示例,需保留真实文案和推荐判据。 +- 新登录页列出用户协议、隐私政策、儿童保护政策;当前只有已有隐私弹层内容,缺少新入口各自明确的文本对应。实施时须确定已有文本的覆盖范围和最终链接。 + +## 7. 实施顺序与交付物 + +### P0:冻结视觉和数据基准 + +- 确定 H5 顶栏口径、流程入口、字体,以及上面的设计示例处理方式。 +- 按画板重新整理 `--u`、卡片宽、间距、圆角、按钮高、字号和色值。 +- 复核 36 张切图:背景裁剪、透明边缘、产品剪贴边界;拍照眼鼻定位线应保留原稿虚线效果,不能只依赖可能近似的 PSD 矢量栅格结果。 +- 建立固定的视觉测试数据和「缺图/空态」样例,与真实评分数据分开。 +- 交付:新版尺寸表、素材映射、显示字段映射、确定的页面路由。 + +### P1:公共样式与既有入口流程 + +- 修改 `h5/css/style.css` 公共组件与背景策略。 +- 修改 `h5/index.html` 首页/授权/档案/拍照引导结构,接入新版素材。 +- 修改 `h5/js/app.js` 文案、肤质图片配置和相应入口,保留档案选择器校验。 +- 交付:首页、授权、档案和引导弹窗的并排截图与可操作流程。 + +### P2:报告页重构(视觉改动最大) + +- 合并汇总卡;显示层统一三个新维度名称。 +- 将一个三维度 Tab 改成三个纵向区块,各自包含 2 / 3 / 2 个分类 Tab。 +- 大图、分隔线、正文、免责声明、护理建议按新版排版。 +- 接入 `brown_area`/`water_area` 视图及独立文案,处理加载/缺失状态。 +- 三档产品列表配置化;按定稿移除旧布局的多余块与无实际动作的按钮。 +- 主要文件:`index.html`、`style.css`、`app.js`,新增展示适配模块;不因换名改评分引擎。 +- 交付:完整长报告、七个视图、三种商品结果和缺数据状态。 + +### P3:拍照页视觉校准 + +- 更新三套轮廓与眼鼻定位线,统一使用同一套 viewBox/坐标系。 +- 同步裁剪范围、前后镜头镜像、扫描区域、轮廓成功提示;继续使用现有脸部检测和自动抓拍流程。 +- 保留实时自动模式和系统相机/相册降级模式;不能因为设计隐藏快门就删除降级模式的快门。 +- 交付:三步静态对照、真实摄像头全流程和 HTTP 降级验证。 + +### P4:登录外观与本机历史闭环 + +- 新增 `s-login` 与 `s-history`;统一返回栈和刷新行为。 +- 游客入口接入首页,“我的报告”显示真实保留数量、日期与详情。 +- 为新/旧记录定义兼容读取及缺图策略;退出相机、关闭放大图、回到列表都要返回正确位置。 +- 登录图上无法确定授权后跳转、重复授权等状态,按明确的流程方案补齐。 +- 交付:游客用户可完整走通;微信入口状态与真实后端能力一致。 + +### P5:真实账号能力(依赖账号与后端范围确定) + +- 登录接入、会话、记录归属及用户级报告 API;补齐跨设备历史和对应权限验证。 +- 同步处理退出、授权取消、会话失效和游客报告迁移策略。 +- 交付:真实微信登录及本人报告查询。完成此阶段才能把所有设计功能视为完成。 + +### P6:联合验收与部署 + +- 固定 375px 基准截图,对照去除原生顶栏的 PSD;再检查 390/414px 及短屏、长屏、底部安全区。 +- 静态结构完成后核对字体、图片裁剪、Tab 下划线、卡片边缘和垂直节奏。 +- 更新现有 `h5/test/e2e-browser.mjs`:新版入口、独立 Tab、列表详情返回、七图、三商品、无空引用和无 JS 异常。 +- 跑 `h5/test/regression.js`;同一测试数据的现有评分/护理建议/推荐结果应保持一致。 +- 实机验证微信内 H5、Android 浏览器、iOS 浏览器;现有局域网 HTTP 只能验证降级拍照,实时自动相机需在可用的 HTTPS 环境验证。 +- 交付:最终对照图、回归结果、可访问页面地址及尚未覆盖的设备范围。 + +## 8. 验收定义 + +1. **结构**:10 个设计画板对应的页面/弹层均有明确落点;新增登录与报告列表不能只是无法操作的截图。 +2. **视觉**:375px 固定视口下,静态主要边界目标误差不超过约 2 CSS px;同字体、同文本、同图片才能做局部像素差比较。不同真实数据不得按整页像素误差判断。 +3. **响应式**:375/390/414px 下无横向溢出、文字截断、底部按钮被安全区遮挡;长报告允许内容自然增长。 +4. **交互**:三块报告 Tab 相互独立,图片点开/返回正常,历史列表与详情可往返,返回不重新启动摄像头。 +5. **数据**:七个展示视图与六分类计算关系清晰;姓名/日期/数量/分数来自真实状态;不会加载上一个报告的图片。 +6. **拍照**:正 → 用户右脸 → 用户左脸,API 仍按 front/left/right 正确送检;自动、拒绝权限、相册、系统相机路径分别检查。 +7. **历史**:列表计数与保留策略一致,缺图或记录损坏有正确提示;若接账号,必须只读本人记录。 +8. **性能**:整页 PNG 与设计示意人脸仅用于验收,业务页面按需加载独立素材;产品图懒加载,避免把 445MB PSD 或大幅设计预览带进页面。 + +## 9. 实施前需要定下的四个选择 + +本轮计划不依赖立即答复;进入实现前按以下顺序确定即可: + +1. **容器**:沿用当前 H5、去除原生顶栏,还是补一条网页标题/返回栏?建议沿用 H5 口径。 +2. **账号范围**:先完成游客及本机历史、再接微信,还是把真实登录和跨设备报告纳入同一批交付?两者的后端工作量明显不同。 +3. **业务口径**:是否同意三维度仅改显示名、六分类算法保持,肤色/水分拆成展示视图?建议如此,独立话术另行定稿。 +4. **原稿整理**:是否按设计保留三商品卡,并去掉旧版额外卡片/占位按钮,同时纠正原稿重复字和错字?建议保留真实推荐信息于护理建议中,清理重复展示。 + +推荐先完成 P0–P3 的视觉主体,再交付 P4 游客闭环;P5 随账号条件落地。报告结构和新增身份/历史能力是主要工作量,字体和相机坐标是还原精度的主要风险。 diff --git a/docs/微信登录接入与公网部署说明.md b/docs/微信登录接入与公网部署说明.md new file mode 100644 index 0000000..8d495e2 --- /dev/null +++ b/docs/微信登录接入与公网部署说明.md @@ -0,0 +1,105 @@ +# 微信登录接入与公网部署说明 + +更新:2026-10-11。当前环境为 Windows 宿主机 + 内网 WSL。 + +## 结论与当前阻碍 + +已完成微信网页授权代码,但**没有完成真实微信账号联调**。当前未配置 `WECHAT_APP_ID`、`WECHAT_APP_SECRET`、`PUBLIC_BASE_URL`,也没有可验证的公众号网页授权域名配置。内网 `http://192.168.3.25:8080/` 不能作为本次正式授权部署地址。因此本地启用「本机浏览器登录」,不会模拟微信登录成功。 + +官方网页授权适用于具有相应权限的认证服务号等账号;需要在公众号后台配置网页授权域名。普通小程序 AppID、开放平台网站应用 AppID 不能直接替代本方案的服务号 AppID。本实现采用 `snsapi_base` 获取 OpenID 做账号关联,不请求头像、昵称。 + +依据:[微信官方 H5 网页授权文档](https://developers.weixin.qq.com/doc/service/guide/h5/auth.html)(2026-10-10 核对)。实际账号权限和后台配置还需由公众号管理员确认。 + +## 已完成的链路 + +1. 登录页先勾选现有协议,再选择微信或本机浏览器登录。 +2. 服务端构造授权地址;回调校验一次性 state、浏览器绑定和 10 分钟有效期。 +3. 服务端用 code 换取 OpenID,AppSecret 不发送至前端、不写入报告。 +4. 同一 AppID + OpenID 对应同一个服务端账号,换手机登录后读取该账号最近 5 份报告。 +5. 会话采用 HttpOnly / SameSite=Lax Cookie;公网 HTTPS 配置下附带 Secure。 +6. 未配置微信、未在微信浏览器内打开、授权失败时提供明确提示,可选择本机浏览器登录。 + +自动化测试使用模拟的微信换码响应,已覆盖 state 缺失、绑定错误、重复回调、子路径回调、同一 OpenID 返回同一账号;这些测试不等同于微信真机验收。 + +## 公网服务器配置 + +部署整个 `h5/`,包括 `lib/`、`js/report-copy.js`、`js/report-ui.js`、`js/account-client.js`、`js/camera-geometry.js`、新版 CSS、图片和字体。需要 Node 18+,建议沿用当前 Node 22。不能仅部署静态文件。 + +通过服务管理器的私有环境文件设置以下内容,不放入前端,不提交 Git: + +```ini +HOST=127.0.0.1 +PORT=8080 +# 仅限 Node 绑定回环、nginx 覆盖 X-Real-IP 的部署方式 +TRUST_LOOPBACK_PROXY=1 +PUBLIC_BASE_URL=https://你的域名/state/ +WECHAT_APP_ID=你的认证服务号AppID +WECHAT_APP_SECRET=你的认证服务号AppSecret +SKIN_API_KEY=你的Face++Key +SKIN_API_SECRET=你的Face++Secret +# 可选:独立持久化目录。未设置则使用 h5/data。 +JUZI_DATA_ROOT=/var/lib/juzi-h5 +``` + +环境文件权限设为仅服务账号可读,systemd 中使用 `EnvironmentFile=`。重启服务后访问 `.../api/auth/config`,`wechat: true` 只代表必需配置齐备,不代表平台授权已经验收通过。 + +`PUBLIC_BASE_URL` 必须是用户实际访问的 HTTPS 地址,包含真实子路径。示例回调为:`https://你的域名/state/api/auth/wechat/callback`。公众号后台填写的是域名,不是完整 URL;按后台要求部署域名验证文件到域名根目录。不要向聊天或代码库粘贴 AppSecret。 + +## 反向代理与后台保护 + +如果使用 `/state/` 子路径,反向代理要剥离该前缀。以下片段加入已经配置证书的 HTTPS server 中;实际部署前先核对现有 nginx 配置,不要直接覆盖。 + +```nginx +location = /state { return 301 /state/; } + +# 管理后台与用户账号接口相互独立;必须继续保护后台。 +location ~ ^/state/(admin\.html|css/admin\.css|js/admin\.js|api/admin/.*)$ { + auth_basic "Juzi Admin"; + auth_basic_user_file /etc/nginx/.htpasswd-juzi; + rewrite ^/state/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + client_max_body_size 20m; + proxy_read_timeout 150s; +} + +location /state/ { + proxy_pass http://127.0.0.1:8080/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + client_max_body_size 20m; + proxy_read_timeout 150s; +} +``` + +根路径部署则设置 `PUBLIC_BASE_URL=https://你的域名/` 并相应去掉上述 `/state/`。不要遗漏 `admin.html` 和 `/api/admin/` 的鉴权,也不要把 Node 8080 直接暴露到公网。现有 Node 管理接口本身不使用新 C 端账号鉴权;公网安全仍依赖后台鉴权和回环监听。 + +`TRUST_LOOPBACK_PROXY=1` 用于按真实客户端 IP 限制登录频率;只信任来自回环代理且格式合法的 `X-Real-IP`。nginx 必须按示例覆盖该头,不能透传浏览器传入值。本地直接访问时不要启用。未启用时按 TCP 对端限流,在反代后会共享同一计数。 + +当前 WSL 使用 `HOST=0.0.0.0` 方便局域网测试,局域网内管理接口也可达,请只在可信网络使用。手机 HTTP 访问不是安全相机上下文,会沿用现有系统相机/上传降级;自动实时抓拍需 HTTPS,或在本机 localhost 测试。这与微信授权是否配置是两个不同限制。 + +## 数据、迁移与备份 + +- `h5/data/accounts/identities.json`:账号、随机身份凭据的哈希、会话、最近报告索引。 +- `h5/data/accounts/reports//.json`:完整报告快照,含三张照片、检测图层、评分、话术和推荐产品。 +- 原有 `data/records.jsonl`、`data/photos`、`data/results` 继续供后台复核。 +- 本机浏览器 localStorage 只保存随机身份凭据,不再存照片与报告。更换设备/浏览器/站点域名,或清除全部站点数据后,不能靠本机身份找回原账号。 +- 微信与本机账号不自动合并;旧版 localStorage 报告未自动认领迁移,避免错误关联。旧数据不删除。 +- 前台只列出并允许回看最近 5 份,超出 5 份的快照/后台记录暂保留,不代表已物理删除。正式上线需业务方确认保留和删除周期。 +- 迁移时停止写入,备份并迁移**完整数据目录**,保留访问权限。只同步代码不会带上报告。禁止将数据目录提交 Git 或由 nginx 作为静态目录公开。 +- 当前为单 Node 进程的文件存储,不可对同一目录开多个 worker/多实例;未来扩容需迁移数据库和对象存储。 + +## 公网真机验收清单 + +- 微信打开 HTTPS 页面,协议可查看、未勾选不能登录。 +- 首次微信登录→首页→三视角检测→我的报告→详情。 +- 同一微信账号换另一台手机,能看到原报告;另一微信账号看不到。 +- 报告七项检测图与初次生成一致,文案、评分、推荐商品不因回看重算。 +- 取消/失败授权后可重新登录或选择本机浏览器登录。 +- 重启服务后报告与账号不丢失。 +- 检查 HTTPS 摄像头权限、三视角自动抓拍、前后镜头、后台鉴权、数据目录不可访问。 + +协议处理:登录和检测授权复用 `h5/index.html` 中已有的「Face++ 个人信息与隐私保护政策」(包含儿童相关章节)。本次未发现独立的品牌方用户协议文件,未凭空编写法律条款;正式发布前请业务/法务确认现有政策是否覆盖品牌方的账号关联、报告存储、保存期限和删除渠道。 diff --git a/graphify-out/.graphify_labels.json b/graphify-out/.graphify_labels.json index f096abd..7b28bd1 100644 --- a/graphify-out/.graphify_labels.json +++ b/graphify-out/.graphify_labels.json @@ -1,69 +1,77 @@ { "0": "face-api.js", - "1": "n", + "1": "t", "2": "app.js", "3": "skin-score.js", - "4": "isTensor", - "5": ".toBatchTensor", - "6": "getEnv", - "7": "FaceLandmarks", - "8": "disposeUnusedWeightTensors", + "4": "n", + "5": "normalize", + "6": "drawFaceExpressions", + "7": "Box", + "8": "admin.js", "9": "__awaiter", - "10": "extractWeightEntryFactory", - "11": "FaceProcessor", + "10": "AgeGenderNet", + "11": "lib/accounts.js", "12": "server.js", "13": "files", - "14": "extractAllFacesAndComputeResults", + "14": ".predictAgeAndGender", "15": ".runAndExtendWithFaceDetection", "16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范", "17": "extractFaces", "18": "1'me 在线肌肤测试 H5", "19": "head-pose.js", "20": "build_fixtures.py", - "21": ".forwardInput", + "21": "disposeUnusedWeightTensors", "22": ".loadFromDisk", "23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)", "24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0", - "25": "extractParamsFromWeigthMap$1", + "25": "mobile-select.iife.min.js", "26": "wi", "27": "gen_voice.py", - "28": "initialize", + "28": "i", "29": "face-api.js 本地化依赖(h5/vendor/faceapi/)", - "30": "regression.js", - "31": "isValidNumber", + "30": "ui-20261009/manifest.json", + "31": ".toString", "32": "FaceMatcher", "33": "build-fixture.py", "34": "smoke-test.js", "35": "DetectSingleFaceLandmarksTask", - "36": "so", - "37": "tt", + "36": "C", + "37": "UI 外发版 10.09 图片资源", "38": "TinyYolov2", - "39": "cn", - "40": ".computeFaceDescriptor", - "41": "SsdMobilenetv1", - "42": "dr", + "39": "UI交互界面调整", + "40": "ComputeAllFaceDescriptorsTask", + "41": ".draw", + "42": "export_psd_assets.py", "43": "FaceLandmark68Net", - "44": "rescaleAndNormalize", + "44": "start-local.sh", "45": "PredictAllAgeAndGenderWithFaceAlignmentTask", "46": "PredictAllFaceExpressionsWithFaceAlignmentTask", - "47": "PredictSingleAgeAndGenderWithFaceAlignmentTask", + "47": "k", "48": "PredictSingleFaceExpressionsWithFaceAlignmentTask", - "49": "Yo", + "49": "mobile-select/README.md", "50": "call_skinanalyze_3view.sh", - "51": "TinyXception", - "52": "Vc", - "53": "FaceDetection", - "54": "ff", - "55": "ge", - "56": "sa", - "57": "po", + "51": "第二轮2反馈.md", + "52": "region-data.js", + "53": "护理建议(全量文本,按需求摘抄)", + "54": "UI 对齐 10.09 设计稿:调研与实施计划", + "55": "FaceLandmarks68", + "56": ".forwardInput", + "57": "regression.js", "58": ".dispose", "59": "call_skinanalyze.sh", "60": "report-fixture.js", - "61": ".extractWeights", - "62": "NeuralNetwork", + "61": "NeuralNetwork", + "62": "提交前 Review(2026-10-11)", "63": "e2e-browser.mjs", - "64": "AgeGenderNet", - "66": "FaceExpressionNet", - "71": "PredictSingleFaceExpressionsTask" + "64": "account-client.js", + "65": "PredictSingleFaceExpressionsTask", + "66": ".runAndExtendWithFaceDetections", + "67": "ui-20261009.mjs", + "68": "ComputeSingleFaceDescriptorTask", + "69": "misans/README.md", + "71": "extractFaceTensors", + "72": "build_report_copy.py", + "73": "camera-geometry.js", + "74": "report-copy.js", + "75": "prepare_h5_assets.py" } diff --git a/graphify-out/.graphify_labels.json.sig b/graphify-out/.graphify_labels.json.sig index 24f5032..90ee6f7 100644 --- a/graphify-out/.graphify_labels.json.sig +++ b/graphify-out/.graphify_labels.json.sig @@ -1 +1 @@ -{"0": "084975bb68797cf4", "1": "6c6ce24229e04595", "2": "64f07a78d61df933", "3": "8ac814cdb72a1a7d", "4": "c18a4a8fab272cc6", "5": "7f62d3cc90a92195", "6": "865611558d18274c", "7": "e7b6c52cb9523ba1", "8": "934b465b2e687d3d", "9": "31a3a505422c0528", "10": "f943c63a37873e9e", "11": "9ed244ba3453a0c1", "12": "8d1af962d46ec19f", "13": "7e3b558a587a4ebe", "14": "2681c1b3f4f3539e", "15": "8bd61518dd267641", "16": "95add6a975c0cb72", "17": "d5ee72467704e6f0", "18": "fd0841b607871dc7", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "880a74c6d255e5cc", "22": "1c67ad3d97e5c622", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "79d2843a716971c8", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "eb3733e409dc7411", "29": "c608f1c6492e1cb1", "30": "f2a40b5f1b29f669", "31": "8f647d3ce08ecd2b", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "c55f872b3e16ab6f", "37": "3088ae48043ee39d", "38": "c96276581a8c98d4", "39": "932f010dc6115fb4", "40": "444ef0744645f5ac", "41": "386a184c6bd5a40e", "42": "2bfc96d4090afd84", "43": "9a25b1230b5fe649", "44": "8d5f4e992bac9095", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "01f68dfc34a29d32", "48": "8c471c7baeaaca54", "49": "a54008a71fdd44fa", "50": "bbb2b57d72922b16", "51": "0985ce7473fdd021", "52": "c5a22f1501fe85b7", "53": "287d60115d9acda8", "54": "1331494302f6c136", "55": "f42d242a020b2f05", "56": "2537fe1309d3ab9a", "57": "7862e3ee5a214160", "58": "440ae90e84499b6f", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "151209871c761759", "62": "d96493a5f110bd4c", "63": "c256b43f75d7809e", "64": "512a2535c88c4284", "66": "e858d75e995c92e5", "71": "cb5ac3401283c378"} \ No newline at end of file +{"0": "796f85041a70fce3", "1": "966174ef01534051", "2": "9cf893b45c314643", "3": "271a5f5154afbdcd", "4": "e8b7c34edb5e6b25", "5": "163ce3e95fc46466", "6": "979dcc63399620ad", "7": "3a3e5d3e70ed45d6", "8": "cf2aba493368e557", "9": "94b2884fbd0d3d50", "10": "d982c7c094e0d01c", "11": "1c4f17c3d154962f", "12": "ab705784bbd1dbdc", "13": "7e3b558a587a4ebe", "14": "a2b13160223b5a98", "15": "5a9014c4a7f87699", "16": "95add6a975c0cb72", "17": "2405e1edb003a83e", "18": "5aa9c1fc3b3c8196", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "8835aeeaa165e9f9", "22": "b683919fcbe30647", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "3ca8c23d3e8220ec", "27": "eab427ce9a5219e9", "28": "536a3a2cb7285674", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "cd6f168abba94fea", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "92f5f0a14da5908e", "37": "cb8f26249485324a", "38": "29790cf710874dd0", "39": "fdd95375b4a5ddb3", "40": "3dd68cce72af5314", "41": "bf75bfc99d4f9e34", "42": "ffd960136fde00ad", "43": "9a25b1230b5fe649", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "188dd5e33354e35d", "48": "8c471c7baeaaca54", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "15606ce5d8eec23a", "56": "880a74c6d255e5cc", "57": "f2a40b5f1b29f669", "58": "de5c2bc5e6fe8070", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "81661714a80b3097", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "86101547c05f96b9", "65": "cb5ac3401283c378", "66": "46204fb2e4d50c53", "67": "98f88c09f1236577", "68": "7ac475ea9bc280f8", "69": "e511846eeba03076", "71": "0b63a2ad1a11ce27", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884"} \ No newline at end of file diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index 48b9a9a..e56e781 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,190 +1,193 @@ -# Graph Report - juzi (2026-09-28) +# Graph Report - juzi (2026-10-11) ## Corpus Check -- 31 files · ~1,845,638 words +- 59 files · ~4,180,488 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 900 nodes · 2117 edges · 67 communities (46 shown, 21 thin omitted) -- Extraction: 92% EXTRACTED · 8% INFERRED · 0% AMBIGUOUS · INFERRED: 168 edges (avg confidence: 0.52) +- 1133 nodes · 2496 edges · 75 communities (57 shown, 18 thin omitted) +- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 178 edges (avg confidence: 0.53) - Token cost: 0 input · 0 output ## Graph Freshness -- Built from commit: `4e3d915b` +- Built from commit: `ee744b6b` - Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `graphify update .` after code changes (no API cost). ## Community Hubs (Navigation) - face-api.js -- n +- t - app.js - skin-score.js -- isTensor -- .toBatchTensor -- getEnv -- FaceLandmarks -- disposeUnusedWeightTensors +- n +- normalize +- drawFaceExpressions +- Box +- admin.js - __awaiter -- extractWeightEntryFactory -- FaceProcessor +- AgeGenderNet +- lib/accounts.js - server.js - files -- extractAllFacesAndComputeResults +- .predictAgeAndGender - .runAndExtendWithFaceDetection - 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范 - extractFaces - 1'me 在线肌肤测试 H5 - head-pose.js - build_fixtures.py -- .forwardInput +- disposeUnusedWeightTensors - .loadFromDisk - facepp-skinanalyze(旷视皮肤分析专业版调用技能) - 巨子测肤 · 1'me 语音播报内容设计文档 V1.0 -- extractParamsFromWeigthMap$1 +- mobile-select.iife.min.js - wi - gen_voice.py -- initialize +- i - face-api.js 本地化依赖(h5/vendor/faceapi/) -- regression.js -- isValidNumber +- ui-20261009/manifest.json +- .toString - FaceMatcher - build-fixture.py - DetectSingleFaceLandmarksTask -- so -- tt +- C +- UI 外发版 10.09 图片资源 - TinyYolov2 -- cn -- .computeFaceDescriptor -- SsdMobilenetv1 -- dr +- UI交互界面调整 +- ComputeAllFaceDescriptorsTask +- .draw +- export_psd_assets.py - FaceLandmark68Net -- rescaleAndNormalize +- start-local.sh - PredictAllAgeAndGenderWithFaceAlignmentTask - PredictAllFaceExpressionsWithFaceAlignmentTask -- PredictSingleAgeAndGenderWithFaceAlignmentTask +- k - PredictSingleFaceExpressionsWithFaceAlignmentTask -- Yo +- mobile-select/README.md - call_skinanalyze_3view.sh -- TinyXception -- Vc -- FaceDetection -- ff -- ge -- sa -- po +- 护理建议(全量文本,按需求摘抄) +- UI 对齐 10.09 设计稿:调研与实施计划 +- FaceLandmarks68 +- .forwardInput +- regression.js - .dispose - call_skinanalyze.sh -- .extractWeights - NeuralNetwork +- 提交前 Review(2026-10-11) - e2e-browser.mjs -- AgeGenderNet -- FaceExpressionNet +- account-client.js - PredictSingleFaceExpressionsTask +- .runAndExtendWithFaceDetections +- ui-20261009.mjs +- ComputeSingleFaceDescriptorTask +- misans/README.md +- extractFaceTensors +- build_report_copy.py ## God Nodes (most connected - your core abstractions) 1. `__awaiter()` - 59 edges 2. `__generator()` - 59 edges -3. `n()` - 36 edges -4. `t()` - 33 edges -5. `i()` - 33 edges -6. `r()` - 32 edges +3. `n()` - 37 edges +4. `r()` - 35 edges +5. `t()` - 33 edges +6. `i()` - 33 edges 7. `C()` - 32 edges 8. `toNetInput()` - 27 edges 9. `e()` - 22 edges 10. `f()` - 20 edges ## Surprising Connections (you probably didn't know these) +- `loadRecords()` --indirect_call--> `it()` [INFERRED] + h5/js/admin.js → h5/vendor/faceapi/face-api.js +- `bind()` --indirect_call--> `a()` [INFERRED] + h5/js/admin.js → h5/vendor/faceapi/face-api.js - `detectFrame()` --indirect_call--> `d()` [INFERRED] h5/js/app.js → h5/vendor/faceapi/face-api.js -- `vizCompose()` --indirect_call--> `h()` [INFERRED] - h5/js/app.js → h5/vendor/faceapi/face-api.js - `vizCompose()` --indirect_call--> `rh()` [INFERRED] h5/js/app.js → h5/vendor/faceapi/face-api.js - `vizCompose()` --indirect_call--> `w()` [INFERRED] h5/js/app.js → h5/vendor/faceapi/face-api.js -- `renderReport()` --indirect_call--> `t()` [INFERRED] - h5/js/app.js → h5/vendor/faceapi/face-api.js ## Import Cycles - None detected. -## Communities (67 total, 21 thin omitted) +## Communities (75 total, 18 thin omitted) ### Community 0 - "face-api.js" Cohesion: 0.02 -Nodes (38): bn(), ca(), cd(), ce(), dn(), ee(), er(), et() (+30 more) +Nodes (32): cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), dr(), en(), extractBoundingBoxes() (+24 more) -### Community 1 - "n" -Cohesion: 0.07 -Nodes (85): a(), ae(), ao(), b(), ba(), be(), bf(), bi() (+77 more) +### Community 1 - "t" +Cohesion: 0.08 +Nodes (33): be(), bi(), Bo(), ce(), de(), e(), Ef(), er() (+25 more) ### Community 2 - "app.js" Cohesion: 0.06 -Nodes (71): afterCurrent(), applyMirror(), armNudge(), beginCountdown(), beginDetect(), bindBackdropClose(), busyWithFlow(), cancelCountdown() (+63 more) +Nodes (74): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), checkStill(), closeOverlay() (+66 more) ### Community 3 - "skin-score.js" Cohesion: 0.12 Nodes (44): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+36 more) -### Community 4 - "isTensor" -Cohesion: 0.33 -Nodes (5): extractorsFactory$3(), extractParamsFromWeigthMap$4(), isTensor(), isTensor1D(), isTensor2D() +### Community 4 - "n" +Cohesion: 0.17 +Nodes (32): a(), ae(), bf(), cf(), d(), drawContour(), dt(), ea() (+24 more) -### Community 5 - ".toBatchTensor" -Cohesion: 0.08 -Nodes (30): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+22 more) - -### Community 6 - "getEnv" +### Community 5 - "normalize" Cohesion: 0.09 -Nodes (26): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), DetectAllFacesTask(), DrawBox(), drawDetections(), drawFaceExpressions() (+18 more) +Nodes (26): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+18 more) -### Community 7 - "FaceLandmarks" -Cohesion: 0.06 -Nodes (16): bgrToRgbTensor(), extendWithFaceLandmarks(), FaceLandmark68NetBase(), FaceLandmarks(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), isDimensions() (+8 more) - -### Community 8 - "disposeUnusedWeightTensors" +### Community 6 - "drawFaceExpressions" Cohesion: 0.22 -Nodes (6): disposeUnusedWeightTensors(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$6(), extractParamsFromWeigthMap$7(), extractParamsFromWeigthMapTiny() +Nodes (6): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceExpressions() + +### Community 7 - "Box" +Cohesion: 0.13 +Nodes (6): Box(), DetectAllFaceLandmarksTask(), extendWithFaceLandmarks(), FaceLandmarks(), minBbox(), ObjectDetection() + +### Community 8 - "admin.js" +Cohesion: 0.25 +Nodes (23): api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(), dSec(), el() (+15 more) ### Community 9 - "__awaiter" -Cohesion: 0.22 -Nodes (8): __awaiter(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), Mtcnn(), toNetInput() +Cohesion: 0.14 +Nodes (12): __awaiter(), extendWithFaceDescriptor(), extractSingleFaceAndComputeResult(), FaceRecognitionNet(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+4 more) -### Community 10 - "extractWeightEntryFactory" -Cohesion: 0.53 -Nodes (6): extractorsFactory$7(), extractWeightEntryFactory(), loadConvParamsFactory(), loadParamsFactory(), loadParamsFactory$1(), loadSeparableConvParamsFactory() +### Community 10 - "AgeGenderNet" +Cohesion: 0.10 +Nodes (6): AgeGenderNet(), extractParams$1(), FaceExpressionNet(), FaceLandmark68NetBase(), FaceLandmark68TinyNet(), FaceProcessor() -### Community 11 - "FaceProcessor" -Cohesion: 0.24 -Nodes (3): extractParams$1(), FaceLandmark68TinyNet(), FaceProcessor() +### Community 11 - "lib/accounts.js" +Cohesion: 0.07 +Nodes (40): build(), count(), fill(), numeric(), atomic(), body(), cookie(), cookies() (+32 more) ### Community 12 - "server.js" -Cohesion: 0.14 -Nodes (16): callFacepp(), CRED, DATA_DIR, ERR_MSG, fs, handleAnalyze(), http, isUsableB64() (+8 more) +Cohesion: 0.08 +Nodes (48): accounts, appendRecord(), callFacepp(), CRED, csvCell(), DATA_DIR, dumpRaw(), ERR_MSG (+40 more) ### Community 13 - "files" Cohesion: 0.12 Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more) -### Community 14 - "extractAllFacesAndComputeResults" -Cohesion: 0.15 -Nodes (8): extendWithAge(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask(), PredictAllFaceExpressionsTask(), PredictSingleAgeAndGenderTask() +### Community 14 - ".predictAgeAndGender" +Cohesion: 0.28 +Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask() ### Community 15 - ".runAndExtendWithFaceDetection" -Cohesion: 0.23 -Nodes (6): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), DetectAllFaceLandmarksTask(), detectAllFaces(), DetectSingleFaceTask() +Cohesion: 0.33 +Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask() ### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范" Cohesion: 0.15 Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more) ### Community 17 - "extractFaces" -Cohesion: 0.22 -Nodes (6): Box(), extractFaces(), extractFaceTensors(), imageTensorToCanvas(), isTensor3D(), isTensor4D() +Cohesion: 0.19 +Nodes (19): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+11 more) ### Community 18 - "1'me 在线肌肤测试 H5" -Cohesion: 0.18 -Nodes (10): 1'me 在线肌肤测试 H5, Windows 本机部署(提测用), 密钥配置, 已知占位清单, 技术说明, 拍照交互(引导式自动三连拍), 本地运行, 检测流程与评分渲染 (+2 more) +Cohesion: 0.07 +Nodes (27): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令, 公网服务器配置, 公网真机验收清单, 反向代理与后台保护 (+19 more) ### Community 19 - "head-pose.js" Cohesion: 0.33 @@ -194,13 +197,13 @@ Nodes (8): assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal(), Cohesion: 0.40 Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more) -### Community 21 - ".forwardInput" -Cohesion: 0.22 -Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more) +### Community 21 - "disposeUnusedWeightTensors" +Cohesion: 0.09 +Nodes (24): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+16 more) ### Community 22 - ".loadFromDisk" -Cohesion: 0.19 -Nodes (5): ComposableTask(), getModelUris(), Gf(), loadWeightMap(), TinyFaceDetector() +Cohesion: 0.25 +Nodes (3): ComposableTask(), getModelUris(), TinyFaceDetector() ### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)" Cohesion: 0.20 @@ -210,101 +213,125 @@ Nodes (9): 1. 凭据(统一管理), 2. 标准调用模板(全可选参数 Cohesion: 0.22 Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策略(前端已实现侧约定), 4. 生成验收, A. 步骤引导(进入每个拍照步骤时播,与屏幕步骤文案一致), B. 姿态提示(实时检测不通过时播,同一提示 4 秒内不重复), C. 拍摄与流程, 巨子测肤 · 1'me 语音播报内容设计文档 V1.0 -### Community 25 - "extractParamsFromWeigthMap$1" -Cohesion: 0.40 -Nodes (3): extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$3(), seperateWeightMaps() +### Community 25 - "mobile-select.iife.min.js" +Cohesion: 0.20 +Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more) ### Community 26 - "wi" -Cohesion: 0.28 -Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more) +Cohesion: 0.32 +Nodes (8): Ci(), Ei(), Fi(), Ii(), ki(), ri(), wi(), Xt() ### Community 27 - "gen_voice.py" Cohesion: 0.30 Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more) -### Community 28 - "initialize" -Cohesion: 0.29 -Nodes (7): createBrowserEnv(), createFileSystem(), createNodejsEnv(), initialize(), isBrowser(), monkeyPatch(), setEnv() +### Community 28 - "i" +Cohesion: 0.08 +Nodes (32): ba(), bn(), ca(), cl(), da(), dn(), ee(), et() (+24 more) ### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)" Cohesion: 0.29 Nodes (6): face-api.js 本地化依赖(h5/vendor/faceapi/), 为什么本地化, 文件清单与体积, 来源与版本, 用法, 许可证 -### Community 30 - "regression.js" -Cohesion: 0.60 -Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr() +### Community 30 - "ui-20261009/manifest.json" +Cohesion: 0.33 +Nodes (5): assets, document_size, layers, pages, source -### Community 31 - "isValidNumber" -Cohesion: 0.50 -Nodes (4): Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender() +### Community 31 - ".toString" +Cohesion: 0.33 +Nodes (5): FaceMatch(), la(), pr(), pt(), vt() ### Community 33 - "build-fixture.py" Cohesion: 0.60 Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url() -### Community 36 - "so" -Cohesion: 0.50 -Nodes (4): Do(), io(), so(), To() +### Community 36 - "C" +Cohesion: 0.08 +Nodes (29): ao(), C(), cd(), co(), Do(), eo(), fo(), Ft() (+21 more) -### Community 37 - "tt" -Cohesion: 0.50 -Nodes (4): kn(), tt(), wo(), z() - -### Community 39 - "cn" -Cohesion: 0.67 -Nodes (3): cn(), en(), yn() - -### Community 40 - ".computeFaceDescriptor" -Cohesion: 0.18 -Nodes (4): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), FaceRecognitionNet() - -### Community 41 - "SsdMobilenetv1" +### Community 37 - "UI 外发版 10.09 图片资源" Cohesion: 0.40 -Nodes (3): extractorsFactory$5(), extractParamsFromWeigthMap$5(), SsdMobilenetv1() +Nodes (4): UI 外发版 10.09 图片资源, 前端素材对应, 文件位置, 重新导出 -### Community 42 - "dr" -Cohesion: 0.67 -Nodes (3): dr(), na(), ta() +### Community 39 - "UI交互界面调整" +Cohesion: 0.40 +Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设计** -### Community 44 - "rescaleAndNormalize" -Cohesion: 0.67 -Nodes (3): getSizesForScale(), normalize$1(), rescaleAndNormalize() +### Community 42 - "export_psd_assets.py" +Cohesion: 0.70 +Nodes (4): bounds(), layer_at(), main(), save_image() -### Community 49 - "Yo" -Cohesion: 0.67 -Nodes (3): qo(), Xo(), Yo() +### Community 44 - "start-local.sh" +Cohesion: 0.50 +Nodes (3): HOST, PORT, start-local.sh script + +### Community 47 - "k" +Cohesion: 0.18 +Nodes (11): b(), gn(), k(), kn(), ne(), tt(), wo(), Yt() (+3 more) + +### Community 53 - "护理建议(全量文本,按需求摘抄)" +Cohesion: 0.10 +Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色(透亮感), 5\.4 平滑度(透亮感), 5\.5 水分(透亮感), 5\.6 泛红(稳韧力), 5\.7 痘痘(稳韧力), 6\.1 轮廓 (+11 more) + +### Community 54 - "UI 对齐 10.09 设计稿:调研与实施计划" +Cohesion: 0.11 +Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more) + +### Community 55 - "FaceLandmarks68" +Cohesion: 0.19 +Nodes (3): FaceLandmarks5(), FaceLandmarks68(), getCenterPoint() + +### Community 56 - ".forwardInput" +Cohesion: 0.22 +Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more) + +### Community 57 - "regression.js" +Cohesion: 0.60 +Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr() ### Community 58 - ".dispose" -Cohesion: 0.14 -Nodes (13): computeReshapedDimensions(), extractBoundingBoxes(), iou(), isEven(), nonMaxSuppression(), nonMaxSuppression$1(), pyramidDown(), scale() (+5 more) +Cohesion: 0.15 +Nodes (12): getSizesForScale(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1(), rescaleAndNormalize(), scale(), SsdMobilenetv1() (+4 more) -### Community 61 - ".extractWeights" -Cohesion: 0.08 -Nodes (25): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+17 more) +### Community 61 - "NeuralNetwork" +Cohesion: 0.05 +Nodes (32): bgrToRgbTensor(), createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), Dimensions() (+24 more) + +### Community 62 - "提交前 Review(2026-10-11)" +Cohesion: 0.40 +Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(2026-10-11), 验证 ### Community 63 - "e2e-browser.mjs" Cohesion: 0.29 Nodes (4): EXPECTED, fails, pageErrors, pwRoot +### Community 64 - "account-client.js" +Cohesion: 0.90 +Nodes (4): browser(), init(), read(), request() + +### Community 71 - "extractFaceTensors" +Cohesion: 0.18 +Nodes (9): extendWithFaceDetection(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractFaceTensors(), isTensor3D(), isWithFaceDetection(), isWithFaceLandmarks(), PredictAllFaceExpressionsTask() (+1 more) + ## Knowledge Gaps -- **63 isolated node(s):** `step1-front`, `step2-rightface`, `step3-leftface`, `hint-noface`, `hint-near` (+58 more) +- **152 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+147 more) These have ≤1 connection - possible missing edges or undocumented components. -- **21 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. +- **18 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. ## Suggested Questions _Questions this graph is uniquely positioned to answer:_ -- **Why does `val()` connect `skin-score.js` to `extractFaces`?** - _High betweenness centrality (0.083) - this node is a cross-community bridge._ -- **Why does `vizCompose()` connect `app.js` to `n`?** +- **Why does `k()` connect `k` to `face-api.js`, `t`, `app.js`, `n`, `lib/accounts.js`, `i`?** + _High betweenness centrality (0.078) - this node is a cross-community bridge._ +- **Why does `renderReport()` connect `app.js` to `t`, `k`?** _High betweenness centrality (0.058) - this node is a cross-community bridge._ -- **Why does `Box()` connect `extractFaces` to `face-api.js`, `.dispose`, `isValidNumber`, `FaceLandmarks`?** - _High betweenness centrality (0.058) - this node is a cross-community bridge._ -- **Are the 23 inferred relationships involving `n()` (e.g. with `ae()` and `be()`) actually correct?** - _`n()` has 23 INFERRED edges - model-reasoned connections that need verification._ +- **Why does `rate()` connect `lib/accounts.js` to `k`?** + _High betweenness centrality (0.055) - this node is a cross-community bridge._ +- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?** + _`n()` has 24 INFERRED edges - model-reasoned connections that need verification._ +- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?** + _`r()` has 27 INFERRED edges - model-reasoned connections that need verification._ - **Are the 7 inferred relationships involving `t()` (e.g. with `renderReport()` and `e()`) actually correct?** _`t()` has 7 INFERRED edges - model-reasoned connections that need verification._ -- **Are the 9 inferred relationships involving `i()` (e.g. with `da()` and `f()`) actually correct?** - _`i()` has 9 INFERRED edges - model-reasoned connections that need verification._ -- **What connects `step1-front`, `step2-rightface`, `step3-leftface` to the rest of the system?** - _63 weakly-connected nodes found - possible documentation gaps or missing edges._ \ No newline at end of file +- **What connects `source`, `document_size`, `pages` to the rest of the system?** + _152 weakly-connected nodes found - possible documentation gaps or missing edges._ \ No newline at end of file diff --git a/graphify-out/graph.html b/graphify-out/graph.html index 4700953..440296b 100644 --- a/graphify-out/graph.html +++ b/graphify-out/graph.html @@ -63,12 +63,12 @@
-
900 nodes · 2117 edges · 67 communities
+
1133 nodes · 2496 edges · 75 communities
+ + + + diff --git a/h5/js/account-client.js b/h5/js/account-client.js new file mode 100644 index 0000000..ceebb7a --- /dev/null +++ b/h5/js/account-client.js @@ -0,0 +1,31 @@ +/* Browser identity is a random credential, never a fingerprint or a report. */ +window.AccountClient = (function () { + 'use strict'; + var key = 'juzi.identity.v1', user = null; + function read() { try { return localStorage.getItem(key); } catch (_) { return null; } } + function request(url, data) { + return fetch(url, { credentials: 'same-origin', method: data ? 'POST' : 'GET', + headers: data ? { 'Content-Type': 'application/json' } : {}, + body: data ? JSON.stringify(data) : undefined }).then(function (r) { + return r.json().then(function (v) { if (!r.ok || !v.ok) throw new Error(v.message || '请求失败'); return v; }); + }); + } + function browser(consent) { + return request('api/auth/browser', { consent: consent, token: read() }).then(function (v) { + try { localStorage.setItem(key, v.token); } catch (_) { /* session cookie still works */ } + user = v.user; return v; + }); + } + function init() { + return request('api/auth/session').then(function (v) { + user = v.user; + if (!user && read()) return browser(true).catch(function (e) { + // Do not replace an existing credential on a transient server/network error. + if (/身份已失效/.test(e.message)) { try { localStorage.removeItem(key); } catch (_) {} } + throw e; + }); + return v; + }); + } + return { request: request, init: init, browser: browser, user: function () { return user; } }; +})(); diff --git a/h5/js/app.js b/h5/js/app.js index 082a86a..22a1f59 100644 --- a/h5/js/app.js +++ b/h5/js/app.js @@ -4,6 +4,22 @@ var $ = function (sel, root) { return (root || document).querySelector(sel); }; + // The PSD geometry uses the same 375 × 812 coordinate space as the capture mask. + (window.CameraGeometry || []).forEach(function (geometry, index) { + document.querySelectorAll('#cam-mask path[data-shape="' + index + '"]').forEach(function (path) { + path.setAttribute('d', geometry.outline); + }); + var group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + group.setAttribute('data-shape', index); + group.setAttribute('class', 'cam-feature-lines'); + geometry.guides.forEach(function (d) { + var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', d); + group.appendChild(path); + }); + $('#cam-mask').appendChild(group); + }); + /* ─── Toast ─── */ var toastEl = $('#toast'), toastTimer = null; function toast(msg) { @@ -15,12 +31,15 @@ } /* ─── 屏幕管理:页面内不放返回按钮,前进时 pushState,微信/浏览器返回键经 popstate 回到上一屏 ─── */ - var currentScreen = 's-splash'; + var currentScreen = 's-login'; function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史 + if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login'; document.querySelectorAll('.screen').forEach(function (s) { s.classList.toggle('active', s.id === id); }); var sc = document.getElementById(id); + window.scrollTo(0, 0); + document.body.scrollTop = 0; sc.scrollTop = 0; currentScreen = id; if (id === 's-report') resetReport(); // 每次进入报告页都还原初始状态 @@ -33,7 +52,7 @@ window.addEventListener('popstate', function (e) { if (Date.now() < popSilentUntil) { popSilentUntil = 0; return; } var to = (e.state && e.state.screen) || 's-splash'; - ['p-privacy', 'p-photoguide', 'p-policy', 'p-zoom', 'p-history'].forEach(closeOverlay); + ['p-privacy', 'p-photoguide', 'p-policy', 'p-zoom'].forEach(closeOverlay); if (currentScreen === 's-cam') leaveCamera(); if (to === 's-cam') { to = 's-profile'; history.replaceState({ screen: to }, ''); } // 「前进」回拍照页不自动开相机 if (to !== currentScreen) showScreen(to); @@ -92,11 +111,11 @@ /* ─── 肤质轮播 ─── */ var SKINS = [ - { img: 'assets/img/skin-0-dry.png', name: '干性肌肤', desc: '全脸干燥,容易紧绷、缺乏光泽' }, - { img: 'assets/img/skin-1-mixeddry.png', name: '混合偏干肌肤', desc: '两颊偏干,T区油脂较少或偶尔出油' }, - { img: 'assets/img/skin-2-neutral.png', name: '中性肌肤', desc: '水油平衡,肤感舒适,无明显干燥出油' }, - { img: 'assets/img/skin-3-oily.png', name: '油性肌肤', desc: '全脸易出油,容易泛油光、毛孔明显' }, - { img: 'assets/img/skin-4-mixedoily.png', name: '混合偏油肌肤', desc: 'T区易出油,两颊相对清爽或轻微干燥' } + { img: 'assets/img/ui-20261009/skin-0-dry.png', name: '干性肌肤', desc: '全脸干燥,容易紧绷、
缺乏光泽' }, + { img: 'assets/img/ui-20261009/skin-1-mixeddry.png', name: '混合偏干肌肤', desc: '两颊偏干,T区油脂较少或偶尔出油' }, + { img: 'assets/img/ui-20261009/skin-2-neutral.png', name: '中性肌肤', desc: '水油平衡,肤感舒适,无明显干燥出油' }, + { img: 'assets/img/ui-20261009/skin-3-oily.png', name: '油性肌肤', desc: '全脸易出油,容易泛油光、毛孔明显' }, + { img: 'assets/img/ui-20261009/skin-4-mixedoily.png', name: '混合偏油肌肤', desc: 'T区易出油,两颊相对清爽或轻微干燥' } ]; var skinIndex = 0; var track = $('#skin-track'), dotsBox = $('#skin-dots'); @@ -711,7 +730,7 @@ // 左脸出镜=头向自己的右侧转(target 'left',产物作 left_image)。 // mask 为 index.html 取景轮廓编号(1=左脸出镜 耳在画面左,2=右脸出镜 耳在画面右,均按镜像预览绘制) var CAM_STEPS = [ - { step: '1/3
请正对屏幕', guides: ['正脸居中', '取下眼镜', '光线充足'], target: 'front', mask: 0, + { step: '1/3
请正脸出镜', guides: ['正脸居中', '取下眼镜', '光线充足'], target: 'front', mask: 0, done: '正脸拍摄完成' }, { step: '2/3
请右脸出镜', guides: ['右脸居中', '取下眼镜', '光线充足'], target: 'right', mask: 2, done: '右脸拍摄完成', preview: '下一张:右脸出镜
请向左转头' }, @@ -826,7 +845,7 @@ } var vm = null; try { - vm = SkinScore.buildViewModel(payload.result, payload.side, { + vm = payload.vm || SkinScore.buildViewModel(payload.result, payload.side, { realAge: profile.age, skinType: profile.skinType }); @@ -838,7 +857,8 @@ return; } renderReport(vm, photos[0], photos[1], payload); // img1=正脸实拍,payload 供检测可视化取 face_maps/marks - storeReport(payload, photos, profile); // 以检测为单位存三张照片+分析结果(本地历史) + if (IS_MOCK) toast('演示报告,不保存至我的报告'); + else if (!payload.report_saved) toast('报告生成成功,但保存失败,请勿关闭页面并联系管理员'); photos = []; // 报告已渲染(图片 src 已挂):释放三连拍 dataURL 内存 showScreen('s-report', 'replace'); // 报告顶替拍照页:返回键回档案页,不再回到相机 }, wait); @@ -879,7 +899,7 @@ } var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null; var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, ANALYZE_TIMEOUT_MS); - fetch('/api/skin-analyze', { + fetch('api/skin-analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -897,7 +917,7 @@ clearTimeout(timer); if (seq !== camSeq) return; // 流程已被重置,丢弃过期响应 if (!data.ok) done(data.message || '检测失败,请重试'); - else done(null, { result: data.result, side: data.side }); + else done(null, data); }) .catch(function (err) { clearTimeout(timer); @@ -952,7 +972,8 @@ { key: 'mole', color: '#282828' }, // 痣 { key: 'acne', color: '#D30C0C' } // 痤疮 ] }, - tone: { img: '#viz-tone', blendMap: 'water_area' }, // 肤色水润:正脸×水分图正片叠底(water_area,接口返回) + water: { img: '#viz-water', blendMap: 'water_area' }, + tone: { img: '#viz-tone', blendMap: 'brown_area' }, // 肤色水润:正脸×水分图正片叠底(water_area,接口返回) sens: { img: '#viz-sens', blendMap: 'red_area' }, // 敏感:正脸×红区图正片叠底(直挂白底过淡不可读) acne: { img: '#viz-acne', photo: true, marks: [ // 痘痘:正脸+闭口粉刺/痤疮标记 { key: 'closed_comedones', color: '#E8A13D' }, // 闭口粉刺 @@ -1029,10 +1050,13 @@ } // canvas 合成:照片绘制到自然尺寸 → (可选压暗)→ (可选盖透明增强图层)→ 画坐标标记 → dataURL 挂到卡片 + var vizEpoch = 0; function vizCompose(photoURL, overlayURL, marks, imgEl, opts) { + var epoch = vizEpoch; vizLoadImage(photoURL, function (photo) { if (!photo) return; // 照片不可用:保留占位 var finish = function (overlay) { + if (epoch !== vizEpoch) return; var w = photo.naturalWidth, h = photo.naturalHeight; var cv = document.createElement('canvas'); cv.width = w; cv.height = h; @@ -1080,6 +1104,8 @@ // 报告页 6 张检测图统一入口:任一输入缺失 → 该卡保留既有占位(不报错) function renderViz(payload, frontPhoto) { + vizEpoch++; + document.querySelectorAll('.ditem-img').forEach(function (img) { img.hidden = true; img.removeAttribute('src'); }); var result = payload && payload.result; var maps = (result && result.face_maps) || {}; for (var key in VIZ_CAT) { @@ -1140,19 +1166,10 @@ ['firm', 'vita', 'stab'].forEach(function (k) { setDim(k, vm.dims[k], k === worstDimKey ? 'worst' : k === bestDimKey ? 'best' : ''); }); - // 关注行(设计稿两行:「近期优先关注:」灰 + 维度名深色下划线,下行为灰色口号) - var DIM_SLOGAN = { - '紧塑力': '紧塑提拉力,轮廓上扬不松垮。', - '活力感': '活力透亮感,水润澎弹有光泽。', - '稳定力': '强韧稳态力,锁水保湿抵御刺激。' // 设计稿原文 - }; - $('#dims-note').innerHTML = vm.focus.name - ? '近期优先关注:' + esc(vm.focus.name) + '
' + esc(DIM_SLOGAN[vm.focus.name] || '') + '' - : '近期优先关注:---'; - - // 三个 Tab 面板:各所辖两个分类(S + 档位徽章 + 话术正文) - var keys = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne']; - for (var i = 0; i < keys.length; i++) setCat(keys[i], vm.cats[keys[i]]); + var presentation = payload && payload.presentation || ReportUI.build(vm, payload && payload.result); + $('#dims-note').textContent = ''; + var textIds = { contour: '#ditem-para1', wrinkles: '#ditem-para2', color: '#tx-tone', smooth: '#tx-smooth', water: '#tx-water', sens: '#tx-sens', acne: '#tx-acne' }; + presentation.views.forEach(function (v) { $(textIds[v.key]).textContent = v.text; }); // 检测可视化:轮廓=正脸实拍,其余按 VIZ_CAT 取 face_maps 图层或叠加 marks(payload 缺失时各卡保留占位) renderViz(payload, frontPhoto); @@ -1170,27 +1187,18 @@ notesEl.style.display = 'none'; } - // 护理建议(V2.3 §8 五段:肤质定制/最差2分类/精华/生活方式);综合分析与生活方式通栏已按需求移除 - // 展示对齐 Figma:纯「序号、正文」,不显示段名【tag】、不加粗 - var careBox = $('#care-list'); - if (careBox) { - careBox.innerHTML = (vm.careAdvice || []).map(function (s, i) { - return '

' + (i + 1) + '、' + esc(s.text) + '

'; - }).join(''); + $('#care-list').innerHTML = presentation.care.map(function (s, i) { + return '

' + (i + 1) + '、' + esc(s.text) + '

'; + }).join('') || '

本次有效数据不足,暂无个性化护理建议。

'; + var product = presentation.product; + $('.card-product').hidden = !product; + $('.prod-title').hidden = !product; + if (product) { + $('#product-title').textContent = product.title; + $('#product-name').textContent = product.name + '双仓精华液'; + $('#product-image').src = product.image; + $('#product-image').alt = product.name + '双仓精华液'; } - - // 精华推荐卡 - // 精华推荐卡只放产品名 + 命中判据:推荐正文已是护理建议第 4 段(V2.3 §8.5),卡内不再重复同一句 - $('#essence-name').textContent = vm.essence.name; - // 命中判据:引擎内部名 → 用户可读措辞(引擎值被真值回归逐字锁定,仅展示层映射) - var TRIGGER_LABEL = { - '均分档': vm.avgS != null ? '六项平均分 ' + vm.avgS : '六项平均分', - '最差项约束': vm.worst ? vm.worst.name + '得分偏低' : '单项得分偏低', - '年龄组档': vm.group ? '年龄段 ' + vm.group.replace('组', '') : '年龄段' - }; - $('#essence-triggers').textContent = vm.essence.triggers.length - ? '推荐依据:' + vm.essence.triggers.map(function (t) { return TRIGGER_LABEL[t] || t; }).join('、') - : ''; } /* ─── 相机可用后加载检测模型(加载完成前快门即手动模式) ─── */ @@ -1252,7 +1260,10 @@ /* ─── 动作分发 ─── */ var actions = { - 'open-privacy': function () { openOverlay('p-privacy'); }, + 'open-policy': function () { openOverlay('p-policy'); }, + 'login-browser': function () { loginBrowser(); }, + 'login-wechat': function () { loginWechat(); }, + 'open-privacy': function () { if (!AccountClient.user()) { showScreen('s-login', 'replace'); return; } openOverlay('p-privacy'); }, 'close-privacy': function () { closeOverlay('p-privacy'); }, 'close-policy': function () { closeOverlay('p-policy'); }, 'agree-continue': function () { @@ -1291,13 +1302,7 @@ startCamera(); // 复用启停与预热(camSeqCam 防竞态) }, 'history': function () { openHistory(); }, - 'close-history': function () { closeOverlay('p-history'); }, - 'clear-history': function () { - saveReports([]); - renderHistoryList(); - toast('历史报告已清空'); - }, - 'more': function () { toast('了解更多功能开发中'); } + 'history-retry': function () { renderHistoryList(); } }; document.addEventListener('click', function (e) { @@ -1307,84 +1312,78 @@ if (fn) fn(); }); - /* ─── 报告历史(localStorage):以检测为单位,存三张照片+分析结果(数据/坐标/文案), - 不含 face_maps 整幅图层(体积原因);上限 5 条,超配额逐条淘汰 ─── */ - var REPORTS_KEY = 'juzi.reports.v1', REPORTS_MAX = 5; - function loadReports() { - try { var v = JSON.parse(localStorage.getItem(REPORTS_KEY)); return Array.isArray(v) ? v : []; } - catch (e) { return []; } - } - function saveReports(list) { - try { localStorage.setItem(REPORTS_KEY, JSON.stringify(list)); return true; } - catch (e) { return false; } // QuotaExceeded:交给调用方淘汰重试 - } - function storeReport(payload, ph, profile) { - if (!payload || !payload.result) return; - var result = Object.assign({}, payload.result); - delete result.face_maps; // 历史不存图层;可视化中图层类(皱纹/肤色/敏感)回看时降级为占位,坐标叠加类(光滑/痘痘)仍可用 - var entry = { - id: 'r' + Date.now(), ts: Date.now(), profile: profile, - total: payload.result.score_info && payload.result.score_info.total_score, - skinAge: payload.result.skin_age && payload.result.skin_age.value, - photos: [ph[0] || null, ph[1] || null, ph[2] || null], - payload: { result: result, side: payload.side || null, face_rectangle: payload.face_rectangle || null } - }; - var list = loadReports(); - list.unshift(entry); - while (list.length > REPORTS_MAX) list.pop(); - while (!saveReports(list) && list.length > 1) list.pop(); // 淘汰至可写 - saveReports(list); - } - function fmtTime(ts) { - var d = new Date(ts); - function p(n) { return (n < 10 ? '0' : '') + n; } - return d.getFullYear() + '/' + (d.getMonth() + 1) + '/' + d.getDate() + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()); - } + /* Reports and complete immutable snapshots live on the authenticated backend. */ + var historySeq = 0; function renderHistoryList() { - var box = $('#history-list'), list = loadReports(); - if (!list.length) { - box.innerHTML = '

暂无历史报告
完成一次肌肤检测后会自动保存在这里

'; - return; - } - box.innerHTML = list.map(function (e) { - var sub = [e.profile && e.profile.age ? e.profile.age + '岁' : '', e.profile && e.profile.skinType || ''] - .filter(Boolean).join(' · '); - var score = e.total != null ? e.total : '--'; - var img = e.photos && e.photos[0] ? '' : ''; - return '
' + img + - '
' + fmtTime(e.ts) + '' + sub + ' · 肌龄' + (e.skinAge != null ? e.skinAge : '--') + '岁
' + - '
' + score + '分
' + - '
'; - }).join(''); + var seq = ++historySeq, box = $('#history-list'); + box.textContent = '正在读取报告…'; + AccountClient.request('api/reports').then(function (data) { + if (seq !== historySeq) return; + $('#history-count').textContent = '您最近已进行过' + data.total + '次面部问题检测'; + $('#identity-note').textContent = AccountClient.user().kind === 'wechat' + ? '保留最近 5 份报告,同一微信账号可跨手机查看。' + : '保留最近 5 份报告。本机身份仅限当前浏览器;更换手机或清除浏览器数据后无法找回。'; + if (!data.items.length) { box.innerHTML = '

暂无报告
完成一次检测后,报告会自动保存在这里。

'; return; } + box.innerHTML = data.items.map(function (item) { + var date = new Date(item.created_at).toLocaleDateString('sv-SE'); + return '

测试时间 ' + esc(date) + + '

'; + }).join(''); + }).catch(function (e) { + if (seq !== historySeq) return; + $('#history-count').textContent = '暂时无法读取'; + box.textContent = e.message + ',请刷新列表重试。'; + }); } function openHistory() { + showScreen('s-history', 'push'); renderHistoryList(); - openOverlay('p-history'); } - $('#history-list').addEventListener('click', function (e) { - var del = e.target.closest('[data-del]'); - if (del) { // 删除单条(不打开报告) - e.stopPropagation(); - saveReports(loadReports().filter(function (it) { return it.id !== del.dataset.del; })); - renderHistoryList(); - return; - } - var item = e.target.closest('.his-item'); - if (!item) return; - var entry = loadReports().filter(function (it) { return it.id === item.dataset.id; })[0]; - if (!entry) return; - var vm = null; - try { - vm = SkinScore.buildViewModel(entry.payload.result, entry.payload.side, { - realAge: entry.profile && entry.profile.age, skinType: entry.profile && entry.profile.skinType - }); - } catch (err) { vm = null; } - if (!vm) { toast('该记录已损坏'); return; } - renderReport(vm, entry.photos && entry.photos[0], null, entry.payload); - closeOverlay('p-history'); - showScreen('s-report', currentScreen === 's-report' ? undefined : 'push'); + $('#history-list').addEventListener('click', function (event) { + var btn = event.target.closest('[data-report-id]'); + if (!btn || btn.disabled) return; + btn.disabled = true; + AccountClient.request('api/reports/' + encodeURIComponent(btn.dataset.reportId)).then(function (data) { + if (currentScreen !== 's-history') return; + var entry = data.report; + renderReport(entry.vm, entry.photos[0], entry.photos[1], Object.assign({}, entry.payload, { presentation: entry.presentation })); + showScreen('s-report', 'push'); + }).catch(function (e) { toast(e.message); }).finally(function () { btn.disabled = false; }); }); + var loginBusy = false; + function loginAllowed() { + if (loginBusy) return false; + if (!$('#login-consent').checked) { toast('请先阅读并同意用户协议和隐私政策'); return false; } + return true; + } + function loginBrowser() { + if (!loginAllowed()) return; + loginBusy = true; + $('#login-message').textContent = '正在登录…'; + AccountClient.browser(true).then(function () { + showScreen('s-splash', 'replace'); + }).catch(function (e) { $('#login-message').textContent = e.message; }) + .finally(function () { loginBusy = false; }); + } + function loginWechat() { + if (!loginAllowed()) return; + loginBusy = true; + AccountClient.request('api/auth/config').then(function (data) { + if (!data.wechat) { + $('#login-message').textContent = data.reason + '。报告仍会保存至服务端。'; + return; + } + if (!/MicroMessenger/i.test(navigator.userAgent)) { + $('#login-message').textContent = '请在微信内打开本页面,或使用本机浏览器登录。'; + return; + } + return AccountClient.request('api/auth/wechat', { consent: true }).then(function (data) { + location.assign(data.url); + }); + }).catch(function (e) { $('#login-message').textContent = e.message; }) + .finally(function () { loginBusy = false; }); + } /* ─── 页面隐藏/来电生命周期:隐藏即停相机与全部定时器,回前台从当前步恢复(保留 photos[] 与 camStep) ─── */ function handleCamHidden() { if (currentScreen !== 's-cam') return; @@ -1423,8 +1422,18 @@ })(); /* ─── 初始化 ─── */ - history.replaceState({ screen: 's-splash' }, ''); // 刷新后从启动页开始,历史栈的屏标记与之对齐 + history.replaceState({ screen: 's-login' }, ''); // 刷新后从启动页开始,历史栈的屏标记与之对齐 renderCamStep(); + if (!(/[?&]mock=1/.test(location.search) && /[?&]screen=report/.test(location.search))) { + loginBusy = true; + AccountClient.init().then(function () { + if (AccountClient.user()) showScreen('s-splash', 'replace'); + else $('#login-message').textContent = /[?&]login_error=/.test(location.search) + ? '微信登录未完成,可重试或使用本机浏览器登录。' + : '本机登录仅限当前浏览器,不支持换手机找回。'; + }).catch(function (e) { $('#login-message').textContent = e.message; }) + .finally(function () { loginBusy = false; }); + } // 仅调试用:?mock=1&screen=report 直渲 mock 报告页(供无头浏览器截图验证布局,非正式入口) // 有 MOCK_FIXTURE 时走 fixture(检测可视化同步出图),否则回退 SkinScore 内置样本 if (IS_MOCK && /[?&]screen=report/.test(location.search) && typeof SkinScore !== 'undefined') { diff --git a/h5/js/camera-geometry.js b/h5/js/camera-geometry.js new file mode 100644 index 0000000..06c2c41 --- /dev/null +++ b/h5/js/camera-geometry.js @@ -0,0 +1 @@ +window.CameraGeometry = [{"outline": "M177.43,234.03L162.85,235.42L151.39,237.50L142.01,239.93L126.39,245.83L114.93,252.08L104.51,259.72L96.53,267.36L89.24,276.39L82.99,286.46L79.17,294.44L75.35,304.86L71.88,318.75L69.44,338.89L69.44,360.07L71.53,380.21L63.19,380.56L59.72,382.29L57.29,384.72L54.17,391.67L53.82,402.08L60.42,431.94L66.32,448.61L71.88,457.99L76.39,462.15L81.94,464.24L89.58,485.42L100.00,507.99L109.38,524.31L119.44,538.54L126.74,547.22L134.72,555.21L144.10,562.85L152.78,568.40L163.89,573.61L173.26,576.39L182.99,577.78L192.01,577.78L201.39,576.39L210.07,573.96L222.22,568.40L229.51,563.89L239.24,556.25L249.65,545.83L261.11,531.25L270.83,515.97L279.86,498.61L288.54,478.12L293.06,464.58L299.31,461.81L303.12,458.33L309.38,447.57L313.19,437.15L317.71,420.14L321.18,402.78L321.53,394.44L320.14,388.54L318.06,384.72L315.97,382.64L311.81,380.21L303.82,379.86L305.90,359.38L305.90,339.58L303.12,317.36L300.35,306.25L293.75,289.24L286.11,276.39L279.51,268.06L272.92,261.46L262.15,253.12L255.21,248.96L239.93,242.01L224.65,237.50L211.46,235.07L201.39,234.03Z", "guides": ["M100.35,382.64L101.39,383.16L102.43,382.81L103.47,382.29L104.51,381.77L105.56,381.08L106.60,380.56L107.64,380.03L108.68,379.51L109.72,379.17L110.76,378.82L111.81,378.12L112.85,377.78L113.89,377.43L114.93,377.08L115.97,376.91L117.01,376.56L118.06,376.22L119.10,376.04L120.14,375.87L121.18,375.52L122.22,375.35L123.26,375.17L124.31,375.00L125.35,374.83L126.39,374.83L127.43,374.65L128.47,374.48L129.51,374.48L130.56,374.48L131.60,374.48L132.64,374.48L133.68,374.48L134.72,374.48L135.76,374.48L136.81,374.65L137.85,374.83L138.89,374.83L139.93,375.00L140.97,375.17L142.01,375.35L143.06,375.52L144.10,375.69L145.14,375.87L146.18,376.22L147.22,376.39L148.26,376.74L149.31,377.08L150.35,377.43L151.39,377.78L152.43,378.12L153.47,378.47L154.51,378.82L155.56,379.51L156.60,379.86L157.64,380.38L158.68,380.90L159.72,381.42L160.76,382.12L161.81,382.81L162.85,383.16L163.89,382.64", "M213.19,382.64L214.24,383.16L215.28,382.81L216.32,382.29L217.36,381.77L218.40,381.08L219.44,380.56L220.49,380.03L221.53,379.51L222.57,379.17L223.61,378.82L224.65,378.12L225.69,377.78L226.74,377.43L227.78,377.08L228.82,376.91L229.86,376.56L230.90,376.22L231.94,376.04L232.99,375.87L234.03,375.52L235.07,375.35L236.11,375.17L237.15,375.00L238.19,374.83L239.24,374.83L240.28,374.65L241.32,374.48L242.36,374.48L243.40,374.48L244.44,374.48L245.49,374.48L246.53,374.48L247.57,374.48L248.61,374.48L249.65,374.65L250.69,374.83L251.74,374.83L252.78,375.00L253.82,375.17L254.86,375.35L255.90,375.52L256.94,375.69L257.99,375.87L259.03,376.22L260.07,376.39L261.11,376.74L262.15,377.08L263.19,377.43L264.24,377.78L265.28,378.12L266.32,378.47L267.36,378.82L268.40,379.51L269.44,379.86L270.49,380.38L271.53,380.90L272.57,381.42L273.61,382.12L274.65,382.81L275.69,383.16L276.74,382.64", "M188.37,413.19L188.37,414.24L188.37,415.28L188.37,416.32L188.37,417.36L188.37,418.40L188.37,419.44L188.37,420.49L188.37,421.53L188.37,422.57L188.37,423.61L188.37,424.65L188.37,425.69L188.37,426.74L188.37,427.78L188.37,428.82L188.37,429.86L188.37,430.90L188.37,431.94L188.37,432.99L188.37,434.03L188.37,435.07L188.37,436.11L188.37,437.15L188.37,438.19L188.37,439.24L188.37,440.28L188.37,441.32L188.37,442.36L188.37,443.40L188.37,444.44L188.37,445.49L188.37,446.53L188.37,447.57L188.37,448.61L188.37,449.65L188.37,450.69L188.37,451.74L188.37,452.78L188.37,453.82L188.37,454.86L188.37,455.90L188.37,456.94L188.37,457.99L188.37,459.03L188.37,460.07L188.37,461.11L188.37,462.15L188.37,463.19L188.37,464.24"]}, {"outline": "M140.62,237.85L129.86,241.67L117.01,247.92L108.33,253.47L97.57,262.15L88.54,271.53L82.64,279.17L75.69,290.28L71.18,299.65L65.97,314.58L62.50,332.29L61.81,356.25L63.19,368.75L66.67,385.07L77.78,385.07L81.60,386.11L86.81,389.24L85.76,389.58L76.74,386.46L70.14,386.11L63.54,388.19L59.38,392.01L57.29,395.49L55.56,401.74L55.56,412.50L59.03,427.78L67.01,445.83L74.65,456.94L78.82,461.11L82.99,463.89L97.22,468.40L104.17,488.89L112.15,504.86L120.49,515.97L130.21,525.69L140.28,534.38L155.90,546.18L171.88,556.60L181.60,562.15L199.65,570.83L210.07,574.65L223.26,577.78L236.81,578.12L244.10,576.74L253.82,572.57L260.07,568.06L266.67,561.46L277.08,546.88L292.01,521.18L302.43,500.69L309.38,484.03L314.93,466.32L317.01,456.94L319.10,441.32L319.10,422.57L316.32,403.82L316.32,396.18L318.40,375.69L318.75,345.83L317.01,328.47L313.89,314.93L309.72,303.82L305.21,295.14L300.00,287.15L292.01,277.43L283.33,268.75L273.96,261.11L262.15,253.47L250.69,247.57L237.85,242.36L222.92,237.85L209.72,235.07L196.53,233.33L184.03,232.64L167.71,232.99L153.82,234.72Z", "guides": ["M257.64,394.97L258.68,394.97L259.72,395.14L260.76,394.44L261.81,393.58L262.85,392.88L263.89,392.19L264.93,391.49L265.97,390.97L267.01,390.45L268.06,389.93L269.10,389.58L270.14,389.06L271.18,388.54L272.22,388.37L273.26,388.02L274.31,387.67L275.35,387.50L276.39,387.33L277.43,387.15L278.47,386.98L279.51,386.98L280.56,386.81L281.60,386.63L282.64,386.63L283.68,386.63L284.72,386.63L285.76,386.81L286.81,386.98L287.85,386.98L288.89,387.15L289.93,387.33L290.97,387.50L292.01,387.85L293.06,388.02L294.10,388.37L295.14,388.72L296.18,389.24L297.22,389.58L298.26,389.93L299.31,390.28L300.35,390.97L301.39,391.49L302.43,392.01L303.47,392.53L304.51,393.23L305.56,393.92L306.60,394.44L307.64,394.10", "M157.99,396.35L159.03,396.88L160.07,396.70L161.11,396.01L162.15,395.49L163.19,394.97L164.24,394.44L165.28,393.75L166.32,393.40L167.36,393.06L168.40,392.36L169.44,392.01L170.49,391.67L171.53,391.32L172.57,390.97L173.61,390.62L174.65,390.28L175.69,390.10L176.74,389.76L177.78,389.58L178.82,389.41L179.86,389.06L180.90,389.06L181.94,388.72L182.99,388.72L184.03,388.54L185.07,388.37L186.11,388.37L187.15,388.37L188.19,388.37L189.24,388.37L190.28,388.19L191.32,388.37L192.36,388.37L193.40,388.37L194.44,388.37L195.49,388.37L196.53,388.72L197.57,388.72L198.61,388.89L199.65,389.06L200.69,389.24L201.74,389.41L202.78,389.58L203.82,389.93L204.86,390.10L205.90,390.45L206.94,390.80L207.99,391.15L209.03,391.49L210.07,391.84L211.11,392.36L212.15,392.71L213.19,393.06L214.24,393.58L215.28,394.10L216.32,394.62L217.36,395.14L218.40,395.66L219.44,396.35L220.49,397.05L221.53,396.70", "M249.13,416.67L248.26,417.71L248.61,418.75L248.96,419.79L249.31,420.83L250.00,421.88L250.35,422.92L250.69,423.96L251.04,425.00L251.39,426.04L252.08,427.08L252.43,428.12L252.78,429.17L253.12,430.21L253.47,431.25L254.17,432.29L254.51,433.33L254.86,434.38L255.21,435.42L255.56,436.46L256.25,437.50L256.60,438.54L256.94,439.58L257.29,440.62L257.64,441.67L257.99,442.71L258.68,443.75L259.03,444.79L259.38,445.83L259.72,446.88L260.07,447.92L260.42,448.96L261.11,450.00L261.46,451.04L261.81,452.08L262.15,453.12L262.50,454.17L262.85,455.21L263.19,456.25L263.54,457.29L264.06,458.33L264.41,459.38L264.76,460.42L264.93,461.46L265.10,462.50L265.10,463.54L265.10,464.58L265.10,465.62L264.76,466.67L264.24,467.71L263.72,468.75L262.67,469.79L261.63,470.83L260.07,471.88L258.33,472.92L256.77,473.96L254.69,475.00L252.60,476.04L250.69,477.08L249.48,478.12L248.61,479.17L248.09,480.21"]}, {"outline": "M234.72,238.19L221.18,235.07L207.99,233.33L180.56,233.33L164.58,235.42L151.74,238.19L136.11,243.06L126.74,246.88L114.24,253.12L103.82,259.72L94.10,267.36L85.07,276.04L76.74,285.76L70.14,295.49L65.28,304.86L61.46,314.93L58.33,327.78L56.60,341.67L56.25,368.40L58.33,390.97L58.33,400.69L55.56,409.38L55.56,420.83L60.07,440.28L59.38,459.72L61.46,471.88L66.32,486.81L73.61,503.82L84.38,525.00L98.96,550.35L107.29,561.81L114.93,569.44L120.83,573.61L129.86,577.43L133.68,578.12L148.61,577.78L161.46,575.00L173.61,570.83L185.42,565.62L200.35,557.64L217.36,546.88L233.33,535.07L250.00,520.49L260.07,508.68L264.93,501.04L269.79,491.32L278.12,468.06L285.76,466.32L291.67,463.89L297.57,459.38L302.08,454.17L307.64,445.49L315.97,426.39L319.10,412.85L319.10,401.39L318.06,397.22L315.97,393.06L310.42,387.85L304.86,386.11L298.26,386.46L289.24,389.58L288.54,389.24L292.36,386.81L300.69,384.72L308.33,385.07L311.46,372.57L313.54,355.90L313.19,335.42L309.38,314.58L304.17,299.65L299.65,290.28L292.01,278.12L285.07,269.44L275.35,259.72L267.36,253.47L259.03,248.26L247.57,242.71Z", "guides": ["M64.24,391.15L65.28,391.32L66.32,391.32L67.36,390.62L68.40,389.76L69.44,389.41L70.49,388.72L71.53,388.19L72.57,387.67L73.61,387.15L74.65,386.81L75.69,386.28L76.74,385.76L77.78,385.59L78.82,385.24L79.86,384.90L80.90,384.72L81.94,384.55L82.99,384.20L84.03,384.20L85.07,383.85L86.11,383.85L87.15,383.85L88.19,383.68L89.24,383.68L90.28,383.68L91.32,383.85L92.36,383.85L93.40,383.85L94.44,384.03L95.49,384.20L96.53,384.38L97.57,384.72L98.61,384.90L99.65,385.24L100.69,385.42L101.74,385.76L102.78,386.46L103.82,386.81L104.86,387.15L105.90,387.67L106.94,388.37L107.99,388.89L109.03,389.41L110.07,390.28L111.11,390.97L112.15,391.67L113.19,392.19L114.24,392.01", "M150.35,395.83L151.39,396.35L152.43,396.01L153.47,395.49L154.51,394.97L155.56,394.27L156.60,393.75L157.64,393.23L158.68,392.71L159.72,392.36L160.76,392.01L161.81,391.32L162.85,390.97L163.89,390.62L164.93,390.28L165.97,390.10L167.01,389.76L168.06,389.41L169.10,389.24L170.14,389.06L171.18,388.72L172.22,388.54L173.26,388.37L174.31,388.19L175.35,388.02L176.39,388.02L177.43,387.85L178.47,387.67L179.51,387.67L180.56,387.67L181.60,387.67L182.64,387.67L183.68,387.67L184.72,387.67L185.76,387.67L186.81,387.85L187.85,388.02L188.89,388.02L189.93,388.19L190.97,388.37L192.01,388.54L193.06,388.72L194.10,388.89L195.14,389.06L196.18,389.41L197.22,389.58L198.26,389.93L199.31,390.28L200.35,390.62L201.39,390.97L202.43,391.32L203.47,391.67L204.51,392.01L205.56,392.71L206.60,393.06L207.64,393.58L208.68,394.10L209.72,394.62L210.76,395.31L211.81,396.01L212.85,396.35L213.89,395.83", "M125.87,418.06L126.91,419.10L126.74,420.14L126.04,421.18L125.69,422.22L125.35,423.26L125.00,424.31L124.65,425.35L123.96,426.39L123.61,427.43L123.26,428.47L122.92,429.51L122.57,430.56L121.88,431.60L121.53,432.64L121.18,433.68L120.83,434.72L120.49,435.76L119.79,436.81L119.44,437.85L119.10,438.89L118.75,439.93L118.40,440.97L117.71,442.01L117.36,443.06L117.01,444.10L116.67,445.14L116.32,446.18L115.97,447.22L115.28,448.26L114.93,449.31L114.58,450.35L114.24,451.39L113.89,452.43L113.54,453.47L113.02,454.51L112.50,455.56L112.15,456.60L111.81,457.64L111.46,458.68L111.11,459.72L110.76,460.76L110.42,461.81L110.24,462.85L109.90,463.89L109.90,464.93L109.90,465.97L110.07,467.01L110.42,468.06L110.76,469.10L111.28,470.14L112.15,471.18L113.37,472.22L114.76,473.26L116.32,474.31L118.23,475.35L120.31,476.39L122.05,477.43L124.13,478.47L125.69,479.51L126.22,480.56L127.08,481.60"]}]; diff --git a/h5/js/report-copy.js b/h5/js/report-copy.js new file mode 100644 index 0000000..1b8ad1a --- /dev/null +++ b/h5/js/report-copy.js @@ -0,0 +1,141 @@ +// Generated from reference/AI测肤报告页话术修改.md; run tools/build_report_copy.py. +(function(root,factory){if(typeof module==="object"&&module.exports)module.exports=factory();else root.ReportCopy=factory();})(typeof window!=="undefined"?window:this,function(){return { + "version": "2026-10-09", + "conclusion": { + "contour": { + "改善": "面部轮廓紧致度有待提升,下颌线清晰度不足,左右下颌线角度分别为左 {la}°/ 右 {ra}°;眼袋明显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积略欠饱满,肌肤支撑力偏弱。", + "良好": "面部轮廓清晰,下颌线较为清晰,左右下颌线角度左 {la}°/ 右 {ra}°;眼周眼袋初显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积无明显流失;肌肤处于健康状态,支撑力表现良好。", + "优秀": "面部轮廓紧致立体,下颌线清晰;眼周无明显眼袋问题、黑眼圈轻/中/重度(根据分数给出判断);中面部容积饱满,肌肤支撑力充足。" + }, + "wrinkles": { + "改善": "全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。", + "良好": "全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面部整体平整,仅眼周或口周有细微的干纹,无明显静态深纹,纹路面积占比相对较低。", + "优秀": "面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。" + }, + "color": { + "改善": "检测到当前肌肤色素分 {melanin},存在色素沉着较显著,整体透亮感不足。", + "良好": "检测到当前肌肤色素分 {melanin},局部有色素沉积;整体状态稳定,局部透亮感稍弱。", + "优秀": "检测到当前肌肤色素分 {melanin},无明显色素沉积;肤色均匀透亮。" + }, + "smooth": { + "改善": "当前肌肤平滑度还有提升空间,检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,肤感整体细腻度有提升空间,纹理不均。", + "良好": "肌肤整体较为平滑,局部区域细腻度不足;检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,有轻微角质粗糙感。", + "优秀": "肌肤质感表现优异,表面细腻平滑,角质层排列整齐,无明显粗大毛孔与黑头。" + }, + "water": { + "改善": "缺水面积 {water_area}%、肌肤缺水比较严重 {water_sev},肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。", + "良好": "水分分 {water_score};缺水面积 {water_area}%、肌肤含水量处于健康水平,整体水润度良好。", + "优秀": "水分分 {water_score};角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。" + }, + "sens": { + "改善": "泛红面积达 {sens_area}%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。", + "良好": "泛红面积 {sens_area}%,肌肤存在局部轻微泛红;对外界环境有良好的耐受度,仅在接触强刺激物时偶有轻微反应。", + "优秀": "泛红面积 {sens_area}%,无明显泛红、敏感迹象;对外界刺激耐受度佳,肌肤处于稳定的健康状态。" + }, + "acne": { + "改善": "检测到 {acne_cnt} 处皮损({acne_detail}){heavy_note},肌肤状态不稳定,毛孔易堵塞,皮脂分泌波动明显。", + "良好": "检测到 {acne_cnt} 处皮损({acne_detail}),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。", + "优秀": "未检出明显痘痘、粉刺或闭口,无明显红肿、脓包迹象。" + } + }, + "care": { + "contour": { + "改善": { + "care": "重点进行轮廓紧致管理,关注面中、苹果肌与下颌线区域。日常增加紧致精华步骤,配合由下向上的轻柔提升按摩,帮助肌肤维持弹润和支撑感,让轮廓看起来更紧实。", + "lifestyle": "保证充足、规律的睡眠,让肌肤在夜间充分修护;坚持防晒,减少紫外线带来的老化影响;减少熬夜。" + }, + "良好": { + "care": "关注面中、苹果肌、下颌线等容易显疲态区域的支撑感。日常可叠加紧致精华步骤,并配合向上提升按摩手法,预防疲态引起的松弛与垮塌累积,稳住面部支撑线。", + "lifestyle": "日常注意防晒,保持规律作息;坚持适度运动,帮助肌肤维持活力。" + }, + "优秀": { + "care": "保持现有紧致抗衰护理节奏,日常叠加使用提亮精华,坚持防晒,加强光损伤防护与状态管理,持续关注面中疲态与轮廓支撑状态,让紧致状态更稳定、更持久。", + "lifestyle": "坚持防晒与规律作息,均衡饮食,保证优质蛋白、新鲜蔬果和健康脂肪摄入,适度运动。" + } + }, + "wrinkles": { + "改善": { + "care": "重视纹路管理,日常可使用含有肽类、胶原类成分的淡纹精华,针对眼周、法令纹等动态纹高发区域配合点按手法,遏制静态纹转化,帮肌肤恢复平滑细腻的状态。", + "lifestyle": "保证睡眠,注意睡姿,减少面部长期受压;减少夸张表情,避免揉眼、托腮等表情性挤压习惯,减少纹路加深。" + }, + "良好": { + "care": "加强日常淡纹与肌肤弹润护理,叠加使用含肽类、胶原类成分的淡纹精华,提升肌肤水润度与饱满感,维持肤表平滑。", + "lifestyle": "减少夸张表情反复牵拉,睡前加强保湿修护。" + }, + "优秀": { + "care": "以预防初纹为核心,搭配抗初老型精华,提前建立抗氧化/紧致护理习惯,延缓第一道静态纹出现。", + "lifestyle": "保持良好作息与运动习惯,坚持防晒,减轻光老化与糖化负担,维持肌肤稳定状态。" + } + }, + "color": { + "改善": { + "care": "重点关照暗沉与色素易沉积区域,强化透亮与肤色均匀,严格防晒,配合温和焕亮或抗氧化精华帮助均匀肤色与透亮感。", + "lifestyle": "日常加强防晒与抗氧化护理,减少熬夜与压力引发的暗沉,适当增加富含维C的蔬果摄入。" + }, + "良好": { + "care": "加强日常光泽管理与局部暗沉护理,做好防晒与抗氧化打底;叠加使用温和焕亮精华,稳住当前肤色状态并提升整体通透感。", + "lifestyle": "持续做好防晒,保持作息规律,避免波动性色沉,守护均匀透亮感。" + }, + "优秀": { + "care": "维持现有防晒与抗氧化护肤习惯,叠加使用温和焕亮精华,巩固肤色均匀度与透亮感。", + "lifestyle": "坚持日常防晒,保持均衡饮食支持皮肤稳态。" + } + }, + "smooth": { + "改善": { + "care": "关注毛孔可见度与肤表粗糙感改善,加强角质管理,提升肌肤平滑细腻度。", + "lifestyle": "减少高糖高脂饮食,降低皮脂分泌刺激,定期温和调理角质,避免用手挤压黑头。" + }, + "良好": { + "care": "加强基础清洁与角质管理,关注鼻翼、下颌等易堵塞部位,提升毛孔通畅度与肌肤细腻度。", + "lifestyle": "温水洁面避免烫洗,保持床品清洁减少局部刺激。" + }, + "优秀": { + "care": "维持当前温和护理节奏,维护角质层健康与肌肤屏障稳定状态。", + "lifestyle": "保持规律清洁与保湿习惯,避免频繁更换功效类产品,减少肌肤适应负担。" + } + }, + "water": { + "改善": { + "care": "强化屏障锁水修复,注意水油平衡,每周2-3次深度补水,深度改善肌肤缺水状态。", + "lifestyle": "每日足量饮水,补充身体水分,改善干燥环境湿度,避免长时间空调直吹。" + }, + "良好": { + "care": "日常需注重水油平衡调理,避免过度清洁损伤皮肤屏障,注意为肌肤持续补水。", + "lifestyle": "每日足量饮水,环境干燥时及时增加肌肤水分补充。" + }, + "优秀": { + "care": "持续为皮肤补水与保湿,避免过度清洁与频繁去角质,巩固肌肤水润度与水油平衡。", + "lifestyle": "坚持多喝水与规律作息,季节交替时提前调整保湿方案。" + } + }, + "sens": { + "改善": { + "care": "关注屏障修复与舒缓,精简护肤流程,选择温和洁面方式,修复受损角质层。若状态持续无明显好转,可寻求专业护肤建议。", + "lifestyle": "减少外界刺激,如酒精、香精、高浓度酸类精华等成分,避免冷热骤变刺激。" + }, + "良好": { + "care": "保持温和护理节奏,注意防晒与环境防护,逐步提升皮肤耐受度。", + "lifestyle": "避免冷热骤变刺激肌肤屏障,运动后及时清洁降温。" + }, + "优秀": { + "care": "持续规律护理并按季节阶段性调整,巩固皮肤耐受稳态。", + "lifestyle": "保持健康作息与均衡饮食,维持皮肤屏障稳态。" + } + }, + "acne": { + "改善": { + "care": "优先针对性调理痘区肌肤状态,精简护肤流程,避免频繁更换护肤方案与叠加功效成分;严重时及时就医。", + "lifestyle": "减少高糖高脂饮食与熬夜,避免手挤痘痘;保持床品、毛巾清洁,运动后及时清洁,减少汗液与皮脂堆积。" + }, + "良好": { + "care": "保持温和清洁与肌肤炎症调理,关注T区、下颌等易出油区域,提升毛孔通畅度,同时注意防晒与保湿。", + "lifestyle": "保持规律作息与清淡饮食,运动后及时清洁,减少汗液与皮脂堆积。" + }, + "优秀": { + "care": "坚持定期清洁,维持水油平衡与毛孔通畅,巩固皮肤耐受稳态。", + "lifestyle": "保持健康作息与均衡饮食,维持皮肤屏障健康稳定。" + } + } + } +};}); diff --git a/h5/js/report-ui.js b/h5/js/report-ui.js new file mode 100644 index 0000000..3027dd8 --- /dev/null +++ b/h5/js/report-ui.js @@ -0,0 +1,72 @@ +/* Presentation only: original scores and recommendation rules remain in SkinScore. */ +(function(root, factory) { + if (typeof module === 'object' && module.exports) module.exports = factory(require('./report-copy.js')); + else root.ReportUI = factory(root.ReportCopy); +})(typeof window !== 'undefined' ? window : this, function(Copy) { + 'use strict'; + var defs = [ + ['contour', '轮廓', 'firm', 'contour'], ['wrinkles', '皱纹', 'firm', 'wrinkles'], + ['color', '肤色', 'vita', 'tone'], ['smooth', '平滑度', 'vita', 'smooth'], ['water', '水分', 'vita', 'tone'], + ['sens', '泛红', 'stab', 'sens'], ['acne', '痘痘', 'stab', 'acne'] + ]; + var products = [ + { tier: 1, match: '25+束紧精华', title: "1'me 主场精华 I", name: '以我本然有光蓄活', image: 'assets/img/ui-20261009/product-25.png' }, + { tier: 2, match: '35+支撑精华', title: "1'me 主场精华 II", name: '以我自在有势挺括', image: 'assets/img/ui-20261009/product-35.png' }, + { tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261009/product-50.png' } + ]; + var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' }; + function numeric(value) { return typeof value === 'number' && isFinite(value); } + function fill(template, vars) { + return template.replace(/\{([a-z_]+)\}/g, function(_, key) { + var v = vars[key]; + return v == null ? '未获取' : numeric(v) ? String(Math.round(v * 100) / 100) : String(v); + }).replace(/\s+/g, ' ').trim(); + } + function count(result, key) { var v = result[key]; return v && numeric(v.count) ? v.count : null; } + function build(vm, result) { + result = result || {}; + var views = defs.map(function(d) { + var cat = vm.cats[d[3]], vars = Object.assign({}, cat.vars || {}); + var level = levelMap[cat.level], valid = cat.P != null; + if (d[0] === 'color' && vars.melanin == null) valid = false; + if (d[0] === 'water' && vars.water_score == null) valid = false; + var template = valid && level ? Copy.conclusion[d[0]][level] : ''; + if (d[0] === 'contour') { + var severity = vars.dark_circle_sev; + var description = numeric(severity) ? ['无明显黑眼圈', '黑眼圈轻度', '黑眼圈中度', '黑眼圈重度'][severity] : null; + template = template.replace(/黑眼圈轻\/中\/重度(根据分数给出判断)/g, description || '黑眼圈数据未获取'); + if (vars.la == null || vars.ra == null) template = template.replace(/左右下颌线角度(?:分别为左|左) \{la\}°\/ 右 \{ra\}°[;;]/g, '侧脸角度数据未完整获取;'); + } + if (d[0] === 'acne') { + var pustules = count(result, 'acne_pustule'), nodules = count(result, 'acne_nodule'); + var heavy = [pustules > 0 ? '脓包' : '', nodules > 0 ? '结节' : ''].filter(Boolean); + vars.heavy_note = heavy.length ? ',含' + heavy.join('、') + '等中重度皮损,炎症反应明显' : ''; + if (heavy.length) { level = '改善'; template = Copy.conclusion.acne[level]; } + else if (vars.acne_cnt > 0 && level === '优秀') { level = '良好'; template = Copy.conclusion.acne[level]; } + if (vars.acne_cnt == null) template = '本次未获取完整皮损计数,请结合检测图片查看。'; + else if (vars.acne_cnt === 0) template = '本次未检出明显皮损,请结合检测图片查看。'; + } + // Missing counts are unknown, not zero: remove only the unavailable metric. + if (vars.fl_cnt == null) template = template.replace(/、细纹 \{fl_cnt\} 条/g, '(细纹计数未获取)'); + if (vars.wr_cnt == null) template = template.replace(/皱纹 \{wr_cnt\} 条/g, '皱纹计数未获取'); + if (vars.water_area == null) template = template.replace(/缺水面积 \{water_area\}%[、,]/g, '缺水面积未获取;'); + if (vars.water_sev == null) template = template.replace(/肌肤缺水比较严重 \{water_sev\},/g, ''); + if (vars.sens_area == null) template = template.replace(/泛红面积(?:达 | )\{sens_area\}%[,;]/g, '泛红面积未获取;'); + if (vars.pore_cnt == null) template = template.replace(/粗大毛孔 \{pore_cnt\} 颗/g, '毛孔计数未获取'); + if (vars.blackhead_cnt == null) template = template.replace(/黑头 \{blackhead_cnt\} 颗/g, '黑头计数未获取'); + var advice = level ? Copy.care[d[0]][level] : null; + return { key: d[0], name: d[1], dim: d[2], source: d[3], P: cat.P, level: level || null, + text: valid ? fill(template, vars) : '本次未检出有效数据,请重新拍摄后查看。', + valid: valid, advice: valid ? advice : null }; + }); + // Preserve selection of the two weakest original categories. Tone expands into + // color+water advice, with thresholds inherited from the same original category. + var ranked = Object.keys(vm.cats).filter(function(k) { return vm.cats[k].P != null; }) + .sort(function(a,b) { return vm.cats[a].P - vm.cats[b].P; }).slice(0,2); + var chosen = views.filter(function(v) { return ranked.indexOf(v.source) >= 0 && v.advice; }); + var care = chosen.map(function(v) { return { name: v.name, text: v.advice.care + v.advice.lifestyle }; }); + var product = products.filter(function(p) { return p.match === vm.essence.name; })[0] || null; + return { version: Copy.version, dims: { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' }, views: views, care: care, product: product }; + } + return { build: build, products: products, levelMap: levelMap }; +}); diff --git a/h5/js/skin-score.js b/h5/js/skin-score.js index 2f9d576..a59f5ea 100644 --- a/h5/js/skin-score.js +++ b/h5/js/skin-score.js @@ -668,6 +668,7 @@ res = def.key === 'contour' ? def.calc(r, sideResults, notes) : def.calc(r, notes); cats[def.key] = { name: def.name, dim: def.dim, + vars: res.vars, // 原始模板变量供新版展示层使用,不参与评分规则变更 P: res.P, S: sShow(res.P), level: levelOf(res.P), text: res.P == null ? (res.vars.missing ? def.text(null, res.vars) : '本次检测' + def.name + '分类数据缺失,暂不参与评分。') diff --git a/h5/lib/accounts.js b/h5/lib/accounts.js new file mode 100644 index 0000000..7a53e1d --- /dev/null +++ b/h5/lib/accounts.js @@ -0,0 +1,161 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const net = require('net'); +const random = () => crypto.randomBytes(32).toString('hex'); +const hash = value => crypto.createHash('sha256').update(value).digest('hex'); + +// One Node process owns this embedded store. No credentials or identity are accepted +// from a report request; the owner always comes from a server-created session. +module.exports = function createAccounts(root, env = process.env) { + fs.mkdirSync(root, { recursive: true, mode: 0o700 }); + const dbFile = path.join(root, 'identities.json'); + const snapshots = path.join(root, 'reports'); + fs.mkdirSync(snapshots, { recursive: true, mode: 0o700 }); + let db; + try { db = JSON.parse(fs.readFileSync(dbFile, 'utf8')); } + catch (e) { if (e.code !== 'ENOENT') throw e; db = { users: [], sessions: {}, states: {} }; } + function atomic(file, value) { + const tmp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(value), { mode: 0o600 }); + fs.renameSync(tmp, file); + } + function save() { atomic(dbFile, db); } + let base = null; + try { base = new URL(env.PUBLIC_BASE_URL); } catch (_) { /* local mode */ } + const prefix = base ? base.pathname.replace(/\/?$/, '/') : '/'; + if (base) { base.pathname = prefix; base.search = ''; base.hash = ''; } + const secure = !!(base && base.protocol === 'https:'); + const appId = env.WECHAT_APP_ID || ''; + const secret = env.WECHAT_APP_SECRET || ''; + const available = !!(appId && secret && secure && !/^(localhost|127\.|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/.test(base.hostname)); + function cookies(req) { + return Object.fromEntries((req.headers.cookie || '').split(';').map(s => s.trim().split('='))); + } + function cookie(res, name, value, age) { + const line = `${name}=${value}; Path=${prefix}; HttpOnly; SameSite=Lax; Max-Age=${age}${secure ? '; Secure' : ''}`; + const old = res.getHeader('Set-Cookie') || []; + res.setHeader('Set-Cookie', [].concat(old, line)); + } + function session(req) { + const token = cookies(req).juzi_session; + const row = token && db.sessions[hash(token)]; + return row && row.expires > Date.now() ? db.users.find(u => u.id === row.user) : null; + } + function login(res, user) { + for (const key of Object.keys(db.sessions)) if (db.sessions[key].expires < Date.now()) delete db.sessions[key]; + const token = random(); + db.sessions[hash(token)] = { user: user.id, expires: Date.now() + 30 * 86400000 }; + cookie(res, 'juzi_session', token, 30 * 86400); + save(); + } + function publicUser(u) { return u ? { id: u.id, kind: u.kind } : null; } + function json(res, status, obj) { + res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }); + res.end(JSON.stringify(obj)); + } + async function body(req, limit = 4096) { + let size = 0; const parts = []; + for await (const part of req) { size += part.length; if (size > limit) throw new Error('请求过大'); parts.push(part); } + return JSON.parse(Buffer.concat(parts).toString() || '{}'); + } + function sameOrigin(req) { + if (!req.headers.origin) return true; + try { + const origin = new URL(req.headers.origin); + return origin.origin === (base ? base.origin : `http://${req.headers.host}`); + } catch (_) { return false; } + } + const limits = new Map(); + function rate(req) { + let key = req.socket.remoteAddress; + // Opt in only behind a loopback proxy that OVERWRITES X-Real-IP. + const forwarded = req.headers['x-real-ip']; + if (env.TRUST_LOOPBACK_PROXY === '1' && ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(key) + && typeof forwarded === 'string' && net.isIP(forwarded)) key = forwarded; + const now = Date.now(); + for (const [k, v] of limits) if (now - v.time > 60000) limits.delete(k); + const value = limits.get(key) || { time: now, count: 0 }; + limits.set(key, value); return ++value.count <= 60; + } + function redirect(res, suffix) { res.writeHead(302, { Location: prefix + suffix, 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' }); res.end(); } + async function route(req, res, pathname) { + if (!pathname.startsWith('/api/auth/') && !pathname.startsWith('/api/reports')) return false; + try { + if (req.method !== 'GET' && !sameOrigin(req)) { json(res, 403, { ok: false, message: '请求来源不匹配' }); return true; } + if (pathname === '/api/auth/config' && req.method === 'GET') { + json(res, 200, { ok: true, wechat: available, fallback: 'browser', reason: available ? null : '当前环境尚未开通微信登录,可使用本机浏览器登录' }); + } else if (pathname === '/api/auth/session' && req.method === 'GET') { + json(res, 200, { ok: true, user: publicUser(session(req)) }); + } else if (pathname === '/api/auth/browser' && req.method === 'POST') { + if (!rate(req)) { json(res, 429, { ok: false, message: '操作频繁,请稍后再试' }); return true; } + const data = await body(req); + if (data.consent !== true) { json(res, 400, { ok: false, message: '请先阅读并同意协议' }); return true; } + let token = data.token, user; + if (token) { + if (typeof token !== 'string' || !/^[a-f0-9]{64}$/.test(token)) token = null; + user = token && db.users.find(u => u.kind === 'browser' && u.credential === hash(token)); + if (!user) { json(res, 401, { ok: false, message: '本机身份已失效,请重新登录' }); return true; } + } else { + token = random(); user = { id: crypto.randomUUID(), kind: 'browser', credential: hash(token), consent: new Date().toISOString(), reports: [] }; + db.users.push(user); + } + login(res, user); json(res, 200, { ok: true, user: publicUser(user), token }); + } else if (pathname === '/api/auth/logout' && req.method === 'POST') { + const token = cookies(req).juzi_session; if (token) delete db.sessions[hash(token)]; + cookie(res, 'juzi_session', '', 0); save(); json(res, 200, { ok: true }); + } else if (pathname === '/api/auth/wechat' && req.method === 'POST') { + const data = await body(req); + if (data.consent !== true) { json(res, 400, { ok: false, message: '请先阅读并同意协议' }); return true; } + if (!available) { json(res, 503, { ok: false, message: '微信登录尚未配置,请使用本机浏览器登录' }); return true; } + if (!rate(req)) { json(res, 429, { ok: false, message: '操作频繁,请稍后重试' }); return true; } + const state = random(), binding = random(); + for (const k of Object.keys(db.states)) if (db.states[k].expires < Date.now()) delete db.states[k]; + db.states[hash(state)] = { binding: hash(binding), expires: Date.now() + 600000 }; + cookie(res, 'juzi_oauth', binding, 600); save(); + const auth = new URL('https://open.weixin.qq.com/connect/oauth2/authorize'); + auth.search = new URLSearchParams({ appid: appId, redirect_uri: new URL('api/auth/wechat/callback', base).href, response_type: 'code', scope: 'snsapi_base', state }).toString(); + auth.hash = 'wechat_redirect'; + json(res, 200, { ok: true, url: auth.href }); + } else if (pathname === '/api/auth/wechat/callback' && req.method === 'GET') { + const url = new URL(req.url, 'http://localhost'); + const state = url.searchParams.get('state') || '', code = url.searchParams.get('code'); + const row = db.states[hash(state)], binding = cookies(req).juzi_oauth || ''; + delete db.states[hash(state)]; save(); cookie(res, 'juzi_oauth', '', 0); + if (!available || !code || !row || row.expires < Date.now() || row.binding !== hash(binding)) { redirect(res, '?login_error=state'); return true; } + const tokenUrl = new URL('https://api.weixin.qq.com/sns/oauth2/access_token'); + tokenUrl.search = new URLSearchParams({ appid: appId, secret, code, grant_type: 'authorization_code' }).toString(); + let data; + try { const r = await fetch(tokenUrl, { signal: AbortSignal.timeout(10000) }); data = await r.json(); } + catch (_) { redirect(res, '?login_error=network'); return true; } + if (!data.openid || data.errcode) { redirect(res, '?login_error=wechat'); return true; } + const identity = hash(appId + ':' + data.openid); + let user = db.users.find(u => u.kind === 'wechat' && u.identity === identity); + if (!user) { user = { id: crypto.randomUUID(), kind: 'wechat', identity, consent: new Date().toISOString(), reports: [] }; db.users.push(user); } + login(res, user); redirect(res, '?login=success'); + } else if (pathname.startsWith('/api/reports') && req.method === 'GET') { + const user = session(req); + if (!user) { json(res, 401, { ok: false, message: '请先登录' }); return true; } + const id = pathname.slice('/api/reports/'.length); + if (pathname === '/api/reports') json(res, 200, { ok: true, total: user.reports.length, items: user.reports }); + else if (/^R[\w-]+$/.test(id) && user.reports.some(r => r.id === id)) { + const file = path.join(snapshots, user.id, id + '.json'); + if (!fs.existsSync(file)) json(res, 404, { ok: false, message: '报告文件暂不可用' }); + else json(res, 200, { ok: true, report: JSON.parse(fs.readFileSync(file, 'utf8')) }); + } else json(res, 404, { ok: false, message: '报告不存在' }); + } else json(res, 404, { ok: false, message: '接口不存在' }); + } catch (_) { if (!res.headersSent) json(res, 400, { ok: false, message: '请求处理失败,请稍后重试' }); } + return true; + } + function storeReport(ownerId, snapshot) { + const user = db.users.find(u => u.id === ownerId); + if (!user || !/^R[\w-]+$/.test(snapshot.id)) throw new Error('Invalid report owner'); + const dir = path.join(snapshots, user.id); fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + atomic(path.join(dir, snapshot.id + '.json'), snapshot); + user.reports = [{ id: snapshot.id, created_at: snapshot.created_at, total: snapshot.vm.total, skinAge: snapshot.vm.skinAgeDisplay }] + .concat(user.reports.filter(r => r.id !== snapshot.id)).slice(0, 5); + save(); + } + return { route, session, storeReport, sameOrigin, wechatAvailable: available }; +}; diff --git a/h5/server.js b/h5/server.js index fffea16..8c937f0 100644 --- a/h5/server.js +++ b/h5/server.js @@ -19,10 +19,12 @@ const PORT = Number(process.env.PORT) || 8080; const HOST = process.env.HOST || '127.0.0.1'; const ROOT = __dirname; const FACEPP_URL = 'https://api-cn.faceplusplus.com/facepp/v1/skinanalyze_pro'; -const DATA_ROOT = path.join(ROOT, 'data'); +const DATA_ROOT = process.env.JUZI_DATA_ROOT ? path.resolve(process.env.JUZI_DATA_ROOT) : path.join(ROOT, 'data'); const DATA_DIR = path.join(DATA_ROOT, 'results'); // 每次检测的原始返回落盘(复核用,已在 .gitignore) const PHOTOS_DIR = path.join(DATA_ROOT, 'photos'); // 检测照片:data/photos//{front,left,right}.jpg const RECORDS_FILE = path.join(DATA_ROOT, 'records.jsonl'); // 检测记录(一行一条 JSON) +const ReportUI = require('./js/report-ui.js'); +const accounts = require('./lib/accounts.js')(path.join(DATA_ROOT, 'accounts')); /* 服务端评分引擎:与 C 端同一份公式(禁止复制公式,直接复用) */ const SkinScore = require('./js/skin-score.js'); @@ -66,12 +68,19 @@ function serveStatic(req, res, pathname) { res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('Bad Request'); } + // Authorize the same canonical path that the filesystem will read. Repeated, + // encoded and Windows-style slashes must not bypass private-directory checks. + p = path.posix.normalize('/' + p.replace(/\\/g, '/')); if (p === '/') p = '/index.html'; // /data/ 下是检测照片/原始 JSON/记录库(敏感个人信息),禁止静态直出: // 照片只能走 /api/admin/photos(由 nginx Basic Auth 保护),此处一律 403 if (p === '/data' || p.startsWith('/data/')) { res.writeHead(403); return res.end('Forbidden'); } + if (p === '/server.js' || /^\/(lib|test)(\/|$)/.test(p) || p.split('/').some(s => s.startsWith('.'))) { + res.writeHead(404); return res.end('Not Found'); + } const file = path.normalize(path.join(ROOT, p)); if (file !== ROOT && !file.startsWith(ROOT + path.sep)) { res.writeHead(403); return res.end('Forbidden'); } + if (file === DATA_ROOT || file.startsWith(DATA_ROOT + path.sep)) { res.writeHead(403); return res.end('Forbidden'); } fs.readFile(file, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('Not Found'); } const ext = path.extname(file); @@ -342,6 +351,9 @@ async function callFacepp(front, left, right) { async function handleAnalyze(req, res) { let rec = null; // 记录:正脸照片校验通过后创建(此前失败无从落库) try { + const owner = accounts.session(req); + if (!owner) return sendJson(res, 401, { ok: false, message: '请先登录后检测' }); + if (!accounts.sameOrigin(req)) return sendJson(res, 403, { ok: false, message: '请求来源不匹配' }); const body = JSON.parse((await readBody(req, 20 * 1024 * 1024)).toString('utf8')); // 三个 image 字段先做类型检查:非字符串视为缺失(左/右图缺失时仅发正脸,接口自动降级为单视图) const front = stripDataPrefix(typeof body.front_image === 'string' ? body.front_image : ''); @@ -362,6 +374,7 @@ async function handleAnalyze(req, res) { const photos = writePhotos(recordId, { front, left, right }); rec = { record_id: recordId, + owner_id: owner.id, created_at: nowIso(), updated_at: nowIso(), real_age: profile.real_age, @@ -412,9 +425,24 @@ async function handleAnalyze(req, res) { // 下颌线结果在响应顶层 left_side_result/right_side_result(不在 result 内), // 个别批次可能嵌套在 side_result 下,两种层级都兼容 const nested = fpJson.side_result || {}; + const payload = { + result, face_rectangle: fpJson.face_rectangle || null, + side: { left: fpJson.left_side_result || nested.left_side_result || null, + right: fpJson.right_side_result || nested.right_side_result || null } + }; + let reportSaved = false, presentation = null, snapshotVm = null; + try { + snapshotVm = SkinScore.buildViewModel(result, payload.side, scoreProfile(rec)); + presentation = ReportUI.build(snapshotVm, result); + accounts.storeReport(owner.id, { id: recordId, created_at: rec.created_at, + version: '2026-10-09', profile, vm: snapshotVm, presentation, payload, + photos: [front, left, right].map(b64 => b64 ? toDataUrl(b64) : null) }); + reportSaved = true; + } catch (e) { console.error('[report] 完整报告保存失败:', e.message); } res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ ok: true, + record_id: recordId, report_saved: reportSaved, presentation, vm: snapshotVm, result, face_rectangle: fpJson.face_rectangle || null, side: { @@ -752,8 +780,9 @@ async function handleAdmin(req, res, pathname, url) { } } -http.createServer((req, res) => { +http.createServer(async (req, res) => { const pathname = req.url.split('?')[0]; + if (await accounts.route(req, res, pathname)) return; if (req.method === 'POST' && pathname === '/api/skin-analyze') return handleAnalyze(req, res); if (req.method === 'GET' && pathname === '/api/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); diff --git a/h5/test/accounts.js b/h5/test/accounts.js new file mode 100644 index 0000000..13d6ef6 --- /dev/null +++ b/h5/test/accounts.js @@ -0,0 +1,92 @@ +'use strict'; +const assert = require('node:assert/strict'); +const http = require('node:http'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const create = require('../lib/accounts'); +const SkinScore = require('../js/skin-score'); +const ReportUI = require('../js/report-ui'); + +(async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-accounts-test-')); + let accounts = create(root, {}); + const server = http.createServer(async (req, res) => { + if (!await accounts.route(req, res, req.url.split('?')[0])) { res.writeHead(404); res.end(); } + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const base = 'http://127.0.0.1:' + server.address().port; + const request = async (url, data, cookie, extra = {}) => { + const r = await fetch(base + url, { redirect: 'manual', method: data ? 'POST' : 'GET', + headers: { ...(cookie ? { cookie } : {}), ...extra }, body: data ? JSON.stringify(data) : undefined }); + const text = await r.text(); + return { status: r.status, data: text ? JSON.parse(text) : null, cookie: r.headers.getSetCookie().map(c => c.split(';')[0]).join('; '), location: r.headers.get('location'), headers: r.headers }; + }; + try { + assert.equal((await request('/api/reports')).status, 401); + assert.equal((await request('/api/auth/browser', {})).status, 400); + assert.equal((await request('/api/auth/browser', { consent: true }, '', { Origin: 'https://unrelated.example' })).status, 403); + const a = await request('/api/auth/browser', { consent: true }); + const b = await request('/api/auth/browser', { consent: true }); + assert.notEqual(a.data.user.id, b.data.user.id); + assert(a.headers.get('set-cookie').includes('HttpOnly')); + const vm = SkinScore.buildViewModel(SkinScore.MOCK_RESULT, SkinScore.MOCK_SIDE, { realAge: 30, skinType: '干性肌肤' }); + const presentation = ReportUI.build(vm, SkinScore.MOCK_RESULT); + const snapshot = { id: 'Rtest0', created_at: new Date().toISOString(), vm, presentation, + photos: ['data:image/jpeg;base64,front', 'data:image/jpeg;base64,left', 'data:image/jpeg;base64,right'], + payload: { result: { ...SkinScore.MOCK_RESULT, face_maps: { brown_area: 'data:image/png;base64,saved-layer' } }, side: SkinScore.MOCK_SIDE } }; + accounts.storeReport(a.data.user.id, snapshot); + assert.deepEqual((await request('/api/reports/Rtest0', null, a.cookie)).data.report, JSON.parse(JSON.stringify(snapshot))); + assert.equal((await request('/api/reports/Rtest0', null, b.cookie)).status, 404); + assert.equal((await request('/api/reports', null, b.cookie)).data.total, 0); + for (let i = 1; i <= 5; i++) accounts.storeReport(a.data.user.id, { ...snapshot, id: 'Rtest' + i }); + assert.equal((await request('/api/reports', null, a.cookie)).data.total, 5); + assert.equal((await request('/api/reports/Rtest0', null, a.cookie)).status, 404); + accounts = create(root, {}); // Process restart reloads sessions and reports. + assert.equal((await request('/api/reports', null, a.cookie)).data.total, 5); + const restored = await request('/api/auth/browser', { consent: true, token: a.data.token }); + assert.equal(restored.data.user.id, a.data.user.id); + assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' })).status, 401); + await request('/api/auth/logout', {}, restored.cookie); + assert.equal((await request('/api/reports', null, restored.cookie)).status, 401); + assert.equal((await request('/api/auth/config')).data.wechat, false); + accounts = create(root, { WECHAT_APP_ID: 'test-app', WECHAT_APP_SECRET: 'test-secret', PUBLIC_BASE_URL: 'https://skin.example.com/state' }); + const start = await request('/api/auth/wechat', { consent: true }); + const url = new URL(start.data.url); + assert.equal(url.searchParams.get('redirect_uri'), 'https://skin.example.com/state/api/auth/wechat/callback'); + assert(start.headers.get('set-cookie').includes('Secure')); + assert(start.headers.get('set-cookie').includes('Path=/state/')); + const state = url.searchParams.get('state'); + assert((await request('/api/auth/wechat/callback?code=test&state=' + state)).location.includes('login_error=state')); + // Mock only the upstream token exchange, not our HTTP routes, state or persistence. + const realFetch = global.fetch; + global.fetch = (url, options) => String(url).startsWith('https://api.weixin.qq.com/') + ? Promise.resolve({ json: async () => ({ openid: 'same-wechat-user' }) }) : realFetch(url, options); + try { + async function wechat() { + const start = await request('/api/auth/wechat', { consent: true }); + const state = new URL(start.data.url).searchParams.get('state'); + const callback = await request('/api/auth/wechat/callback?code=test&state=' + state, null, start.cookie); + assert.equal(callback.location, '/state/?login=success'); + assert((await request('/api/auth/wechat/callback?code=test&state=' + state, null, start.cookie)).location.includes('login_error=state')); + return (await request('/api/auth/session', null, callback.cookie)).data.user; + } + assert.deepEqual(await wechat(), await wechat()); + } finally { global.fetch = realFetch; } + accounts = create(root, { TRUST_LOOPBACK_PROXY: '1' }); + // Malformed credentials exercise the limiter without creating 60 users. + for (let i = 0; i < 60; i++) assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.1' })).status, 401); + assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.1' })).status, 429); + assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.2' })).status, 401); + accounts = create(root, {}); + for (let i = 0; i < 60; i++) await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.1' }); + assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' }, '', { 'X-Real-IP': '203.0.113.2' })).status, 429, 'untrusted forwarding headers cannot bypass rate limit'); + assert.equal(presentation.views.length, 7); + assert(presentation.views.every(v => !/\{[a-z_]+\}/.test(v.text))); + assert(!presentation.views.find(v => v.key === 'wrinkles').text.includes('未获取 条')); + console.log('PASS: consent, CSRF, isolated owners, exact snapshots, latest 5, restart, restore, logout, OAuth state/replay/subpath/same identity, seven-view copy'); + } finally { + await new Promise(resolve => server.close(resolve)); + fs.rmSync(root, { recursive: true, force: true }); // Only this test-created temp directory. + } +})().catch(e => { console.error(e); process.exitCode = 1; }); diff --git a/h5/test/e2e-browser.mjs b/h5/test/e2e-browser.mjs index 83fbbbd..36ec39b 100644 --- a/h5/test/e2e-browser.mjs +++ b/h5/test/e2e-browser.mjs @@ -109,13 +109,16 @@ try { return b && b.textContent && b.textContent !== '--'; }, null, { timeout: 8000 }); const score = await page.locator('#stat-score b').innerText(); - const essence = await page.locator('#essence-name').innerText(); + const essence = await page.locator('#product-title').innerText(); const reportOn = await page.locator('#s-report.active').count(); check('直达报告页', reportOn === 1 && /^\d/.test(score) && essence && essence !== '--', `score=${score} essence=${essence}`); // 3) 完整 mock 流程:启动 → 档案 → 三连拍 → 报告,并记录语音起播 await page.goto(BASE + '/?mock=1', { waitUntil: 'networkidle' }); + await page.locator('#login-consent').check(); + await page.locator('[data-action="login-browser"]').click(); + await page.waitForSelector('#s-splash.active'); await page.getByRole('button', { name: '开始检测' }).click(); await page.locator('#privacy-check').click(); await page.getByRole('button', { name: '继续' }).click(); @@ -138,11 +141,11 @@ try { return v && v.videoWidth > 0 && document.querySelector('#s-cam').classList.contains('cam-live'); }, null, { timeout: 12000 }); const step1 = (await page.locator('#cam-step').innerText()).replace(/\s+/g, ''); - check('进入拍照 1/3', step1.includes('1/3') && step1.includes('正对屏幕'), step1); + check('进入拍照 1/3', step1.includes('1/3') && step1.includes('正脸出镜'), step1); // mock 无人脸检测,快门作为调试入口保留;听完每步引导再按(最长 step2 4.3s):引导、「拍摄完成」、「分析中」都应完整播出 // 步骤顺序与轮廓对齐 Figma:正脸 → 右脸(耳在画面右,mask 2)→ 左脸(耳在画面左,mask 1) - const FIGMA_STEPS = [['正对屏幕', '0'], ['右脸', '2'], ['左脸', '1']]; + const FIGMA_STEPS = [['正脸出镜', '0'], ['右脸', '2'], ['左脸', '1']]; for (let i = 0; i < 3; i++) { const n = i + 1; await page.waitForFunction((num) => { diff --git a/h5/test/report-persistence.mjs b/h5/test/report-persistence.mjs new file mode 100644 index 0000000..c67447e --- /dev/null +++ b/h5/test/report-persistence.mjs @@ -0,0 +1,92 @@ +// Isolated HTTP + browser integration. Upstream Face++ is fixture-backed; no paid call. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import vm from 'node:vm'; +import http from 'node:http'; +import { spawn, execSync } from 'node:child_process'; +import { once } from 'node:events'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href); +const h5 = fileURLToPath(new URL('../', import.meta.url)); +const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-report-test-')); +const fx = {}; +vm.runInNewContext(fs.readFileSync(path.join(h5, 'assets/mock/report-fixture.js'), 'utf8'), { window: fx }); +const fixture = fx.MOCK_FIXTURE; +const child = spawn(process.execPath, ['-e', ` + const fs=require('fs'), vm=require('vm'), w={}; + vm.runInNewContext(fs.readFileSync('assets/mock/report-fixture.js','utf8'),{window:w}); + const f=w.MOCK_FIXTURE; + global.fetch=async()=>({ok:true,status:200,json:async()=>({result:f.result,face_rectangle:f.face_rectangle,left_side_result:f.side.left,right_side_result:f.side.right})}); + require('./server.js'); +`], { cwd: h5, env: { ...process.env, PORT: '18089', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot, + SKIN_API_KEY: 'fixture-key', SKIN_API_SECRET: 'fixture-secret', PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '' }, stdio: ['ignore', 'pipe', 'pipe'] }); +let browser; +try { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('test server startup timeout')), 10000); + child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timeout); resolve(); } }); + child.once('exit', code => { clearTimeout(timeout); reject(new Error('server exited: ' + code)); }); + }); + const base = 'http://127.0.0.1:18089/'; + browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); + const page = await browser.newPage({ viewport: { width: 375, height: 726 } }); + const errors = []; + page.on('pageerror', e => errors.push(e.message)); + await page.goto(base); + await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查')); + await page.check('#login-consent'); + await page.click('[data-action="login-browser"]'); + await page.waitForSelector('#s-splash.active'); + const api = page.context().request; + const result = await (await api.post(base + 'api/skin-analyze', { data: { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0], profile: { real_age: 28, daily_skin_type: '干性肌肤' } } })).json(); + assert.equal(result.ok, true); + assert.equal(result.report_saved, true); + assert(result.vm && result.presentation); + const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json(); + assert.equal(snapshot.report.photos.length, 3); + assert.deepEqual(snapshot.report.presentation, result.presentation); + assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps); + assert.equal(snapshot.report.photos[0], fixture.photos[0]); + // A separate browser cannot access this user's report, even knowing its ID. + const other = await browser.newContext(); + assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 401); + await other.request.post(base + 'api/auth/browser', { data: { consent: true } }); + assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 404); + for (const route of ['data/accounts/identities.json', 'lib/accounts.js', 'server.js', 'test/accounts.js', '%2edata/identities.json']) { + assert([403,404].includes((await api.get(base + route)).status()), route); + } + // Raw HTTP avoids URL clients normalizing away the malicious path under test. + for (const route of ['//data/accounts/identities.json', '/%2fdata/accounts/identities.json', + '/%5cdata/accounts/identities.json', '//lib/accounts.js', '/%2fserver.js', '/assets/../data/accounts/identities.json']) { + const status = await new Promise((resolve, reject) => { + http.get({ hostname: '127.0.0.1', port: 18089, path: route }, r => { r.resume(); resolve(r.statusCode); }).on('error', reject); + }); + assert([403,404].includes(status), route); + } + await page.click('[data-action="history"]'); + await page.click('[data-report-id]'); + await page.waitForSelector('#s-report.active'); + await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth)); + const imagesBefore = await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src)); + const copyBefore = await page.locator('.ditem-para').allTextContents(); + await page.reload(); + await page.waitForSelector('#s-splash.active'); + await page.click('[data-action="history"]'); + await page.click('[data-report-id]'); + await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth)); + assert.deepEqual(await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src)), imagesBefore); + assert.deepEqual(await page.locator('.ditem-para').allTextContents(), copyBefore); + await page.goBack(); + await page.waitForSelector('#s-history.active'); + await page.goBack(); + await page.waitForSelector('#s-splash.active'); + assert.deepEqual(errors, []); + console.log('PASS: authenticated analyze → full snapshot → backend history → reload → seven identical images/copy; owner isolation, private files, browser back'); +} finally { + if (browser) await browser.close(); + child.kill('SIGTERM'); + if (child.exitCode === null) await once(child, 'exit'); + fs.rmSync(dataRoot, { recursive: true, force: true }); +} diff --git a/h5/test/ui-20261009.mjs b/h5/test/ui-20261009.mjs new file mode 100644 index 0000000..6b25842 --- /dev/null +++ b/h5/test/ui-20261009.mjs @@ -0,0 +1,72 @@ +import { execSync } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; +const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href); +import fs from 'node:fs'; +import assert from 'node:assert/strict'; +const root = new URL('../../design/ui-20261009/verification/', import.meta.url); +fs.mkdirSync(root, { recursive: true }); +const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); +const page = await browser.newPage({ viewport: { width: 375, height: 726 } }); +const errors = []; +page.on('pageerror', e => errors.push(e.message)); +const base = process.env.H5_BASE_URL || 'http://127.0.0.1:8080/'; +await page.goto(base); +await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查')); +await page.evaluate(() => document.fonts.ready); +await page.waitForTimeout(400); +await page.screenshot({ path: new URL('01-login.png', root).pathname }); +await page.click('[data-action="login-browser"]'); +assert(await page.locator('#s-login').evaluate(e => e.classList.contains('active'))); +await page.check('#login-consent'); +await page.click('[data-action="login-wechat"]'); +await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('本机浏览器')); +await page.click('[data-action="login-browser"]'); +await page.waitForSelector('#s-splash.active'); +await page.waitForFunction(() => !document.querySelector('#toast').classList.contains('show')); +await page.waitForTimeout(400); +await page.screenshot({ path: new URL('02-home.png', root).pathname }); +await page.click('[data-action="history"]'); +await page.waitForFunction(() => document.querySelector('#history-count').textContent.includes('次')); +await page.waitForTimeout(400); +await page.screenshot({ path: new URL('03-history-empty.png', root).pathname }); +await page.goBack(); +await page.waitForSelector('#s-splash.active'); +await page.reload(); +await page.waitForSelector('#s-splash.active'); +await page.click('[data-action="open-privacy"]'); +await page.waitForTimeout(400); +await page.screenshot({ path: new URL('04-privacy.png', root).pathname }); +await page.click('#privacy-check'); +await page.click('[data-action="agree-continue"]'); +await page.waitForTimeout(400); +assert.equal(await page.locator('#s-profile').evaluate(e => e.getBoundingClientRect().top), 0); +await page.screenshot({ path: new URL('05-profile.png', root).pathname }); +// Static mask layout audit only; real capture flow is covered by e2e-browser.mjs. +await page.evaluate(() => document.querySelectorAll('.screen').forEach(s => s.classList.toggle('active', s.id === 's-cam'))); +await page.waitForTimeout(400); +for (const [step, name, label] of [['0', 'front', '1/3
请正脸出镜'], ['2', 'right', '2/3
请右脸出镜'], ['1', 'left', '3/3
请左脸出镜']]) { + await page.evaluate(({ step, label }) => { document.querySelector('#cam-mask').dataset.step = step; document.querySelector('#cam-step').innerHTML = label; }, { step, label }); + await page.screenshot({ path: new URL('08-camera-' + name + '-layout.png', root).pathname }); +} +await page.goto(base + '?mock=1&screen=report'); +await page.waitForFunction(() => document.querySelector('#stat-score b').textContent !== '--'); +await page.waitForTimeout(1200); +assert.equal(await page.locator('.card-detail').count(), 3); +assert.equal(await page.locator('.dtab').count(), 7); +assert.equal(await page.locator('.card-product img#product-image').count(), 1); +await page.screenshot({ path: new URL('06-report-top.png', root).pathname }); +await page.locator('#s-report').evaluate(e => { e.style.position = 'relative'; e.style.height = 'auto'; e.style.overflow = 'visible'; document.body.style.overflow = 'visible'; }); +await page.screenshot({ path: new URL('07-report-full.png', root).pathname, fullPage: true }); +for (const card of await page.locator('.card-detail').all()) { + for (const tab of await card.locator('.dtab').all()) { + await tab.click(); + assert.equal(await card.locator('.dpanel.active').count(), 1); + } +} +for (const width of [320, 390, 480]) { + await page.setViewportSize({ width, height: 844 }); + assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'horizontal overflow ' + width); +} +assert.deepEqual(errors, []); +console.log('PASS: login/consent/fallback/refresh/history/profile/report/7 tabs/3 viewport widths, no JS errors'); +await browser.close(); diff --git a/h5/test/wechat-browser.mjs b/h5/test/wechat-browser.mjs new file mode 100644 index 0000000..3d9c6ed --- /dev/null +++ b/h5/test/wechat-browser.mjs @@ -0,0 +1,80 @@ +// Real HTTPS cookies + /state/ reverse-proxy layout; only WeChat is simulated. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import https from 'node:https'; +import { execFileSync, execSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { pathToFileURL, fileURLToPath } from 'node:url'; +const require = createRequire(import.meta.url); +const createAccounts = require('../lib/accounts'); +const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href); +const root = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-wechat-test-')); +const h5 = fileURLToPath(new URL('../', import.meta.url)); +let browser, server; +const realFetch = global.fetch; +try { + execFileSync('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', path.join(root, 'key.pem'), + '-out', path.join(root, 'cert.pem'), '-days', '1', '-subj', '/CN=skin.example.test'], { stdio: 'ignore' }); + let accounts; + server = https.createServer({ key: fs.readFileSync(path.join(root, 'key.pem')), cert: fs.readFileSync(path.join(root, 'cert.pem')) }, async (req, res) => { + const url = new URL(req.url, 'https://skin.example.test'); + if (!url.pathname.startsWith('/state/')) { res.writeHead(404); return res.end(); } + req.url = req.url.slice('/state'.length); // nginx proxy_pass trailing slash + const route = url.pathname.slice('/state'.length); + if (await accounts.route(req, res, route)) return; + const file = route === '/' ? '/index.html' : route; + if (!/^\/(index\.html|(?:js|css|assets|vendor)\/[^.][^]*)$/.test(file) || file.includes('..')) { res.writeHead(404); return res.end(); } + try { + const types = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff2': 'font/woff2' }; + res.setHeader('Content-Type', types[path.extname(file)] || 'application/octet-stream'); + res.end(fs.readFileSync(path.join(h5, file))); + } catch (_) { res.writeHead(404); res.end(); } + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const base = 'https://skin.example.test:' + server.address().port + '/state/'; + accounts = createAccounts(path.join(root, 'accounts'), { WECHAT_APP_ID: 'test-app', WECHAT_APP_SECRET: 'test-secret', PUBLIC_BASE_URL: base }); + global.fetch = async url => { + assert.equal(new URL(url).origin, 'https://api.weixin.qq.com'); + return { ok: true, json: async () => ({ openid: 'test-same-wechat-user' }) }; + }; + browser = await chromium.launch({ headless: true, args: ['--no-proxy-server', '--host-resolver-rules=MAP skin.example.test 127.0.0.1'] }); + let firstId; + for (let device = 0; device < 2; device++) { + const context = await browser.newContext({ ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 MicroMessenger/8.0', viewport: { width: 375, height: 726 } }); + const page = await context.newPage(); + const errors = []; + page.on('pageerror', e => errors.push(e.message)); + await page.route('https://open.weixin.qq.com/**', async route => { + const auth = new URL(route.request().url()); + assert.equal(auth.searchParams.get('scope'), 'snsapi_base'); + const callback = new URL(auth.searchParams.get('redirect_uri')); + assert.equal(callback.href, base + 'api/auth/wechat/callback'); + callback.search = new URLSearchParams({ code: 'test-code', state: auth.searchParams.get('state') }); + await route.fulfill({ status: 302, headers: { location: callback.href }, body: '' }); + }); + await page.goto(base); + await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查')); + await page.check('#login-consent'); + await page.click('[data-action="login-wechat"]'); + // Redirects may preserve WeChat's fragment; it is not sent to the backend. + await page.waitForURL(url => url.origin + url.pathname === base && url.searchParams.get('login') === 'success'); + await page.waitForSelector('#s-splash.active'); + const user = await page.evaluate(() => AccountClient.user()); + assert.equal(user.kind, 'wechat'); + if (firstId) assert.equal(user.id, firstId); else firstId = user.id; + const cookie = (await context.cookies()).find(c => c.name === 'juzi_session'); + assert(cookie.secure && cookie.httpOnly && cookie.path === '/state/' && cookie.sameSite === 'Lax'); + await page.click('[data-action="history"]'); + await page.waitForFunction(() => document.querySelector('#identity-note').textContent.includes('同一微信账号')); + assert.deepEqual(errors, []); + await context.close(); + } + console.log('PASS: HTTPS /state/ login → simulated WeChat → callback with bound Secure cookie → homepage/history; two browser contexts share WeChat identity'); +} finally { + global.fetch = realFetch; + if (browser) await browser.close(); + if (server) await new Promise(resolve => server.close(resolve)); + fs.rmSync(root, { recursive: true, force: true }); +} diff --git a/reference/AI测肤报告页话术修改.md b/reference/AI测肤报告页话术修改.md new file mode 100644 index 0000000..fa9f314 --- /dev/null +++ b/reference/AI测肤报告页话术修改.md @@ -0,0 +1,209 @@ +# AI测肤报告页话术修改 + +# 优化版 + +### **三维度名称** + +紧塑力(5\.1 \& 5\.2) + +透亮感(5\.3 \& 5\.4 \& 5\.5) + +稳韧力(5\.6 \& 5\.7) + + + +### **报告结论** + +#### 5\.1 轮廓(紧塑力) + +**【改善】** 面部轮廓紧致度有待提升,下颌线清晰度不足,左右下颌线角度分别为左 \{la\}°/ 右 \{ra\}°;眼袋明显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积略欠饱满,肌肤支撑力偏弱。 + +**【良好】** 面部轮廓清晰,下颌线较为清晰,左右下颌线角度左 \{la\}°/ 右 \{ra\}°;眼周眼袋初显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积无明显流失;肌肤处于健康状态,支撑力表现良好。 + +**【优秀】** 面部轮廓紧致立体,下颌线清晰;眼周无明显眼袋问题、黑眼圈轻/中/重度(根据分数给出判断);中面部容积饱满,肌肤支撑力充足。 + +#### 5\.2 皱纹(紧塑力) + +**【改善】** 全脸共检测到皱纹 \{wr\_cnt\} 条、细纹 \{fl\_cnt\} 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。 + +**【良好】** 全脸共检测到皱纹 \{wr\_cnt\} 条、细纹 \{fl\_cnt\} 条,面部整体平整,仅眼周或口周有细微的干纹,无明显静态深纹,纹路面积占比相对较低。 + +**【优秀】** 面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。 + +#### 5\.3 肤色(透亮感) + +**【改善】** 检测到当前肌肤色素分 \{melanin\},存在色素沉着较显著,整体透亮感不足。 + +**【良好】** 检测到当前肌肤色素分 \{melanin\},局部有色素沉积;整体状态稳定,局部透亮感稍弱。 + +**【优秀】** 检测到当前肌肤色素分 \{melanin\},无明显色素沉积;肤色均匀透亮。 + +#### 5\.4 平滑度(透亮感) + +**【改善】** 当前肌肤平滑度还有提升空间,检测到粗大毛孔 \{pore\_cnt\} 颗、黑头 \{blackhead\_cnt\} 颗,肤感整体细腻度有提升空间,纹理不均。 + +**【良好】** 肌肤整体较为平滑,局部区域细腻度不足;检测到粗大毛孔 \{pore\_cnt\} 颗、黑头 \{blackhead\_cnt\} 颗,有轻微角质粗糙感。 + +**【优秀】** 肌肤质感表现优异,表面细腻平滑,角质层排列整齐,无明显粗大毛孔与黑头。 + +#### 5\.5 水分(透亮感) + +**【改善】** 缺水面积 \{water\_area\}%、肌肤缺水比较严重 \{water\_sev\},肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。 + +**【良好】**水分分 \{water\_score\};缺水面积 \{water\_area\}%、肌肤含水量处于健康水平,整体水润度良好。 + +**【优秀】** 水分分 \{water\_score\};角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。 + +#### 5\.6 泛红(稳韧力) + +**【改善】** 泛红面积达 \{sens\_area\}%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。 + +**【良好】** 泛红面积 \{sens\_area\}%,肌肤存在局部轻微泛红;对外界环境有良好的耐受度,仅在接触强刺激物时偶有轻微反应。 + +**【优秀】** 泛红面积 \{sens\_area\}%,无明显泛红、敏感迹象;对外界刺激耐受度佳,肌肤处于稳定的健康状态。 + +#### 5\.7 痘痘(稳韧力) + +**【改善】** 检测到 \{acne\_cnt\} 处皮损(\{acne\_detail\})\{heavy\_note\},肌肤状态不稳定,毛孔易堵塞,皮脂分泌波动明显。 + +> 注:\{heavy\_note\} 仅在真实检出脓包 / 结节时输出「含脓包、结节等中重度皮损,炎症反应明显」,禁止模板硬套。 +> +> + +**【良好】** 检测到 \{acne\_cnt\} 处皮损(\{acne\_detail\}),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。 + +**【优秀】** 未检出明显痘痘、粉刺或闭口,无明显红肿、脓包迹象。 + + + +### 护理建议(全量文本,按需求摘抄) + +#### 6\.1 轮廓 + +【改善·护理】重点进行轮廓紧致管理,关注面中、苹果肌与下颌线区域。日常增加紧致精华步骤,配合由下向上的轻柔提升按摩,帮助肌肤维持弹润和支撑感,让轮廓看起来更紧实。 + +【改善·生活方式】保证充足、规律的睡眠,让肌肤在夜间充分修护;坚持防晒,减少紫外线带来的老化影响;减少熬夜。 + + + +【良好·护理】关注面中、苹果肌、下颌线等容易显疲态区域的支撑感。日常可叠加紧致精华步骤,并配合向上提升按摩手法,预防疲态引起的松弛与垮塌累积,稳住面部支撑线。 + +【良好·生活方式】日常注意防晒,保持规律作息;坚持适度运动,帮助肌肤维持活力。 + + + +【优秀·护理】保持现有紧致抗衰护理节奏,日常叠加使用提亮精华,坚持防晒,加强光损伤防护与状态管理,持续关注面中疲态与轮廓支撑状态,让紧致状态更稳定、更持久。 + +【优秀·生活方式】坚持防晒与规律作息,均衡饮食,保证优质蛋白、新鲜蔬果和健康脂肪摄入,适度运动。 + + + +#### 6\.2 皱纹 + +【改善·护理】重视纹路管理,日常可使用含有肽类、胶原类成分的淡纹精华,针对眼周、法令纹等动态纹高发区域配合点按手法,遏制静态纹转化,帮肌肤恢复平滑细腻的状态。 + +【改善·生活方式】保证睡眠,注意睡姿,减少面部长期受压;减少夸张表情,避免揉眼、托腮等表情性挤压习惯,减少纹路加深。 + + + +【良好·护理】加强日常淡纹与肌肤弹润护理,叠加使用含肽类、胶原类成分的淡纹精华,提升肌肤水润度与饱满感,维持肤表平滑。 + +【良好·生活方式】减少夸张表情反复牵拉,睡前加强保湿修护。 + + + +【优秀·护理】以预防初纹为核心,搭配抗初老型精华,提前建立抗氧化/紧致护理习惯,延缓第一道静态纹出现。 + +【优秀·生活方式】保持良好作息与运动习惯,坚持防晒,减轻光老化与糖化负担,维持肌肤稳定状态。 + + + +#### 6\.3 肤色 + +【改善·护理】重点关照暗沉与色素易沉积区域,强化透亮与肤色均匀,严格防晒,配合温和焕亮或抗氧化精华帮助均匀肤色与透亮感。 + +【改善·生活方式】日常加强防晒与抗氧化护理,减少熬夜与压力引发的暗沉,适当增加富含维C的蔬果摄入。 + + + +【良好·护理】加强日常光泽管理与局部暗沉护理,做好防晒与抗氧化打底;叠加使用温和焕亮精华,稳住当前肤色状态并提升整体通透感。 + +【良好·生活方式】持续做好防晒,保持作息规律,避免波动性色沉,守护均匀透亮感。 + + + +【优秀·护理】维持现有防晒与抗氧化护肤习惯,叠加使用温和焕亮精华,巩固肤色均匀度与透亮感。 + +【优秀·生活方式】坚持日常防晒,保持均衡饮食支持皮肤稳态。 + +#### 6\.4 平滑度 + +【改善·护理】关注毛孔可见度与肤表粗糙感改善,加强角质管理,提升肌肤平滑细腻度。 + +【改善·生活方式】减少高糖高脂饮食,降低皮脂分泌刺激,定期温和调理角质,避免用手挤压黑头。 + + + +【良好·护理】加强基础清洁与角质管理,关注鼻翼、下颌等易堵塞部位,提升毛孔通畅度与肌肤细腻度。 + +【良好·生活方式】温水洁面避免烫洗,保持床品清洁减少局部刺激。 + + + +【优秀·护理】维持当前温和护理节奏,维护角质层健康与肌肤屏障稳定状态。 + +【优秀·生活方式】保持规律清洁与保湿习惯,避免频繁更换功效类产品,减少肌肤适应负担。 + +#### 6\.5 水分 + +【改善·护理】强化屏障锁水修复,注意水油平衡,每周2\-3次深度补水,深度改善肌肤缺水状态。 + +【改善·生活方式】每日足量饮水,补充身体水分,改善干燥环境湿度,避免长时间空调直吹。 + + + +【良好·护理】日常需注重水油平衡调理,避免过度清洁损伤皮肤屏障,注意为肌肤持续补水。 + +【良好·生活方式】每日足量饮水,环境干燥时及时增加肌肤水分补充。 + + + +【优秀·护理】持续为皮肤补水与保湿,避免过度清洁与频繁去角质,巩固肌肤水润度与水油平衡。 + +【优秀·生活方式】坚持多喝水与规律作息,季节交替时提前调整保湿方案。 + +#### 6\.6 泛红 + +【改善·护理】关注屏障修复与舒缓,精简护肤流程,选择温和洁面方式,修复受损角质层。若状态持续无明显好转,可寻求专业护肤建议。 + +【改善·生活方式】减少外界刺激,如酒精、香精、高浓度酸类精华等成分,避免冷热骤变刺激。 + + + +【良好·护理】保持温和护理节奏,注意防晒与环境防护,逐步提升皮肤耐受度。 + +【良好·生活方式】避免冷热骤变刺激肌肤屏障,运动后及时清洁降温。 + + + +【优秀·护理】持续规律护理并按季节阶段性调整,巩固皮肤耐受稳态。 + +【优秀·生活方式】保持健康作息与均衡饮食,维持皮肤屏障稳态。 + +#### 6\.7 痘痘 + +【改善·护理】优先针对性调理痘区肌肤状态,精简护肤流程,避免频繁更换护肤方案与叠加功效成分;严重时及时就医。 + +【改善·生活方式】减少高糖高脂饮食与熬夜,避免手挤痘痘;保持床品、毛巾清洁,运动后及时清洁,减少汗液与皮脂堆积。 + + + +【良好·护理】保持温和清洁与肌肤炎症调理,关注T区、下颌等易出油区域,提升毛孔通畅度,同时注意防晒与保湿。 + +【良好·生活方式】保持规律作息与清淡饮食,运动后及时清洁,减少汗液与皮脂堆积。 + + + +【优秀·护理】坚持定期清洁,维持水油平衡与毛孔通畅,巩固皮肤耐受稳态。 + +【优秀·生活方式】保持健康作息与均衡饮食,维持皮肤屏障健康稳定。 diff --git a/tools/build_report_copy.py b/tools/build_report_copy.py new file mode 100644 index 0000000..ef0a8dc --- /dev/null +++ b/tools/build_report_copy.py @@ -0,0 +1,30 @@ +"""Generate versioned presentation copy directly from the approved reference.""" +import json +import re +from pathlib import Path + +root = Path(__file__).resolve().parents[1] +text = (root / 'reference/AI测肤报告页话术修改.md').read_text() +text = re.sub(r'\\([.{}_&-])', r'\1', text) +keys = ['contour', 'wrinkles', 'color', 'smooth', 'water', 'sens', 'acne'] +data = {'version': '2026-10-09', 'conclusion': {}, 'care': {}} +section, key = None, None +for line in text.splitlines(): + heading = re.match(r'#### ([56])\.(\d)', line) + if heading: + section = 'conclusion' if heading[1] == '5' else 'care' + key = keys[int(heading[2]) - 1] + data[section][key] = {} + conclusion = re.match(r'\*\*【(改善|良好|优秀)】\*\*\s*(.+)', line) + care = re.match(r'【(改善|良好|优秀)·(护理|生活方式)】\s*(.+)', line) + if section == 'conclusion' and conclusion: + data[section][key][conclusion[1]] = conclusion[2].strip() + if section == 'care' and care: + data[section][key].setdefault(care[1], {})['care' if care[2] == '护理' else 'lifestyle'] = care[3].strip() +for category in keys: + assert len(data['conclusion'][category]) == len(data['care'][category]) == 3 +payload = json.dumps(data, ensure_ascii=False, indent=2) +(root / 'h5/js/report-copy.js').write_text( + '// Generated from reference/AI测肤报告页话术修改.md; run tools/build_report_copy.py.\n' + '(function(root,factory){if(typeof module==="object"&&module.exports)module.exports=factory();else root.ReportCopy=factory();})(typeof window!=="undefined"?window:this,function(){return ' + + payload + ';});\n') diff --git a/tools/export_psd_assets.py b/tools/export_psd_assets.py new file mode 100644 index 0000000..13eec06 --- /dev/null +++ b/tools/export_psd_assets.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Export the October UI PSD without changing existing application assets. + +Requires: pip install psd-tools Pillow aggdraw +""" +import argparse +import hashlib +import json +from pathlib import Path + +from PIL import Image, ImageDraw +from psd_tools import PSDImage + +ROOT = Path(__file__).resolve().parents[1] +DESIGN = ROOT / "design/ui-20261009" +ASSETS = ROOT / "h5/assets/img/ui-20261009" + +# PSD layer indices are recorded alongside descriptive filenames for traceability. +ASSET_SPECS = [ + ("bg-splash.jpg", "0/0", "0"), + ("bg-profile.jpg", "3/0/0", "3"), + ("bg-login.jpg", "9/0/0", "9"), + ("bg-report.jpg", "8/0", "8"), + ("bg-history.jpg", "2/0", "2"), + ("brand-logo.png", "0/1/0", None), + ("brand-login.png", "9/7/1", None), + ("icon-close.png", "1/6", None), + ("icon-dropdown.png", "3/1/2", None), + ("icon-back.png", "3/6/0", None), + ("icon-album.png", "5/1/1", None), + ("icon-switch-camera.png", "5/1/0", None), + ("icon-wechat.png", "9/7/6", None), + ("icon-guest.png", "9/7/7", None), + ("icon-shutter.png", "5/1/2", None), + ("skin-0-dry.png", "3/3/1/0/2", None), + ("skin-1-mixeddry.png", "3/3/1/1/2", None), + ("skin-2-neutral.png", "3/3/1/2/2", None), + ("skin-3-oily.png", "3/3/1/3/2", None), + ("skin-4-mixedoily.png", "3/3/1/4/2", None), + ("product-25.png", "8/1/0/1", None), + ("product-35.png", "8/1/1/2", None), + ("product-50.png", "8/1/2/4", None), + ("product-badge.png", "8/1/0/3", None), + ("score-ring-base.png", "8/12/5", None), + ("score-ring-value.png", "8/12/6", None), + ("sample-contour.png", "8/9/8", None), + ("sample-lines.png", "8/9/10", None), + ("sample-tone.png", "8/10/10", None), + ("sample-redness.png", "8/11/8", None), + ("camera-mask-front.png", "5/0", "5"), + ("camera-mask-right.png", "6/0", "6"), + ("camera-mask-left.png", "7/0", "7"), + ("camera-guide-front.png", ["5/8", "5/9", "5/10"], "5"), + ("camera-guide-right.png", ["6/8", "6/9", "6/10"], "6"), + ("camera-guide-left.png", ["7/8", "7/9", "7/10"], "7"), +] + + +def layer_at(psd, indices): + layer = psd + for part in indices.split("/"): + layer = layer[int(part)] + return layer + + +def bounds(layer): + # Hidden groups can report an empty bbox; union their children explicitly. + if layer.is_group(): + boxes = [bounds(child) for child in layer] + boxes = [b for b in boxes if b[2] > b[0] and b[3] > b[1]] + if boxes: + return (min(b[0] for b in boxes), min(b[1] for b in boxes), + max(b[2] for b in boxes), max(b[3] for b in boxes)) + return tuple(layer.bbox) + + +def save_image(image, target): + target.parent.mkdir(parents=True, exist_ok=True) + if target.suffix == ".jpg": + background = Image.new("RGB", image.size, "white") + background.paste(image, mask=image.getchannel("A") if image.mode == "RGBA" else None) + background.save(target, quality=92, optimize=True, progressive=True) + else: + image.save(target, optimize=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("psd", type=Path) + parser.add_argument("--assets-only", action="store_true", help="Reuse existing page exports") + args = parser.parse_args() + psd = PSDImage.open(args.psd) + DESIGN.mkdir(parents=True, exist_ok=True) + manifest = {"source": args.psd.name, "document_size": psd.size, + "pages": [], "assets": [], "layers": []} + + def inventory(group, prefix=""): + for index, layer in enumerate(group): + key = f"{prefix}/{index}".strip("/") + entry = {"path": key, "name": layer.name, "kind": layer.kind, + "bbox": list(layer.bbox), "visible": layer.visible} + if layer.kind == "type": + entry["text"] = layer.text + manifest["layers"].append(entry) + if layer.is_group(): + inventory(layer, key) + inventory(psd) + + previews = [] + for index, page in enumerate(psd): + print(f"Page {index}: {page.name}", flush=True) + # Cached layer pixels preserve Photoshop text appearance without local fonts. + target = DESIGN / "pages" / f"{index + 1:02d}-{page.name}.png" + if args.assets_only: + image = Image.open(target) + else: + image = page.composite(viewport=page.bbox) + save_image(image, target) + preview = image.copy() + preview.thumbnail((216, 1553)) + previews.append((page.name, preview)) + manifest["pages"].append({"file": str(target.relative_to(ROOT)), + "name": page.name, "size": image.size, + "bbox": list(page.bbox)}) + + thumbs = [] + for filename, indices, crop_page in ASSET_SPECS: + source_indices = indices if isinstance(indices, list) else [indices] + layers = [layer_at(psd, key) for key in source_indices] + layer = layers[0] + viewport = layer_at(psd, crop_page).bbox if crop_page else bounds(layer) + print(f"Asset: {filename}", flush=True) + # A Photoshop clipping layer composites to empty if its base is omitted. + # Disable only this root layer's clipping while exporting it independently. + image = Image.new("RGBA", (viewport[2]-viewport[0], viewport[3]-viewport[1])) + for source in layers: + was_clipping = source.clipping + if was_clipping: + source.clipping = False + part = source.composite(viewport=viewport, layer_filter=lambda _: True) + if was_clipping: + source.clipping = True + if part is not None: + image = Image.alpha_composite(image, part.convert("RGBA")) + if image is None: + raise RuntimeError(f"No image for {indices}: {layer.name}") + if image.mode == "RGBA" and image.getchannel("A").getextrema()[1] == 0: + raise RuntimeError(f"Empty transparent export for {indices}: {filename}") + target = ASSETS / filename + save_image(image, target) + manifest["assets"].append({"file": str(target.relative_to(ROOT)), + "source_layer": indices, "layer_name": layer.name, + "size": image.size, "bbox": list(viewport), + "bytes": target.stat().st_size, + "sha256": hashlib.sha256(target.read_bytes()).hexdigest(), + "original_visible": layer.is_visible(), + "usage": "reference-only" if filename.startswith("sample-") + else "frontend-asset"}) + thumb = image.copy() + thumb.thumbnail((180, 190)) + thumbs.append((filename, thumb)) + + sheet = Image.new("RGB", (5 * 236, 2 * 1620), "#eeeeee") + draw = ImageDraw.Draw(sheet) + for i, (_, preview) in enumerate(previews): + x, y = (i % 5) * 236 + 10, (i // 5) * 1620 + 35 + draw.text((x, y - 25), f"Page {i+1:02d}", fill="black") + sheet.paste(preview, (x, y), preview if preview.mode == "RGBA" else None) + save_image(sheet, DESIGN / "pages-overview.jpg") + + sheet = Image.new("RGB", (6 * 205, ((len(thumbs)+5)//6)*230), "#e3e3e3") + draw = ImageDraw.Draw(sheet) + for i, (name, thumb) in enumerate(thumbs): + x, y = (i % 6)*205 + 10, (i // 6)*230 + 30 + draw.text((x, y - 22), name, fill="black") + sheet.paste(thumb, (x, y), thumb if thumb.mode == "RGBA" else None) + save_image(sheet, DESIGN / "assets-overview.jpg") + (DESIGN / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2)+"\n") + print(f"Exported {len(previews)} pages and {len(thumbs)} assets", flush=True) + + +if __name__ == "__main__": + main() diff --git a/tools/prepare_h5_assets.py b/tools/prepare_h5_assets.py new file mode 100644 index 0000000..034a52c --- /dev/null +++ b/tools/prepare_h5_assets.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Derive H5 crops from exported PSD assets; never crop the original exports.""" +from pathlib import Path +from PIL import Image +import json +import cv2 +import numpy as np + +root = Path(__file__).resolve().parents[1] / 'h5/assets/img/ui-20261009' +for name in ['bg-splash', 'bg-login', 'bg-profile', 'bg-history', 'bg-report']: + source = Image.open(root / (name + '.jpg')) + # Native status/navigation area belongs to the host, not the H5 document. + top = 253 if name == 'bg-report' else 248 + source.crop((0, top, source.width, source.height)).save(root / (name + '-h5.jpg'), quality=92, optimize=True) + +geometry = [] +for direction in ['front', 'left', 'right']: + alpha = np.array(Image.open(root / ('camera-mask-' + direction + '.png')).getchannel('A')) + contours, _ = cv2.findContours((alpha < 100).astype('uint8'), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contour = max((c for c in contours if 500 < cv2.boundingRect(c)[1] < 1600), key=cv2.contourArea) + points = cv2.approxPolyDP(contour, 1.4, True).reshape(-1, 2) + outline = 'M' + 'L'.join(f'{x/2.88:.2f},{y/2.88:.2f}' for x, y in points) + 'Z' + guide_alpha = np.array(Image.open(root / ('camera-guide-' + direction + '.png')).getchannel('A')) + count, labels, stats, _ = cv2.connectedComponentsWithStats((guide_alpha > 100).astype('uint8')) + guides = [] + for i in range(1, count): + x, y, w, h, area = stats[i] + if area < 50: + continue + points = [] + if w > h: + for col in range(x, x+w, 3): + rows = np.where(labels[:, col] == i)[0] + if len(rows): points.append((col, rows.mean())) + else: + for row in range(y, y+h, 3): + cols = np.where(labels[row, :] == i)[0] + if len(cols): points.append((cols.mean(), row)) + guides.append('M' + 'L'.join(f'{px/2.88:.2f},{py/2.88:.2f}' for px, py in points)) + geometry.append({'outline': outline, 'guides': guides}) +(root.parents[2] / 'js/camera-geometry.js').write_text('window.CameraGeometry = ' + json.dumps(geometry) + ';\n')