feat(miniapp): align UI with H5 and harden device debugging
五个页面按 h5/css 的 style.css → ui-20261009 → ui-20261012 覆盖顺序逐条重排, 换算规则固定为 calc(N * var(--u)) → 2N rpx,固定 px 的字号保持 px。 - app.acss:换回 H5 设计 token(方角小倒角按钮 10rpx、卡片 14rpx、.chev 折线箭头) - splash/profile/history:底图与按钮文字图改用 H5 资源,隐私/政策弹窗按 H5 重排 - report:环形分用 conic-gradient、人脸裁切窗口叠加 face_maps、标记点百分比盒、 侧脸皱纹 pending/unavailable 文案;用 Playwright 抓 H5 94 个选择器几何比对, 整页高 2438.7 vs 2438.67 - camera:淘宝 <camera> 组件在真机整页崩溃(嵌了微信专有的 <cover-view>), 改为默认系统相机 + `config.nativeCamera` 开关 + pages/camtest 冒烟页验证组件 - permissions:淘宝用带前缀的 scope.camera(裸名返回 5022 Scope:camera信息不存在), 先试带前缀再退回裸名,拒绝后不再重复弹窗 - voice:非 HTTPS 资源导致的异常不再冒泡成整页崩溃;app.js 加开发版错误 toast - 开发版入口:权限诊断(启动页/相机页)、相机组件测试 - 测试 20/20(新增权限与相机能力用例)
This commit is contained in:
+69
-39
@@ -1,83 +1,113 @@
|
||||
/* ═════ 全局样式(对齐 H5 设计变量:--yellow #F5F078 / --ink #282828 / --bg #EFEFEF) ═════ */
|
||||
/* ═════ 全局样式 —— 对齐 H5 h5/css/style.css 的 1'me 设计变量 ═════
|
||||
单位换算:H5 的 calc(N * var(--u))(375 设计宽基准)→ 2N rpx(750rpx = 屏宽)。
|
||||
H5 里固定 px 的字号保持 px 不变。
|
||||
注意:淘宝小程序 acss 不支持 @font-face,字体退化为系统字体栈。 */
|
||||
page {
|
||||
background: #101823;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
background: #EFEFEF;
|
||||
color: #282828;
|
||||
font-size: 28rpx;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ─── 主按钮(品牌黄) ─── */
|
||||
/* ─── 主按钮:长方形 + 小倒角(H5 设计稿 181×37,圆角 5) ─── */
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
width: 362rpx;
|
||||
height: 74rpx;
|
||||
background: #F5F078;
|
||||
border-radius: 10rpx;
|
||||
color: #282828;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
font-size: 14.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1rpx;
|
||||
border: none;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.btn-primary::after { border: none; }
|
||||
.btn-primary.btn-disabled { opacity: 0.5; }
|
||||
.btn-primary.btn-disabled { opacity: .5; }
|
||||
|
||||
/* ─── 白色卡片(报告页通用) ─── */
|
||||
.card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
margin: 24rpx 32rpx 0;
|
||||
padding: 32rpx;
|
||||
/* ─── 折线箭头(H5 .chev) ─── */
|
||||
.chev {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border: solid #2C2C2C;
|
||||
border-width: 0 2px 2px 0;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.card-heading {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24rpx;
|
||||
.chev.left { transform: rotate(135deg); }
|
||||
.chev.right { transform: rotate(-45deg); }
|
||||
.chev.down { transform: rotate(45deg); }
|
||||
|
||||
/* ─── 圆形返回按钮(H5 .nav-back) ─── */
|
||||
.nav-back {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #E5E5E5;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ─── 弹窗遮罩 ─── */
|
||||
/* ─── 弹窗(H5 隐私协议/政策层) ─── */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background: rgba(0, 0, 0, .6);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.popup {
|
||||
width: 600rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 40rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
width: 640rpx;
|
||||
max-height: 78vh;
|
||||
background: #FFFFFF;
|
||||
border-radius: 14rpx;
|
||||
padding: 44rpx 40rpx 36rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.popup-scroll { max-height: 52vh; }
|
||||
.popup-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
line-height: 1.4;
|
||||
color: #282828;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.popup-body {
|
||||
font-size: 26rpx;
|
||||
font-size: 13px;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 32rpx;
|
||||
line-height: 1.75;
|
||||
}
|
||||
.popup-close {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
line-height: 48rpx;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
line-height: 56rpx;
|
||||
text-align: center;
|
||||
font-size: 36rpx;
|
||||
font-size: 18px;
|
||||
color: #868686;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.popup-close::after { border: none; }
|
||||
|
||||
/* ─── 卡片(H5 .card) ─── */
|
||||
.card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 14rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -16,5 +16,14 @@ App({
|
||||
},
|
||||
onHide: function () {
|
||||
require('./utils/voice.js').stop(true);
|
||||
},
|
||||
// 真机上看不到 console:开发版里把未捕获异常弹出到屏幕上,方便截图定位。
|
||||
onError: function (error) {
|
||||
try {
|
||||
var config = require('./config');
|
||||
if (config.environment === 'production') return;
|
||||
var text = String((error && (error.message || error.stack)) || error);
|
||||
my.showToast({ content: 'JS错误: ' + text.slice(0, 120), type: 'none', duration: 8000 });
|
||||
} catch (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
"pages/camera/camera",
|
||||
"pages/report/report",
|
||||
"pages/history/history",
|
||||
"pages/mvp/mvp"
|
||||
"pages/mvp/mvp",
|
||||
"pages/camtest/camtest"
|
||||
],
|
||||
"window": {
|
||||
"defaultTitle": "1'me在线肌肤测试",
|
||||
|
||||
+21
-10
@@ -1,16 +1,27 @@
|
||||
// Public configuration only. AppSecret and Face++ keys stay on the server.
|
||||
//
|
||||
// Cloud test for 聚石塔 SAE:
|
||||
// cloudType 'tower' = 聚石塔云应用(不向网关传 domain)
|
||||
// cloudType 'empty' = 空应用/外部服务(传 domain,baseUrl 必须 HTTPS)
|
||||
// To go back to local direct:
|
||||
// transport: 'direct', baseUrl: 'http://127.0.0.1:18180', mvpLocal: true
|
||||
module.exports = {
|
||||
environment: 'development',
|
||||
cameraDebug: true, // Offline camera acceptance only; disabled outside local development.
|
||||
transport: 'direct', // Local development. Use 'cloud' with real gateway configuration.
|
||||
baseUrl: 'http://127.0.0.1:18180',
|
||||
assetBase: 'http://127.0.0.1:18180/assets/',
|
||||
cloudAppId: '',
|
||||
cloudEnv: 'online', // External/empty cloud applications use online
|
||||
basePath: '', // E.g. '/state' behind an existing reverse proxy
|
||||
// SAE/Java MVP page only: force the local Java backend without changing the main app config.
|
||||
// Set false and edit transport/baseUrl/cloudAppId above when testing the cloud gateway.
|
||||
mvpLocal: true,
|
||||
cameraDebug: false,
|
||||
transport: 'cloud',
|
||||
baseUrl: 'https://miniappcloud.taobao.com', // 'tower' 模式不参与请求;仅占位
|
||||
// 真机可通过公网 HTTPS 加载的静态资源(启动页底图、肤质卡、语音、商品图)。
|
||||
// 127.0.0.1 在手机上指向手机自己,会全部加载失败。
|
||||
assetBase: 'https://1me.xajuzi.com/state/assets/',
|
||||
cloudType: 'tower',
|
||||
cloudAppId: '', // 聚石塔默认云应用可留空;多个云应用时填关联页的 cloudAppId
|
||||
cloudEnv: 'test',
|
||||
basePath: '',
|
||||
// 淘宝 <camera> 组件在真机(SDK 2.9.4)上会整页崩溃,且 canIUse('camera') 是假阳性。
|
||||
// 关掉后拍照页改为「系统相机 / 相册」两条 my.chooseImage 通道(权限 scope.camera 已授予)。
|
||||
// 等确认组件可用后把这里改回 true 即可恢复页内取景。
|
||||
nativeCamera: false,
|
||||
mvpLocal: false,
|
||||
mvpBaseUrl: 'http://127.0.0.1:8080',
|
||||
mock: false,
|
||||
productItemIds: {} // Optional real Taobao item IDs keyed by tier 1/2/3
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
https://miniapp.open.taobao.com/docV3.htm?docId=119114&docType=1&tag=dev
|
||||
https://open.taobao.com/doc.htm?docId=121164&docType=1
|
||||
https://miniapp.open.taobao.com/docV3.htm?spm=a219a.13922336.0.0.6e99286cfVnRmI&docId=118541&docType=1&tag=dev
|
||||
@@ -3,11 +3,24 @@
|
||||
"compileType": "mini",
|
||||
"miniprogramRoot": "./",
|
||||
"appid": "",
|
||||
"exclude": ["scripts/**", "test/**", "docs/**", "README.md", "package-lock.json"],
|
||||
"uploadExclude": ["scripts/**", "test/**", "docs/**", "README.md", "package-lock.json"],
|
||||
"exclude": [
|
||||
"scripts/**",
|
||||
"test/**",
|
||||
"docs/**",
|
||||
"README.md",
|
||||
"package-lock.json"
|
||||
],
|
||||
"uploadExclude": [
|
||||
"scripts/**",
|
||||
"test/**",
|
||||
"docs/**",
|
||||
"README.md",
|
||||
"package-lock.json"
|
||||
],
|
||||
"enableAppxNg": true,
|
||||
"component2": true,
|
||||
"transpile": {
|
||||
"es6": true
|
||||
}
|
||||
},
|
||||
"miniappCanary": false
|
||||
}
|
||||
|
||||
+183
-107
@@ -1,183 +1,259 @@
|
||||
/* ═════ 拍照页样式 ═════ */
|
||||
/* ═════ 05-07 拍照页 ═════
|
||||
对齐 H5 h5/css/style.css 的 #s-cam 静态态:
|
||||
.cam-step top 7.1%,21.5px/500,letter-spacing 1u,text-shadow
|
||||
.cam-guide bottom 22.4%,14.5px,分隔线 15u 高
|
||||
.cam-controls bottom 5.2%,高 62u,内边距 56u,快门 62u
|
||||
单位换算:calc(N * var(--u)) → 2N rpx */
|
||||
.cam {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #101823;
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
.cam-device {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
/* H5 静态态:白底 + 85% 黑蒙版 ≈ #262626 */
|
||||
.cam-bg {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
/* ─── 步骤文案 ─── */
|
||||
/* ─── 顶部步骤文案 ─── */
|
||||
.cam-step {
|
||||
position: absolute;
|
||||
top: 80rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 0; right: 0;
|
||||
top: 7.1%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 21.5px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 2rpx;
|
||||
text-shadow: 0 1px 10px rgba(0, 0, 0, .4);
|
||||
z-index: 2;
|
||||
}
|
||||
.cam-step-no {
|
||||
color: #FFFFFF;
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cam-step-tip {
|
||||
color: #FFFFFF;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 8rpx;
|
||||
.cam-step-tip { font-size: 21.5px; }
|
||||
.cam-step-no { font-size: 21.5px; }
|
||||
|
||||
/* ─── 真机取景相机(铺满,不能有子节点) ─── */
|
||||
.cam-device {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ─── 取景蒙版(椭圆线框) ─── */
|
||||
/* ─── 取景蒙版:H5 用 SVG 镂空出人脸轮廓;这里用「椭圆开窗 + 巨大 box-shadow」实现同样的暗框 + 中间透亮 ─── */
|
||||
.cam-mask {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cam-oval {
|
||||
width: 560rpx;
|
||||
height: 720rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.9);
|
||||
.cam-hole {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 440rpx;
|
||||
height: 580rpx;
|
||||
margin-left: -220rpx;
|
||||
margin-top: -290rpx;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 0 3000rpx rgba(0, 0, 0, .82);
|
||||
}
|
||||
/* 侧脸步轮廓偏移提示(骨架版:向左/右微移示意朝向) */
|
||||
.mask-1 { transform: translateX(-60rpx); } /* 左脸出镜:耳在画面左 */
|
||||
.mask-2 { transform: translateX(60rpx); } /* 右脸出镜:耳在画面右 */
|
||||
.mask-1 { transform: translateX(-26rpx) rotate(-4deg); }
|
||||
.mask-2 { transform: translateX(26rpx) rotate(4deg); }
|
||||
|
||||
/* ─── 底部引导要点 ─── */
|
||||
/* ─── 底部引导条 ─── */
|
||||
.cam-guide {
|
||||
position: absolute;
|
||||
bottom: 260rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 0; right: 0;
|
||||
bottom: 22.4%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.cam-guide-item {
|
||||
color: #FFFFFF;
|
||||
font-size: 26rpx;
|
||||
padding: 0 24rpx;
|
||||
border-right: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
color: #fff;
|
||||
font-size: 14.5px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.cam-guide-sep {
|
||||
width: 0;
|
||||
height: 30rpx;
|
||||
border-left: 1px solid rgba(255, 255, 255, .7);
|
||||
margin: 0 52rpx;
|
||||
}
|
||||
.cam-guide-item:last-child { border-right: none; }
|
||||
|
||||
/* ─── 控制条 ─── */
|
||||
/* ─── 控制条:相册 / 快门 / 翻转 ─── */
|
||||
.cam-controls {
|
||||
position: absolute;
|
||||
bottom: 80rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 0; right: 0;
|
||||
bottom: 5.2%;
|
||||
height: 124rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
justify-content: space-between;
|
||||
padding: 0 112rpx;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 2;
|
||||
}
|
||||
.cam-side {
|
||||
width: 120rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.cam-shutter {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border: 6rpx solid #FFFFFF;
|
||||
border-radius: 50%;
|
||||
height: 124rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14.5px;
|
||||
}
|
||||
.cam-shutter {
|
||||
width: 124rpx;
|
||||
height: 124rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cam-shutter-dot {
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: #F5F078;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ─── 停顿卡 ─── */
|
||||
.cam-done {
|
||||
/* ─── 提示/错误 ─── */
|
||||
.cam-hint {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 60rpx;
|
||||
right: 60rpx;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
left: 64rpx;
|
||||
right: 64rpx;
|
||||
bottom: 34%;
|
||||
text-align: center;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
color: rgba(255, 255, 255, .9);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* ─── 拍后预览 ─── */
|
||||
.cam-preview {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: #000000;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.cam-preview-img {
|
||||
width: 100%;
|
||||
height: 78%;
|
||||
height: 68%;
|
||||
}
|
||||
.cam-preview-btns {
|
||||
position: absolute;
|
||||
bottom: 80rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 40rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
.cam-preview-btn {
|
||||
width: 280rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
width: 220rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
border-radius: 44rpx;
|
||||
border: 2rpx solid #FFFFFF;
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
border-radius: 10rpx;
|
||||
border: 1px solid rgba(255, 255, 255, .6);
|
||||
color: #fff;
|
||||
font-size: 14.5px;
|
||||
}
|
||||
.cam-preview-btn.ok {
|
||||
background: #F5F078;
|
||||
border-color: #F5F078;
|
||||
color: #282828;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ─── 分析中 ─── */
|
||||
.cam-analyzing {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: rgba(0, 0, 0, .78);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20;
|
||||
}
|
||||
.cam-analyzing-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.cam-cancel {
|
||||
margin-top: 32rpx;
|
||||
color: rgba(255, 255, 255, .7);
|
||||
font-size: 13px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ─── 隐藏画布(压缩降级用) ─── */
|
||||
.compress-canvas {
|
||||
position: fixed;
|
||||
left: -9999rpx;
|
||||
top: -9999rpx;
|
||||
width: 640px;
|
||||
height: 640px;
|
||||
/* ─── 权限诊断(开发版) ─── */
|
||||
.cam-diag-entry {
|
||||
position: absolute;
|
||||
top: 88rpx;
|
||||
right: 32rpx;
|
||||
z-index: 30;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(0, 0, 0, .55);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cam-analyzing { flex-direction: column; }
|
||||
.cam-cancel { margin-top: 48rpx; color: #fff; font-size: 26rpx; padding: 24rpx; }
|
||||
.camera-permission { padding: 56rpx 40rpx; color: #fff; line-height: 1.8; }
|
||||
.camera-permission button { margin-top: 24rpx; }
|
||||
.debug-banner { padding: 24rpx; background: #F5F078; color: #282828; font-size: 24rpx; }
|
||||
.debug-results { padding: 32rpx; background: #101823; color: white; line-height: 1.8; }
|
||||
.debug-title { font-size: 32rpx; font-weight: 600; }
|
||||
.debug-result { margin: 24rpx 0; }
|
||||
.debug-result image { display: block; width: 100%; height: 320rpx; }
|
||||
.debug-results button { margin-top: 24rpx; }
|
||||
.cam-diag {
|
||||
position: absolute;
|
||||
left: 32rpx; right: 32rpx;
|
||||
top: 160rpx; bottom: 160rpx;
|
||||
z-index: 40;
|
||||
background: rgba(6, 16, 26, .96);
|
||||
border: 1rpx solid rgba(120, 200, 255, .35);
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.cam-diag-scroll { flex: 1; }
|
||||
.cam-diag-text {
|
||||
color: #9fd0ff;
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.cam-diag-close {
|
||||
margin-top: 16rpx;
|
||||
text-align: center;
|
||||
background: #F5F078;
|
||||
color: #282828;
|
||||
border-radius: 12rpx;
|
||||
padding: 14rpx 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── 调试模式结果 ─── */
|
||||
.cam-debug {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: #fff;
|
||||
padding: 88rpx 40rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
z-index: 25;
|
||||
}
|
||||
.cam-debug-title { font-size: 17px; font-weight: 600; margin-bottom: 16rpx; }
|
||||
.cam-debug-note { font-size: 12px; color: #707070; line-height: 1.6; margin-bottom: 24rpx; }
|
||||
.cam-debug-item { margin-bottom: 32rpx; }
|
||||
.cam-debug-img { width: 100%; height: 320rpx; background: #f2f2f2; }
|
||||
.cam-debug-meta { font-size: 12px; color: #4A4A4A; margin-top: 12rpx; }
|
||||
.cam-debug-btn { width: 100%; margin-top: 8rpx; }
|
||||
.cam-debug-exit { margin-top: 24rpx; text-align: center; font-size: 13px; color: #707070; text-decoration: underline; }
|
||||
|
||||
@@ -1,77 +1,80 @@
|
||||
<!-- ═════ 05-07 拍照页 ═════
|
||||
覆盖在 camera 原生组件上的一切必须用 cover-view / cover-image -->
|
||||
两种形态,由 config.nativeCamera 决定:
|
||||
① cameraEnabled=true → 真机取景:<camera> 铺满 + 椭圆镂空蒙版 + 快门直接 takePhoto(对齐 H5 #s-cam)
|
||||
② cameraEnabled=false → 系统相机:静态版面 + 快门调 my.chooseImage
|
||||
关键:<camera> 不能嵌 <cover-view>(微信专有组件,淘宝上会导致整页崩溃),
|
||||
所以相机是兄弟节点,覆盖层用普通 view + box-shadow 镂空。 -->
|
||||
<view class="cam">
|
||||
<view a:if="{{debugMode && !cameraEnabled}}" class="debug-banner">相机验收 · 免登录 · 照片只在本页处理,不上传</view>
|
||||
<view a:if="{{debugComplete}}" class="debug-results">
|
||||
<view class="debug-title">三步拍摄与 JPEG 压缩已完成</view>
|
||||
<view>请核对实际照片方向是否为正脸、右脸、左脸;本页不判断姿态是否合格。</view>
|
||||
<view class="debug-result" a:for="{{debugPhotos}}" a:key="key">
|
||||
<image src="{{item.photo}}" mode="aspectFit" />
|
||||
<view>{{item.title}} · JPEG {{item.width}}×{{item.height}} · 约{{item.kilobytes}}KB</view>
|
||||
</view>
|
||||
<button class="btn-primary" onTap="onDebugRestart">重新验收</button>
|
||||
<button onTap="onCancel">退出并清空本页照片</button>
|
||||
<!-- ①真机取景:相机铺满,不加任何子节点 -->
|
||||
<camera a:if="{{cameraEnabled}}" id="skin-camera" class="cam-device"
|
||||
device-position="{{devicePosition}}" flash="off"
|
||||
onError="onCameraError" onStop="onCameraStop" />
|
||||
|
||||
<!-- ②系统相机兜底:H5 相机静态态(白底 + 85% 黑蒙版 ≈ #262626) -->
|
||||
<view class="cam-bg" a:if="{{!cameraEnabled}}" />
|
||||
|
||||
<!-- 取景蒙版:H5 用 SVG 镂空;这里用带巨大 box-shadow 的椭圆开窗,视觉等效 -->
|
||||
<view class="cam-mask">
|
||||
<view class="cam-hole mask-{{maskStep}}" />
|
||||
</view>
|
||||
<view a:if="{{!cameraEnabled && !debugComplete}}" class="camera-permission">
|
||||
<view>{{stepText}} · {{stepTip}}</view>
|
||||
<view>{{permissionMessage}}</view>
|
||||
<button class="btn-primary" onTap="onEnableCamera">开启相机</button>
|
||||
<button onTap="onOpenSettings">授权设置</button>
|
||||
<button onTap="onSystemCamera">使用系统相机</button>
|
||||
<button onTap="onAlbum">从相册选择</button>
|
||||
<view a:if="{{previewUrl}}">
|
||||
<image src="{{previewUrl}}" mode="aspectFit" style="width:100%;height:400rpx;" />
|
||||
<button onTap="onRetake">重新选择</button><button class="btn-primary" onTap="onConfirm">使用照片</button>
|
||||
</view>
|
||||
<view a:if="{{analyzing}}">{{debugMode ? '正在验证本地图片压缩…' : '正在生成您的肌肤报告…'}}</view>
|
||||
|
||||
<view class="cam-step">
|
||||
<view class="cam-step-no">{{stepText}}</view>
|
||||
<view class="cam-step-tip">{{stepTip}}</view>
|
||||
</view>
|
||||
<camera a:if="{{cameraEnabled}}" id="skin-camera" class="cam-device" device-position="{{devicePosition}}"
|
||||
flash="off" onError="onCameraError" onStop="onCameraStop">
|
||||
<!-- 顶部步骤文案 -->
|
||||
<cover-view class="cam-step">
|
||||
<cover-view a:if="{{debugMode}}">开发调试 · 不上传照片</cover-view>
|
||||
<cover-view class="cam-step-no">{{stepText}}</cover-view>
|
||||
<cover-view class="cam-step-tip">{{stepTip}}</cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 取景蒙版:人脸轮廓(骨架版用椭圆线框,朝向提示随步切换;精确轮廓 TODO:换 cover-image 轮廓图) -->
|
||||
<cover-view class="cam-mask">
|
||||
<cover-view class="cam-oval mask-{{maskStep}}"></cover-view>
|
||||
</cover-view>
|
||||
<view class="cam-guide">
|
||||
<block a:for="{{guides}}" a:key="*this">
|
||||
<view a:if="{{index > 0}}" class="cam-guide-sep" />
|
||||
<text class="cam-guide-item">{{item}}</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 底部引导要点 -->
|
||||
<cover-view class="cam-guide">
|
||||
<cover-view class="cam-guide-item" a:for="{{guides}}" a:key="*this">{{item}}</cover-view>
|
||||
</cover-view>
|
||||
<!-- 拍后预览:确认使用 / 重拍 -->
|
||||
<view class="cam-preview" a:if="{{previewUrl}}" catchTap="noop">
|
||||
<image class="cam-preview-img" src="{{previewUrl}}" mode="aspectFit" />
|
||||
<view class="cam-preview-btns">
|
||||
<view class="cam-preview-btn" onTap="onRetake">重拍</view>
|
||||
<view class="cam-preview-btn ok" onTap="onConfirm">使用照片</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 控制条:相册 / 快门 / 翻转 -->
|
||||
<cover-view class="cam-controls">
|
||||
<cover-view class="cam-side" onTap="onAlbum">相册</cover-view>
|
||||
<cover-view class="cam-shutter" onTap="onShutter">
|
||||
<cover-view class="cam-shutter-dot"></cover-view>
|
||||
</cover-view>
|
||||
<cover-view class="cam-side" onTap="onFlip">翻转</cover-view>
|
||||
</cover-view>
|
||||
<!-- 分析中 -->
|
||||
<view class="cam-analyzing" a:if="{{analyzing}}">
|
||||
<view class="cam-analyzing-text">{{debugMode ? '正在验证本地图片压缩…' : '正在生成您的肌肤报告…'}}</view>
|
||||
<view class="cam-cancel" onTap="onCancel">{{debugMode ? '退出验收' : '返回,稍后查看历史报告'}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 本步完成停顿卡 -->
|
||||
<cover-view class="cam-done" a:if="{{doneTip}}">{{doneTip}}</cover-view>
|
||||
<!-- 底部控制条:相册 / 快门 / 翻转 -->
|
||||
<view class="cam-controls" a:if="{{!previewUrl && !analyzing}}">
|
||||
<view class="cam-side" onTap="onAlbum">相册</view>
|
||||
<view class="cam-shutter" onTap="onShutter">
|
||||
<view class="cam-shutter-dot" />
|
||||
</view>
|
||||
<view class="cam-side" onTap="{{cameraEnabled ? 'onFlip' : 'onOpenSettings'}}">{{cameraEnabled ? '翻转' : '授权设置'}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 拍后预览:确认使用 / 重拍 -->
|
||||
<cover-view class="cam-preview" a:if="{{previewUrl}}" catchTap="noop">
|
||||
<cover-image class="cam-preview-img" src="{{previewUrl}}"></cover-image>
|
||||
<cover-view class="cam-preview-btns">
|
||||
<cover-view class="cam-preview-btn" onTap="onRetake">重拍</cover-view>
|
||||
<cover-view class="cam-preview-btn ok" onTap="onConfirm">使用照片</cover-view>
|
||||
</cover-view>
|
||||
</cover-view>
|
||||
<!-- 提示/错误文案 -->
|
||||
<view class="cam-hint" a:if="{{permissionMessage && !previewUrl && !analyzing}}">{{permissionMessage}}</view>
|
||||
|
||||
<!-- 分析中 -->
|
||||
<cover-view class="cam-analyzing" a:if="{{analyzing}}">
|
||||
<cover-view class="cam-analyzing-text">{{debugMode ? '正在验证本地图片压缩…' : '正在生成您的肌肤报告…'}}</cover-view>
|
||||
<cover-view class="cam-cancel" onTap="onCancel">{{debugMode ? '退出验收' : '返回,稍后查看历史报告'}}</cover-view>
|
||||
</cover-view>
|
||||
</camera>
|
||||
<!-- 开发版:权限诊断 -->
|
||||
<view class="cam-diag-entry" a:if="{{showDiagnose}}" onTap="onDiagnose">权限诊断</view>
|
||||
<view class="cam-diag" a:if="{{diagText}}" catchTap="noop">
|
||||
<scroll-view class="cam-diag-scroll" scroll-y>
|
||||
<text class="cam-diag-text">{{diagText}}</text>
|
||||
</scroll-view>
|
||||
<view class="cam-diag-close" onTap="onCloseDiagnose">关闭</view>
|
||||
</view>
|
||||
|
||||
<!-- 压缩降级用的隐藏画布(canvas 重绘压缩,不可见) -->
|
||||
<canvas id="compress-canvas" class="compress-canvas" width="{{compressWidth}}" height="{{compressHeight}}" style="width:{{compressWidth}}px;height:{{compressHeight}}px;" />
|
||||
<!-- 调试模式结果页 -->
|
||||
<view class="cam-debug" a:if="{{debugComplete}}">
|
||||
<view class="cam-debug-title">三步拍摄与 JPEG 压缩已完成</view>
|
||||
<view class="cam-debug-note">请核对实际照片方向是否为正脸、右脸、左脸;本页不判断姿态是否合格。</view>
|
||||
<view class="cam-debug-item" a:for="{{debugPhotos}}" a:key="key">
|
||||
<image class="cam-debug-img" src="{{item.photo}}" mode="aspectFit" />
|
||||
<view class="cam-debug-meta">{{item.title}} · JPEG {{item.width}}×{{item.height}} · 约{{item.kilobytes}}KB</view>
|
||||
</view>
|
||||
<button class="btn-primary cam-debug-btn" onTap="onDebugRestart">重新验收</button>
|
||||
<view class="cam-debug-exit" onTap="onCancel">退出并清空本页照片</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
var api = require('../../utils/api'), image = require('../../utils/image'), voice = require('../../utils/voice');
|
||||
var permissions = require('../../utils/permissions');
|
||||
var development = require('../../utils/development');
|
||||
var config = require('../../config');
|
||||
var STEPS = [
|
||||
{ target: 'front', tip: '请单人正脸出镜', turn: '正脸居中', mask: 0 },
|
||||
{ target: 'right', tip: '请右脸出镜', turn: '请向左转头', mask: 2 },
|
||||
@@ -9,14 +10,24 @@ var STEPS = [
|
||||
Page({
|
||||
data: { stepIndex: 0, stepText: '1/3', stepTip: STEPS[0].tip, guides: ['正脸居中', '取下眼镜', '光线充足'],
|
||||
maskStep: 0, devicePosition: 'front', camError: false, previewUrl: '', doneTip: '', analyzing: false,
|
||||
compressWidth: 1, compressHeight: 1, cameraEnabled: false, permissionMessage: '拍摄面部照片用于肌肤检测,请开启相机权限。',
|
||||
debugMode: false, debugComplete: false, debugPhotos: [] },
|
||||
compressWidth: 1, compressHeight: 1, cameraEnabled: false, permissionMessage: '',
|
||||
debugMode: false, debugComplete: false, debugPhotos: [],
|
||||
showDiagnose: config.environment !== 'production', diagText: '', cameraSupported: false },
|
||||
onLoad: function (query) {
|
||||
this.alive = true; this.visible = true; this.photos = []; this.busy = false; this.seq = 0;
|
||||
var supported = config.nativeCamera === true &&
|
||||
(typeof permissions.cameraSupported === 'function' ? permissions.cameraSupported() : true);
|
||||
this.cameraSupported = supported;
|
||||
this.debugMode = !!(query && query.debug === 'camera' && development.cameraDebugEnabled());
|
||||
this.setData({ debugMode: this.debugMode });
|
||||
this.setData({ debugMode: this.debugMode, cameraSupported: supported });
|
||||
if (this.debugMode) return;
|
||||
if (!api.user() || !getApp().globalData.profile) my.redirectTo({ url: '/pages/splash/splash' });
|
||||
var app = typeof getApp === 'function' ? getApp() : null;
|
||||
if (!api.user() || !app || !app.globalData.profile) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
||||
if (!supported) {
|
||||
// 淘宝 <camera> 组件不可用:直接用系统相机/相册,页面不再挂相机组件(否则整页崩溃)
|
||||
this.setData({ cameraEnabled: false, camError: true,
|
||||
permissionMessage: '请用下方按钮拍摄,三张依次为正脸、右脸、左脸。' });
|
||||
}
|
||||
},
|
||||
onReady: function () {
|
||||
this.pageReady = true;
|
||||
@@ -25,15 +36,17 @@ Page({
|
||||
onShow: function () {
|
||||
this.visible = true;
|
||||
if (!this.data.previewUrl && !this.data.analyzing && !this.data.debugComplete) this.renderStep();
|
||||
if (!this.cameraSupported) return; // 平台没有内置相机:直接用系统相机/相册
|
||||
var that = this;
|
||||
permissions.settings().then(function (current) {
|
||||
if (!that.alive || !that.visible) return;
|
||||
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
|
||||
else if (permissions.state(current, 'scope.camera') === false) that.setData({ cameraEnabled: false, permissionMessage: '相机权限未开启,可前往授权设置或使用相册。' });
|
||||
});
|
||||
}).catch(function () {});
|
||||
},
|
||||
mountCamera: function () {
|
||||
if (!this.alive || !this.visible || !this.pageReady || this.data.cameraEnabled || this.data.debugComplete) return;
|
||||
if (!this.cameraSupported) return;
|
||||
var that = this;
|
||||
this.setData({ cameraEnabled: true, camError: false }, function () {
|
||||
if (!that.alive || !that.visible) return;
|
||||
@@ -56,6 +69,17 @@ Page({
|
||||
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
|
||||
}).catch(function (err) { if (that.alive) my.showToast({ content: err.message, type: 'none' }); });
|
||||
},
|
||||
onCloseDiagnose: function () { this.setData({ diagText: '' }); },
|
||||
// 开发版:把真机上的相机能力/授权返回值直接打在页面上,供截图定位
|
||||
onDiagnose: function () {
|
||||
var that = this;
|
||||
this.setData({ diagText: '检测中…' });
|
||||
permissions.diagnose().then(function (info) {
|
||||
if (that.alive) that.setData({ diagText: info.join('\n') });
|
||||
}).catch(function (err) {
|
||||
if (that.alive) that.setData({ diagText: 'diagnose error: ' + (err && err.message ? err.message : String(err)) });
|
||||
});
|
||||
},
|
||||
onHide: function () { this.pause(); },
|
||||
onUnload: function () { this.pause(); this.alive = false; this.photos = []; this.debugResults = []; },
|
||||
pause: function () {
|
||||
@@ -89,7 +113,12 @@ Page({
|
||||
if (file) that.setData({ previewUrl: file });
|
||||
else my.showToast({ content: '拍摄失败,请重试', type: 'none' });
|
||||
}, fail: function () {
|
||||
if (that.alive && that.visible) my.showToast({ content: '拍摄失败,请重试', type: 'none' });
|
||||
if (!that.alive || !that.visible || seq !== that.seq) return;
|
||||
// 真机上内置相机 takePhoto 失败(机型/组件差异、权限被拒等):
|
||||
// 不打断流程,直接退回系统相机走 my.chooseImage 那条已验证过的路。
|
||||
that.busy = false;
|
||||
my.showToast({ content: '内置相机不可用,改用系统相机', type: 'none' });
|
||||
that.choose(['camera']);
|
||||
}, complete: function () { if (seq === that.seq) that.busy = false; } });
|
||||
},
|
||||
choose: function (sourceType) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.wrap {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: #000;
|
||||
}
|
||||
.cam {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tip {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
bottom: 80rpx;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<!-- 相机组件冒烟测试:页面里只有一个 <camera>,不放任何嵌套覆盖组件。
|
||||
用来判断淘宝真机到底支不支持相机组件(之前整页崩,怀疑是嵌了微信专有的 <cover-view>)。 -->
|
||||
<view class="wrap">
|
||||
<camera id="test-camera" class="cam" device-position="front" flash="off" onError="onError" />
|
||||
<view class="tip">如果能看到实时画面 → 相机组件可用</view>
|
||||
</view>
|
||||
@@ -0,0 +1,10 @@
|
||||
Page({
|
||||
data: {},
|
||||
onLoad: function () {
|
||||
this.alive = true;
|
||||
},
|
||||
onUnload: function () { this.alive = false; },
|
||||
onError: function (err) {
|
||||
my.showToast({ content: '相机组件报错: ' + JSON.stringify(err || {}), type: 'none', duration: 6000 });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "相机组件测试",
|
||||
"titleBarColor": "#101823",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
@@ -1,5 +1,208 @@
|
||||
/* ═════ 历史报告 —— 逐条对照 H5:
|
||||
style.css .card(L355-358) .toast(L588-599) .policy-header .history-clear
|
||||
ui-20261009.css L8(#s-history 字体栈) L33-44(.history-content/.history-count/.identity-note/
|
||||
.history-item/.history-detail/.history-retry) L99-106(.font-credit/.account-status)
|
||||
ui-20261009.css L47(.report-title 全局:font-size 23px / padding 0 / margin-bottom)
|
||||
ui-20261009.css L92(.report-title font-weight 600)
|
||||
换算:calc(N * var(--u)) → 2N rpx;字号/行高/圆角固定 px 原样保留。 ═════ */
|
||||
|
||||
page { background: #EFEFEF; }
|
||||
.history { padding-bottom: 48rpx; }
|
||||
.history-note { padding: 32rpx; color: #666; text-align: center; }
|
||||
.history-item { line-height: 1.9; }
|
||||
.history-open { color: #666; text-align: right; }
|
||||
|
||||
/* H5 .screen#s-history */
|
||||
.history {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
/* H5 #s-history 的 background-image center/cover + background-attachment:fixed
|
||||
→ 等价实现:fixed 定位、铺满视口的 image */
|
||||
.history-bg {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* H5 .history-content(ui-20261009 L34) */
|
||||
.history-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 56rpx 34rpx 72rpx; /* calc(28*u) calc(17*u) calc(36*u) */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* H5 .report-title(style.css L350-354 + ui-20261009 L47/L92) */
|
||||
.report-title {
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
color: #282828;
|
||||
text-align: center;
|
||||
letter-spacing: 1rpx; /* calc(.5 * var(--u)) */
|
||||
padding: 0;
|
||||
margin-bottom: 46rpx; /* calc(23 * var(--u)) */
|
||||
}
|
||||
|
||||
/* H5 .card(style.css L355-358 + ui-20261009 L48:圆角 calc(7*u)) */
|
||||
.history .card {
|
||||
background: #fff;
|
||||
border-radius: 14rpx;
|
||||
margin-bottom: 24rpx; /* calc(12 * var(--u)) */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* H5 .account-status / .account-status-history(ui-20261009 L100/L106) */
|
||||
.account-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.account-status-history {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
margin-bottom: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
.account-label { display: flex; align-items: center; }
|
||||
.account-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 7px;
|
||||
border-radius: 50%;
|
||||
background: #b7bac0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.account-status-on .account-dot { background: #34ca7b; }
|
||||
.account-text { white-space: nowrap; }
|
||||
/* H5 .account-logout(ui-20261012 L4) */
|
||||
.account-logout {
|
||||
font-size: 11px;
|
||||
opacity: .8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* H5 .history-count(ui-20261009 L35-38)
|
||||
H5 里 .history-count 没设 font-size,行盒取浏览器默认 16px;app.acss 把 page 设成
|
||||
14px,所以这里显式补回 16px,内联的「测试数量」才会落在同一基线上。 */
|
||||
.history .history-count {
|
||||
text-align: center;
|
||||
padding: 32rpx 24rpx 42rpx; /* calc(16*u) calc(12*u) calc(21*u) */
|
||||
font-size: 16px;
|
||||
}
|
||||
/* H5 .history-count > span 是内联元素(内含 display:block 的 ::after 分隔线) */
|
||||
.history-count-label {
|
||||
display: inline;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* H5 .history-count > span::after */
|
||||
.history-count-label::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 90rpx; /* calc(45 * var(--u)) */
|
||||
margin: 24rpx auto; /* calc(12 * var(--u)) auto */
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.history-count-value { font-size: 12px; }
|
||||
|
||||
/* H5 .identity-note(ui-20261009 L39) */
|
||||
.identity-note {
|
||||
color: #888;
|
||||
font-size: 10px;
|
||||
line-height: 1.6;
|
||||
margin: 24rpx 8rpx 40rpx; /* calc(12*u) calc(4*u) calc(20*u) */
|
||||
}
|
||||
|
||||
/* H5 .history-empty(style.css L637) */
|
||||
.history-empty {
|
||||
text-align: center;
|
||||
color: #868686;
|
||||
font-size: 12.5px;
|
||||
padding: 60px 0;
|
||||
}
|
||||
/* H5 的加载中/失败文案是直接写进 #history-list 的纯文本(继承 16px、左对齐、无内边距) */
|
||||
.history-list-note {
|
||||
font-size: 16px;
|
||||
color: #282828;
|
||||
}
|
||||
|
||||
/* H5 .history-item(ui-20261009 L40-42) */
|
||||
.history .history-item {
|
||||
padding: 26rpx 38rpx 20rpx; /* calc(13*u) calc(19*u) calc(10*u) */
|
||||
overflow: hidden;
|
||||
margin-top: 12rpx; /* calc(6 * var(--u)) */
|
||||
}
|
||||
.history-item-line {
|
||||
font-size: 12px;
|
||||
padding-bottom: 14rpx; /* calc(7 * var(--u)) */
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.history-item-label {
|
||||
color: #888;
|
||||
margin-right: 16rpx; /* calc(8 * var(--u)) */
|
||||
}
|
||||
|
||||
/* H5 .history-detail(ui-20261009 L43) */
|
||||
.history .history-detail {
|
||||
display: block;
|
||||
margin: 14rpx 0 0 auto; /* calc(7 * var(--u)) 0 0 auto */
|
||||
background: #F5F078;
|
||||
border-radius: 10rpx; /* calc(5 * var(--u)) */
|
||||
width: 160rpx; /* calc(80 * var(--u)) */
|
||||
min-height: 46rpx; /* calc(23 * var(--u)) */
|
||||
font-size: 12px;
|
||||
color: #282828;
|
||||
line-height: 46rpx;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.history .history-detail::after { border: none; }
|
||||
|
||||
/* H5 .history-retry(ui-20261009 L44) */
|
||||
.history .history-retry {
|
||||
display: block;
|
||||
width: fit-content; /* H5 是 block + width:auto 的按钮 → 收缩到文字宽再 margin:auto 居中 */
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
color: #707070;
|
||||
text-decoration: underline;
|
||||
font-size: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.history .history-retry::after { border: none; }
|
||||
|
||||
/* ─── H5 .toast ─── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 15%;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
max-width: 86%;
|
||||
background: rgba(20, 20, 20, .85);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
padding: 9px 18px;
|
||||
border-radius: 18px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all .25s ease;
|
||||
z-index: 99;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
|
||||
@@ -1,10 +1,48 @@
|
||||
<!-- ═════ 历史报告 —— 对照 H5 index.html #s-history(L32-42) 与 app.js renderHistoryList()(L1956-1985) ═════ -->
|
||||
<view class="history">
|
||||
<view class="history-note">最近5份检测报告</view>
|
||||
<view a:if="{{loading}}" class="history-note">加载中…</view>
|
||||
<view a:if="{{error}}" class="history-note" onTap="load">{{error}},点击重试</view>
|
||||
<view a:if="{{!loading && !error && !items.length}}" class="history-note">暂无检测报告</view>
|
||||
<view class="card history-item" a:for="{{items}}" a:key="id" data-id="{{item.id}}" onTap="onOpen">
|
||||
<view>{{item.date}}</view><view>综合评分 {{item.total}} · 肌肤年龄 {{item.skinAge}} 岁</view>
|
||||
<view class="history-open">查看报告 ›</view>
|
||||
<!-- H5 #s-history{background:#efefef url(bg-history-h5) center/cover fixed} -->
|
||||
<image class="history-bg" src="{{bgUrl}}" mode="aspectFill" />
|
||||
|
||||
<!-- H5 .history-content -->
|
||||
<view class="history-content">
|
||||
<view class="report-title">您的报告</view>
|
||||
|
||||
<!-- H5 .account-status.account-status-history -->
|
||||
<view class="account-status account-status-history {{signedIn ? 'account-status-on' : ''}}">
|
||||
<view class="account-label">
|
||||
<view class="account-dot"></view>
|
||||
<text class="account-text">{{accountLabel}}</text>
|
||||
</view>
|
||||
<view class="account-logout" a:if="{{signedIn}}" onTap="onLogout">退出登录</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .card.history-count -->
|
||||
<view class="card history-count">
|
||||
<view class="history-count-label">测试数量</view>
|
||||
<view class="history-count-value">{{countText}}</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .identity-note -->
|
||||
<view class="identity-note">{{identityNote}}</view>
|
||||
|
||||
<!-- H5 #history-list -->
|
||||
<view class="history-list">
|
||||
<view class="history-list-note" a:if="{{loading && !items.length}}">正在读取报告…</view>
|
||||
<view class="history-list-note" a:if="{{!loading && error}}">{{error}},请刷新列表重试。</view>
|
||||
<view class="history-empty" a:if="{{!loading && !error && !items.length}}">
|
||||
<view>暂无检测报告</view>
|
||||
<view>完成一次检测后,报告会自动保存在这里。</view>
|
||||
</view>
|
||||
<view class="card history-item" a:for="{{items}}" a:key="id" data-id="{{item.id}}" onTap="onOpen">
|
||||
<view class="history-item-line"><text class="history-item-label">测试时间</text> {{item.date}}</view>
|
||||
<button class="history-detail" data-id="{{item.id}}" catchTap="onOpen">查看详情</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .history-retry -->
|
||||
<button class="history-retry" onTap="load">刷新列表</button>
|
||||
</view>
|
||||
|
||||
<!-- H5 .toast -->
|
||||
<view class="toast {{toastOn ? 'show' : ''}}">{{toastText}}</view>
|
||||
</view>
|
||||
|
||||
@@ -1,34 +1,81 @@
|
||||
/* ═════ 历史报告:api.request('reports') 拉列表 + 点条目进报告页
|
||||
对照 H5 index.html #s-history(L32-42) 与 app.js renderHistoryList()/openHistory()(L1944-2001)
|
||||
跳转前把 payload 放进 globalData,由 pages/report 渲染。 ═════ */
|
||||
var api = require('../../utils/api');
|
||||
|
||||
function pad2(n) { return (n < 10 ? '0' : '') + n; }
|
||||
// H5 用 toLocaleDateString('sv-SE'),即 YYYY-MM-DD 零填充
|
||||
function formatDate(value) {
|
||||
var d = new Date(value);
|
||||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
||||
}
|
||||
|
||||
Page({
|
||||
data: { items: [], loading: false, error: '' },
|
||||
data: {
|
||||
bgUrl: api.assetUrl('img/ui-20261009/bg-history-h5.jpg'),
|
||||
items: [], loading: false, error: '',
|
||||
signedIn: false, accountLabel: '尚未登录',
|
||||
countText: '正在读取报告…', // H5 #history-count 初值
|
||||
identityNote: '',
|
||||
toastText: '', toastOn: false
|
||||
},
|
||||
onShow: function () {
|
||||
if (!api.user()) { this.setData({ items: [] }); my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
||||
this.setData({ signedIn: true, accountLabel: '淘宝已登录' });
|
||||
this.load();
|
||||
},
|
||||
onUnload: function () { this.closed = true; },
|
||||
|
||||
// H5 .toast
|
||||
toast: function (msg) {
|
||||
var that = this;
|
||||
this.setData({ toastText: msg, toastOn: true });
|
||||
clearTimeout(this.toastTimer);
|
||||
this.toastTimer = setTimeout(function () {
|
||||
that.setData({ toastOn: false });
|
||||
}, Math.max(1800, String(msg).length * 130));
|
||||
},
|
||||
|
||||
load: function () {
|
||||
var that = this;
|
||||
this.closed = false;
|
||||
this.setData({ loading: true, error: '' });
|
||||
api.request('reports').then(function (data) {
|
||||
if (that.closed) return;
|
||||
that.setData({ items: data.items.map(function (item) {
|
||||
var date = new Date(item.created_at);
|
||||
return { id: item.id, total: item.total == null ? '--' : item.total, skinAge: item.skinAge == null ? '--' : item.skinAge,
|
||||
date: date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() };
|
||||
}) });
|
||||
}).catch(function (err) { if (!that.closed) that.setData({ error: err.message }); })
|
||||
.then(function () { if (!that.closed) that.setData({ loading: false }); });
|
||||
that.setData({
|
||||
countText: '您最近已进行过' + data.total + '次面部问题检测',
|
||||
identityNote: '保留最近 5 份报告,同一淘宝账号可跨手机查看。',
|
||||
items: data.items.map(function (item) {
|
||||
return { id: item.id, date: formatDate(item.created_at) };
|
||||
})
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (!that.closed) that.setData({ error: err.message, countText: '暂时无法读取' });
|
||||
}).then(function () { if (!that.closed) that.setData({ loading: false }); });
|
||||
},
|
||||
|
||||
onOpen: function (e) {
|
||||
if (this.data.loading) return;
|
||||
var id = e.currentTarget.dataset.id;
|
||||
if (!id) return;
|
||||
var that = this;
|
||||
this.closed = false;
|
||||
this.setData({ loading: true });
|
||||
api.request('reports', { id: e.currentTarget.dataset.id }).then(function (data) {
|
||||
api.request('reports', { id: id }).then(function (data) {
|
||||
if (that.closed) return;
|
||||
getApp().globalData.photos = data.report.photos;
|
||||
getApp().globalData.payload = Object.assign({}, data.report.payload, { vm: data.report.vm, presentation: data.report.presentation });
|
||||
my.navigateTo({ url: '/pages/report/report' });
|
||||
}).catch(function (err) { my.showToast({ content: err.message, type: 'none' }); })
|
||||
}).catch(function (err) { that.toast(err.message); })
|
||||
.then(function () { if (!that.closed) that.setData({ loading: false }); });
|
||||
},
|
||||
|
||||
// H5 data-action="logout-wechat"(历史页右上角)
|
||||
onLogout: function () {
|
||||
var that = this;
|
||||
api.logout().then(function () {
|
||||
that.setData({ signedIn: false, accountLabel: '尚未登录', items: [], countText: '', identityNote: '' });
|
||||
my.redirectTo({ url: '/pages/splash/splash' });
|
||||
}).catch(function (err) { that.toast(err.message); });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
{ "defaultTitle": "历史报告", "backgroundColor": "#EFEFEF" }
|
||||
{ "defaultTitle": "历史报告", "titleBarColor": "#FFFFFF", "backgroundColor": "#EFEFEF" }
|
||||
|
||||
@@ -19,6 +19,7 @@ Page({
|
||||
configText: JSON.stringify({
|
||||
environment: config.environment,
|
||||
transport: config.transport,
|
||||
cloudType: config.cloudType || 'empty',
|
||||
baseUrl: config.baseUrl,
|
||||
cloudAppId: config.cloudAppId || '(empty)',
|
||||
cloudEnv: config.cloudEnv
|
||||
@@ -33,10 +34,11 @@ Page({
|
||||
transport.request(path, body || {}).then(function (data) {
|
||||
that.setData({ loading: false, resultText: JSON.stringify(data, null, 2) });
|
||||
}).catch(function (err) {
|
||||
that.setData({
|
||||
loading: false,
|
||||
resultText: 'ERROR: ' + (err && err.message ? err.message : String(err))
|
||||
});
|
||||
// 真机上没法看 console,把云网关的 code/msg 一起显示出来
|
||||
var detail = err && err.message ? err.message : String(err);
|
||||
if (err && err.code) detail = err.code + ' / ' + detail;
|
||||
if (err && err.msg && err.msg !== err.message) detail += ' | ' + err.msg;
|
||||
that.setData({ loading: false, resultText: 'ERROR: ' + detail });
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1,141 +1,494 @@
|
||||
/* ═════ 档案填写页样式 ═════ */
|
||||
/* ═════ 03 档案填写 —— 逐条对照 H5:
|
||||
style.css #s-profile(L105-205) .profile-bg/.profile-body/.page-title/.field/.field-label/
|
||||
.form-row.two/.pick/.pick-value/.pick .chev/.skin-card/.skin-axis/.axis-item/.chip/
|
||||
.skin-stage/.skin-arrow/.skin-carousel/.skin-track/.skin-item/.skin-name/.skin-desc/
|
||||
.skin-dots/.btn-submit/.ms-mobile-select(滚轮外观)/.overlay/.popup/.privacy 类
|
||||
ui-20261009.css L31-32,L89-92
|
||||
ui-20261012.css L34-69(.popup 与 #p-photoguide 定位), L169-198(#s-profile 的 --profile-space 收缩规则)
|
||||
换算:calc(N * var(--u)) → 2N rpx;字号/行高/圆角固定 px 原样保留。
|
||||
注:H5 的 --profile-space = min(var(--u), max(.3px, calc((100vh - 420px)/306))),
|
||||
这里写成同构的 min(2rpx, max(.6rpx, ...)),并用 var(--profile-space, 2rpx) 兜底。 ═════ */
|
||||
|
||||
page { background: #0e0f18; }
|
||||
|
||||
/* H5 #s-profile(style.css L105 + ui-20261012 L172-175) */
|
||||
.profile {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
background: #0e0f18;
|
||||
--profile-space: min(2rpx, max(0.6rpx, calc((100vh - 420px) / 306)));
|
||||
}
|
||||
|
||||
/* H5 .profile-bg(ui-20261009 L31:bg-profile-h5,cover) */
|
||||
.profile-bg {
|
||||
position: fixed;
|
||||
left: 0; top: 0;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* 上半部分露出背景图,表单区由白色卡片承载 */
|
||||
}
|
||||
|
||||
/* H5 .profile-body(style.css L113-117 + ui-20261012 L179-184) */
|
||||
.profile-body {
|
||||
position: relative;
|
||||
padding: 48rpx 32rpx 64rpx;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.page-title {
|
||||
color: #FFFFFF;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 48rpx;
|
||||
padding: 68rpx 128rpx 80rpx; /* calc(34*u) calc(64*u) calc(40*u) */
|
||||
padding-top: max(env(safe-area-inset-top), calc(34 * var(--profile-space, 2rpx)));
|
||||
padding-bottom: max(env(safe-area-inset-bottom), calc(40 * var(--profile-space, 2rpx)));
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.field { margin-bottom: 32rpx; }
|
||||
.field-half { width: 48%; }
|
||||
.field-label {
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 16rpx;
|
||||
/* H5 .page-title(style.css L118-122 + ui-20261009 L92 + ui-20261012 L185)
|
||||
注:#s-profile 根节点自身不接收 padding-top(safe-area),与 H5 一致 */
|
||||
.profile .page-title {
|
||||
color: #fff;
|
||||
font-size: 21.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx; /* calc(1 * var(--u)) */
|
||||
text-align: center;
|
||||
margin-bottom: calc(22 * var(--profile-space, 2rpx));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pick {
|
||||
|
||||
/* H5 .field / .field-label(ui-20261012 L186-187) */
|
||||
.profile .field {
|
||||
margin-bottom: calc(19 * var(--profile-space, 2rpx));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.profile .field-label {
|
||||
display: block;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 14.5px;
|
||||
font-weight: 400;
|
||||
margin-bottom: calc(9 * var(--profile-space, 2rpx));
|
||||
letter-spacing: 1rpx; /* calc(.5 * var(--u)) */
|
||||
}
|
||||
|
||||
/* H5 .form-row.two */
|
||||
.profile .form-row.two {
|
||||
display: flex;
|
||||
gap: 46rpx; /* calc(23 * var(--u)) */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.profile .form-row.two .field { flex: 1; min-width: 0; }
|
||||
|
||||
/* H5 .pick(style.css L133-142) —— 三个选择控件同一套外观 */
|
||||
.profile .pick {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
height: 74rpx; /* calc(37 * var(--u)) */
|
||||
padding: 0 26rpx 0 24rpx; /* 0 calc(13*u) 0 calc(12*u) */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.pick-value {
|
||||
background: #fff;
|
||||
border-radius: 10rpx; /* calc(5 * var(--u)) */
|
||||
font-size: 13.5px;
|
||||
color: #282828;
|
||||
font-size: 28rpx;
|
||||
text-align: left;
|
||||
}
|
||||
.chev {
|
||||
color: #868686;
|
||||
font-size: 28rpx;
|
||||
.profile .pick:active { background: #F7F7F7; }
|
||||
.profile .pick-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.profile .pick .chev {
|
||||
flex: none;
|
||||
width: 16rpx; /* calc(8 * var(--u)) */
|
||||
height: 16rpx;
|
||||
margin-top: -8rpx; /* calc(-4 * var(--u)) */
|
||||
border-width: 0 1.5px 1.5px 0;
|
||||
}
|
||||
.profile .pick-host { display: block; width: 100%; }
|
||||
|
||||
/* ─── 肤质卡 ─── */
|
||||
.skin-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
/* ─── H5 .skin-card(style.css L144-148 + ui-20261012 L189) ─── */
|
||||
.profile .skin-card {
|
||||
background: #fff;
|
||||
border-radius: 10rpx; /* calc(5 * var(--u)) */
|
||||
padding-top: calc(21 * var(--profile-space, 2rpx));
|
||||
padding-bottom: calc(24 * var(--profile-space, 2rpx));
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.skin-axis {
|
||||
/* H5 .skin-axis / .axis-item / .chip */
|
||||
.profile .skin-axis {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
gap: 24rpx; /* calc(12 * var(--u)) */
|
||||
margin-bottom: calc(18 * var(--profile-space, 2rpx));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.axis-item {
|
||||
display: flex;
|
||||
.profile .axis-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin: 0 16rpx;
|
||||
gap: 10rpx; /* calc(5 * var(--u)) */
|
||||
font-size: 9px;
|
||||
color: #282828;
|
||||
}
|
||||
.chip {
|
||||
display: inline-block;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
.profile .chip {
|
||||
width: 16rpx; /* calc(8 * var(--u)) */
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 8rpx;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chip-dry { background: #EAE6E1; }
|
||||
.chip-oily { background: #F5F078; }
|
||||
.chip-balance { background: #DBF6D6; }
|
||||
|
||||
.skin-stage {
|
||||
/* H5 .skin-stage / .skin-arrow(ui-20261012 L191) */
|
||||
.profile .skin-stage {
|
||||
position: relative;
|
||||
padding: 0 56rpx; /* 0 calc(28 * var(--u)) */
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
.profile .skin-arrow {
|
||||
position: absolute;
|
||||
top: 84rpx; /* calc(42 * var(--u)) */
|
||||
z-index: 1;
|
||||
width: 52rpx; /* calc(26 * var(--u)) */
|
||||
height: 52rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.skin-arrow {
|
||||
width: 64rpx;
|
||||
font-size: 56rpx;
|
||||
color: #868686;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
.profile .skin-arrow.prev { left: 10rpx; } /* calc(5 * var(--u)) */
|
||||
.profile .skin-arrow.next { right: 10rpx; }
|
||||
.profile .skin-arrow .chev {
|
||||
width: 18rpx; /* calc(9 * var(--u)) */
|
||||
height: 18rpx;
|
||||
border-width: 0 1.5px 1.5px 0;
|
||||
}
|
||||
.skin-swiper {
|
||||
.profile .skin-arrow.disabled { opacity: .2; }
|
||||
|
||||
/* H5 .skin-carousel / .skin-track(ui-20261012 L192-193) */
|
||||
.profile .skin-carousel {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
height: 460rpx;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.skin-item {
|
||||
.profile .skin-track {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
transition: transform .32s cubic-bezier(.22, .61, .36, 1);
|
||||
}
|
||||
/* H5 .skin-item / .skin-item img / .skin-name / .skin-desc */
|
||||
.profile .skin-item {
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.skin-img {
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
.profile .skin-img {
|
||||
width: 218rpx; /* calc(109 * var(--u)) */
|
||||
height: 218rpx;
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.skin-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 16rpx;
|
||||
.profile .skin-name {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
margin-top: calc(16 * var(--profile-space, 2rpx));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.skin-desc {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
.profile .skin-desc {
|
||||
font-size: 11.5px; /* ui-20261009 L32 */
|
||||
color: #282828;
|
||||
line-height: 1.55;
|
||||
text-align: center;
|
||||
margin-top: 8rpx;
|
||||
margin-top: 10rpx; /* calc(5 * var(--u)) */
|
||||
width: 300rpx; /* calc(150 * var(--u)) */
|
||||
max-width: 300rpx;
|
||||
min-height: 35.65px; /* calc(2 * 1.55 * 11.5px) */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.skin-dots {
|
||||
/* H5 第一条描述含 <br>:用两个块级行还原硬换行 */
|
||||
.profile .skin-desc-line { display: block; }
|
||||
|
||||
/* H5 .skin-dots / .skin-dots i */
|
||||
.profile .skin-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 16rpx;
|
||||
gap: 10rpx; /* calc(5 * var(--u)) */
|
||||
margin-top: calc(22 * var(--profile-space, 2rpx));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
.profile .skin-dots .dot {
|
||||
width: 16rpx; /* calc(8 * var(--u)) */
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #E5E5E5;
|
||||
margin: 0 6rpx;
|
||||
background: #D6D6D6;
|
||||
transition: background .25s;
|
||||
}
|
||||
.dot.on { background: #282828; }
|
||||
.profile .skin-dots .dot.on { background: #000; }
|
||||
|
||||
.btn-submit { margin-top: 24rpx; }
|
||||
.picker-sheet { background: white; width: 100%; align-self: flex-end; border-radius: 24rpx 24rpx 0 0; }
|
||||
.picker-actions { display: flex; justify-content: space-between; padding: 32rpx; }
|
||||
.native-picker { width: 100%; height: 440rpx; text-align: center; line-height: 80rpx; }
|
||||
/* H5 .btn-submit */
|
||||
.profile .btn-submit {
|
||||
margin: calc(25 * var(--profile-space, 2rpx)) auto 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.btn-press { transform: scale(.96); }
|
||||
|
||||
/* ═══ 滚轮弹层:H5 vendor/mobile-select 外观(style.css L191-205) ═══ */
|
||||
.profile .overlay-sheet {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
background: rgba(0, 0, 0, .55);
|
||||
}
|
||||
.picker-sheet {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 12px 12px 0 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.picker-btn-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 15px;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.picker-cancel,
|
||||
.picker-ensure {
|
||||
width: 64px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
.picker-cancel { color: #757575; }
|
||||
.picker-ensure { color: #282828; font-weight: 600; }
|
||||
.picker-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #282828;
|
||||
}
|
||||
.native-picker {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
font-size: 16px;
|
||||
color: #282828;
|
||||
}
|
||||
.picker-cell {
|
||||
font-size: 16px;
|
||||
color: #282828;
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
/* ═══ 拍照引导弹窗 H5 #p-photoguide(L311-327) ═══ */
|
||||
.profile .overlay-guide {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #101823;
|
||||
}
|
||||
.profile .guide-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.profile .guide-bg-img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.profile .guide-dim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
}
|
||||
/* H5 .splash-slogan.splash-slogan-updated(ui-20261012 L9-20) */
|
||||
.profile .splash-slogan {
|
||||
position: absolute;
|
||||
left: calc(50% - 1.041667px);
|
||||
top: max(6.838833%, calc(env(safe-area-inset-top) + 48px));
|
||||
width: 249.305556px;
|
||||
height: 75.347222px;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 23px;
|
||||
font-weight: 400;
|
||||
line-height: 1.16;
|
||||
letter-spacing: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
.profile .splash-slogan-copy-clip {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 51.236111px;
|
||||
height: 24.111111px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile .splash-slogan-copy {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -51.236111px;
|
||||
width: 100%;
|
||||
height: 75.347222px;
|
||||
}
|
||||
.profile .splash-slogan-brand,
|
||||
.profile .splash-slogan-english {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 23px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
}
|
||||
.profile .splash-slogan-brand { top: -4px; font-size: 28px; }
|
||||
.profile .splash-slogan-english { top: 26.736111px; }
|
||||
|
||||
/* H5 #p-photoguide .popup(ui-20261012 L34-38) */
|
||||
.profile .popup {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 307.986111px;
|
||||
height: 313.194444px;
|
||||
min-height: 313.194444px;
|
||||
top: -42.881944px;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
border-radius: 8.680556px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
.profile .popup-close {
|
||||
position: absolute;
|
||||
top: 10.055556px;
|
||||
right: 10.055556px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.profile .popup-close::after { border: none; }
|
||||
.popup-close-img {
|
||||
width: 17.708333px;
|
||||
height: 17.361111px;
|
||||
}
|
||||
/* H5 #p-photoguide .popup-title(L44) */
|
||||
.profile .popup-title {
|
||||
position: absolute;
|
||||
left: 37.5px;
|
||||
top: 61.388889px;
|
||||
margin: 0;
|
||||
font-size: 23.148148px;
|
||||
line-height: 28.935185px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
color: #282828;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
/* H5 .guide-description(L45-52) */
|
||||
.profile .guide-description {
|
||||
position: absolute;
|
||||
left: 37.847222px;
|
||||
top: 135.069444px;
|
||||
width: 237px;
|
||||
margin: 0;
|
||||
font-size: 11.574074px;
|
||||
line-height: 17.361111px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.profile .guide-line { white-space: nowrap; }
|
||||
/* H5 #p-photoguide .popup-btn(L57-64) */
|
||||
.profile .popup-btn {
|
||||
position: absolute;
|
||||
left: 63.194444px;
|
||||
top: 219.791667px;
|
||||
margin: 0;
|
||||
width: 181.25px;
|
||||
height: 38.888889px;
|
||||
line-height: 38.888889px;
|
||||
font-size: 14.467593px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
border-radius: 6.944444px;
|
||||
}
|
||||
@media (max-height: 440px) {
|
||||
.profile .popup { top: 0; }
|
||||
}
|
||||
|
||||
/* ─── H5 .toast ─── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 15%;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
max-width: 86%;
|
||||
background: rgba(20, 20, 20, .85);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
padding: 9px 18px;
|
||||
border-radius: 18px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all .25s ease;
|
||||
z-index: 99;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
|
||||
@@ -1,91 +1,130 @@
|
||||
<!-- ═════ 03 档案填写 ═════ -->
|
||||
<!-- ═════ 03 档案填写 —— 对照 H5 index.html #s-profile(L61-108) + #p-photoguide(L311-327) ═════ -->
|
||||
<view class="profile">
|
||||
<!-- H5 .profile-bg -->
|
||||
<image class="profile-bg" src="{{bgUrl}}" mode="aspectFill" />
|
||||
|
||||
<!-- H5 .profile-body -->
|
||||
<view class="profile-body">
|
||||
<view class="page-title">您的肌肤状态</view>
|
||||
|
||||
<!-- 出生年月 + 性别(原生 picker 底部滚轮) -->
|
||||
<view class="form-row">
|
||||
<view class="field field-half">
|
||||
<!-- H5 .form-row.two:出生年月 + 性别 -->
|
||||
<view class="form-row two">
|
||||
<view class="field">
|
||||
<view class="field-label">您的出生年月</view>
|
||||
<view onTap="onOpenBirth">
|
||||
<view class="pick">
|
||||
<text class="pick-value">{{birthText}}</text>
|
||||
<text class="chev">⌄</text>
|
||||
</view>
|
||||
<view class="pick" onTap="onOpenBirth">
|
||||
<text class="pick-value">{{birthText}}</text>
|
||||
<view class="chev down"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field field-half">
|
||||
<view class="field">
|
||||
<view class="field-label">您的性别</view>
|
||||
<picker mode="selector" range="{{genderRange}}" onChange="onGenderChange">
|
||||
<picker class="pick-host" mode="selector" range="{{genderRange}}" onChange="onGenderChange">
|
||||
<view class="pick">
|
||||
<text class="pick-value">{{genderText}}</text>
|
||||
<text class="chev">⌄</text>
|
||||
<view class="chev down"></view>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 所在地区(省+市联动) -->
|
||||
<!-- H5 .field:所在地区 -->
|
||||
<view class="field">
|
||||
<view class="field-label">您的所在地区</view>
|
||||
<view onTap="onOpenRegion">
|
||||
<view class="pick">
|
||||
<text class="pick-value">{{regionText}}</text>
|
||||
<text class="chev">⌄</text>
|
||||
</view>
|
||||
<view class="pick" onTap="onOpenRegion">
|
||||
<text class="pick-value">{{regionText}}</text>
|
||||
<view class="chev down"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日常肤质(轮播卡) -->
|
||||
<view class="field">
|
||||
<!-- H5 .field.profile-skin-field:日常肤质轮播 -->
|
||||
<view class="field profile-skin-field">
|
||||
<view class="field-label">您的日常肤质</view>
|
||||
<view class="skin-card">
|
||||
<view class="skin-axis">
|
||||
<text class="axis-item"><text class="chip chip-dry"></text>偏干</text>
|
||||
<text class="axis-item"><text class="chip chip-oily"></text>偏油</text>
|
||||
<text class="axis-item"><text class="chip chip-balance"></text>水油平衡</text>
|
||||
<view class="axis-item"><view class="chip chip-dry"></view><text>偏干</text></view>
|
||||
<view class="axis-item"><view class="chip chip-oily"></view><text>偏油</text></view>
|
||||
<view class="axis-item"><view class="chip chip-balance"></view><text>水油平衡</text></view>
|
||||
</view>
|
||||
<view class="skin-stage">
|
||||
<view class="skin-arrow" onTap="onSkinPrev">‹</view>
|
||||
<swiper class="skin-swiper" current="{{skinIndex}}" onChange="onSkinChange" circular="{{false}}">
|
||||
<swiper-item a:for="{{skins}}" a:key="name">
|
||||
<view class="skin-item">
|
||||
<image src="{{item.img}}" class="skin-img" mode="aspectFit" lazy-load />
|
||||
<view class="skin-name">{{item.name}}</view>
|
||||
<view class="skin-desc">{{item.desc}}</view>
|
||||
<view class="skin-arrow prev {{skinIndex === 0 ? 'disabled' : ''}}" onTap="onSkinPrev">
|
||||
<view class="chev left"></view>
|
||||
</view>
|
||||
<view class="skin-carousel" onTouchStart="onSkinTouchStart" onTouchEnd="onSkinTouchEnd">
|
||||
<view class="skin-track" style="transform: translateX(-{{skinPercent}}%);">
|
||||
<view class="skin-item" a:for="{{skins}}" a:key="name">
|
||||
<image class="skin-img" src="{{item.img}}" mode="aspectFit" />
|
||||
<text class="skin-name">{{item.name}}</text>
|
||||
<!-- H5 第一条描述里带 <br>:全脸干燥,容易紧绷、/缺乏光泽 -->
|
||||
<view class="skin-desc">
|
||||
<view class="skin-desc-line">{{item.desc}}</view>
|
||||
<view class="skin-desc-line" a:if="{{item.desc2}}">{{item.desc2}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="skin-arrow" onTap="onSkinNext">›</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="skin-arrow next {{skinIndex === skinLastIndex ? 'disabled' : ''}}" onTap="onSkinNext">
|
||||
<view class="chev right"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="skin-dots">
|
||||
<text a:for="{{skins}}" a:key="name" class="dot {{index === skinIndex ? 'on' : ''}}"></text>
|
||||
<view a:for="{{skins}}" a:key="name" class="dot {{index === skinIndex ? 'on' : ''}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="btn-primary btn-submit" onTap="onSubmit">提交</button>
|
||||
<button class="btn-primary btn-submit" hover-class="btn-press" onTap="onSubmit">提交</button>
|
||||
</view>
|
||||
|
||||
<!-- ═══ 弹窗:拍照引导 ═══ -->
|
||||
<view class="overlay" a:if="{{showGuide}}" onTap="onCloseGuide">
|
||||
<view class="popup" catchTap="noop">
|
||||
<button class="popup-close" onTap="onCloseGuide">✕</button>
|
||||
<view class="popup-title">
|
||||
<text>请拍摄或上传</text>
|
||||
<text>正面及左右侧面照片</text>
|
||||
</view>
|
||||
<button class="btn-primary popup-btn" onTap="onStartCamera">开始</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overlay" a:if="{{picker}}" onTap="onPickerClose">
|
||||
<!-- ═══ 弹层:出生年月/地区滚轮(保留现有 picker-view 自绘实现,仅统一外观) ═══ -->
|
||||
<view class="overlay overlay-sheet" a:if="{{picker}}" onTap="onPickerClose">
|
||||
<view class="picker-sheet" catchTap="noop">
|
||||
<view class="picker-actions"><text onTap="onPickerClose">取消</text><text onTap="onPickerConfirm">确定</text></view>
|
||||
<picker-view value="{{pickerValue}}" onChange="onPickerChange" class="native-picker">
|
||||
<picker-view-column><view a:for="{{picker === 'birth' ? birthRange[0] : regionRange[0]}}" a:key="*this">{{item}}</view></picker-view-column>
|
||||
<picker-view-column><view a:for="{{picker === 'birth' ? birthRange[1] : regionRange[1]}}" a:key="*this">{{item}}</view></picker-view-column>
|
||||
<view class="picker-btn-bar">
|
||||
<view class="picker-cancel" onTap="onPickerClose">取消</view>
|
||||
<view class="picker-title">{{pickerTitle}}</view>
|
||||
<view class="picker-ensure" onTap="onPickerConfirm">确定</view>
|
||||
</view>
|
||||
<picker-view class="native-picker" value="{{pickerValue}}" onChange="onPickerChange">
|
||||
<picker-view-column>
|
||||
<view class="picker-cell" a:for="{{picker === 'birth' ? birthRange[0] : regionRange[0]}}" a:key="*this">{{item}}</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view class="picker-cell" a:for="{{picker === 'birth' ? birthRange[1] : regionRange[1]}}" a:key="*this">{{item}}</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ═══ 弹窗:拍照引导(H5 #p-photoguide:L311-327) ═══ -->
|
||||
<view class="overlay overlay-guide" a:if="{{showGuide}}" onTap="onCloseGuide">
|
||||
<view class="guide-bg">
|
||||
<image class="guide-bg-img" src="{{guideBgUrl}}" mode="aspectFill" />
|
||||
<!-- 背景只保留启动页底图 + Slogan -->
|
||||
<view class="splash-slogan">
|
||||
<view class="splash-slogan-copy-clip">
|
||||
<image class="splash-slogan-copy" src="{{sloganUrl}}" mode="scaleToFill" />
|
||||
</view>
|
||||
<view class="splash-slogan-brand">1'me,</view>
|
||||
<view class="splash-slogan-english">FROM THE INSIDE OUT</view>
|
||||
</view>
|
||||
<!-- H5 的 .guide-bg::after 是 .guide-bg 的最后一个子层,会盖在 Slogan 上 → 这里放在 Slogan 之后 -->
|
||||
<view class="guide-dim"></view>
|
||||
</view>
|
||||
<view class="popup guide-popup" catchTap="noop">
|
||||
<button class="popup-close" onTap="onCloseGuide">
|
||||
<image class="popup-close-img" src="{{closeIconUrl}}" mode="aspectFit" />
|
||||
</button>
|
||||
<view class="popup-title popup-title-center">
|
||||
<view>请拍摄或上传</view>
|
||||
<view>正面及左右侧面照片</view>
|
||||
</view>
|
||||
<view class="guide-description">
|
||||
<view class="guide-line">为使测试结果更准确,请关闭美颜/滤镜、并确</view>
|
||||
<view class="guide-line">保光线充足均匀。</view>
|
||||
</view>
|
||||
<button class="btn-primary popup-btn" hover-class="btn-press" onTap="onStartCamera">开始</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .toast -->
|
||||
<view class="toast {{toastOn ? 'show' : ''}}">{{toastText}}</view>
|
||||
</view>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* ═════ 档案填写页:出生年月/性别/地区(原生 picker)+ 日常肤质选择(对齐 H5 #s-profile) ═════
|
||||
* 档案字段与 H5 一致:{ real_age, gender, region, daily_skin_type } */
|
||||
/* ═════ 档案填写页:出生年月/性别/地区 + 日常肤质轮播 → 拍照
|
||||
对照 H5 index.html #s-profile(L61-108)、#p-photoguide(L311-327)
|
||||
档案字段与 H5 一致:{ real_age, gender, region, daily_skin_type }
|
||||
选择器弹层保持现有实现(自绘 picker-view 滚轮 + 原生 selector),只调整触发控件外观。 ═════ */
|
||||
var api = require('../../utils/api.js');
|
||||
var REGION_DATA = require('../../utils/region-data.js');
|
||||
|
||||
// 肤质选项(与 H5 SKINS 一致,图片远程加载)
|
||||
// 肤质选项(与 H5 SKINS 一致,图片远程加载;第一条描述在 H5 里含 <br>,拆成两行)
|
||||
var SKINS = [
|
||||
{ img: 'img/ui-20261009/skin-0-dry.png', name: '干性肌肤', desc: '全脸干燥,容易紧绷、缺乏光泽' },
|
||||
{ img: 'img/ui-20261009/skin-0-dry.png', name: '干性肌肤', desc: '全脸干燥,容易紧绷、', desc2: '缺乏光泽' },
|
||||
{ img: 'img/ui-20261009/skin-1-mixeddry.png', name: '混合偏干肌肤', desc: '两颊偏干,T区油脂较少或偶尔出油' },
|
||||
{ img: 'img/ui-20261009/skin-2-neutral.png', name: '中性肌肤', desc: '水油平衡,肤感舒适,无明显干燥出油' },
|
||||
{ img: 'img/ui-20261009/skin-3-oily.png', name: '油性肌肤', desc: '全脸易出油,容易泛油光、毛孔明显' },
|
||||
@@ -21,7 +23,10 @@ function ageFromBirth(y, m) { return NOW_Y - y - (NOW_M < m ? 1 : 0); } // 未
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bgUrl: api.assetUrl('img/bg-splash.jpg'),
|
||||
bgUrl: api.assetUrl('img/ui-20261009/bg-profile-h5.jpg'),
|
||||
guideBgUrl: api.assetUrl('img/ui-20261009/bg-splash-h5.jpg'),
|
||||
sloganUrl: api.assetUrl('img/ui-20261012/splash-slogan.png'),
|
||||
closeIconUrl: api.assetUrl('img/ui-20261009/icon-close.png'),
|
||||
|
||||
// 出生年月(两列滚轮:年+月,默认 30 岁 1 月)
|
||||
birthRange: [YEARS, MONTHS],
|
||||
@@ -44,11 +49,14 @@ Page({
|
||||
|
||||
// 肤质轮播
|
||||
skins: SKINS.map(function (s) {
|
||||
return { img: api.assetUrl(s.img), name: s.name, desc: s.desc };
|
||||
return { img: api.assetUrl(s.img), name: s.name, desc: s.desc, desc2: s.desc2 || '' };
|
||||
}),
|
||||
skinIndex: 0,
|
||||
skinPercent: 0,
|
||||
skinLastIndex: SKINS.length - 1,
|
||||
|
||||
showGuide: false, picker: '', pickerValue: [0, 0]
|
||||
showGuide: false, picker: '', pickerTitle: '', pickerValue: [0, 0],
|
||||
toastText: '', toastOn: false
|
||||
},
|
||||
onLoad: function () {
|
||||
if (!api.user()) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
||||
@@ -60,6 +68,8 @@ Page({
|
||||
this.setData(saved);
|
||||
}
|
||||
} catch (_) {}
|
||||
// H5 goSkin(0) 的等价初始化:轨道位移与当前下标同步
|
||||
this.setData({ skinIndex: this.data.skinIndex || 0, skinPercent: (this.data.skinIndex || 0) * 100 });
|
||||
},
|
||||
onShow: function () {
|
||||
if (!api.user() || this.ownerId !== api.user().id) {
|
||||
@@ -67,11 +77,25 @@ Page({
|
||||
my.redirectTo({ url: '/pages/splash/splash' });
|
||||
}
|
||||
},
|
||||
onOpenBirth: function () { this.setData({ picker: 'birth', pickerValue: this.data.birthIndex.slice() }); },
|
||||
|
||||
// H5 .toast
|
||||
toast: function (msg) {
|
||||
var that = this;
|
||||
this.setData({ toastText: msg, toastOn: true });
|
||||
clearTimeout(this.toastTimer);
|
||||
this.toastTimer = setTimeout(function () {
|
||||
that.setData({ toastOn: false });
|
||||
}, Math.max(1800, String(msg).length * 130));
|
||||
},
|
||||
|
||||
onOpenBirth: function () {
|
||||
// H5 的 mobile-select title 为「出生年月」/「所在地区」
|
||||
this.setData({ picker: 'birth', pickerTitle: '出生年月', pickerValue: this.data.birthIndex.slice() });
|
||||
},
|
||||
onOpenRegion: function () {
|
||||
var index = this.data.regionIndex.slice();
|
||||
// A canceled draft may have changed the cities while the saved province stayed put.
|
||||
this.setData({ picker: 'region', pickerValue: index,
|
||||
this.setData({ picker: 'region', pickerTitle: '所在地区', pickerValue: index,
|
||||
regionRange: [REGION_DATA.map(function (p) { return p[0]; }), REGION_DATA[index[0]][1]] });
|
||||
},
|
||||
onPickerChange: function (e) {
|
||||
@@ -91,13 +115,13 @@ Page({
|
||||
/* ─── 出生年月 ─── */
|
||||
onBirthChange: function (e) {
|
||||
var idx = e.detail.value;
|
||||
var y = parseInt(YEARS[idx[0]], 10);
|
||||
var m = idx[1] + 1;
|
||||
var yv = parseInt(YEARS[idx[0]], 10);
|
||||
var mv = idx[1] + 1;
|
||||
this.setData({
|
||||
birthIndex: idx,
|
||||
birthText: y + '年' + m + '月',
|
||||
birth: y + '-' + (m < 10 ? '0' : '') + m,
|
||||
age: ageFromBirth(y, m)
|
||||
birthText: yv + '年' + mv + '月',
|
||||
birth: yv + '-' + (mv < 10 ? '0' : '') + mv,
|
||||
age: ageFromBirth(yv, mv)
|
||||
});
|
||||
},
|
||||
|
||||
@@ -121,30 +145,39 @@ Page({
|
||||
this.setData({ regionIndex: idx, regionText: text });
|
||||
},
|
||||
|
||||
/* ─── 肤质轮播 ─── */
|
||||
onSkinChange: function (e) {
|
||||
this.setData({ skinIndex: e.detail.current });
|
||||
/* ─── 肤质轮播(H5 goSkin 的等价实现:轨道 translateX) ─── */
|
||||
applySkin: function (index) {
|
||||
var i = Math.max(0, Math.min(SKINS.length - 1, index));
|
||||
this.setData({ skinIndex: i, skinPercent: i * 100 });
|
||||
},
|
||||
onSkinPrev: function () {
|
||||
if (this.data.skinIndex > 0) this.setData({ skinIndex: this.data.skinIndex - 1 });
|
||||
onSkinPrev: function () { this.applySkin(this.data.skinIndex - 1); },
|
||||
onSkinNext: function () { this.applySkin(this.data.skinIndex + 1); },
|
||||
onSkinTouchStart: function (e) {
|
||||
var t = (e.changedTouches || e.touches || [])[0];
|
||||
this.touchX = t ? (t.clientX != null ? t.clientX : t.pageX) : 0;
|
||||
},
|
||||
onSkinNext: function () {
|
||||
if (this.data.skinIndex < SKINS.length - 1) this.setData({ skinIndex: this.data.skinIndex + 1 });
|
||||
onSkinTouchEnd: function (e) {
|
||||
var t = (e.changedTouches || e.touches || [])[0];
|
||||
if (!t) return;
|
||||
var x = t.clientX != null ? t.clientX : t.pageX;
|
||||
var dx = x - (this.touchX || 0);
|
||||
if (Math.abs(dx) < 40) return;
|
||||
this.applySkin(this.data.skinIndex + (dx < 0 ? 1 : -1));
|
||||
},
|
||||
|
||||
/* ─── 提交 → 拍照引导弹窗 ─── */
|
||||
/* ─── 提交 → 拍照引导弹窗(H5 data-action="submit-profile") ─── */
|
||||
onSubmit: function () {
|
||||
var d = this.data;
|
||||
if (!d.birth || !(d.age >= 1 && d.age <= 99)) {
|
||||
my.showToast({ content: '请选择出生年月', type: 'none' });
|
||||
this.toast('请选择出生年月');
|
||||
return;
|
||||
}
|
||||
if (!d.genderText) {
|
||||
my.showToast({ content: '请选择性别', type: 'none' });
|
||||
this.toast('请选择性别');
|
||||
return;
|
||||
}
|
||||
if (!d.regionText) {
|
||||
my.showToast({ content: '请选择所在地区', type: 'none' });
|
||||
this.toast('请选择所在地区');
|
||||
return;
|
||||
}
|
||||
this.setData({ showGuide: true });
|
||||
@@ -154,7 +187,7 @@ Page({
|
||||
this.setData({ showGuide: false });
|
||||
},
|
||||
|
||||
// 「开始」:档案入全局态,进入拍照页
|
||||
// H5 data-action="start-camera":档案入全局态,进入拍照页
|
||||
onStartCamera: function () {
|
||||
if (!api.user()) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
||||
var d = this.data;
|
||||
|
||||
+597
-209
@@ -1,28 +1,106 @@
|
||||
/* ═════ 报告页样式(灰底白卡,对齐设计稿 08) ═════ */
|
||||
page { background: #EFEFEF; }
|
||||
.report { padding-bottom: 64rpx; }
|
||||
/* ═════ 08 报告页 —— 1:1 移植 H5 #s-report ═════
|
||||
对照来源(逐条核对,后者覆盖前者):
|
||||
index.html #s-report L174-291
|
||||
css/style.css L347-503 (基础)
|
||||
css/ui-20261009.css L45-87 (第二轮 PSD)
|
||||
css/ui-20261012.css L71-172 (第三轮 PSD,最终生效值)
|
||||
换算:H5 的 calc(N * var(--u))(375 设计宽基准)→ 2N rpx;
|
||||
H5 里固定的 px 字号、1px 描边、百分比、transform 原样保留;
|
||||
注释用「基础 → ui-20261009 → ui-20261012」标出覆盖关系。
|
||||
限制:只能用 class/id 选择器;acss 无 @font-face,字体沿用 app.acss 的系统栈,
|
||||
故 H5 的 "MiSans Demibold"(700) 退化为系统字体 + font-weight:700。 */
|
||||
|
||||
/* ─── 页面容器(H5 #s-report / .report-body) ───
|
||||
#s-report 还有一条 background:#efefef url(bg-report-h5.webp) center top/100% auto。
|
||||
该 webp 实测为 1080×7512 的纯 #EFEFEF 单色图(全图只有一种颜色 239,239,239),
|
||||
与 app.acss 里 page 的底色等价,所以这里不再引这张空图
|
||||
(<image> 会额外解码约 32MB,并把页面滚动高度撑到 2608px)。
|
||||
.report-body { padding: calc(28u) calc(17u) calc(42u) } */
|
||||
.report { position: relative; }
|
||||
.report-body { position: relative; z-index: 1; padding: 56rpx 34rpx 84rpx; }
|
||||
|
||||
/* .report-title { font-size:23px; font-weight:600(覆盖 700); letter-spacing:calc(.5u);
|
||||
text-align:center; margin-bottom:calc(23u) };行高固定 30px = H5 实测行盒 */
|
||||
.report-title {
|
||||
font-size: 40rpx;
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
color: #282828;
|
||||
text-align: center;
|
||||
padding: 32rpx 0 8rpx;
|
||||
letter-spacing: 1rpx;
|
||||
line-height: 30px;
|
||||
margin-bottom: 46rpx;
|
||||
}
|
||||
|
||||
/* ─── 总分卡 ─── */
|
||||
/* .card { background:#fff; border-radius:calc(7u)(覆盖 8u); margin-bottom:calc(12u) } */
|
||||
.card { margin-bottom: 24rpx; }
|
||||
|
||||
/* ─── 总分卡:H5 是 display:grid 两列 1fr + row-gap calc(15u),padding calc(16u) calc(14u)
|
||||
acss 用 flex-wrap 等价实现:两个 50% 列 + 整行 .card-dims ─── */
|
||||
.card-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 32rpx 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ring-box {
|
||||
|
||||
/* .sum-score { flex:none; border-right:1px solid #EFEFEF; flex-direction:column;
|
||||
align-items:center; justify-content:center } */
|
||||
.sum-score {
|
||||
flex: 0 0 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
border-right: 1px solid #EFEFEF;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ring-canvas {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
|
||||
/* .score-dial { position:relative; width:calc(108u); height:calc(108u) } */
|
||||
.score-dial { position: relative; width: 216rpx; height: 216rpx; }
|
||||
|
||||
/* H5 用 <svg viewBox="0 0 120 120"> 画环形:r=52、stroke-width=9
|
||||
→ 在 108px 画盘上外径 50.85、内径 42.75。
|
||||
acss 无 svg,改成等价的三层几何:轨道圆环 + conic-gradient 进度弧 + 圆头补点。 */
|
||||
.ring-track {
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
width: 203.4rpx; height: 203.4rpx;
|
||||
margin: -101.7rpx 0 0 -101.7rpx;
|
||||
border-radius: 50%;
|
||||
background: #EFEFEF;
|
||||
}
|
||||
/* 进度弧:report.js 按总分写入 conic-gradient(#F5F078 0 deg, 透明 deg)。
|
||||
conic-gradient 的 0deg 在正上方、顺时针递增,与 H5 SVG rotate(-90deg) 的进度方向一致。
|
||||
圆的直径必须与 .ring-track 一致(203.4rpx):若铺满 216rpx 画盘,
|
||||
环会变成 11.25rpx 厚、外径 54(H5 是 8.1rpx 厚、外径 50.85)。 */
|
||||
.ring-arc {
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
width: 203.4rpx; height: 203.4rpx;
|
||||
margin: -101.7rpx 0 0 -101.7rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
/* 圆形内孔(卡片底色为白):半径 42.75 → 直径 171rpx */
|
||||
.ring-hole {
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
width: 171rpx; height: 171rpx;
|
||||
margin: -85.5rpx 0 0 -85.5rpx;
|
||||
border-radius: 50%;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
/* stroke-linecap:round 的等价物:半径 8.1rpx 的圆点落在弧的两端 */
|
||||
.ring-cap {
|
||||
position: absolute;
|
||||
width: 16.2rpx; height: 16.2rpx;
|
||||
margin: -8.1rpx 0 0 -8.1rpx;
|
||||
border-radius: 50%;
|
||||
background: #F5F078;
|
||||
}
|
||||
/* .ring-center { position:absolute; inset:0; flex-direction:column; align-items:center;
|
||||
justify-content:center; gap:0 } */
|
||||
.ring-center {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
@@ -31,215 +109,525 @@ page { background: #EFEFEF; }
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* .stat-value { color:var(--ink); line-height:1; white-space:nowrap; display:flex; align-items:baseline } */
|
||||
.stat-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: #282828;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* .ring-center .stat-value b { font-size:40px } + #stat-score b { letter-spacing:0; display:block } */
|
||||
.stat-b { font-size: 40px; font-weight: 700; line-height: 1; letter-spacing: 0; }
|
||||
/* #stat-score i { margin-left:2px } + .ring-center .stat-value i { font-size:14px } */
|
||||
.stat-i { font-size: 14px; font-weight: 700; line-height: 1; margin-left: 2px; }
|
||||
/* .ring-center .stat-label / .sum-age > .stat-label { margin:0; font-size:12px; line-height:1.5; color:var(--ink) } */
|
||||
.stat-label { font-size: 12px; line-height: 1.5; color: #282828; margin: 0; }
|
||||
|
||||
/* .sum-age { flex:1; align-self:stretch; flex-direction:column; align-items:center;
|
||||
justify-content:center; gap:0; padding-left:calc(16u)(覆盖 26u) } */
|
||||
.sum-age {
|
||||
flex: 0 0 50%;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
}
|
||||
.stat-value { display: flex; align-items: baseline; }
|
||||
.stat-num {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #282828;
|
||||
}
|
||||
.stat-unit {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
.age-pill {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #282828;
|
||||
background: #F5F078;
|
||||
border-radius: 20rpx;
|
||||
padding: 4rpx 20rpx;
|
||||
}
|
||||
|
||||
/* ─── 三维度 ─── */
|
||||
.dims-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dim {
|
||||
width: 31%;
|
||||
/* .age-dial { width:calc(108u); height:auto; display:flex; align-items:center; justify-content:center } */
|
||||
.age-dial {
|
||||
position: relative;
|
||||
}
|
||||
.dim-name {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
}
|
||||
.dim-score { margin: 8rpx 0; }
|
||||
.dim-int {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.dim-dec {
|
||||
font-size: 28rpx;
|
||||
color: #868686;
|
||||
}
|
||||
.dim.worst .dim-int, .dim.worst .dim-dec { color: #D30C0C; }
|
||||
.dim-bar {
|
||||
height: 8rpx;
|
||||
background: #EFEFEF;
|
||||
border-radius: 4rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dim-bar-fill {
|
||||
height: 100%;
|
||||
background: #E5E5E5;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.dim.best .dim-bar-fill { background: #DBF6D6; }
|
||||
.dim.worst .dim-bar-fill { background: #F5C6C6; }
|
||||
.dim-flag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 20rpx;
|
||||
color: #D30C0C;
|
||||
}
|
||||
.dims-note { margin-top: 24rpx; }
|
||||
.dims-note-l1 {
|
||||
font-size: 26rpx;
|
||||
color: #868686;
|
||||
}
|
||||
.dims-focus {
|
||||
color: #282828;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.dims-note-l2 {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ─── 6 分类 Tab ─── */
|
||||
.detail-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2rpx solid #E5E5E5;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.dtab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #868686;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
.dtab.active {
|
||||
color: #282828;
|
||||
font-weight: 600;
|
||||
border-bottom: 4rpx solid #F5F078;
|
||||
}
|
||||
.ditem { margin-bottom: 32rpx; }
|
||||
.ditem-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.ditem-img {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
background: #EFEFEF;
|
||||
}
|
||||
.ditem-para {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ─── 降级标注 + 免责 ─── */
|
||||
.vm-notes {
|
||||
font-size: 22rpx;
|
||||
color: #868686;
|
||||
padding: 16rpx 32rpx 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.report-disclaimer {
|
||||
font-size: 22rpx;
|
||||
color: #868686;
|
||||
text-align: center;
|
||||
line-height: 1.8;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ─── 护理建议 / 精华 ─── */
|
||||
.care-item {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.care-summary {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.essence-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.essence-triggers {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* ─── 产品推荐 ─── */
|
||||
.prod-title {
|
||||
text-align: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
padding: 40rpx 0 8rpx;
|
||||
}
|
||||
.prod-title text { display: block; line-height: 1.5; }
|
||||
.prod-card {
|
||||
width: 216rpx;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
}
|
||||
.prod-info { flex: 1; padding-right: 24rpx; }
|
||||
.prod-badge { height: 40rpx; margin-bottom: 16rpx; }
|
||||
.prod-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
/* .age-dial .stat-age { margin-top:0; display:flex; align-items:baseline } */
|
||||
.stat-age { margin-top: 0; display: flex; align-items: baseline; }
|
||||
/* .age-dial .stat-age b { font-size:54px(覆盖 58px); display:block; line-height:1 }
|
||||
letter-spacing 继承 .stat-value b 的 calc(.5u) → 1rpx */
|
||||
.stat-age .stat-b { font-size: 54px; line-height: 1; letter-spacing: 1rpx; }
|
||||
/* .stat-age i { font-size:19px; margin-left:2px } */
|
||||
.stat-age .stat-i { font-size: 19px; margin-left: 2px; }
|
||||
/* H5 ui-20261009.css: .age-pill, .dims-note { display:none !important }
|
||||
ui-20261012 只恢复了 #dims-note,肌龄对比胶囊仍是隐藏态。
|
||||
ageNote 仍由 report-model 产出(接口不变),这里与 H5 一致:保留数据不展示。 */
|
||||
.age-pill { display: none; }
|
||||
|
||||
/* ─── 三维度(.card-dims 并进总分卡:H5 grid-column:1/-1;acss 用 width:100% + row-gap)
|
||||
.card-dims { padding:0(覆盖 calc(18u) calc(7u) calc(7u)) }
|
||||
.dim { flex:1; align-items:flex-start; padding:0 0 calc(4u) calc(14u) }
|
||||
.dim + .dim { border-left:1px solid #EFEFEF } */
|
||||
.card-dims { width: 100%; margin-top: 30rpx; }
|
||||
.dims-row { display: flex; }
|
||||
.dim {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0 0 8rpx 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.prod-sub {
|
||||
font-size: 22rpx;
|
||||
color: #868686;
|
||||
.dim + .dim { border-left: 1px solid #EFEFEF; }
|
||||
/* .dim-name { font-size:11px(覆盖 12px); color:var(--ink) }
|
||||
行高固定成 H5 实测行盒 14px:acss 是系统字体(H5 是 MiSans),
|
||||
若放任 normal 行高,三维度卡会整体高 1px 并向下累积。 */
|
||||
.dim-name { font-size: 11px; line-height: 14px; color: #282828; }
|
||||
/* .dim-score { margin-top:calc(4u); font-size:27px(覆盖 30px); line-height:1.1; font-weight:700 }
|
||||
.dim-score b { font-size:36px(覆盖 42px); font-weight:700 } */
|
||||
.dim-score {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 8rpx;
|
||||
font-size: 27px;
|
||||
font-weight: 700;
|
||||
color: #282828;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.prod-img {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
.dim-int { font-size: 36px; font-weight: 700; line-height: 1.1; }
|
||||
.dim-dec { font-size: 27px; font-weight: 700; line-height: 1.1; }
|
||||
/* .dim-score.dim-bad { color:var(--bad) } */
|
||||
.dim-bad { color: #D30C0C; }
|
||||
/* .dim-bar { margin-top:calc(6u); width:calc(69u); height:calc(6u);
|
||||
border-radius:calc(3u); background:#EFEFEF; overflow:hidden } */
|
||||
.dim-bar {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
width: 138rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #EFEFEF;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* .dim-bar i { height:100%; background:var(--yellow); border-radius:inherit } */
|
||||
.dim-fill { height: 100%; border-radius: 6rpx; background: #F5F078; }
|
||||
.dim.best .dim-fill { background: #D7EE6A; }
|
||||
.dim.worst .dim-fill { background: #F4B04F; }
|
||||
/* .dim-flag { visibility:hidden; margin-top:calc(6u); font-size:8px(覆盖 9px); color:var(--ink);
|
||||
background:#F8F4A6; border-radius:calc(8u); padding:calc(1u) calc(8u) }
|
||||
→ 非最低分维度用 visibility 保留占位,三列高度与 H5 一致 */
|
||||
.dim-flag {
|
||||
visibility: hidden;
|
||||
margin-top: 12rpx;
|
||||
font-size: 8px;
|
||||
line-height: 10px;
|
||||
color: #282828;
|
||||
background: #F8F4A6;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
padding: 2rpx 16rpx;
|
||||
}
|
||||
.dim.worst .dim-flag { visibility: visible; }
|
||||
|
||||
/* #dims-note { display:block !important; margin-top:12px; padding:10px 12px 10px 24px;
|
||||
background:#F2F2F2; border-radius:calc(6u); position:relative }
|
||||
#dims-note::before { left:12px; top:10px; bottom:10px; width:calc(3u); background:var(--yellow) } */
|
||||
.dims-note {
|
||||
position: relative;
|
||||
margin-top: 24rpx;
|
||||
background: #F2F2F2;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx 20rpx 48rpx;
|
||||
text-align: left;
|
||||
}
|
||||
.dims-note::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 24rpx;
|
||||
top: 20rpx;
|
||||
bottom: 20rpx;
|
||||
width: 6rpx;
|
||||
border-radius: 4rpx;
|
||||
background: #F5F078;
|
||||
}
|
||||
/* #dims-note .dims-note-l1 { font-size:12px; line-height:1.5; color:var(--para) }
|
||||
.dims-note-l1 b { font-weight:500; color:var(--ink); text-decoration:underline } */
|
||||
.dims-note-l1 { font-size: 12px; line-height: 1.5; color: #4A4A4A; }
|
||||
.dims-focus {
|
||||
font-weight: 500;
|
||||
color: #282828;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* #dims-note .dims-note-l2 { font-size:11px; line-height:1.5; margin-top:3px; color:var(--grey) } */
|
||||
.dims-note-l2 { margin-top: 6rpx; font-size: 11px; line-height: 1.5; color: #868686; }
|
||||
|
||||
/* ─── 维度标题(H5 h2.dimension-title,落在灰底上的卡外标题)
|
||||
.dimension-title { font-size:23px; font-weight:600; text-align:center;
|
||||
margin:calc(18u) 0(覆盖 24u/12u) },行高固定 30px = H5 实测行盒
|
||||
.dimension-stability-title { font-size:22.106597px; line-height:28.738542px } */
|
||||
.dimension-title {
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
color: #282828;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
margin: 36rpx 0;
|
||||
}
|
||||
.dimension-stability-title { font-size: 22.106597px; line-height: 28.738542px; }
|
||||
|
||||
/* ─── 维度详情卡(.card.card-detail)
|
||||
.card-detail { margin-top:0; padding:calc(4u) calc(39u) calc(19u); border-radius:calc(7u) } */
|
||||
.card-detail { margin-top: 0; padding: 8rpx 78rpx 38rpx; box-sizing: border-box; }
|
||||
|
||||
/* .detail-tabs { display:flex; border:none(覆盖 1px 底线) }
|
||||
H5 高度由 .dtab 的 padding-top 13px + .dtab-tx 26px 撑成 40px → acss 固定 80rpx 更稳 */
|
||||
.detail-tabs { display: flex; height: 80rpx; }
|
||||
/* .dtab { flex:1; padding:calc(13u) 0 0; text-align:center; font-weight:500; color:var(--ink) }
|
||||
容器 80rpx 内:上留白 28rpx(= H5 的 13px + 行盒基线偏移 1px),余下 52rpx 归 .dtab-tx;
|
||||
用 flex 起点对齐消掉 inline-block 的基线偏移,让黄色下划线与 H5 一样停在
|
||||
距标签卡顶 34px、底 41px 的位置。 */
|
||||
.dtab {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-top: 28rpx;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #282828;
|
||||
}
|
||||
.dtab.active { font-weight: 700; }
|
||||
/* .dtab-tx { position:relative; display:inline-block; font-size:12px;
|
||||
padding:0 calc(5u) calc(10u) } */
|
||||
.dtab-tx {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 0 10rpx 20rpx;
|
||||
}
|
||||
/* .dtab.active .dtab-tx::after { left:50%; transform:translateX(-50%); bottom:-1px;
|
||||
width:calc(76u); height:calc(7u);
|
||||
border-radius:calc(3.5u); background:var(--yellow) } */
|
||||
.dtab.active .dtab-tx::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -1px;
|
||||
width: 152rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 7rpx;
|
||||
background: #F5F078;
|
||||
}
|
||||
|
||||
.btn-more {
|
||||
margin: 40rpx 32rpx 0;
|
||||
/* .detail-viewport { display:block; overflow:hidden } / .detail-panels { display:flex } →
|
||||
acss 只渲染当前 Tab 的面板,视觉效果等价(H5 用 transform 横滑切换) */
|
||||
.detail-viewport { overflow: hidden; }
|
||||
.detail-panels { display: block; }
|
||||
/* .detail-panels .dpanel { display:block; flex:0 0 100%; padding-top:calc(10u) } */
|
||||
.dpanel { display: block; padding-top: 20rpx; }
|
||||
/* .ditem { display:block(覆盖 flex) } */
|
||||
.ditem { display: block; }
|
||||
|
||||
/* ─── 检测图框(.viz-frame + .contour-front-frame 的 90% 高裁切)
|
||||
.viz-frame { position:relative; width:calc(215u); height:calc(278u); margin:0 auto calc(12u);
|
||||
background:#f6f6f6; border-radius:calc(5u); overflow:hidden }
|
||||
.contour-front-frame { height:calc(250.2u); margin-bottom:calc(7.56u) }
|
||||
.contour-front-frame .ditem-img { height:calc(278u) } */
|
||||
.viz-frame {
|
||||
position: relative;
|
||||
width: 430rpx;
|
||||
height: 556rpx;
|
||||
margin: 0 auto 24rpx;
|
||||
background: #F6F6F6;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.detail-tabs { display: block; white-space: nowrap; }
|
||||
.detail-tabs-inner { display: flex; width: max-content; }
|
||||
.dtab { flex-shrink: 0; padding: 16rpx 20rpx; }
|
||||
.analysis-image { position: relative; width: 100%; overflow: hidden; border-radius: 16rpx; background: #202020; }
|
||||
.analysis-photo { display: block; width: 100%; }
|
||||
.dark-photo { opacity: .45; }
|
||||
.analysis-layer { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
|
||||
.mark-box { position: absolute; box-sizing: border-box; border: 2rpx solid; border-radius: 4rpx; }
|
||||
.side-views { display: flex; gap: 16rpx; margin: 24rpx 0; }
|
||||
.side-view { flex: 1; min-width: 0; font-size: 24rpx; line-height: 1.8; }
|
||||
.side-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.4); color: white; }
|
||||
.side-note { color: #777; }
|
||||
.detail-grade { color: #666; font-size: 24rpx; }
|
||||
.care-name { font-weight: 600; margin-bottom: 8rpx; }
|
||||
.dim-level { font-size: 22rpx; margin-top: 8rpx; color: #777; }
|
||||
.ring-box { width: 120px; height: 120px; }
|
||||
.contour-front-frame { height: 500.4rpx; margin-bottom: 15.12rpx; }
|
||||
/* 画框内部固定 430×556rpx 的「照片坐标系」:照片与每个 face_maps 图层共用同一套
|
||||
left/top/width/height(report.js 按 H5 vizFaceCrop 算出),保证叠加层与人脸严格对齐 */
|
||||
.viz-stage {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 430rpx; height: 556rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.viz-photo { position: absolute; display: block; }
|
||||
.viz-layer { position: absolute; display: block; }
|
||||
/* H5 vizCompose: ctx.globalCompositeOperation='multiply'(白底色沉/缺水/红区图层) */
|
||||
.viz-blend { mix-blend-mode: multiply; }
|
||||
/* H5 vizCompose({dim:.55}):照片先压暗再叠增强线(仅皱纹) */
|
||||
.viz-dim {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: #000000;
|
||||
opacity: .55;
|
||||
}
|
||||
/* H5 坐标标注 canvas:lineWidth = max(3, 画布宽/200),缩到 430rpx 画框后约 3rpx;
|
||||
rectangle 用 strokeRect,所以只描边、不填充。 */
|
||||
.viz-mark {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-width: 3rpx;
|
||||
}
|
||||
/* .viz-missing { position:absolute; inset:0; display:flex; align-items:center;
|
||||
justify-content:center; color:#888; font-size:12px } */
|
||||
.viz-missing {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #888888;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* .ditem-text { border-top:1px solid #eee; padding-top:calc(10u) }
|
||||
.ditem-para { font-size:12px(覆盖 10.1px); line-height:1.55; color:var(--para);
|
||||
font-weight:400; white-space:pre-line } */
|
||||
.ditem-text { border-top: 1px solid #EEEEEE; padding-top: 20rpx; }
|
||||
.ditem-para {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.55;
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* .viz-caption { color:#707070; font-size:11px; line-height:1.6; margin-top:10px } */
|
||||
.viz-caption {
|
||||
color: #707070;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
/* 稳韧力卡的标签/正文再小一号(.stability-card .dtab-tx / .ditem-para) */
|
||||
.stability-card .dtab-tx { font-size: 11.055903px; }
|
||||
.stability-card .ditem-para { font-size: 11.574074px; line-height: 14.467593px; }
|
||||
|
||||
/* ─── 侧脸照片(.contour-sides / .contour-side-photo / .wrinkle-side-photo)
|
||||
.contour-sides { width:calc(215u); margin:0 auto calc(12u);
|
||||
display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:calc(6.3u) }
|
||||
.contour-side-photo { width:100%; height:calc(92.25u); border-radius:calc(5u);
|
||||
background:#f6f6f6; overflow:hidden; position:relative }
|
||||
.contour-side-photo img { position:absolute; inset:0; width:100%; height:100%;
|
||||
object-fit:cover; transform:translateY(-25%) scale(2.8) } */
|
||||
.contour-sides {
|
||||
width: 430rpx;
|
||||
margin: 0 auto 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.contour-side {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: 208.7rpx;
|
||||
height: 184.5rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
background: #F6F6F6;
|
||||
}
|
||||
/* H5 grid: figure[data-view="right"] { grid-column:2 } → 只剩右脸时仍占第二列 */
|
||||
.contour-side.right { margin-left: auto; }
|
||||
.side-img {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 100%; height: 100%;
|
||||
transform: translateY(-25%) scale(2.8);
|
||||
transform-origin: center;
|
||||
}
|
||||
.side-dim {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: #000000;
|
||||
opacity: .55;
|
||||
}
|
||||
/* .wrinkle-loading { inset:0; background:rgba(0,0,0,.4); color:#fff; text-align:center;
|
||||
font-size:calc(10u); line-height:1.6 } */
|
||||
.side-loading {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* .wrinkle-layer-note { width:calc(215u); margin:0 auto calc(12u); color:#888;
|
||||
font-size:10px; line-height:1.4 } */
|
||||
.wrinkle-layer-note {
|
||||
width: 430rpx;
|
||||
margin: 0 auto 24rpx;
|
||||
color: #888888;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ─── 产品区大标题(.prod-title,落在灰底)
|
||||
ui-20261012 最终值:width:calc(206.597222u); margin:calc(18u) auto;
|
||||
transform:translateX(calc(.347222u)); font-size:22.106597px; line-height:28.738542px;
|
||||
font-weight:700; white-space:nowrap; letter-spacing:0 */
|
||||
.prod-title {
|
||||
width: 413.194444rpx;
|
||||
max-width: 100%;
|
||||
margin: 36rpx auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
transform: translateX(0.694444rpx);
|
||||
font-size: 22.106597px;
|
||||
line-height: 28.738542px;
|
||||
font-weight: 700;
|
||||
color: #282828;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ─── 日常护理 + 推荐产品(同一张白卡 .card.card-care.report-care-products)
|
||||
.report-care-products { width:calc(340.625u); margin-left:auto; margin-right:auto;
|
||||
padding:calc(21.180556u) calc(24.652778u) calc(7.986111u) } */
|
||||
.report-care-products {
|
||||
width: 681.25rpx;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 42.361112rpx 49.305556rpx 15.972222rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* .report-care-products .card-heading { font-size:13.888889px; line-height:19.444444px;
|
||||
font-weight:700; text-align:left;
|
||||
margin-bottom:calc(5.208333u) } */
|
||||
.card-heading {
|
||||
font-size: 13.888889px;
|
||||
line-height: 19.444444px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
margin-bottom: 10.416666rpx;
|
||||
}
|
||||
/* .report-care-products .care-item { font-size:11.574074px; line-height:14.467593px;
|
||||
color:var(--para); margin-bottom:calc(14.467593u) }
|
||||
.care-item:last-child { margin-bottom:0 } → acss 避开伪类,由 report.js 打 '.last' 标记 */
|
||||
.care-item {
|
||||
font-size: 11.574074px;
|
||||
line-height: 14.467593px;
|
||||
color: #4A4A4A;
|
||||
margin-bottom: 28.935186rpx;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.care-item.last { margin-bottom: 0; }
|
||||
/* .product-section-heading { position:relative; margin:calc(24.0625u) 0 calc(5.555556u);
|
||||
font-size:13.888889px; line-height:19.444444px;
|
||||
font-weight:700; text-align:left }
|
||||
::before { left:0; right:0; top:calc(-12.03125u); border-top:1px solid #DDD } */
|
||||
.product-section-heading {
|
||||
position: relative;
|
||||
margin: 48.125rpx 0 11.111112rpx;
|
||||
font-size: 13.888889px;
|
||||
line-height: 19.444444px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
color: #282828;
|
||||
}
|
||||
.product-section-heading::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -24.0625rpx;
|
||||
border-top: 1px solid #DDDDDD;
|
||||
}
|
||||
/* H5 #product-unavailable 也是 .care-item(文案见 report.axml,与 H5 index.html L274 一致) */
|
||||
.product-unavailable { margin-bottom: 0; }
|
||||
|
||||
/* .report-care-products .card-product { width:auto; margin:0 calc(-18.402778u);
|
||||
border-radius:calc(3.472222u);
|
||||
background:#F6F6F6; overflow:hidden } */
|
||||
.card-product {
|
||||
width: auto;
|
||||
margin: 0 -36.805556rpx;
|
||||
border-radius: 6.944444rpx;
|
||||
background: #F6F6F6;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* .report-care-products .prod-card { height:auto; aspect-ratio:945/511 }
|
||||
→ 卡片内容宽 582.64rpx + 左右各 36.8rpx = 656.25rpx,
|
||||
高 = 656.25 × 511 ÷ 945 = 354.84rpx(定值,不依赖 aspect-ratio 支持度) */
|
||||
.prod-card {
|
||||
position: relative;
|
||||
height: 354.84rpx;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
/* .report-care-products .prod-info { flex-basis:49.100529%; padding:0 } */
|
||||
.prod-info {
|
||||
flex: 0 0 49.100529%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/* .report-care-products #product-title { position:absolute; left:8.571429%; top:29.158513%;
|
||||
font-size:14.467593px; line-height:18.06279px;
|
||||
font-weight:700; white-space:nowrap } */
|
||||
.prod-card-title {
|
||||
position: absolute;
|
||||
left: 8.571429%;
|
||||
top: 29.158513%;
|
||||
margin: 0;
|
||||
font-size: 14.467593px;
|
||||
line-height: 18.06279px;
|
||||
font-weight: 700;
|
||||
color: #282828;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* .report-care-products .prod-name { left:8.677249%; top:41.09589%; font-size:12.013166px;
|
||||
line-height:15.070408px; font-weight:700; color:#989898;
|
||||
white-space:nowrap } */
|
||||
.prod-name {
|
||||
position: absolute;
|
||||
left: 8.677249%;
|
||||
top: 41.09589%;
|
||||
margin: 0;
|
||||
font-size: 12.013166px;
|
||||
line-height: 15.070408px;
|
||||
font-weight: 700;
|
||||
color: #989898;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* .report-care-products .prod-badge { left:8.783069%; top:56.751468%; width:10.687831%; margin:0 } */
|
||||
.prod-badge {
|
||||
position: absolute;
|
||||
left: 8.783069%;
|
||||
top: 56.751468%;
|
||||
width: 10.687831%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
}
|
||||
/* .report-care-products .prod-scroll { flex-basis:50.899471% }
|
||||
.prod-scroll img { width:100%; height:100%; aspect-ratio:auto; object-fit:fill } */
|
||||
.prod-scroll {
|
||||
flex: 0 0 50.899471%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.prod-img { display: block; width: 100%; height: 100%; }
|
||||
|
||||
/* ─── 免责声明(.report-care-products + .report-disclaimer)
|
||||
margin:calc(12.152778u) calc(5.208333u) calc(20u);
|
||||
font-size:7.233681px; line-height:10.127431px */
|
||||
.report-disclaimer {
|
||||
margin: 24.305556rpx 10.416666rpx 40rpx;
|
||||
font-size: 7.233681px;
|
||||
line-height: 10.127431px;
|
||||
color: #4A4A4A;
|
||||
}
|
||||
.report-disclaimer-line { display: block; }
|
||||
|
||||
/* ─── 小程序独有:历史报告入口(H5 报告页无此按钮)
|
||||
沿用 H5 style.css 里已有的 .btn-more { margin: calc(26u) auto 0 },保持既有跳转不丢失 ─── */
|
||||
.btn-more { margin: 52rpx auto 0; }
|
||||
|
||||
+155
-45
@@ -1,53 +1,163 @@
|
||||
<!-- ═════ 08 报告页 —— 对照 H5 index.html #s-report L174-291 ═════
|
||||
结构逐块对应:.report-title → .card.card-summary(环形评分 + 生物可视年龄 + 三维度)
|
||||
→ 三组 .dimension-title + .card.card-detail(紧塑力 2 视图 / 透亮感 3 视图 / 稳韧力 2 视图)
|
||||
→ .prod-title → .card.report-care-products(日常护理 + 推荐产品)→ .report-disclaimer。
|
||||
样式见 report.acss;数据仍由 utils/report-model.js 的 model()/layers()/marks() 产出。 -->
|
||||
<view class="report">
|
||||
<view class="report-title">您的肌肤状态表现</view>
|
||||
<view class="card card-summary">
|
||||
<view class="sum-score"><view class="ring-box">
|
||||
<canvas id="score-ring" width="120" height="120" class="ring-canvas" style="width:120px;height:120px;" />
|
||||
<view class="ring-center"><view class="stat-value"><text class="stat-num">{{total != null ? total : '--'}}</text><text class="stat-unit">分</text></view><view class="stat-label">综合评分</view></view>
|
||||
</view></view>
|
||||
<view class="sum-age"><view class="stat-label">肌肤年龄</view><view class="stat-value"><text class="stat-num">{{skinAge != null ? skinAge : '--'}}</text><text class="stat-unit">岁</text></view><view class="age-pill" a:if="{{ageNote}}">{{ageNote}}</view></view>
|
||||
</view>
|
||||
<view class="card card-dims">
|
||||
<view class="dims-row"><view class="dim {{item.rank}}" a:for="{{dims}}" a:key="key">
|
||||
<view class="dim-name">{{item.name}}</view><view class="dim-score"><text class="dim-int">{{item.int}}</text><text class="dim-dec" a:if="{{item.dec}}">.{{item.dec}}</text></view>
|
||||
<view class="dim-bar"><view class="dim-bar-fill" style="width:{{item.barWidth}}%;"></view></view><view class="dim-level">{{item.level}}</view>
|
||||
<view class="dim-flag" a:if="{{item.rank === 'worst'}}">优先关注</view>
|
||||
</view></view>
|
||||
<view class="dims-note" a:if="{{focusName}}"><view>近期优先关注:{{focusName}}</view><view>{{focusSlogan}}</view></view>
|
||||
</view>
|
||||
<view class="card card-detail" a:if="{{views.length}}">
|
||||
<scroll-view class="detail-tabs" scroll-x="{{true}}"><view class="detail-tabs-inner">
|
||||
<view class="dtab {{index === viewIndex ? 'active' : ''}}" a:for="{{views}}" a:key="key" data-tab="{{index}}" onTap="onTabChange">{{item.name}}</view>
|
||||
</view></scroll-view>
|
||||
<block a:for="{{views}}" a:key="key">
|
||||
<view class="ditem" a:if="{{index === viewIndex}}">
|
||||
<view class="ditem-title">{{item.name}} <text class="detail-grade">{{item.score}} · {{item.level}}</text></view>
|
||||
<view class="analysis-image" a:if="{{frontPhoto}}">
|
||||
<image class="analysis-photo {{item.dim ? 'dark-photo' : ''}}" src="{{frontPhoto}}" mode="widthFix" data-index="{{index}}" onLoad="onPhotoLoad" />
|
||||
<image class="analysis-layer" a:for="{{activeLayers}}" a:for-item="layer" a:key="*this" src="{{layer}}" mode="scaleToFill" style="opacity:{{item.alpha}};" />
|
||||
<view class="mark-box" a:for="{{item.marks}}" a:for-item="mark" a:key="id" style="left:{{mark.left}}%;top:{{mark.top}}%;width:{{mark.width}}%;height:{{mark.height}}%;border-color:{{mark.color}};"></view>
|
||||
<!-- H5 #s-report: background:#efefef url(bg-report-h5.webp) center top/100% auto。
|
||||
该 webp 为 1080×7512 的纯 #EFEFEF 单色图(全图仅一种颜色 239,239,239),
|
||||
与 app.acss 里 page 的底色完全等价;acss 侧不再引 32MB 解码量的空图,
|
||||
直接用 page 底色(否则 <image> 还会把页面滚动高度撑到 2608px)。 -->
|
||||
|
||||
<view class="report-body">
|
||||
<!-- H5 h1.report-title -->
|
||||
<view class="report-title">您的肌肤状态表现</view>
|
||||
|
||||
<!-- H5 .card.card-summary -->
|
||||
<view class="card card-summary">
|
||||
<view class="sum-score">
|
||||
<view class="score-dial">
|
||||
<!-- H5 <svg class="ring"><circle class="ring-bg">+<circle class="ring-fg">:
|
||||
环轨道 + conic-gradient 进度弧 + 圆头补点(acss 无 svg,几何等价) -->
|
||||
<view class="ring-track"></view>
|
||||
<view class="ring-arc" style="background-image: conic-gradient(#F5F078 0deg {{ringDeg}}deg, rgba(0,0,0,0) {{ringDeg}}deg);"></view>
|
||||
<view class="ring-cap" a:if="{{ringCap}}" style="left:108rpx;top:14.4rpx;"></view>
|
||||
<view class="ring-cap" a:if="{{ringCap}}" style="{{ringCapEnd}}"></view>
|
||||
<view class="ring-hole"></view>
|
||||
<view class="ring-center">
|
||||
<view class="stat-value"><text class="stat-b">{{total != null ? total : '--'}}</text><text class="stat-i">分</text></view>
|
||||
<view class="stat-label">综合评分</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="side-views" a:if="{{item.key === 'wrinkles'}}">
|
||||
<view class="side-view" a:for="{{sideViews}}" a:for-item="side" a:key="key">
|
||||
<view>{{side.title}}</view><view class="analysis-image" a:if="{{side.photo}}">
|
||||
<image class="analysis-photo dark-photo" src="{{side.photo}}" mode="widthFix" />
|
||||
<image a:if="{{side.layer}}" class="analysis-layer" src="{{side.layer}}" mode="scaleToFill" />
|
||||
<view class="side-loading" a:if="{{side.loading}}">标注生成中…</view>
|
||||
</view><view class="side-note" a:if="{{side.note}}">{{side.note}}</view>
|
||||
</view>
|
||||
<view class="sum-age">
|
||||
<!-- H5 .sum-age > .stat-label -->
|
||||
<view class="stat-label">生物可视年龄</view>
|
||||
<view class="age-dial">
|
||||
<view class="stat-value stat-age"><text class="stat-b">{{skinAge != null ? skinAge : '--'}}</text><text class="stat-i">岁</text></view>
|
||||
</view>
|
||||
<!-- H5 ui-20261009.css 把 .age-pill 置为 display:none !important(新版设计不再展示对比胶囊);
|
||||
report-model 仍产出 ageNote,这里与 H5 一样保留数据但不展示 -->
|
||||
<view class="age-pill" a:if="{{ageNote}}">{{ageNote}}</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .card-dims:grid-column 1/-1,占满总分卡整行 -->
|
||||
<view class="card-dims">
|
||||
<view class="dims-row">
|
||||
<view class="dim {{item.rank}}" a:for="{{dims}}" a:key="key">
|
||||
<view class="dim-name">{{item.name}}</view>
|
||||
<view class="dim-score {{item.rank === 'worst' ? 'dim-bad' : ''}}"><text class="dim-int">{{item.int}}</text><text class="dim-dec">.{{item.dec ? item.dec : '-'}}</text></view>
|
||||
<view class="dim-bar"><view class="dim-fill" style="width:{{item.barWidth}}%;"></view></view>
|
||||
<!-- H5 用 visibility 占位,非最低分维度也保留 12px 高度 -->
|
||||
<view class="dim-flag">优先关注</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- H5 #dims-note(ui-20261012 恢复了这块「近期优先关注」) -->
|
||||
<view class="dims-note" a:if="{{focusName}}">
|
||||
<view class="dims-note-l1">近期优先关注:<text class="dims-focus">{{focusName}}</text></view>
|
||||
<view class="dims-note-l2">{{focusSlogan}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 三组 <h2 class="dimension-title"> + <div class="card card-detail"> -->
|
||||
<block a:for="{{cards}}" a:for-item="card" a:for-index="ci" a:key="key">
|
||||
<view class="dimension-title {{card.titleCls}}">{{card.title}}</view>
|
||||
<view class="card card-detail {{card.cardCls}}">
|
||||
<!-- H5 .detail-tabs > button.dtab > span.dtab-tx -->
|
||||
<view class="detail-tabs">
|
||||
<view class="dtab {{card.index === vi ? 'active' : ''}}" a:for="{{card.views}}" a:for-item="tab" a:for-index="vi" a:key="key"
|
||||
data-card="{{card.key}}" data-index="{{vi}}" onTap="onTabTap">
|
||||
<text class="dtab-tx">{{tab.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-viewport">
|
||||
<view class="detail-panels">
|
||||
<block a:for="{{card.views}}" a:for-item="view" a:for-index="vi" a:key="key">
|
||||
<!-- H5 用 transform 横滑切换;acss 只渲染当前面板,视觉等价 -->
|
||||
<view class="dpanel" a:if="{{card.index === vi}}">
|
||||
<view class="ditem">
|
||||
<view class="viz-frame {{view.frameCls}}">
|
||||
<!-- hasVisual=false 时整块画框只留占位文案(H5 renderViz 的降级:轮廓/肤色/平滑度/痘痘
|
||||
必须有人脸照片,皱纹/水分/泛红可只挂 face_maps 图层) -->
|
||||
<view class="viz-stage" a:if="{{view.hasVisual}}">
|
||||
<!-- 照片与 face_maps 图层共用 view.photoStyle(人脸裁切窗口),保证叠加层位置一致 -->
|
||||
<image class="viz-photo" a:if="{{frontPhoto}}" src="{{frontPhoto}}" mode="{{view.fit}}" style="{{view.photoStyle}}" onLoad="onPhotoLoad" />
|
||||
<view class="viz-dim" a:if="{{view.dark}}"></view>
|
||||
<image class="viz-layer {{view.blend ? 'viz-blend' : ''}}" a:for="{{view.layers}}" a:for-item="layer" a:key="*this"
|
||||
src="{{layer}}" mode="scaleToFill" style="opacity:{{view.alpha}};{{view.photoStyle}}" />
|
||||
<!-- H5 canvas 里按矩形坐标描边的痘/斑标记 -->
|
||||
<view class="viz-mark" a:for="{{view.marks}}" a:for-item="mark" a:key="id"
|
||||
style="left:{{mark.left}}%;top:{{mark.top}}%;width:{{mark.width}}%;height:{{mark.height}}%;border-color:{{mark.color}};"></view>
|
||||
</view>
|
||||
<view class="viz-missing" a:if="{{!view.hasVisual}}">本次未获取{{view.name}}检测图</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .contour-sides(轮廓=原图;皱纹=原图压暗 + 侧脸皱纹图层 / 加载态) -->
|
||||
<view class="contour-sides {{view.dim ? 'wrinkle-sides' : ''}}" a:if="{{sidesShown && (view.dim || view.key === 'contour')}}">
|
||||
<block a:for="{{sides}}" a:for-item="side" a:key="key">
|
||||
<view class="contour-side {{side.key === 'right' ? 'right' : ''}}" a:if="{{side.photo}}">
|
||||
<image class="side-img" src="{{side.photo}}" mode="aspectFill" />
|
||||
<block a:if="{{view.dim}}">
|
||||
<view class="side-dim"></view>
|
||||
<image class="side-img" a:if="{{side.layer}}" src="{{side.layer}}" mode="aspectFill" />
|
||||
<view class="side-loading" a:if="{{side.loading}}"><view>皱纹标注</view><view>加载中…</view></view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="wrinkle-layer-note" a:if="{{view.dim && wrinkleNote}}">{{wrinkleNote}}</view>
|
||||
|
||||
<!-- H5 .ditem-text > p.ditem-para -->
|
||||
<view class="ditem-text">
|
||||
<view class="ditem-para" a:if="{{view.text}}">{{view.text}}</view>
|
||||
</view>
|
||||
<!-- H5 #smooth-map-note(仅平滑度面板) -->
|
||||
<view class="viz-caption" a:if="{{view.caption}}">{{view.caption}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ditem-para">{{item.text}}</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- H5 h2.prod-title(两行固定文案) -->
|
||||
<view class="prod-title">
|
||||
<view>感受高光时刻</view>
|
||||
<view>从日常的好状态开始</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .card.card-care.report-care-products -->
|
||||
<view class="card report-care-products">
|
||||
<view class="card-heading">日常护理</view>
|
||||
<view class="care-list">
|
||||
<view class="care-item {{ci === careAdvice.length - 1 ? 'last' : ''}}" a:for="{{careAdvice}}" a:for-item="care" a:for-index="ci" a:key="key">{{ci + 1}}、{{care.text}}</view>
|
||||
<view class="care-item last" a:if="{{!careAdvice.length}}">本次有效数据不足,暂无个性化护理建议。</view>
|
||||
</view>
|
||||
|
||||
<view class="product-section-heading">推荐产品</view>
|
||||
<view class="care-item product-unavailable" a:if="{{!product}}">本次未获取有效肌龄,暂无法推荐产品,建议重新检测。</view>
|
||||
<view class="card-product" a:if="{{product}}" onTap="onMore">
|
||||
<view class="prod-card">
|
||||
<view class="prod-info">
|
||||
<view class="prod-card-title">{{product.title}}</view>
|
||||
<view class="prod-name">{{product.name}}</view>
|
||||
<image class="prod-badge" src="{{productBadgeUrl}}" mode="widthFix" />
|
||||
</view>
|
||||
<view class="prod-scroll"><image class="prod-img" src="{{product.image}}" mode="scaleToFill" /></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 p.report-disclaimer(三行固定文案) -->
|
||||
<view class="report-disclaimer">
|
||||
<view class="report-disclaimer-line">*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。</view>
|
||||
<view class="report-disclaimer-line">*本次检测仅代表当前肌肤状态参考。</view>
|
||||
<view class="report-disclaimer-line">*仅根据您最近的测肤记录推荐。</view>
|
||||
</view>
|
||||
|
||||
<!-- 小程序独有入口(H5 报告页无此按钮):保留既有的历史报告跳转 -->
|
||||
<button class="btn-primary btn-more" onTap="onHistory">查看历史报告</button>
|
||||
</view>
|
||||
<view class="report-disclaimer">您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。本次检测仅供当前肌肤状态参考。</view>
|
||||
<view class="card card-care"><view class="card-heading">护理建议</view>
|
||||
<view class="care-item" a:for="{{careAdvice}}" a:key="key"><view class="care-name">{{item.name}}</view>{{item.text}}</view>
|
||||
<view class="care-summary">好状态,是内外同调的长期节奏。</view>
|
||||
</view>
|
||||
<view class="card card-product" a:if="{{product}}"><view class="prod-card">
|
||||
<view class="prod-info"><view class="prod-name">{{product.title}}</view><view class="prod-sub">{{product.name}}</view></view>
|
||||
<image class="prod-img" src="{{product.image}}" mode="aspectFit" />
|
||||
</view></view>
|
||||
<button class="btn-primary btn-more" onTap="onHistory">查看历史报告</button>
|
||||
</view>
|
||||
|
||||
+260
-35
@@ -1,67 +1,292 @@
|
||||
/* ═════ 08 报告页 —— 1:1 移植 H5 #s-report ═════
|
||||
数据来源不变:utils/report-model.js 的 model()/layers()/marks()(接口未改),
|
||||
本文件只做 H5 版面映射:
|
||||
1) 七个检测视角按 H5 的三张维度卡分组(紧塑力 2 / 透亮感 3 / 稳韧力 2);
|
||||
2) H5 SVG 环形评分 → conic-gradient 角度 + 圆头补点(acss 无 svg);
|
||||
3) H5 app.js vizFaceCrop/vizCompose 的人脸裁切窗口 → 照片与 face_maps 图层共用的
|
||||
「同一画框绝对定位」,保证叠加层位置与 H5 canvas 合成一致;
|
||||
4) 痘/斑标记坐标:接口像素坐标 → 画框百分比(含 H5 的最小框放大规则);
|
||||
5) 侧脸皱纹标注:payload.wrinkle_job + api.wrinkleViews,pending/unavailable 文案同 H5。 */
|
||||
var api = require('../../utils/api'), report = require('../../utils/report-model'), config = require('../../config');
|
||||
|
||||
/* H5 检测图框 .viz-frame:calc(215 * var(--u)) × calc(278 * var(--u)) → 430×556rpx(report.acss 同值) */
|
||||
var FRAME_W = 430, FRAME_H = 556, FRAME_RATIO = 215 / 278;
|
||||
/* H5 canvas 过小标记的最小框 = 画布宽 / 36 ≈ 6px;缩到画框后约 12rpx */
|
||||
var MIN_MARK = 12;
|
||||
/* H5 环形评分 viewBox 0 0 120 120、r=52 → 216rpx 画盘上半径 93.6rpx、圆心 108rpx */
|
||||
var RING_R = 93.6, RING_C = 108;
|
||||
/* 照片未拿到自然尺寸前的兜底:整框 + contain(H5 object-fit:contain 语义) */
|
||||
var DEFAULT_BOX = 'left:0;top:0;width:' + FRAME_W + 'rpx;height:' + FRAME_H + 'rpx;';
|
||||
|
||||
/* H5 报告页的三张维度卡(index.html L209-264:紧塑力/透亮感/稳韧力) */
|
||||
var GROUPS = [
|
||||
{ key: 'firm', title: '紧塑力', titleCls: '', cardCls: '', views: ['contour', 'wrinkles'] },
|
||||
{ key: 'vita', title: '透亮感', titleCls: '', cardCls: '', views: ['color', 'smooth', 'water'] },
|
||||
{ key: 'stab', title: '稳韧力', titleCls: 'dimension-stability-title', cardCls: 'stability-card', views: ['sens', 'acne'] }
|
||||
];
|
||||
/* H5 app.js VIZ_CAT:blend=正片叠底(白底色沉/缺水/红区图层);wrinkles 需把照片压暗 0.55 */
|
||||
var BLEND = { color: true, water: true, sens: true };
|
||||
var DARK = { wrinkles: .55 };
|
||||
/* H5 app.js vizCompose({contentDown}):轮廓/皱纹画框只露上部 90%,主体需下移 9% */
|
||||
var CONTENT_DOWN = { contour: .09, wrinkles: .09 };
|
||||
/* 有色沉图层时 H5 不再重复画黑色素框(VIZ_CAT.tone 的 filter) */
|
||||
var MELANIN_COLOR = '#9F641F';
|
||||
|
||||
function numeric(value) { return typeof value === 'number' && isFinite(value); }
|
||||
function px(value) { return (Math.round(value * 100) / 100) + 'rpx'; }
|
||||
function pct(ratio) { return Math.round(ratio * 10000) / 100; }
|
||||
|
||||
/* H5 SVG 环形评分:#ring-fg 的 stroke-dashoffset = 326.73 × (1 - 总分/100),顺时针、从正上方开始。
|
||||
acss 用 conic-gradient 画弧 + 两个圆点还原 stroke-linecap:round 的两端。 */
|
||||
function ringOf(total) {
|
||||
var value = total == null ? 0 : Math.max(0, Math.min(100, total));
|
||||
var angle = Math.PI * 2 * value / 100;
|
||||
var x = RING_C + RING_R * Math.sin(angle), y = RING_C - RING_R * Math.cos(angle);
|
||||
return { ringDeg: Math.round(360 * value / 100 * 100) / 100, ringCap: total != null && value > 0,
|
||||
ringCapEnd: 'left:' + px(x) + ';top:' + px(y) + ';' };
|
||||
}
|
||||
|
||||
/* H5 style.css .dim.best(最高分浅绿条)+ 既有 .dim.worst(report-model 已按原始 P 判定的优先关注) */
|
||||
function rankDims(dims) {
|
||||
var worstKey = '', bestKey = '', best = -1, scored = 0;
|
||||
dims.forEach(function (dim) {
|
||||
if (dim.rank === 'worst') worstKey = dim.key;
|
||||
if (typeof dim.int === 'string' && dim.int !== '-') scored++;
|
||||
});
|
||||
dims.forEach(function (dim) {
|
||||
if (dim.key === worstKey || dim.int === '-') return;
|
||||
if (dim.barWidth > best) { best = dim.barWidth; bestKey = dim.key; }
|
||||
});
|
||||
if (scored < 2) bestKey = '';
|
||||
return dims.map(function (dim) {
|
||||
var copy = Object.assign({}, dim);
|
||||
copy.rank = dim.key === worstKey ? 'worst' : dim.key === bestKey ? 'best' : '';
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
/* H5 app.js vizFaceCrop:人脸矩形 → 215:278 裁切窗口(矩形不可用时退回整幅) */
|
||||
function faceCrop(width, height, rectangle) {
|
||||
var full = { left: 0, top: 0, width: width, height: height, focused: false };
|
||||
if (!rectangle) return full;
|
||||
var keys = ['left', 'top', 'width', 'height'];
|
||||
for (var i = 0; i < keys.length; i++) if (!numeric(rectangle[keys[i]])) return full;
|
||||
if (rectangle.width <= 0 || rectangle.height <= 0 || rectangle.left < 0 || rectangle.top < 0 ||
|
||||
rectangle.left + rectangle.width > width || rectangle.top + rectangle.height > height) return full;
|
||||
var ch = Math.min(Math.max(rectangle.height * 1.55, rectangle.width * 1.18 / FRAME_RATIO), height, width / FRAME_RATIO);
|
||||
var cw = ch * FRAME_RATIO;
|
||||
if (cw < rectangle.width || ch < rectangle.height) return full;
|
||||
var left = Math.max(0, Math.min(width - cw, rectangle.left + rectangle.width / 2 - cw / 2));
|
||||
var top = Math.max(0, Math.min(height - ch, rectangle.top - rectangle.height * .3));
|
||||
if (top + ch < rectangle.top + rectangle.height) top = rectangle.top + rectangle.height - ch;
|
||||
return { left: left, top: top, width: cw, height: ch, focused: true };
|
||||
}
|
||||
/* H5 app.js vizShiftContentDown:把画面整体下移(源窗口上移) */
|
||||
function shiftContentDown(crop, rectangle, fraction) {
|
||||
if (!crop.focused || !rectangle || !fraction) return crop;
|
||||
var minimumTop = Math.max(0, rectangle.top + rectangle.height - crop.height);
|
||||
var copy = Object.assign({}, crop);
|
||||
copy.top = Math.max(minimumTop, crop.top - crop.height * fraction);
|
||||
return copy;
|
||||
}
|
||||
/* 画框(430×556rpx)内的照片/图层定位:left/top/width/height 均为 rpx,
|
||||
照片与图层共用同一套值,图层因此与照片逐像素贴合 */
|
||||
function frameBox(width, height, rectangle, contentDown) {
|
||||
var crop = faceCrop(width, height, rectangle), scale, left, top, boxW, boxH;
|
||||
if (crop.focused) {
|
||||
crop = shiftContentDown(crop, rectangle, contentDown);
|
||||
scale = FRAME_W / crop.width;
|
||||
left = -crop.left * scale; top = -crop.top * scale;
|
||||
boxW = width * scale; boxH = height * scale;
|
||||
} else {
|
||||
// 无可用人脸矩形:H5 canvas 保留整幅比例,再由 object-fit:contain 内嵌 → 这里等价等比内嵌
|
||||
scale = Math.min(FRAME_W / width, FRAME_H / height);
|
||||
boxW = width * scale; boxH = height * scale;
|
||||
left = (FRAME_W - boxW) / 2; top = (FRAME_H - boxH) / 2;
|
||||
}
|
||||
return { left: left, top: top, width: boxW, height: boxH, scale: scale,
|
||||
style: 'left:' + px(left) + ';top:' + px(top) + ';width:' + px(boxW) + ';height:' + px(boxH) + ';' };
|
||||
}
|
||||
/* 标记:接口像素坐标 → 画框(430×556rpx)百分比;过小标记按 H5 规则放大并保持中心。
|
||||
照片/图层在画框内的定位是 box.left = -裁切窗口左边界 × scale,
|
||||
所以原始像素 X 落在画框里的位置就是 box.left + X × scale。 */
|
||||
function placeMarks(marks, width, height, box) {
|
||||
var scale = box.scale;
|
||||
return (marks || []).map(function (mark) {
|
||||
var left = box.left + (mark.left / 100 * width) * scale;
|
||||
var top = box.top + (mark.top / 100 * height) * scale;
|
||||
var w = mark.width / 100 * width * scale, h = mark.height / 100 * height * scale;
|
||||
if (w < MIN_MARK) { left -= (MIN_MARK - w) / 2; w = MIN_MARK; }
|
||||
if (h < MIN_MARK) { top -= (MIN_MARK - h) / 2; h = MIN_MARK; }
|
||||
return { id: mark.id, color: mark.color, left: pct(left / FRAME_W), top: pct(top / FRAME_H),
|
||||
width: pct(w / FRAME_W), height: pct(h / FRAME_H) };
|
||||
});
|
||||
}
|
||||
/* H5 app.js renderViz 的 #smooth-map-note 文案(毛孔=蓝、黑头=红) */
|
||||
function smoothCaption(result) {
|
||||
var maps = (result && result.face_maps) || {}, notes = [];
|
||||
if (report.mapURL(maps.texture_enhanced_pores)) notes.push('蓝色为毛孔增强标记');
|
||||
if (report.mapURL(maps.texture_enhanced_blackheads)) notes.push('红色为黑头增强标记');
|
||||
return notes.length ? notes.join(',') + ';标记不代表严重程度,具体以检测计数和结果为准。'
|
||||
: '本次未获取毛孔、黑头增强图;此处仅展示拍摄照片,不绘制色斑、痣或痤疮框。';
|
||||
}
|
||||
|
||||
Page({
|
||||
data: { total: null, skinAge: null, ageNote: '', dims: [], focusName: '', focusSlogan: '',
|
||||
views: [], viewIndex: 0, frontPhoto: '', activeLayers: [], careAdvice: [], product: null, sideViews: [] },
|
||||
data: {
|
||||
// 图片统一走 api.assetUrl(与 H5 h5/assets/ 同相对路径)
|
||||
productBadgeUrl: api.assetUrl('img/ui-20261015/product-badge.png'),
|
||||
// report-model.model() 产出(接口不变)
|
||||
total: null, skinAge: null, ageNote: '', dims: [], focusName: '', focusSlogan: '',
|
||||
views: [], viewIndex: 0, frontPhoto: '', activeLayers: [], careAdvice: [], product: null,
|
||||
// 版面对齐 H5 所需的派生数据
|
||||
cards: [], sides: [], sidesShown: false, wrinkleNote: '',
|
||||
ringDeg: 0, ringCap: false, ringCapEnd: ''
|
||||
},
|
||||
onLoad: function () {
|
||||
this.alive = true; this.visible = true;
|
||||
this.ownerId = api.user() && api.user().id;
|
||||
var gd = getApp().globalData;
|
||||
if (!gd.payload || !gd.payload.result) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
||||
this.payload = gd.payload; this.photos = gd.photos || [];
|
||||
try { this.setData(report.model(this.payload, this.photos, gd.profile || {})); }
|
||||
catch (_) { my.showToast({ content: '报告生成失败,请重新检测', type: 'none' }); }
|
||||
var model;
|
||||
try { model = report.model(this.payload, this.photos, gd.profile || {}); }
|
||||
catch (_) { my.showToast({ content: '报告生成失败,请重新检测', type: 'none' }); return; }
|
||||
var result = this.payload.result, ring = ringOf(model.total);
|
||||
this.setData(Object.assign({}, model, {
|
||||
dims: rankDims(model.dims),
|
||||
cards: this.buildCards(model, result),
|
||||
ringDeg: ring.ringDeg, ringCap: ring.ringCap, ringCapEnd: ring.ringCapEnd
|
||||
}));
|
||||
this.renderSides(this.payload.wrinkle_views);
|
||||
},
|
||||
onReady: function () { this.ready = true; this.drawRing(this.data.total); this.loadSides(); },
|
||||
onReady: function () { this.ready = true; this.measurePhoto(); this.loadSides(); },
|
||||
onShow: function () {
|
||||
if (!api.user() || this.ownerId !== api.user().id) {
|
||||
this.onHide(); this.setData({ frontPhoto: '', views: [], sideViews: [], careAdvice: [], product: null });
|
||||
this.onHide();
|
||||
this.setData({ frontPhoto: '', views: [], cards: [], sides: [], careAdvice: [], product: null });
|
||||
my.redirectTo({ url: '/pages/splash/splash' }); return;
|
||||
}
|
||||
this.visible = true; if (this.ready) this.loadSides();
|
||||
},
|
||||
onHide: function () { this.visible = false; if (this.signal) this.signal.canceled = true; },
|
||||
onUnload: function () { this.onHide(); this.alive = false; this.photos = []; this.payload = null; },
|
||||
onTabChange: function (e) {
|
||||
var i = Number(e.currentTarget.dataset.tab), view = this.data.views[i];
|
||||
if (!view) return;
|
||||
this.setData({ viewIndex: i, activeLayers: report.layers(this.payload.result, view.key) });
|
||||
|
||||
/* ─── H5 三张维度卡的视图分组(七个检测视角各带图层/标记/框型) ─── */
|
||||
buildCards: function (model, result) {
|
||||
var byKey = {};
|
||||
model.views.forEach(function (view) { byKey[view.key] = view; });
|
||||
var hasPhoto = !!model.frontPhoto;
|
||||
return GROUPS.map(function (group) {
|
||||
var views = [];
|
||||
group.views.forEach(function (key) {
|
||||
var view = byKey[key];
|
||||
if (!view) return;
|
||||
var layers = result ? report.layers(result, key) : [];
|
||||
// 有水印图层即可出图;轮廓/肤色/平滑度/痘痘必须有人脸照片(H5 renderViz 的降级规则)
|
||||
var needsPhotoOnly = key === 'contour' || key === 'color' || key === 'smooth' || key === 'acne';
|
||||
views.push({
|
||||
key: key, name: view.name, text: view.text, dim: !!view.dim, alpha: view.alpha,
|
||||
layers: layers, marks: [],
|
||||
blend: !!BLEND[key], dark: DARK[key] || 0,
|
||||
frameCls: CONTENT_DOWN[key] ? 'contour-front-frame' : '',
|
||||
fit: 'aspectFit', photoStyle: DEFAULT_BOX,
|
||||
hasVisual: needsPhotoOnly ? hasPhoto : (hasPhoto || layers.length > 0),
|
||||
caption: key === 'smooth' ? smoothCaption(result) : ''
|
||||
});
|
||||
});
|
||||
return { key: group.key, title: group.title, titleCls: group.titleCls, cardCls: group.cardCls, index: 0, views: views };
|
||||
});
|
||||
},
|
||||
onPhotoLoad: function (e) {
|
||||
var i = Number(e.currentTarget.dataset.index), view = this.data.views[i];
|
||||
if (!view || !this.payload) return;
|
||||
var points = report.marks(this.payload.result, view.key, e.detail.width, e.detail.height);
|
||||
var patch = {}; patch['views[' + i + '].marks'] = points; this.setData(patch);
|
||||
|
||||
/* ─── 人脸裁切窗口 + 标记坐标:照片自然尺寸已知后计算一次 ─── */
|
||||
measurePhoto: function () {
|
||||
var that = this, src = this.data.frontPhoto;
|
||||
if (!src || this.geoDone || typeof my.getImageInfo !== 'function') return;
|
||||
my.getImageInfo({ src: src,
|
||||
success: function (info) { that.applyGeometry(info && info.width, info && info.height); },
|
||||
fail: function () {} });
|
||||
},
|
||||
// H5 img.onload 兜底(getImageInfo 不可用时也能拿到自然尺寸)
|
||||
onPhotoLoad: function (event) {
|
||||
var detail = (event && event.detail) || {};
|
||||
this.applyGeometry(detail.width, detail.height);
|
||||
},
|
||||
applyGeometry: function (width, height) {
|
||||
if (!(width > 0 && height > 0) || this.geoDone) return;
|
||||
this.geoDone = true;
|
||||
var that = this, result = this.payload && this.payload.result;
|
||||
var rectangle = this.payload && this.payload.face_rectangle;
|
||||
var patch = {};
|
||||
this.data.cards.forEach(function (card, ci) {
|
||||
card.views.forEach(function (view, vi) {
|
||||
var box = frameBox(width, height, rectangle, CONTENT_DOWN[view.key] || 0);
|
||||
var marks = report.marks(result, view.key, width, height);
|
||||
// H5:有色沉图层时不再画黑色素框(避免和 browning 图层重复)
|
||||
if (view.key === 'color' && view.layers && view.layers.length) {
|
||||
marks = marks.filter(function (mark) { return mark.color !== MELANIN_COLOR; });
|
||||
}
|
||||
var key = 'cards[' + ci + '].views[' + vi + ']';
|
||||
patch[key + '.photoStyle'] = box.style;
|
||||
patch[key + '.fit'] = 'scaleToFill';
|
||||
patch[key + '.marks'] = placeMarks(marks, width, height, box);
|
||||
});
|
||||
});
|
||||
if (that.alive !== false) this.setData(patch);
|
||||
},
|
||||
|
||||
/* ─── 视角切换:保持既有 viewIndex / activeLayers 语义 ─── */
|
||||
onTabTap: function (event) {
|
||||
var dataset = (event && event.currentTarget && event.currentTarget.dataset) || {};
|
||||
var key = dataset.card, index = Number(dataset.index), patch = {}, selected = null;
|
||||
this.data.cards.forEach(function (card, ci) {
|
||||
if (card.key !== key) return;
|
||||
patch['cards[' + ci + '].index'] = index;
|
||||
selected = card.views[index] || null;
|
||||
});
|
||||
if (!selected) return;
|
||||
var flat = this.data.viewIndex;
|
||||
this.data.views.forEach(function (view, i) { if (view.key === selected.key) flat = i; });
|
||||
patch.viewIndex = flat;
|
||||
patch.activeLayers = this.payload && this.payload.result ? report.layers(this.payload.result, selected.key) : [];
|
||||
this.setData(patch);
|
||||
},
|
||||
|
||||
/* ─── 侧脸照片 + 皱纹标注(H5 renderWrinkleSides / renderContourSides) ───
|
||||
pending:未标注一侧显示「皱纹标注加载中…」
|
||||
unavailable:显示「部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。」 */
|
||||
renderSides: function (views) {
|
||||
var that = this;
|
||||
this.setData({ sideViews: ['left', 'right'].map(function (key, i) {
|
||||
var item = views && views[key] || {}, layer = report.mapURL(item.result && item.result.face_maps && item.result.face_maps.texture_enhanced_lines);
|
||||
return { key: key, title: i ? '右脸皱纹' : '左脸皱纹', photo: that.photos[i + 1] || '', layer: layer,
|
||||
loading: item.status === 'pending', note: item.status === 'unavailable' ? '侧脸标注暂不可用' : '' };
|
||||
}) });
|
||||
var payload = this.payload || {}, photos = this.photos || [];
|
||||
views = views || payload.wrinkle_views || {};
|
||||
var pending = !!(payload.wrinkle_job && payload.wrinkle_job.status === 'pending');
|
||||
var sides = ['left', 'right'].map(function (key, i) {
|
||||
var item = views[key];
|
||||
// 只使用该侧照片自己的接口数据:wrinkle_views → payload.side(H5 同序回退)
|
||||
if (!item || !item.result) item = (payload.side || {})[key];
|
||||
var source = (item && (item.result || item)) || {};
|
||||
var layer = report.mapURL(source.face_maps && source.face_maps.texture_enhanced_lines);
|
||||
return { key: key, photo: photos[i + 1] || '', layer: layer, loading: pending && !layer };
|
||||
});
|
||||
var shown = sides.filter(function (side) { return !!side.photo; });
|
||||
var plain = pending ? false : shown.some(function (side) { return !side.layer; });
|
||||
this.setData({ sides: sides, sidesShown: shown.length > 0,
|
||||
wrinkleNote: plain ? '部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。' : '' });
|
||||
},
|
||||
loadSides: function () {
|
||||
var that = this, job = this.payload && this.payload.wrinkle_job;
|
||||
if (!job || job.status === 'complete' || !this.visible || this.loadingSides) return;
|
||||
this.loadingSides = true; this.signal = { canceled: false }; var signal = this.signal;
|
||||
if (!job || job.status !== 'pending' || !this.visible || this.loadingSides) return;
|
||||
this.loadingSides = true; this.signal = { canceled: false };
|
||||
var signal = this.signal;
|
||||
api.wrinkleViews(job.token, signal).then(function (data) {
|
||||
if (!that.alive || !that.visible || signal.canceled) return;
|
||||
that.payload.wrinkle_views = data.wrinkle_views; job.status = 'complete'; that.renderSides(data.wrinkle_views);
|
||||
that.payload.wrinkle_views = (data && data.wrinkle_views) || {};
|
||||
job.status = 'complete'; that.renderSides(that.payload.wrinkle_views);
|
||||
}).catch(function () {
|
||||
if (that.alive && that.visible && !signal.canceled) that.renderSides({ left: { status: 'unavailable' }, right: { status: 'unavailable' } });
|
||||
if (!that.alive || !that.visible || signal.canceled) return;
|
||||
job.status = 'unavailable'; that.renderSides(null);
|
||||
}).then(function () { that.loadingSides = false; if (signal.canceled && that.alive && that.visible) that.loadSides(); });
|
||||
},
|
||||
drawRing: function (total) {
|
||||
var ctx = my.createCanvasContext('score-ring'), ratio = total == null ? 0 : Math.max(0, Math.min(100, total)) / 100;
|
||||
ctx.clearRect(0, 0, 120, 120); ctx.setLineWidth(8);
|
||||
ctx.beginPath(); ctx.arc(60, 60, 52, 0, 2 * Math.PI); ctx.setStrokeStyle('#E5E5E5'); ctx.stroke();
|
||||
if (total != null) {
|
||||
ctx.beginPath(); ctx.arc(60, 60, 52, -Math.PI / 2, -Math.PI / 2 + ratio * 2 * Math.PI);
|
||||
ctx.setStrokeStyle('#F5F078'); ctx.setLineCap('round'); ctx.stroke();
|
||||
}
|
||||
ctx.draw();
|
||||
},
|
||||
|
||||
/* 推荐产品 → 淘宝商品详情(config.productItemIds 未配置时为 no-op,不伪造链接) */
|
||||
onMore: function () {
|
||||
var id = this.data.product && config.productItemIds[this.data.product.tier];
|
||||
if (!id) return;
|
||||
|
||||
+405
-96
@@ -1,145 +1,454 @@
|
||||
/* ═════ 启动页样式 ═════ */
|
||||
/* ═════ 01 启动页 —— 逐条对照 H5:
|
||||
style.css #s-splash(L83-102) .splash-bg .splash-slogan .splash-bottom .brand-row .btn-primary
|
||||
ui-20261009.css L8,L20-30,L88,L100-105
|
||||
ui-20261012.css L4,L9-30 .splash-slogan / .splash-slogan-copy / .splash-bottom / .brand-lockup
|
||||
换算:calc(N * var(--u)) → 2N rpx;固定 px 的字号/行高/圆角保持不变。 ═════ */
|
||||
|
||||
page { background: #101823; }
|
||||
|
||||
/* H5 .screen(#s-splash: position:absolute; inset:0; overflow-y:auto) */
|
||||
.splash {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #101823;
|
||||
}
|
||||
|
||||
/* ─── H5 .splash-bg ─── */
|
||||
.splash-bg {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.splash-slogan {
|
||||
position: absolute;
|
||||
top: 200rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.splash-slogan text {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.splash-bottom {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 0 64rpx 96rpx;
|
||||
/* ─── H5 .account-status(.account-status-home) ─── */
|
||||
.account-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.account-status-home {
|
||||
position: absolute;
|
||||
top: max(10px, env(safe-area-inset-top));
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
background: rgba(0, 0, 0, .533);
|
||||
color: #fff;
|
||||
}
|
||||
/* H5 .account-status span::before */
|
||||
.account-label { display: flex; align-items: center; }
|
||||
.account-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 7px;
|
||||
border-radius: 50%;
|
||||
background: #b7bac0;
|
||||
}
|
||||
.account-status-on .account-dot { background: #34ca7b; }
|
||||
.account-text { white-space: nowrap; }
|
||||
/* H5 .account-logout(ui-20261012 L4) */
|
||||
.account-logout {
|
||||
font-size: 11px;
|
||||
opacity: .8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ─── H5 .splash-slogan.splash-slogan-updated(ui-20261012 L9-20) ─── */
|
||||
.splash-slogan {
|
||||
position: absolute;
|
||||
left: calc(50% - 1.041667px);
|
||||
top: max(6.838833%, calc(env(safe-area-inset-top) + 48px));
|
||||
width: 249.305556px;
|
||||
height: 75.347222px;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 23px;
|
||||
font-weight: 400;
|
||||
line-height: 1.16;
|
||||
letter-spacing: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
/* H5 .splash-slogan-updated .splash-slogan-copy{width:100%;height:100%;clip-path:inset(68% 0 0)}
|
||||
→ 等价实现:只露出底部 32%(24.111111px),image 整体上移 68%(51.236111px) */
|
||||
.splash-slogan-copy-clip {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 51.236111px;
|
||||
height: 24.111111px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.splash-slogan-copy {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -51.236111px;
|
||||
width: 100%;
|
||||
height: 75.347222px;
|
||||
}
|
||||
.splash-slogan-brand,
|
||||
.splash-slogan-english {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 23px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
}
|
||||
.splash-slogan-brand { top: -4px; font-size: 28px; }
|
||||
.splash-slogan-english { top: 26.736111px; }
|
||||
|
||||
/* ─── H5 .splash-bottom(ui-20261012 L25) ─── */
|
||||
.splash-bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: max(5.882353%, env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14.583334rpx; /* calc(7.291667 * var(--u)) */
|
||||
padding-bottom: 0;
|
||||
}
|
||||
/* H5 .brand-row(ui-20261012 L26) */
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
gap: 0;
|
||||
margin-bottom: 20.833334rpx; /* calc(10.416667 * var(--u)) */
|
||||
}
|
||||
.brand-logo {
|
||||
height: 48rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.brand-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
.brand-lockup {
|
||||
width: 179.513889px;
|
||||
height: 20.486111px;
|
||||
}
|
||||
|
||||
/* ─── 隐私授权弹窗 ─── */
|
||||
.privacy-row {
|
||||
/* ─── H5 #s-splash .btn-primary(ui-20261012 L28) ─── */
|
||||
.splash .btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.checkbox {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid #E5E5E5;
|
||||
border-radius: 50%;
|
||||
margin-right: 16rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.checkbox.checked {
|
||||
justify-content: center;
|
||||
width: 362.5rpx; /* calc(181.25 * var(--u)) */
|
||||
height: 77.777778rpx; /* calc(38.888889 * var(--u)) */
|
||||
line-height: 77.777778rpx;
|
||||
border-radius: 13.888888rpx; /* calc(6.944444 * var(--u)) */
|
||||
background: #F5F078;
|
||||
border-color: #F5F078;
|
||||
}
|
||||
.checkbox.checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10rpx;
|
||||
top: 6rpx;
|
||||
width: 8rpx;
|
||||
height: 14rpx;
|
||||
border-right: 4rpx solid #282828;
|
||||
border-bottom: 4rpx solid #282828;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.privacy-text {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
}
|
||||
.privacy-link {
|
||||
color: #282828;
|
||||
font-size: 14.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1rpx;
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
transition: transform .12s ease;
|
||||
}
|
||||
.splash .btn-primary::after { border: none; }
|
||||
.btn-press { transform: scale(.96); }
|
||||
/* H5 .btn-history{background:#fff} */
|
||||
.splash .btn-history { background: #fff; }
|
||||
/* H5 .splash-button-label(ui-20261012 L29-30) */
|
||||
.splash-button-label {
|
||||
width: 55.902778px;
|
||||
height: 13.888889px;
|
||||
}
|
||||
.splash .btn-history .splash-button-label,
|
||||
.splash-button-label.btn-history-label {
|
||||
width: 56.597222px;
|
||||
height: 13.541667px;
|
||||
}
|
||||
.btn-busy { opacity: .6; }
|
||||
|
||||
/* ─── 弹层基类(H5 .overlay) ─── */
|
||||
.splash .overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
/* ═══ 隐私授权弹窗 H5 #p-privacy ═══ */
|
||||
/* H5 #p-privacy{background:linear-gradient(#0008,#0008),url(bg-splash-h5) center/cover} */
|
||||
.overlay-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
.overlay-dim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: rgba(0, 0, 0, .533);
|
||||
}
|
||||
/* H5 #p-privacy .popup(ui-20261012 L34-38) */
|
||||
.splash .popup {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 307.986111px;
|
||||
height: 313.194444px;
|
||||
min-height: 313.194444px;
|
||||
top: -42.881944px;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
border-radius: 8.680556px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
/* H5 .popup-close / #p-privacy .popup-close(L65-66) */
|
||||
.splash .popup-close {
|
||||
position: absolute;
|
||||
top: 10.055556px;
|
||||
right: 10.055556px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.splash .popup-close::after { border: none; }
|
||||
.popup-close-img {
|
||||
width: 17.708333px;
|
||||
height: 17.361111px;
|
||||
}
|
||||
/* H5 .popup-title(L39-44) */
|
||||
.splash .popup-title {
|
||||
position: absolute;
|
||||
left: 31.944444px;
|
||||
top: 35px;
|
||||
margin: 0;
|
||||
font-size: 23.148148px;
|
||||
line-height: 28.935185px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
color: #282828;
|
||||
white-space: nowrap;
|
||||
text-align: left; /* 覆盖 app.acss .popup-title{text-align:center} */
|
||||
}
|
||||
/* H5 .popup-body(L45-49) */
|
||||
.splash .popup-body {
|
||||
position: absolute;
|
||||
left: 31.944444px;
|
||||
top: 109.027778px;
|
||||
width: 243.75px;
|
||||
margin: 0;
|
||||
font-size: 11.574074px;
|
||||
line-height: 17.361111px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
color: #282828;
|
||||
}
|
||||
/* H5 .privacy-row / .checkbox / .privacy-text / .privacy-link(L53-56) */
|
||||
.splash .privacy-row {
|
||||
position: absolute;
|
||||
left: 29.861111px;
|
||||
top: 184.375px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10.069444px;
|
||||
}
|
||||
.splash .checkbox {
|
||||
width: 11.805556px;
|
||||
height: 12.152778px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #282828;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.splash .checkbox.checked::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
border-radius: 50%;
|
||||
background: #2C2C2C;
|
||||
}
|
||||
.splash .privacy-text {
|
||||
font-size: 10.127315px;
|
||||
line-height: 18.807870px;
|
||||
color: #757575;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.splash .privacy-link {
|
||||
color: #282828;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.popup-btn { width: 100%; }
|
||||
/* H5 #p-privacy .popup-btn(L57-63) —— 必须排在 .btn-primary 之后才能覆盖宽高 */
|
||||
.splash .popup-btn {
|
||||
position: absolute;
|
||||
left: 63.194444px;
|
||||
top: 239.930556px;
|
||||
margin: 0;
|
||||
width: 181.25px;
|
||||
height: 38.888889px;
|
||||
line-height: 38.888889px;
|
||||
font-size: 14.467593px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
border-radius: 6.944444px;
|
||||
}
|
||||
@media (max-height: 440px) {
|
||||
.splash .popup { top: 0; }
|
||||
}
|
||||
|
||||
/* ─── 隐私协议全文弹层 ─── */
|
||||
.overlay-policy { padding: 0; }
|
||||
/* ═══ 隐私政策全文 H5 #p-policy ═══ */
|
||||
.splash .overlay-policy { background: #F5F5F5; z-index: 60; }
|
||||
.policy-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
.policy-back {
|
||||
/* H5 .policy-float-back + .nav-back */
|
||||
.splash .policy-float-back {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
left: 24rpx;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 2;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
line-height: 56rpx;
|
||||
font-size: 56rpx;
|
||||
color: #282828;
|
||||
background: none;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.policy-back::after { border: none; }
|
||||
.policy-scroll {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border: 1px solid #E5E5E5;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, .12);
|
||||
box-sizing: border-box;
|
||||
padding: 96rpx 40rpx 64rpx;
|
||||
}
|
||||
.splash .policy-float-back::after { border: none; }
|
||||
/* H5 .policy-body + #p-policy .policy-body{padding-top:52px} */
|
||||
.policy-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 52px 20px 48px;
|
||||
background: #fff;
|
||||
}
|
||||
.policy-title {
|
||||
font-size: 36rpx;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 40rpx;
|
||||
color: #282828;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.policy-h2 {
|
||||
font-size: 30rpx;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 40rpx 0 16rpx;
|
||||
color: #282828;
|
||||
margin: 26px 0 10px;
|
||||
padding-left: 9px;
|
||||
border-left: 4px solid #F5F078;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.policy-p {
|
||||
font-size: 26rpx;
|
||||
font-size: 12.5px;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.9;
|
||||
margin-bottom: 10px;
|
||||
text-align: justify;
|
||||
}
|
||||
.policy-end {
|
||||
text-align: center;
|
||||
font-size: 12.5px;
|
||||
color: #868686;
|
||||
font-size: 24rpx;
|
||||
margin: 48rpx 0;
|
||||
line-height: 1.9;
|
||||
text-align: center;
|
||||
margin: 28px 0 10px;
|
||||
}
|
||||
|
||||
/* ─── H5 .toast ─── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 15%;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
max-width: 86%;
|
||||
background: rgba(20, 20, 20, .85);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
padding: 9px 18px;
|
||||
border-radius: 18px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all .25s ease;
|
||||
z-index: 99;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
|
||||
/* ═══ 开发版入口(H5 无;保持低调) ═══ */
|
||||
.dev-entries {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: max(10px, env(safe-area-inset-top));
|
||||
left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.dev-entry {
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, .42);
|
||||
}
|
||||
.diag-panel {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
left: 32rpx;
|
||||
right: 32rpx;
|
||||
top: 240rpx;
|
||||
bottom: 160rpx;
|
||||
background: rgba(6, 16, 26, .96);
|
||||
border: 1rpx solid rgba(120, 200, 255, .35);
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.diag-scroll { flex: 1; min-height: 0; }
|
||||
.diag-text {
|
||||
color: #9fd0ff;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.diag-close {
|
||||
margin-top: 16rpx;
|
||||
text-align: center;
|
||||
color: #0b1622;
|
||||
background: #ffd94d;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 0;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.account-links { display: flex; justify-content: center; gap: 40rpx; color: #fff; margin-top: 24rpx; font-size: 24rpx; }
|
||||
.splash-slogan { top: 10.7%; }
|
||||
.splash-slogan text { font-size: 44rpx; line-height: 1.4; }
|
||||
.brand-row { justify-content: center; }
|
||||
.brand-logo { width: 450rpx; height: 52rpx; margin: 0; }
|
||||
.camera-debug-entry { position: absolute; z-index: 2; top: 24rpx; right: 24rpx; padding: 16rpx 24rpx; border-radius: 16rpx; color: #fff; background: rgba(0,0,0,.65); font-size: 24rpx; }
|
||||
.mvp-debug-entry { top: 100rpx; }
|
||||
|
||||
@@ -1,49 +1,84 @@
|
||||
<!-- ═════ 01 启动页 ═════ -->
|
||||
<!-- ═════ 01 启动页 —— 对照 H5 index.html #s-splash(L43-58) + #p-privacy(L294-307) + #p-policy(L335-466) ═════ -->
|
||||
<view class="splash">
|
||||
<view class="camera-debug-entry" a:if="{{showCameraDebug}}" onTap="onCameraDebug">开发调试 · 相机验收</view>
|
||||
<view class="camera-debug-entry mvp-debug-entry" a:if="{{showMvpDebug}}" onTap="onMvpDebug">SAE MVP 连通性</view>
|
||||
<image class="splash-bg" src="{{bgUrl}}" mode="aspectFill" />
|
||||
<view class="splash-slogan">
|
||||
<text>1'me,</text>
|
||||
<text>FROM THE INSIDE OUT</text>
|
||||
<text>以我为始 自有表现</text>
|
||||
<!-- 开发版入口(H5 无此入口,保持现有实现,样式低调) -->
|
||||
<view class="dev-entries" a:if="{{showCameraDebug || showMvpDebug || showDiagnose}}">
|
||||
<view class="dev-entry" a:if="{{showCameraDebug}}" onTap="onCameraDebug">开发调试 · 相机验收</view>
|
||||
<view class="dev-entry" a:if="{{showMvpDebug}}" onTap="onMvpDebug">SAE MVP 连通性</view>
|
||||
<view class="dev-entry" a:if="{{showDiagnose}}" onTap="onDiagnose">权限诊断</view>
|
||||
<view class="dev-entry" a:if="{{showDiagnose}}" onTap="onCamTest">相机组件测试</view>
|
||||
</view>
|
||||
<!-- 诊断结果浮层:真机上唯一能看到相机能力/授权返回值的地方 -->
|
||||
<view class="diag-panel" a:if="{{diagText}}">
|
||||
<scroll-view class="diag-scroll" scroll-y>
|
||||
<text class="diag-text">{{diagText}}</text>
|
||||
</scroll-view>
|
||||
<view class="diag-close" onTap="onCloseDiagnose">关闭</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .splash-bg -->
|
||||
<image class="splash-bg" src="{{bgUrl}}" mode="aspectFill" />
|
||||
|
||||
<!-- H5 .account-status.account-status-home -->
|
||||
<view class="account-status account-status-home {{signedIn ? 'account-status-on' : ''}}">
|
||||
<view class="account-label">
|
||||
<view class="account-dot"></view>
|
||||
<text class="account-text">{{accountLabel}}</text>
|
||||
</view>
|
||||
<view class="account-logout" a:if="{{signedIn}}" onTap="onLogout">退出登录</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .splash-slogan.splash-slogan-updated
|
||||
.splash-slogan-copy 原为 object-fit + clip-path:inset(68% 0 0) 只露底部 32%(中文那行),
|
||||
acss 里改成等价的 overflow 裁切容器 + 上移 68% 的 image,像素结果一致 -->
|
||||
<view class="splash-slogan">
|
||||
<view class="splash-slogan-copy-clip">
|
||||
<image class="splash-slogan-copy" src="{{sloganUrl}}" mode="scaleToFill" />
|
||||
</view>
|
||||
<view class="splash-slogan-brand">1'me,</view>
|
||||
<view class="splash-slogan-english">FROM THE INSIDE OUT</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .splash-bottom:brand-lockup + 开始检测 + 我的报告 -->
|
||||
<view class="splash-bottom">
|
||||
<view class="brand-row">
|
||||
<image src="{{logoUrl}}" class="brand-logo" mode="heightFix" />
|
||||
</view>
|
||||
<button class="btn-primary splash-btn" onTap="onOpenPrivacy">开始检测</button>
|
||||
<view a:if="{{signedIn}}" class="account-links">
|
||||
<text onTap="onHistory">历史报告</text><text onTap="onLogout">退出登录</text>
|
||||
<image class="brand-lockup" src="{{logoUrl}}" mode="aspectFit" />
|
||||
</view>
|
||||
<button class="btn-primary" hover-class="btn-press" onTap="onOpenPrivacy">
|
||||
<image class="splash-button-label" src="{{startLabelUrl}}" mode="aspectFit" />
|
||||
</button>
|
||||
<button class="btn-primary btn-history" hover-class="btn-press" onTap="onHistory">
|
||||
<image class="splash-button-label btn-history-label" src="{{historyLabelUrl}}" mode="aspectFit" />
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- ═══ 弹窗:隐私授权 ═══ -->
|
||||
<view class="overlay" a:if="{{showPrivacy}}" onTap="onClosePrivacy">
|
||||
<view class="popup" catchTap="noop">
|
||||
<button class="popup-close" onTap="onClosePrivacy">✕</button>
|
||||
<!-- ═══ 弹窗:隐私授权(H5 #p-privacy) ═══ -->
|
||||
<view class="overlay overlay-privacy" a:if="{{showPrivacy}}" onTap="onClosePrivacy">
|
||||
<image class="overlay-bg" src="{{bgUrl}}" mode="aspectFill" />
|
||||
<view class="overlay-dim"></view>
|
||||
<view class="popup privacy-popup" catchTap="noop">
|
||||
<button class="popup-close" onTap="onClosePrivacy">
|
||||
<image class="popup-close-img" src="{{closeIconUrl}}" mode="aspectFit" />
|
||||
</button>
|
||||
<view class="popup-title">
|
||||
<text>欢迎体验1'me</text>
|
||||
<text>肌肤状态评估服务!</text>
|
||||
<view>欢迎体验1'me</view>
|
||||
<view>肌肤状态评估服务!</view>
|
||||
</view>
|
||||
<view class="popup-body">授权淘宝账号后,我们将采集您的面部影像和基础档案,用于生成肌肤分析报告,并在服务器保存检测记录及照片,供您查看最近5份报告。照片将交由 Face⁺⁺ 提供检测分析。</view>
|
||||
<view class="popup-body">我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。</view>
|
||||
<view class="privacy-row" onTap="onToggleAgree">
|
||||
<view class="checkbox {{agreed ? 'checked' : ''}}"></view>
|
||||
<view class="privacy-text">
|
||||
我已阅读并同意
|
||||
<text class="privacy-link" catchTap="onOpenPolicy">相关隐私协议</text>
|
||||
</view>
|
||||
<view class="privacy-text">我已阅读并同意相关<text class="privacy-link" catchTap="onOpenPolicy">隐私协议</text></view>
|
||||
</view>
|
||||
<button class="btn-primary popup-btn" loading="{{busy}}" disabled="{{busy}}" onTap="onAgreeContinue">授权登录并继续</button>
|
||||
<button class="btn-primary popup-btn {{busy ? 'btn-busy' : ''}}" hover-class="btn-press" onTap="onAgreeContinue">继续</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ═══ 弹层:隐私政策全文(Face++ 个人信息与隐私保护政策,与 H5 同文) ═══ -->
|
||||
<!-- ═══ 弹层:隐私政策全文(H5 #p-policy) ═══ -->
|
||||
<view class="overlay overlay-policy" a:if="{{showPolicy}}">
|
||||
<view class="policy-page">
|
||||
<button class="policy-back" onTap="onClosePolicy">‹</button>
|
||||
<scroll-view scroll-y class="policy-scroll">
|
||||
<button class="nav-back policy-float-back" onTap="onClosePolicy">
|
||||
<view class="chev left"></view>
|
||||
</button>
|
||||
<scroll-view scroll-y class="policy-body">
|
||||
<view class="policy-title">Face⁺⁺个人信息与隐私保护政策</view>
|
||||
|
||||
<view class="policy-h2">前言</view>
|
||||
@@ -80,4 +115,7 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- H5 .toast -->
|
||||
<view class="toast {{toastOn ? 'show' : ''}}">{{toastText}}</view>
|
||||
</view>
|
||||
|
||||
@@ -1,23 +1,51 @@
|
||||
/* ═════ 启动页:品牌底图 + Slogan + 隐私授权弹窗(对齐 H5 #s-splash + #p-privacy) ═════ */
|
||||
/* ═════ 启动页:品牌底图 + Slogan + 开始检测/我的报告 + 隐私授权弹窗
|
||||
对照 H5 index.html #s-splash(L43-58)、#p-privacy(L294-307)、#p-policy(L335-466)
|
||||
交互保持:登录(必须勾选协议)、restore、历史入口、退出登录、政策全文、开发版入口。 ═════ */
|
||||
var api = require('../../utils/api.js');
|
||||
var development = require('../../utils/development');
|
||||
var config = require('../../config');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bgUrl: api.assetUrl('img/bg-splash.jpg'), // 底图远程加载
|
||||
// 图片全部走 api.assetUrl(与 H5 h5/assets/ 同相对路径)
|
||||
bgUrl: api.assetUrl('img/ui-20261009/bg-splash-h5.jpg'),
|
||||
logoUrl: api.assetUrl('img/ui-20261012/brand-lockup.png'),
|
||||
sloganUrl: api.assetUrl('img/ui-20261012/splash-slogan.png'),
|
||||
startLabelUrl: api.assetUrl('img/ui-20261012/button-start-label.png'),
|
||||
historyLabelUrl: api.assetUrl('img/ui-20261012/button-history-label.png'),
|
||||
closeIconUrl: api.assetUrl('img/ui-20261009/icon-close.png'),
|
||||
|
||||
showPrivacy: false, // 隐私授权弹窗
|
||||
agreed: false, // 隐私协议勾选
|
||||
showPolicy: false, busy: false, signedIn: false, showCameraDebug: development.cameraDebugEnabled(),
|
||||
showMvpDebug: config.mvpLocal === true
|
||||
showPolicy: false,
|
||||
busy: false,
|
||||
signedIn: false,
|
||||
accountLabel: '尚未登录', // H5 renderAccountStatus()
|
||||
|
||||
toastText: '', toastOn: false,
|
||||
|
||||
showCameraDebug: development.cameraDebugEnabled(),
|
||||
showMvpDebug: config.mvpLocal === true || (config.transport === 'cloud' && config.environment !== 'production'),
|
||||
showDiagnose: config.environment !== 'production',
|
||||
diagText: ''
|
||||
},
|
||||
onShow: function () {
|
||||
var that = this;
|
||||
api.restore().then(function (user) { that.setData({ signedIn: !!user }); }).catch(function () {});
|
||||
api.restore().then(function (user) { that.setData({ signedIn: !!user, accountLabel: user ? '淘宝已登录' : '尚未登录' }); })
|
||||
.catch(function () {});
|
||||
},
|
||||
|
||||
// 「开始检测」→ 隐私授权弹窗
|
||||
// H5 .toast(自定义轻提示,替代系统 toast 以获得一致的视觉)
|
||||
toast: function (msg) {
|
||||
var that = this;
|
||||
this.setData({ toastText: msg, toastOn: true });
|
||||
clearTimeout(this.toastTimer);
|
||||
this.toastTimer = setTimeout(function () {
|
||||
that.setData({ toastOn: false });
|
||||
}, Math.max(1800, String(msg).length * 130));
|
||||
},
|
||||
|
||||
// 「开始检测」→ 隐私授权弹窗(H5 data-action="open-privacy")
|
||||
onOpenPrivacy: function () {
|
||||
if (api.user()) { my.navigateTo({ url: '/pages/profile/profile' }); return; }
|
||||
this.setData({ showPrivacy: true });
|
||||
@@ -26,7 +54,7 @@ Page({
|
||||
this.setData({ showPrivacy: false });
|
||||
},
|
||||
|
||||
// 勾选/取消隐私协议(点链接不触发,由 catchtap 分隔)
|
||||
// 勾选/取消隐私协议(点链接不触发,由 catchTap 分隔)
|
||||
onToggleAgree: function () {
|
||||
this.setData({ agreed: !this.data.agreed });
|
||||
},
|
||||
@@ -40,37 +68,52 @@ Page({
|
||||
|
||||
// 「继续」:必须已勾选协议
|
||||
onAgreeContinue: function () {
|
||||
var that = this;
|
||||
if (this.data.busy) return;
|
||||
if (!this.data.agreed) {
|
||||
my.showToast({ content: '请先阅读并同意隐私协议', type: 'none' });
|
||||
this.toast('请先阅读并同意隐私协议');
|
||||
return;
|
||||
}
|
||||
var that = this;
|
||||
this.setData({ busy: true });
|
||||
api.login(true).then(function () {
|
||||
that.setData({ showPrivacy: false, signedIn: true });
|
||||
that.setData({ showPrivacy: false, signedIn: true, accountLabel: '淘宝已登录' });
|
||||
my.navigateTo({ url: '/pages/profile/profile' });
|
||||
}).catch(function (err) {
|
||||
my.showToast({ content: err.message, type: 'none' });
|
||||
that.toast(err.message);
|
||||
}).then(function () { that.setData({ busy: false }); });
|
||||
},
|
||||
// H5 data-action="history" / 「我的报告」
|
||||
onHistory: function () {
|
||||
if (!api.user()) { this.onOpenPrivacy(); return; }
|
||||
my.navigateTo({ url: '/pages/history/history' });
|
||||
},
|
||||
onLogout: function () {
|
||||
var that = this;
|
||||
api.logout().then(function () { that.setData({ signedIn: false, agreed: false }); })
|
||||
.catch(function (err) { my.showToast({ content: err.message, type: 'none' }); });
|
||||
api.logout().then(function () {
|
||||
that.setData({ signedIn: false, agreed: false, accountLabel: '尚未登录' });
|
||||
}).catch(function (err) { that.toast(err.message); });
|
||||
},
|
||||
onCameraDebug: function () {
|
||||
if (!development.cameraDebugEnabled()) return;
|
||||
my.navigateTo({ url: '/pages/camera/camera?debug=camera' });
|
||||
},
|
||||
onMvpDebug: function () {
|
||||
if (!config.mvpLocal) return;
|
||||
if (!config.mvpLocal && !(config.transport === 'cloud' && config.environment !== 'production')) return;
|
||||
my.navigateTo({ url: '/pages/mvp/mvp' });
|
||||
},
|
||||
// 相机/相册权限诊断:相机页可能整页崩掉,所以在启动页也能点
|
||||
onDiagnose: function () {
|
||||
var that = this;
|
||||
this.setData({ diagText: '检测中…' });
|
||||
require('../../utils/permissions').diagnose().then(function (info) {
|
||||
that.setData({ diagText: info.join('\n') });
|
||||
}).catch(function (err) {
|
||||
that.setData({ diagText: 'diagnose error: ' + (err && err.message ? err.message : String(err)) });
|
||||
});
|
||||
},
|
||||
onCloseDiagnose: function () { this.setData({ diagText: '' }); },
|
||||
// 相机组件冒烟测试:单独一页只放 <camera>,用来判断淘宝真机是否支持该组件
|
||||
onCamTest: function () { my.navigateTo({ url: '/pages/camtest/camtest' }); },
|
||||
|
||||
noop: function () {} // 弹窗卡片内点击拦截冒泡用
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ test('offline camera debug skips identity/profile, never uploads or plays remote
|
||||
'../../utils/development': { cameraDebugEnabled: () => true },
|
||||
'../../utils/api': { user() { authReads++; return null; }, analyze() { backendCalls++; throw Error('debug must not upload'); } },
|
||||
'../../utils/voice': voice,
|
||||
'../../utils/permissions': { settings: async () => ({}), state: () => undefined, ensure: async () => {} },
|
||||
'../../utils/permissions': { settings: async () => ({}), state: () => undefined, ensure: async () => {}, cameraSupported: () => true },
|
||||
'../../utils/image': { compressToBase64: async (photo, p) => {
|
||||
p.setData({ compressWidth: 1280, compressHeight: 960 }); return 'data:image/jpeg;base64,/9j/';
|
||||
} }
|
||||
@@ -89,7 +89,8 @@ test('camera blocks duplicate shots/confirm, cancels late callbacks and submits
|
||||
const api = { user: () => ({ id: 'a' }), analyze: (photos) => { submitted++; ordered = photos; return new Promise(resolve => { resolver = resolve; }); } };
|
||||
const voice = { play() {}, stop() {}, VOICE: { step: [], shot: '', analyzing: '', fail: '' } };
|
||||
const camera = page('pages/camera/camera.js', { '../../utils/api': api, '../../utils/voice': voice,
|
||||
'../../utils/permissions': { settings: async () => ({ 'scope.camera': true }), state: (s, key) => s[key], ensure: async () => {} },
|
||||
'../../config': { environment: 'development', nativeCamera: true },
|
||||
'../../utils/permissions': { settings: async () => ({ 'scope.camera': true }), state: (s, key) => s[key], ensure: async () => {}, cameraSupported: () => true },
|
||||
'../../utils/image': { compressToBase64: async src => src + '-base64' } }, {
|
||||
getApp: () => app, my: { createCameraContext(id) { assert.equal(id, 'skin-camera'); return { takePhoto(opts) { captured = opts; calls++; } }; }, showToast() {}, redirectTo() { navigated++; } }
|
||||
});
|
||||
|
||||
@@ -5,26 +5,65 @@ function setup(my) {
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../utils/permissions.js'), 'utf8'), { module, Promise, Error, my });
|
||||
return module.exports;
|
||||
}
|
||||
test('camera and album authorization are separate from login; deny never loops and only explicit settings can restore', async () => {
|
||||
// 2026-11-01 真机实测(SDK 2.9.4):淘宝用带前缀的 scope.camera(裸名 camera 返回 5022 "Scope:camera信息不存在")。
|
||||
// 所以先试 scope.camera,失败再退到裸名,两者都失败才拒绝,且拒绝后不再重复弹授权。
|
||||
test('deny never loops: tries prefixed then bare once, then requires explicit settings', async () => {
|
||||
let granted = { 'scope.userInfo': true }, calls = [], openCalls = 0;
|
||||
const permissions = setup({
|
||||
getSetting({ success }) { success({ authSetting: granted }); },
|
||||
authorize({ scopes, success, fail }) { calls.push(scopes); if (scopes === 'scope.camera') fail(); else success(); },
|
||||
authorize({ scopes, success, fail }) { calls.push(scopes); fail({ error: 11, errorMessage: 'no such scope' }); },
|
||||
openSetting({ success }) { openCalls++; granted['scope.camera'] = true; success({ authSetting: granted }); }
|
||||
});
|
||||
await assert.rejects(permissions.ensure('scope.camera'), e => e.permission === 'scope.camera');
|
||||
await assert.rejects(permissions.ensure('scope.camera'), e => e.permission === 'scope.camera');
|
||||
assert.deepEqual(calls, ['scope.camera']); assert.equal(openCalls, 0);
|
||||
await permissions.ensure('scope.album'); assert.deepEqual(calls, ['scope.camera', 'scope.album']);
|
||||
assert.deepEqual(calls, ['scope.camera', 'camera'], '第二次不再尝试授权');
|
||||
assert.equal(openCalls, 0);
|
||||
await permissions.open(); await permissions.ensure('scope.camera');
|
||||
assert.equal(calls.length, 2); assert.equal(openCalls, 1);
|
||||
assert.equal(calls.length, 2, '授权设置里开启后不再调用 authorize');
|
||||
assert.equal(openCalls, 1);
|
||||
});
|
||||
test('existing denial does not reauthorize; short-form settings keys work and unknown scope requests on demand', async () => {
|
||||
|
||||
test('prefixed scope is used first and succeeds without falling back', async () => {
|
||||
let calls = [];
|
||||
const permissions = setup({
|
||||
getSetting({ success }) { success({ authSetting: {} }); },
|
||||
authorize({ scopes, success }) { calls.push(scopes); success(); }
|
||||
});
|
||||
await permissions.ensure('scope.camera');
|
||||
assert.deepEqual(calls, ['scope.camera']);
|
||||
await permissions.ensure('scope.album');
|
||||
assert.deepEqual(calls, ['scope.camera', 'scope.album']);
|
||||
});
|
||||
|
||||
test('existing denial does not reauthorize; short-form settings keys work', async () => {
|
||||
let calls = 0;
|
||||
const permissions = setup({ getSetting({ success }) { success({ authSetting: { camera: false, album: true } }); },
|
||||
authorize() { calls++; } });
|
||||
await assert.rejects(permissions.ensure('scope.camera')); await permissions.ensure('scope.album'); assert.equal(calls, 0);
|
||||
const first = setup({ getSetting({ success }) { success({ authSetting: {} }); },
|
||||
authorize({ scopes, success }) { assert.equal(scopes, 'scope.camera'); calls++; success(); } });
|
||||
await first.ensure('scope.camera'); assert.equal(calls, 1);
|
||||
const permissions = setup({
|
||||
getSetting({ success }) { success({ authSetting: { camera: false, album: true } }); },
|
||||
authorize() { calls++; }
|
||||
});
|
||||
await assert.rejects(permissions.ensure('scope.camera'));
|
||||
await permissions.ensure('scope.album');
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test('diagnose reports capability and authorization outcomes', async () => {
|
||||
const permissions = setup({
|
||||
SDKVersion: '2.9.4',
|
||||
canIUse() { return true; },
|
||||
getSetting({ success }) { success({ authSetting: { 'scope.camera': true } }); },
|
||||
authorize({ scopes, success, fail }) { scopes === 'scope.camera' ? success() : fail({ errorCode: '5022' }); },
|
||||
createCameraContext() {}, chooseImage() {}, getImageInfo() {}, createCanvasContext() {}, openSetting() {}
|
||||
});
|
||||
const info = await permissions.diagnose();
|
||||
const text = info.join('\n');
|
||||
assert.match(text, /SDKVersion=2\.9\.4/);
|
||||
assert.match(text, /createCameraContext: typeof=function canIUse=yes/);
|
||||
assert.match(text, /authSetting=\{"scope\.camera":true\}/);
|
||||
assert.match(text, /authorize\(scope\.camera\)=ok/);
|
||||
});
|
||||
|
||||
test('cameraSupported requires the native camera API and canIUse', () => {
|
||||
assert.equal(setup({ canIUse: () => true }).cameraSupported(), false, '没有 createCameraContext 就不支持');
|
||||
assert.equal(setup({ canIUse: () => true, createCameraContext() {} }).cameraSupported(), true);
|
||||
assert.equal(setup({ canIUse: (api) => api !== 'camera', createCameraContext() {} }).cameraSupported(), false);
|
||||
});
|
||||
|
||||
@@ -19,3 +19,19 @@ test('cloud transport initializes once, preserves subpath/auth, caps timeout and
|
||||
config.transport = 'direct';
|
||||
await assert.rejects(transport.request('/api/taobao/me', {}), /仅用于本机/);
|
||||
});
|
||||
test('聚石塔 tower transport omits domain and does not require HTTPS', async () => {
|
||||
const config = { transport: 'cloud', cloudType: 'tower', baseUrl: 'http://127.0.0.1:8080', cloudAppId: '', cloudEnv: 'test', basePath: '' };
|
||||
let options;
|
||||
const cloud = { init: async ({ env }) => { assert.equal(env, 'test'); return true; },
|
||||
application: { httpRequest: async input => { options = input; return { success: true, data: { ok: true } }; } } };
|
||||
const module = { exports: {} };
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../utils/transport.js'), 'utf8'), {
|
||||
module, Promise, require(name) { return name === '../config' ? config : { default: cloud }; }
|
||||
});
|
||||
const transport = module.exports;
|
||||
const data = await transport.request('/api/taobao/ping', {});
|
||||
assert.equal(data.ok, true);
|
||||
assert.equal(options.path, '/api/taobao/ping');
|
||||
assert.equal(options.exts.domain, undefined);
|
||||
assert.equal(options.exts.cloudAppId, undefined);
|
||||
});
|
||||
|
||||
+81
-10
@@ -1,29 +1,74 @@
|
||||
// Taobao scopes are independent: userInfo login does not grant camera/album.
|
||||
//
|
||||
// 平台差异(2026-11-01 真机实测,SDKVersion=2.9.4):
|
||||
// my.getSetting 返回 {"scope.userInfo":true,"scope.camera":true}
|
||||
// my.authorize({scopes:'camera'}) → 失败 {"errorCode":"5022","errorMessage":"Scope:camera信息不存在"}
|
||||
// my.authorize({scopes:'scope.camera'}) → 成功
|
||||
// 结论:淘宝用带前缀的 scope.camera / scope.album(和微信一致),裸名不存在。
|
||||
// 这里优先用带前缀的写法,失败再退回裸名(兼容其它淘宝/SDK 版本)。
|
||||
var denied = {};
|
||||
var lastError = null;
|
||||
|
||||
function bare(scope) { return scope.replace(/^scope\./, ''); }
|
||||
function prefixed(scope) { return /^scope\./.test(scope) ? scope : 'scope.' + scope; }
|
||||
|
||||
function state(settings, scope) {
|
||||
settings = settings || {};
|
||||
var key = scope.replace(/^scope\./, '');
|
||||
var key = bare(scope);
|
||||
return typeof settings[scope] === 'boolean' ? settings[scope] : settings[key];
|
||||
}
|
||||
function settings() {
|
||||
return new Promise(function (resolve) {
|
||||
if (typeof my.getSetting !== 'function') { resolve({}); return; }
|
||||
my.getSetting({ success: function (res) { resolve(res.authSetting || {}); }, fail: function () { resolve({}); } });
|
||||
try {
|
||||
my.getSetting({
|
||||
success: function (res) { resolve((res && res.authSetting) || {}); },
|
||||
fail: function () { resolve({}); }
|
||||
});
|
||||
} catch (e) { resolve({}); } // API 不存在/同步抛错:当作空设置,走后续兜底
|
||||
});
|
||||
}
|
||||
|
||||
// 平台是否具备内置相机能力(淘宝部分版本没有 <camera> / createCameraContext)
|
||||
function cameraSupported() {
|
||||
try {
|
||||
if (typeof my.createCameraContext !== 'function') return false;
|
||||
if (typeof my.canIUse === 'function') return !!my.canIUse('camera') && !!my.canIUse('createCameraContext');
|
||||
return true;
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
function describe(err) {
|
||||
if (err === undefined || err === null) return 'null';
|
||||
try { return JSON.stringify(err).slice(0, 200); } catch (e) { return String(err).slice(0, 200); }
|
||||
}
|
||||
function rejected(scope) {
|
||||
var err = new Error(scope === 'scope.camera' ? '相机权限未开启,可在授权设置中开启,或使用相册' : '相册权限未开启,请在授权设置中开启');
|
||||
err.permission = scope; return err;
|
||||
var err = new Error(scope === 'scope.camera'
|
||||
? '相机权限未开启:请点「授权设置」开启,或直接用系统相机/相册'
|
||||
: '相册权限未开启,请在授权设置中开启');
|
||||
err.permission = scope; err.detail = lastError;
|
||||
return err;
|
||||
}
|
||||
// 授权:先按支付宝/淘宝的裸名字(camera),失败且不是"用户拒绝"时再试微信式(scope.camera)
|
||||
function authorizeOnce(scopes) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
my.authorize({ scopes: scopes,
|
||||
success: function (res) { resolve({ scopes: scopes, ok: true, res: res }); },
|
||||
fail: function (err) { reject({ scopes: scopes, ok: false, err: err }); } });
|
||||
});
|
||||
}
|
||||
function ensure(scope) {
|
||||
return settings().then(function (current) {
|
||||
if (state(current, scope) === true) { delete denied[scope]; return; }
|
||||
if (state(current, scope) === false || denied[scope]) throw rejected(scope);
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (typeof my.authorize !== 'function') { reject(new Error('当前淘宝版本不支持授权,请升级后重试')); return; }
|
||||
my.authorize({ scopes: scope, success: function () { delete denied[scope]; resolve(); },
|
||||
fail: function () { denied[scope] = true; reject(rejected(scope)); } });
|
||||
});
|
||||
if (typeof my.authorize !== 'function') throw new Error('当前淘宝版本不支持授权,请升级后重试');
|
||||
return authorizeOnce(prefixed(scope)).catch(function (first) {
|
||||
lastError = describe(first.err);
|
||||
return authorizeOnce(bare(scope)).catch(function (second) {
|
||||
lastError = describe(first.err) + ' / ' + describe(second.err);
|
||||
denied[scope] = true;
|
||||
throw rejected(scope);
|
||||
});
|
||||
}).then(function () { delete denied[scope]; });
|
||||
});
|
||||
}
|
||||
function open() {
|
||||
@@ -36,4 +81,30 @@ function open() {
|
||||
}, fail: function () { reject(new Error('无法打开授权设置,请从小程序右上角进入')); } });
|
||||
});
|
||||
}
|
||||
module.exports = { ensure: ensure, settings: settings, state: state, open: open };
|
||||
|
||||
// 权限诊断:把真机上的真实能力/返回值打出来,供页面展示与截图
|
||||
function diagnose() {
|
||||
var info = [];
|
||||
try { info.push('SDKVersion=' + (my.SDKVersion || '(none)')); } catch (e) { info.push('SDKVersion=error'); }
|
||||
['camera', 'createCameraContext', 'chooseImage', 'authorize', 'getSetting', 'openSetting', 'getImageInfo', 'createCanvasContext'].forEach(function (api) {
|
||||
var use = 'n/a';
|
||||
try { use = typeof my.canIUse === 'function' ? (my.canIUse(api) ? 'yes' : 'no') : 'no-canIUse'; } catch (e) { use = 'canIUse-err'; }
|
||||
info.push(api + ': typeof=' + (typeof my[api]) + ' canIUse=' + use);
|
||||
});
|
||||
return settings().then(function (current) {
|
||||
info.push('authSetting=' + describe(current));
|
||||
if (typeof my.authorize !== 'function') { info.push('authorize: 无此 API'); return info; }
|
||||
return authorizeOnce(prefixed('camera')).then(function (r) {
|
||||
info.push('authorize(scope.camera)=ok ' + describe(r.res));
|
||||
}, function (r) {
|
||||
info.push('authorize(scope.camera)=fail ' + describe(r.err));
|
||||
return authorizeOnce(bare('camera')).then(function (r2) {
|
||||
info.push('authorize(camera)=ok ' + describe(r2.res));
|
||||
}, function (r2) {
|
||||
info.push('authorize(camera)=fail ' + describe(r2.err));
|
||||
});
|
||||
}).then(function () { return info; });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { ensure: ensure, settings: settings, state: state, open: open, diagnose: diagnose, bare: bare, cameraSupported: cameraSupported };
|
||||
|
||||
@@ -35,10 +35,16 @@ function request(path, body, token) {
|
||||
}, fail: function () { reject(new Error('网络异常,请稍后重试')); } });
|
||||
});
|
||||
}
|
||||
if (!config.cloudAppId || !/^https:\/\//.test(config.baseUrl)) return Promise.reject(new Error('请先配置淘宝云应用和 HTTPS 服务地址'));
|
||||
var cloudType = config.cloudType || 'empty';
|
||||
if (cloudType === 'empty' && (!config.cloudAppId || !/^https:\/\//.test(config.baseUrl))) {
|
||||
return Promise.reject(new Error('请先配置淘宝云应用和 HTTPS 服务地址'));
|
||||
}
|
||||
return initCloud().then(function (cloud) {
|
||||
var exts = { timeout: 10000 };
|
||||
if (config.cloudAppId) exts.cloudAppId = config.cloudAppId;
|
||||
if (cloudType === 'empty') exts.domain = config.baseUrl;
|
||||
return cloud.application.httpRequest({ path: config.basePath + path, method: 'POST', headers: headers,
|
||||
params: {}, body: body, exts: { cloudAppId: config.cloudAppId, domain: config.baseUrl, timeout: 10000 } });
|
||||
params: {}, body: body, exts: exts });
|
||||
}).then(unwrap);
|
||||
}
|
||||
module.exports = { request: request, unwrap: unwrap };
|
||||
|
||||
+11
-3
@@ -47,9 +47,17 @@ function start(id, kind) {
|
||||
lastAt[id] = Date.now();
|
||||
cur = { id: id, kind: kind, token: {} };
|
||||
var token = cur.token;
|
||||
audio.stop();
|
||||
audio.src = api.assetUrl('voice/' + id + '.mp3');
|
||||
audio.play();
|
||||
try {
|
||||
audio.stop();
|
||||
audio.src = api.assetUrl('voice/' + id + '.mp3');
|
||||
audio.play();
|
||||
} catch (_) {
|
||||
// 资源域名不可用、平台拒绝非 HTTPS src 等情况:静默跳过。
|
||||
// 语音只是辅助,绝不能让异常冒泡成页面崩溃(真机曾整页白屏)。
|
||||
cur = null;
|
||||
drain();
|
||||
return;
|
||||
}
|
||||
// 播放失败/结束时 cur 清理放在 drain 前,避免排队卡死
|
||||
cur.token = token;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user