Compare commits
No commits in common. "main-v2" and "main" have entirely different histories.
@ -35,9 +35,6 @@ dependencies {
|
|||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
implementation "androidx.media3:media3-transformer:1.10.0"
|
|
||||||
implementation "androidx.media3:media3-effect:1.10.0"
|
|
||||||
implementation "androidx.media3:media3-common:1.10.0"
|
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import com.getcapacitor.BridgeActivity;
|
|||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
registerPlugin(VideoCompressPlugin.class);
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Allow autoplay media without a user gesture (Android WebView).
|
// Allow autoplay media without a user gesture (Android WebView).
|
||||||
|
|||||||
@ -1,196 +0,0 @@
|
|||||||
package io.ionic.starter;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.media.MediaMetadataRetriever;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.getcapacitor.JSObject;
|
|
||||||
import com.getcapacitor.Plugin;
|
|
||||||
import com.getcapacitor.PluginCall;
|
|
||||||
import com.getcapacitor.PluginMethod;
|
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import androidx.media3.common.MediaItem;
|
|
||||||
import androidx.media3.common.MimeTypes;
|
|
||||||
import androidx.media3.common.util.UnstableApi;
|
|
||||||
import androidx.media3.transformer.DefaultEncoderFactory;
|
|
||||||
import androidx.media3.transformer.EditedMediaItem;
|
|
||||||
import androidx.media3.transformer.ExportException;
|
|
||||||
import androidx.media3.transformer.ExportResult;
|
|
||||||
import androidx.media3.transformer.Transformer;
|
|
||||||
import androidx.media3.transformer.VideoEncoderSettings;
|
|
||||||
|
|
||||||
@CapacitorPlugin(name = "VideoCompress")
|
|
||||||
public class VideoCompressPlugin extends Plugin {
|
|
||||||
|
|
||||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
private static long tryGetDurationMs(Context ctx, Uri uri) {
|
|
||||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
||||||
try {
|
|
||||||
retriever.setDataSource(ctx, uri);
|
|
||||||
String dur = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
|
||||||
if (dur == null) return -1;
|
|
||||||
return Long.parseLong(dur);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return -1;
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
retriever.release();
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long tryGetFileSize(Uri uri) {
|
|
||||||
try {
|
|
||||||
if ("file".equals(uri.getScheme())) {
|
|
||||||
File f = new File(uri.getPath() == null ? "" : uri.getPath());
|
|
||||||
return f.exists() ? f.length() : -1;
|
|
||||||
}
|
|
||||||
// content:// 体积用 outFile.length 就足够比较(输入大小无法保证可取到)
|
|
||||||
return -1;
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Uri coerceToUri(String input) {
|
|
||||||
if (input == null) return null;
|
|
||||||
String s = input.trim();
|
|
||||||
if (s.isEmpty()) return null;
|
|
||||||
if (s.startsWith("file://") || s.startsWith("content://")) {
|
|
||||||
return Uri.parse(s);
|
|
||||||
}
|
|
||||||
// Assume absolute file path.
|
|
||||||
return Uri.fromFile(new File(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PluginMethod
|
|
||||||
@UnstableApi
|
|
||||||
public void compressTo4s(PluginCall call) {
|
|
||||||
try {
|
|
||||||
final String inputPath = call.getString("path", "");
|
|
||||||
// <= 0 means keep full duration (no clipping).
|
|
||||||
final int durationMs = call.getInt("durationMs", 0);
|
|
||||||
final long videoBitrate = call.getLong("videoBitrate", 600_000L);
|
|
||||||
final boolean removeAudio = call.getBoolean("removeAudio", true);
|
|
||||||
|
|
||||||
final Uri inputUri = coerceToUri(inputPath);
|
|
||||||
if (inputUri == null) {
|
|
||||||
call.reject("path 不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transformer 必须在同一条应用线程上创建与访问;统一放到主线程执行。
|
|
||||||
mainHandler.post(() -> {
|
|
||||||
try {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
final long inputDurationMs = tryGetDurationMs(ctx, inputUri);
|
|
||||||
final long inputFileSize = tryGetFileSize(inputUri);
|
|
||||||
|
|
||||||
File outDir = new File(ctx.getCacheDir(), "video_exports");
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
outDir.mkdirs();
|
|
||||||
String suffix = durationMs > 0 ? ("_" + durationMs + "ms") : "_full";
|
|
||||||
File outFile = new File(outDir, "step2_" + System.currentTimeMillis() + suffix + ".mp4");
|
|
||||||
String outputPath = outFile.getAbsolutePath();
|
|
||||||
|
|
||||||
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(inputUri);
|
|
||||||
if (durationMs > 0) {
|
|
||||||
MediaItem.ClippingConfiguration clipping =
|
|
||||||
new MediaItem.ClippingConfiguration.Builder()
|
|
||||||
.setStartPositionMs(0)
|
|
||||||
.setEndPositionMs(durationMs)
|
|
||||||
.build();
|
|
||||||
mediaItemBuilder.setClippingConfiguration(clipping);
|
|
||||||
}
|
|
||||||
MediaItem mediaItem = mediaItemBuilder.build();
|
|
||||||
|
|
||||||
EditedMediaItem edited =
|
|
||||||
new EditedMediaItem.Builder(mediaItem)
|
|
||||||
.setRemoveAudio(removeAudio)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
int videoBitrateInt;
|
|
||||||
if (videoBitrate > Integer.MAX_VALUE) {
|
|
||||||
videoBitrateInt = Integer.MAX_VALUE;
|
|
||||||
} else if (videoBitrate < 1) {
|
|
||||||
videoBitrateInt = 1;
|
|
||||||
} else {
|
|
||||||
videoBitrateInt = (int) videoBitrate;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoEncoderSettings videoEncoderSettings =
|
|
||||||
new VideoEncoderSettings.Builder()
|
|
||||||
.setBitrate(videoBitrateInt)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
DefaultEncoderFactory encoderFactory =
|
|
||||||
new DefaultEncoderFactory.Builder(ctx)
|
|
||||||
.setRequestedVideoEncoderSettings(videoEncoderSettings)
|
|
||||||
.setEnableFallback(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Transformer.Listener listener =
|
|
||||||
new Transformer.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onCompleted(@NonNull androidx.media3.transformer.Composition composition, @NonNull ExportResult result) {
|
|
||||||
long outputDurationMs = tryGetDurationMs(ctx, Uri.fromFile(outFile));
|
|
||||||
JSObject ret = new JSObject();
|
|
||||||
ret.put("outputPath", outputPath);
|
|
||||||
ret.put("outputFileUri", "file://" + outputPath);
|
|
||||||
ret.put("fileSize", outFile.length());
|
|
||||||
|
|
||||||
JSObject inputInfo = new JSObject();
|
|
||||||
inputInfo.put("durationMs", inputDurationMs);
|
|
||||||
inputInfo.put("fileSize", inputFileSize);
|
|
||||||
inputInfo.put("uri", String.valueOf(inputUri));
|
|
||||||
|
|
||||||
JSObject outputInfo = new JSObject();
|
|
||||||
outputInfo.put("durationMs", outputDurationMs);
|
|
||||||
outputInfo.put("fileSize", outFile.length());
|
|
||||||
outputInfo.put("uri", "file://" + outputPath);
|
|
||||||
|
|
||||||
ret.put("input", inputInfo);
|
|
||||||
ret.put("output", outputInfo);
|
|
||||||
call.resolve(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(
|
|
||||||
@NonNull androidx.media3.transformer.Composition composition,
|
|
||||||
@NonNull ExportResult result,
|
|
||||||
@NonNull ExportException exception) {
|
|
||||||
call.reject("视频压缩失败: " + exception.getMessage(), exception);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Transformer transformer =
|
|
||||||
new Transformer.Builder(ctx)
|
|
||||||
.setEncoderFactory(encoderFactory)
|
|
||||||
.setVideoMimeType(MimeTypes.VIDEO_H264)
|
|
||||||
.setAudioMimeType(MimeTypes.AUDIO_AAC)
|
|
||||||
.experimentalSetTrimOptimizationEnabled(true)
|
|
||||||
.addListener(listener)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
transformer.start(edited, outputPath);
|
|
||||||
} catch (Exception e) {
|
|
||||||
call.reject("启动压缩失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
call.reject("参数错误: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
267
package-lock.json
generated
@ -18,10 +18,7 @@
|
|||||||
"@capgo/camera-preview": "^8.3.1",
|
"@capgo/camera-preview": "^8.3.1",
|
||||||
"@ionic/vue": "^8.0.0",
|
"@ionic/vue": "^8.0.0",
|
||||||
"@ionic/vue-router": "^8.0.0",
|
"@ionic/vue-router": "^8.0.0",
|
||||||
"html2canvas": "^1.4.1",
|
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"silly-datetime": "^0.1.2",
|
|
||||||
"vant": "^4.9.24",
|
"vant": "^4.9.24",
|
||||||
"vconsole": "^3.15.1",
|
"vconsole": "^3.15.1",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
@ -29,7 +26,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "8.3.0",
|
"@capacitor/cli": "8.3.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
|
||||||
"@vitejs/plugin-legacy": "^5.0.0",
|
"@vitejs/plugin-legacy": "^5.0.0",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
@ -39,6 +35,7 @@
|
|||||||
"eslint-plugin-vue": "^9.9.0",
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"postcss-px-to-viewport": "^1.1.1",
|
"postcss-px-to-viewport": "^1.1.1",
|
||||||
|
"silly-datetime": "^0.1.2",
|
||||||
"terser": "^5.4.0",
|
"terser": "^5.4.0",
|
||||||
"typescript": "~5.9.0",
|
"typescript": "~5.9.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
@ -3095,15 +3092,6 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/qrcode": {
|
|
||||||
"version": "1.5.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz",
|
|
||||||
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.7.1",
|
"version": "7.7.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.7.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.7.1.tgz",
|
||||||
@ -3863,6 +3851,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -3871,6 +3860,7 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
},
|
},
|
||||||
@ -4033,14 +4023,6 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/base64-arraybuffer": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@ -4288,14 +4270,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/camelcase": {
|
|
||||||
"version": "5.3.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
|
|
||||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001787",
|
"version": "1.0.30001787",
|
||||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
|
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
|
||||||
@ -4465,33 +4439,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cliui": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"string-width": "^4.2.0",
|
|
||||||
"strip-ansi": "^6.0.0",
|
|
||||||
"wrap-ansi": "^6.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
|
||||||
"version": "6.2.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
|
||||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.0.0",
|
|
||||||
"string-width": "^4.1.0",
|
|
||||||
"strip-ansi": "^6.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
},
|
},
|
||||||
@ -4502,7 +4454,8 @@
|
|||||||
"node_modules/color-name": {
|
"node_modules/color-name": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/colorette": {
|
"node_modules/colorette": {
|
||||||
"version": "2.0.20",
|
"version": "2.0.20",
|
||||||
@ -4628,14 +4581,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-line-break": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
|
||||||
"dependencies": {
|
|
||||||
"utrie": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@ -4790,14 +4735,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/decamelize": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/decimal.js": {
|
"node_modules/decimal.js": {
|
||||||
"version": "10.6.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz",
|
"resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
@ -4849,11 +4786,6 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dijkstrajs": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
|
||||||
},
|
|
||||||
"node_modules/dir-glob": {
|
"node_modules/dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
@ -4981,7 +4913,8 @@
|
|||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
@ -5664,14 +5597,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-caller-file": {
|
|
||||||
"version": "2.0.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
|
||||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
|
||||||
"engines": {
|
|
||||||
"node": "6.* || 8.* || >= 10.*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-func-name": {
|
"node_modules/get-func-name": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||||
@ -5941,18 +5866,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html2canvas": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
|
||||||
"dependencies": {
|
|
||||||
"css-line-break": "^2.1.0",
|
|
||||||
"text-segmentation": "^1.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||||
@ -6155,6 +6068,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -7104,14 +7018,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-try": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
@ -7152,6 +7058,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -7295,14 +7202,6 @@
|
|||||||
"node": ">=10.4.0"
|
"node": ">=10.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pngjs": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.9",
|
"version": "8.5.9",
|
||||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.9.tgz",
|
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.9.tgz",
|
||||||
@ -7474,22 +7373,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qrcode": {
|
|
||||||
"version": "1.5.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
|
|
||||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"dijkstrajs": "^1.0.1",
|
|
||||||
"pngjs": "^5.0.0",
|
|
||||||
"yargs": "^15.3.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"qrcode": "bin/qrcode"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.2",
|
"version": "6.14.2",
|
||||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz",
|
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz",
|
||||||
@ -7619,19 +7502,6 @@
|
|||||||
"throttleit": "^1.0.0"
|
"throttleit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/require-directory": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/require-main-filename": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
|
||||||
},
|
|
||||||
"node_modules/requires-port": {
|
"node_modules/requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
@ -7994,11 +7864,6 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/set-blocking": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@ -8107,7 +7972,8 @@
|
|||||||
"node_modules/silly-datetime": {
|
"node_modules/silly-datetime": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/silly-datetime/-/silly-datetime-0.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/silly-datetime/-/silly-datetime-0.1.2.tgz",
|
||||||
"integrity": "sha512-q8hnO91rRvQsYTYaZCJc6UpljzfdmWD3bNljDLKGVBT2ukj7snE+ENkVVkXfo529ABLEBeN6PHoEaT1ONEq81w=="
|
"integrity": "sha512-q8hnO91rRvQsYTYaZCJc6UpljzfdmWD3bNljDLKGVBT2ukj7snE+ENkVVkXfo529ABLEBeN6PHoEaT1ONEq81w==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/sisteransi": {
|
"node_modules/sisteransi": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
@ -8224,6 +8090,7 @@
|
|||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
@ -8252,6 +8119,7 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
},
|
},
|
||||||
@ -8393,14 +8261,6 @@
|
|||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/text-segmentation": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
|
||||||
"dependencies": {
|
|
||||||
"utrie": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
|
||||||
@ -8733,14 +8593,6 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/utrie": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-arraybuffer": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",
|
||||||
@ -9115,11 +8967,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/which-module": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
|
||||||
},
|
|
||||||
"node_modules/why-is-node-running": {
|
"node_modules/why-is-node-running": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
@ -9315,98 +9162,12 @@
|
|||||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/y18n": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
|
||||||
},
|
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/yargs": {
|
|
||||||
"version": "15.4.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
|
|
||||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
|
||||||
"dependencies": {
|
|
||||||
"cliui": "^6.0.0",
|
|
||||||
"decamelize": "^1.2.0",
|
|
||||||
"find-up": "^4.1.0",
|
|
||||||
"get-caller-file": "^2.0.1",
|
|
||||||
"require-directory": "^2.1.1",
|
|
||||||
"require-main-filename": "^2.0.0",
|
|
||||||
"set-blocking": "^2.0.0",
|
|
||||||
"string-width": "^4.2.0",
|
|
||||||
"which-module": "^2.0.0",
|
|
||||||
"y18n": "^4.0.0",
|
|
||||||
"yargs-parser": "^18.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs-parser": {
|
|
||||||
"version": "18.1.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
|
||||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"camelcase": "^5.0.0",
|
|
||||||
"decamelize": "^1.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs/node_modules/find-up": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
|
||||||
"dependencies": {
|
|
||||||
"locate-path": "^5.0.0",
|
|
||||||
"path-exists": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs/node_modules/locate-path": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
|
||||||
"dependencies": {
|
|
||||||
"p-locate": "^4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs/node_modules/p-limit": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
|
||||||
"dependencies": {
|
|
||||||
"p-try": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs/node_modules/p-locate": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
|
||||||
"dependencies": {
|
|
||||||
"p-limit": "^2.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yauzl": {
|
"node_modules/yauzl": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz",
|
"resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc && vite build",
|
"build": "vue-tsc && vite build",
|
||||||
"build:app": "npm run build && npx cap sync android && cd android && .\\\\gradlew assembleDebug",
|
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:e2e": "cypress run",
|
"test:e2e": "cypress run",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
@ -23,18 +22,15 @@
|
|||||||
"@capgo/camera-preview": "^8.3.1",
|
"@capgo/camera-preview": "^8.3.1",
|
||||||
"@ionic/vue": "^8.0.0",
|
"@ionic/vue": "^8.0.0",
|
||||||
"@ionic/vue-router": "^8.0.0",
|
"@ionic/vue-router": "^8.0.0",
|
||||||
"html2canvas": "^1.4.1",
|
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"silly-datetime": "^0.1.2",
|
|
||||||
"vant": "^4.9.24",
|
"vant": "^4.9.24",
|
||||||
"vconsole": "^3.15.1",
|
"vconsole": "^3.15.1",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
"vue-router": "^4.2.0"
|
"vue-router": "^4.2.0",
|
||||||
|
"silly-datetime": "^0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "8.3.0",
|
"@capacitor/cli": "8.3.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
|
||||||
"@vitejs/plugin-legacy": "^5.0.0",
|
"@vitejs/plugin-legacy": "^5.0.0",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
@ -44,7 +40,6 @@
|
|||||||
"eslint-plugin-vue": "^9.9.0",
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"postcss-px-to-viewport": "^1.1.1",
|
"postcss-px-to-viewport": "^1.1.1",
|
||||||
"sass-embedded": "^1.100.0",
|
|
||||||
"terser": "^5.4.0",
|
"terser": "^5.4.0",
|
||||||
"typescript": "~5.9.0",
|
"typescript": "~5.9.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
|
|||||||
112
seed2前端方法(1).txt
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
async function analyzeVideoWithArk(videoBase64: string) {
|
||||||
|
try {
|
||||||
|
const apiUrl = "https://ark.cn-beijing.volces.com/api/v3/responses";
|
||||||
|
const apiKey = "3496e327-0454-426c-8e69-13e905a1e756";
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
model: "doubao-seed-2-0-pro-260215",
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "input_video",
|
||||||
|
video_url: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input_text",
|
||||||
|
text:"角色设定\n" +
|
||||||
|
"你是一位基于计算机视觉的医疗级AI分析师。你的核心能力是通过分析面部微细血管的颜色变化(rPPG技术原理)、皮肤纹理细节、微表情特征来推断生理数据。\n" +
|
||||||
|
"核心原则:拒绝凭空捏造\n" +
|
||||||
|
"基于证据:每一个数据结论必须基于视频中的视觉特征(如:面部红润度变化推断心率,皮肤纹理推断年龄,肌肉紧张度推断压力)。\n" +
|
||||||
|
"异常检测:如果视频光线过暗、人脸模糊、遮挡严重或帧率过低导致无法提取有效信号,必须将对应指标标记为 invalid,严禁编造数据。\n" +
|
||||||
|
"逻辑自洽:数据必须符合生理常识(例如:心率与呼吸频率的比值通常在4:1左右,如果心率180且呼吸10,则数据存疑)。\n" +
|
||||||
|
"分析步骤\n" +
|
||||||
|
"视觉特征提取:\n" +
|
||||||
|
"观察前额/脸颊区域的像素颜色微小波动(用于计算心率、血压)。\n" +
|
||||||
|
"观察眼周、嘴角的纹理与下垂程度(用于计算皮肤年龄)。\n" +
|
||||||
|
"观察眉间紧锁程度、眨眼频率(用于计算心理压力)。\n" +
|
||||||
|
"数值估算与校验:\n" +
|
||||||
|
"根据特征估算数值。\n" +
|
||||||
|
"对照下方的【绝对生理极限表】,超出范围直接标记为 invalid。\n" +
|
||||||
|
"报告生成:基于有效数据生成JSON。\n" +
|
||||||
|
"指标参考与极限表\n" +
|
||||||
|
"| 指标 | 正常范围 | 绝对极限 (超出即无效) | 视觉依据 |\n" +
|
||||||
|
"| :--- | :--- | :--- | :--- |\n" +
|
||||||
|
"| 心率 | 60-100 bpm | 40-180 bpm | 面部皮下血流搏动频率 |\n" +
|
||||||
|
"| 呼吸 | 12-20 rpm | 8-40 rpm | 鼻翼/胸部起伏频率 |\n" +
|
||||||
|
"| 收缩压 | 90-139 mmHg | 80-200 mmHg | 血流搏动强度与波形 |\n" +
|
||||||
|
"| 舒张压 | 60-90 mmHg | 50-120 mmHg | 血管弹性估算 |\n" +
|
||||||
|
"| 血糖 | 3.9-6.1 mmol/L | 3.0-15.0 mmol/L | 巩膜/肤色特定光谱特征 |\n" +
|
||||||
|
"| 血红蛋白 | 110-165 g/L | 90-180 g/L | 面部血色充盈度 |\n" +
|
||||||
|
"| 甘油三酯 | 0.56-1.96 mmol/L | 0.4-5.0 mmol/L | 皮肤油脂光泽度 |\n" +
|
||||||
|
"| 皮肤年龄 | 实际年龄±5岁 | 5-90 岁 | 皱纹深度、皮肤紧致度 |\n" +
|
||||||
|
"| 压力/焦虑 | 0-10 分 | 0-10 分 | 眉间纹、咬肌紧张度 |\n" +
|
||||||
|
"\n" +
|
||||||
|
"输出格式\n" +
|
||||||
|
"请严格按照以下JSON格式返回,不要输出任何Markdown标记:" +
|
||||||
|
"{\n" +
|
||||||
|
" \"visual_quality_check\": {\n" +
|
||||||
|
" \"lighting\": \"good\",\n" +
|
||||||
|
" \"face_clarity\": \"high\",\n" +
|
||||||
|
" \"signal_reliability\": \"valid\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"metrics\": {\n" +
|
||||||
|
" \"vital_signs\": {\n" +
|
||||||
|
" \"heart_rate\": { \"value\": 78, \"unit\": \"bpm\", \"status\": \"normal\", \"desc\": \"心率\" },\n" +
|
||||||
|
" \"respiratory_rate\": { \"value\": 16, \"unit\": \"rpm\", \"status\": \"normal\", \"desc\": \"呼吸频率\" },\n" +
|
||||||
|
" \"systolic_bp\": { \"value\": 125, \"unit\": \"mmHg\", \"status\": \"normal\", \"desc\": \"收缩压\" },\n" +
|
||||||
|
" \"diastolic_bp\": { \"value\": 82, \"unit\": \"mmHg\", \"status\": \"normal\", \"desc\": \"舒张压\" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"blood_health\": {\n" +
|
||||||
|
" \"glucose\": { \"value\": 5.4, \"unit\": \"mmol/L\", \"status\": \"normal\", \"desc\": \"血糖\" },\n" +
|
||||||
|
" \"hemoglobin\": { \"value\": 135, \"unit\": \"g/L\", \"status\": \"normal\", \"desc\": \"血红蛋白\" },\n" +
|
||||||
|
" \"triglycerides\": { \"value\": 1.2, \"unit\": \"mmol/L\", \"status\": \"normal\", \"desc\": \"甘油三酯\" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"skin_status\": {\n" +
|
||||||
|
" \"skin_age\": { \"value\": 26, \"unit\": \"years\", \"status\": \"normal\", \"desc\": \"皮肤年龄\" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"mental_health\": {\n" +
|
||||||
|
" \"mental_score\": { \"value\": 80, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"心理健康指数\" },\n" +
|
||||||
|
" \"stress\": { \"value\": 4, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"压力指数\" },\n" +
|
||||||
|
" \"depression\": { \"value\": 2, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"抑郁指数\" },\n" +
|
||||||
|
" \"anxiety\": { \"value\": 3, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"焦虑指数\" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"brief_report\": {\n" +
|
||||||
|
" \"personality\": \"阳光自信\",\n" +
|
||||||
|
" \"emotion\": \"高兴\",\n" +
|
||||||
|
" \"overall_status\": \"优秀\",\n" +
|
||||||
|
" \"abnormal_items\": [],\n" +
|
||||||
|
" \"summary_text\": \"检测显示您的生理机能处于极佳状态,皮肤状况良好,心理压力较低,整体呈现出阳光自信的状态。\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
requestBody.input[0].content[0].video_url = videoBase64;
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`API请求失败: ${errorData.error?.message || "未知错误"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("调用Ark API失败:", error);
|
||||||
|
errorMessage.value = `视频分析失败: ${error.message || "未知错误"}`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 525 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 901 B |
|
Before Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 691 B |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
@ -1,41 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<div class="title">{{ title }}</div>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
title: string
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.item{
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 37px;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 956px;
|
|
||||||
border:1px solid #fff;
|
|
||||||
background: #fff;
|
|
||||||
padding: 50px;
|
|
||||||
.title{
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 900;
|
|
||||||
color: #000;
|
|
||||||
position: relative;
|
|
||||||
&::before{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 8px;
|
|
||||||
height: 28px;
|
|
||||||
background: url('@/assets/images/icon.png') no-repeat left top / cover;
|
|
||||||
left: -18px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
Before Width: | Height: | Size: 7.7 KiB |
@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="tips">
|
|
||||||
<div class="title">{{ title }}</div>
|
|
||||||
<div class="text">{{ text }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
title: string;
|
|
||||||
text?: string;
|
|
||||||
}>(),
|
|
||||||
{ text: '' },
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.tips{
|
|
||||||
padding: 27px 50px;
|
|
||||||
background:rgba(226, 206, 137, 0.12);
|
|
||||||
border-radius: 18px;
|
|
||||||
.title{
|
|
||||||
padding-left: 52px;
|
|
||||||
font-size: 30px;
|
|
||||||
// font-weight: 550;
|
|
||||||
color: #000;
|
|
||||||
background: url('./icon.png') no-repeat left center / 40px 40px;
|
|
||||||
line-height: 42px;
|
|
||||||
}
|
|
||||||
.text{
|
|
||||||
margin-top: 14px;
|
|
||||||
font-size: 30px;
|
|
||||||
line-height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -9,10 +9,10 @@ import 'vant/lib/index.css';
|
|||||||
|
|
||||||
// vConsole:仅开发环境启用(避免生产包默认开启)
|
// vConsole:仅开发环境启用(避免生产包默认开启)
|
||||||
// if (import.meta.env.DEV) {
|
// if (import.meta.env.DEV) {
|
||||||
// import('vconsole').then(({ default: VConsole }) => {
|
import('vconsole').then(({ default: VConsole }) => {
|
||||||
// // eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
// new VConsole();
|
new VConsole();
|
||||||
// });
|
});
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|||||||
@ -22,7 +22,10 @@ const routes: Array< RouteRecordRaw > = [
|
|||||||
path: '/step4',
|
path: '/step4',
|
||||||
component: () => import('@/views/step4.vue')
|
component: () => import('@/views/step4.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/step5',
|
||||||
|
component: () => import('@/views/step5.vue')
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@ -1,49 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="step1">
|
<div class="step1">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<div class="title-row">
|
<div class="title"><img src="@/assets/step1/title.png" alt=""></div>
|
||||||
<div class="title"><img src="@/assets/step1/title.png" alt=""></div>
|
|
||||||
<div class="logo"><img src="@/assets/step1/logo.png" alt=""></div>
|
|
||||||
</div>
|
|
||||||
<div class="sub-title"><img src="@/assets/step1/sub-title.png" alt=""></div>
|
<div class="sub-title"><img src="@/assets/step1/sub-title.png" alt=""></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="main-top">
|
<div class="ip" ref="ipRef">
|
||||||
<div class="ip" ref="ipRef">
|
<div class="l">
|
||||||
<!-- muted -->
|
<img src="@/assets/step1/avatars.png" alt="">
|
||||||
<div class="ip-videoWrap">
|
|
||||||
<video
|
|
||||||
v-show="!showLoopVideo"
|
|
||||||
ref="introVideoEl"
|
|
||||||
class="ip-video"
|
|
||||||
poster="@/assets/step1/1.jpg"
|
|
||||||
:src="introVideoSrc"
|
|
||||||
autoplay
|
|
||||||
playsinline
|
|
||||||
webkit-playsinline
|
|
||||||
preload="auto"
|
|
||||||
@ended="onIntroEnded"
|
|
||||||
/>
|
|
||||||
<video
|
|
||||||
v-show="showLoopVideo"
|
|
||||||
ref="loopVideoEl"
|
|
||||||
class="ip-video"
|
|
||||||
:src="loopVideoSrc"
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
webkit-playsinline
|
|
||||||
preload="auto"
|
|
||||||
loop
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="r">
|
|
||||||
<div class="video-tips">
|
|
||||||
<img src="@/assets/step1/video-tips.png" alt="">
|
|
||||||
</div>
|
|
||||||
<img class="avatar" src="@/assets/step1/r-bg.png" alt="">
|
|
||||||
</div>
|
</div>
|
||||||
|
<van-popover
|
||||||
|
v-model:show="showHelp"
|
||||||
|
placement="top-start"
|
||||||
|
:offset="[8, 0]"
|
||||||
|
:teleport="teleportTarget"
|
||||||
|
class="help-popover"
|
||||||
|
>
|
||||||
|
<div class="help-content">我可以帮您检测常规生命体<br>征和心理压力指数噢~</div>
|
||||||
|
<template #reference>
|
||||||
|
<span aria-hidden="true" id="help-anchor"/>
|
||||||
|
</template>
|
||||||
|
</van-popover>
|
||||||
|
<!-- muted -->
|
||||||
|
<video
|
||||||
|
class="ip-video"
|
||||||
|
src="@/assets/step1/1.mp4"
|
||||||
|
autoplay
|
||||||
|
playsinline
|
||||||
|
webkit-playsinline
|
||||||
|
preload="auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
@ -54,62 +40,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onMounted, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import introVideoUrl from '@/assets/step1/2.mp4?url';
|
|
||||||
import introVideoUrl_ from '@/assets/step1/1_.mp4?url';
|
|
||||||
|
|
||||||
const showHelp = ref(true);
|
const showHelp = ref(true);
|
||||||
|
const ipRef = ref<HTMLElement | null>(null);
|
||||||
|
const teleportTarget = computed(() => ipRef.value ?? 'body');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const introVideoEl = ref<HTMLVideoElement | null>(null);
|
|
||||||
const loopVideoEl = ref<HTMLVideoElement | null>(null);
|
|
||||||
const introVideoSrc = ref<string>(introVideoUrl);
|
|
||||||
const loopVideoSrc = ref<string>(introVideoUrl_);
|
|
||||||
|
|
||||||
const showLoopVideo = ref(false);
|
|
||||||
|
|
||||||
async function ensureLoopPreloaded() {
|
|
||||||
const el = loopVideoEl.value;
|
|
||||||
if (!el) return;
|
|
||||||
try {
|
|
||||||
// 强制触发一次加载,尽量把首帧/码流提前缓冲好
|
|
||||||
el.load();
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function switchToLoopVideo() {
|
|
||||||
showLoopVideo.value = true;
|
|
||||||
await nextTick();
|
|
||||||
const el = loopVideoEl.value;
|
|
||||||
if (!el) return;
|
|
||||||
try {
|
|
||||||
el.currentTime = 0;
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await el.play();
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onIntroEnded() {
|
|
||||||
void switchToLoopVideo();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 每次进入页面:先播 1.mp4,后台预加载 1_.mp4,结束后无缝切换
|
|
||||||
showLoopVideo.value = false;
|
|
||||||
introVideoSrc.value = introVideoUrl;
|
|
||||||
loopVideoSrc.value = introVideoUrl_;
|
|
||||||
void ensureLoopPreloaded();
|
|
||||||
});
|
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
router.push('/step2');
|
router.push('/step2');
|
||||||
}
|
}
|
||||||
@ -117,46 +54,28 @@ const start = () => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.step1 {
|
.step1 {
|
||||||
padding-top: 52.97px;
|
padding-top: 52.97px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
// background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
||||||
background: #F3F3F3;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
.title-row {
|
.title {
|
||||||
display: flex;
|
width: 340px;
|
||||||
justify-content: space-between;
|
height: 60px;
|
||||||
align-items: center;
|
padding-left: 49px;
|
||||||
padding: 0 49px;
|
|
||||||
|
|
||||||
.title {
|
img {
|
||||||
width: 340px;
|
width: 100%;
|
||||||
height: 60px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 500px;
|
|
||||||
height: 250px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-title {
|
.sub-title {
|
||||||
margin: 82px auto 0;
|
margin: 0 auto;
|
||||||
width: 930px;
|
width: 930px;
|
||||||
height: 124.57px;
|
height: 187.57px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -167,28 +86,13 @@ const start = () => {
|
|||||||
.main {
|
.main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.main-top {
|
|
||||||
padding-top: 40px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 与 step4 的 main 同布局:居中的 ip 容器 + 左右绝对定位元素 */
|
/* 与 step4 的 main 同布局:居中的 ip 容器 + 左右绝对定位元素 */
|
||||||
.ip {
|
.ip {
|
||||||
.ip-videoWrap {
|
position: absolute;
|
||||||
width: 100%;
|
top: 86px;
|
||||||
height: 100%;
|
left: 50%;
|
||||||
position: relative;
|
transform: translate(-50%, 0);
|
||||||
}
|
|
||||||
|
|
||||||
.ip-video {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 485.04px;
|
width: 485.04px;
|
||||||
height: 1309px;
|
height: 1309px;
|
||||||
|
|
||||||
@ -203,40 +107,52 @@ const start = () => {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
.l,
|
||||||
|
.r {
|
||||||
.r {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.video-tips{
|
|
||||||
top: -359px;
|
|
||||||
left: -69px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 497px;
|
top: 125px;
|
||||||
height: 256px;
|
width: 137px;
|
||||||
img{
|
height: 644px;
|
||||||
|
|
||||||
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.l {
|
||||||
width: 423px;
|
left: -222px;
|
||||||
height: 493.43px;
|
}
|
||||||
|
|
||||||
|
.r {
|
||||||
|
right: -222px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content {
|
||||||
|
// max-width: 520px;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 16px 18px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最简单:把 wrapper 当作“help-anchor”,只调这里就能移动 Popover */
|
||||||
|
:deep(.van-popover__wrapper) {
|
||||||
|
position: absolute;
|
||||||
|
top: 200px;
|
||||||
|
right: 120px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 102px;
|
bottom: 102px;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 284px;
|
width: 284px;
|
||||||
@ -252,12 +168,52 @@ const start = () => {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<!-- Popover 现 teleport 到 .ip,覆盖样式仍放全局,避免 scoped 限制 -->
|
||||||
.help-content {
|
<style lang="scss">
|
||||||
max-width: 497px;
|
.help-popover {
|
||||||
font-size: 28px;
|
|
||||||
padding: 16px 18px;
|
.van-popover__content {
|
||||||
color: #fff;
|
white-space: nowrap;
|
||||||
white-space: wrap;
|
// width: 520px;
|
||||||
|
background: rgba(0, 0, 0, 1);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-popover__arrow {
|
||||||
|
/* 用自定义“对话框小尾巴”替换默认菱形箭头 */
|
||||||
|
width: 64px;
|
||||||
|
height: 56px;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
bottom: -8px !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.van-popover__arrow::before,
|
||||||
|
.van-popover__arrow::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.88);
|
||||||
|
/* 形状:上边平直 + 左下斜切的小尾巴(接近截图效果) */
|
||||||
|
clip-path: polygon(
|
||||||
|
0 0,
|
||||||
|
100% 0,
|
||||||
|
100% 38%,
|
||||||
|
62% 38%,
|
||||||
|
18% 100%,
|
||||||
|
18% 38%,
|
||||||
|
0 38%
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -5,17 +5,15 @@
|
|||||||
|
|
||||||
<!-- 叠加层:圆形取景框 + 关闭按钮 + 进度环 -->
|
<!-- 叠加层:圆形取景框 + 关闭按钮 + 进度环 -->
|
||||||
<div class="camera-overlay">
|
<div class="camera-overlay">
|
||||||
<div class="back" @click="onBackClick"><img src="@/assets/step2/back.png" alt=""></div>
|
<div class="back" @click="router.replace('/')"><img src="@/assets/close.png" alt=""></div>
|
||||||
|
|
||||||
<div class="tipsvideo" >
|
<div class="tipsvideo" v-if="showTipsVideo">
|
||||||
<video
|
<video
|
||||||
ref="tipsVideoEl"
|
ref="tipsVideoEl"
|
||||||
:src="tipsVideoSrc"
|
:src="tipsVideoSrc"
|
||||||
:poster="tipsVideoPosterSrc"
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
preload="auto"
|
preload="auto"
|
||||||
@loadeddata="tipsVideoReady = true"
|
|
||||||
@error="tipsVideoReady = false"
|
|
||||||
@ended="onTipsEnded"
|
@ended="onTipsEnded"
|
||||||
@timeupdate="onTipsTimeUpdate"
|
@timeupdate="onTipsTimeUpdate"
|
||||||
@click="tryPlayTips"
|
@click="tryPlayTips"
|
||||||
@ -26,17 +24,9 @@
|
|||||||
<div class="reticle__ring reticle__ring--outer" />
|
<div class="reticle__ring reticle__ring--outer" />
|
||||||
<div class="reticle__ring reticle__ring--inner" />
|
<div class="reticle__ring reticle__ring--inner" />
|
||||||
<div class="reticle__glow" />
|
<div class="reticle__glow" />
|
||||||
<div class="reticle__hole" :class="{ 'is-recording': isRecording }">
|
|
||||||
<div v-if="cameraReady" class="reticle__faceWrap">
|
|
||||||
<img class="reticle__face" :src="faceSvgUrl" alt="" />
|
|
||||||
</div>
|
|
||||||
<div v-if="cameraReady" class="reticle__line reticle__line--top" />
|
|
||||||
<div v-if="cameraReady" class="reticle__line reticle__line--bottom" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<div class="tips-text">{{ tipsText }}</div>
|
|
||||||
<VanCircle
|
<VanCircle
|
||||||
class="progressCircle"
|
class="progressCircle"
|
||||||
:rate="progress"
|
:rate="progress"
|
||||||
@ -75,18 +65,14 @@ import { Circle as VanCircle } from 'vant';
|
|||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import tips2Url from '@/assets/step2/2.mp4?url';
|
import tips2Url from '@/assets/step2/2.mp4?url';
|
||||||
import tips2PosterUrl from '@/assets/step2/2.jpg?url';
|
|
||||||
import faceSvgUrl from '@/assets/step2/face.svg?url';
|
|
||||||
|
|
||||||
const PREVIEW_ID = 'step2-preview';
|
const PREVIEW_ID = 'step2-preview';
|
||||||
const tipsText = ref('');
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const statusText = ref('');
|
const statusText = ref('');
|
||||||
const progress = ref(0);
|
const progress = ref(0);
|
||||||
const currentRate = ref(0);
|
const currentRate = ref(0);
|
||||||
let started = false;
|
let started = false;
|
||||||
const isRecording = ref(false);
|
|
||||||
const cameraReady = ref(false);
|
|
||||||
let released = false;
|
let released = false;
|
||||||
let removeResizeListener: (() => void) | null = null;
|
let removeResizeListener: (() => void) | null = null;
|
||||||
const PREVIEW_ASPECT = 4 / 3; // 4:3,不裁剪;想更宽可改 16/9
|
const PREVIEW_ASPECT = 4 / 3; // 4:3,不裁剪;想更宽可改 16/9
|
||||||
@ -94,22 +80,10 @@ const PREVIEW_ASPECT = 4 / 3; // 4:3,不裁剪;想更宽可改 16/9
|
|||||||
const RETICLE_CENTER_Y = 0.3;
|
const RETICLE_CENTER_Y = 0.3;
|
||||||
|
|
||||||
const tipsVideoEl = ref<HTMLVideoElement | null>(null);
|
const tipsVideoEl = ref<HTMLVideoElement | null>(null);
|
||||||
// 提示视频始终显示,但不自动播放(由用户点击触发播放)
|
// 相机未授权/未启动时不要播放提示视频
|
||||||
|
const showTipsVideo = ref(false);
|
||||||
const tipsVideoReady = ref(false);
|
|
||||||
const tipsVideoSrc = computed(() => tips2Url);
|
const tipsVideoSrc = computed(() => tips2Url);
|
||||||
const tipsVideoPosterSrc = computed(() => tips2PosterUrl);
|
|
||||||
let navigatingToStep3 = false;
|
let navigatingToStep3 = false;
|
||||||
|
|
||||||
const changeTipsText = (arr: {text: string, time: number}[]) => {
|
|
||||||
for (const {text, time} of arr) {
|
|
||||||
tipsText.value = text;
|
|
||||||
setTimeout(() => {
|
|
||||||
tipsText.value = text;
|
|
||||||
}, time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTipsTimeUpdate() {
|
function onTipsTimeUpdate() {
|
||||||
const el = tipsVideoEl.value;
|
const el = tipsVideoEl.value;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@ -125,7 +99,6 @@ function tryPlayTips() {
|
|||||||
el.play().catch(() => {
|
el.play().catch(() => {
|
||||||
// iOS/部分机型可能需要用户手势;这里允许用户点一下视频开始播放
|
// iOS/部分机型可能需要用户手势;这里允许用户点一下视频开始播放
|
||||||
});
|
});
|
||||||
changeTipsText([{text: '请正视镜头', time: 0}, {text: '请向右转头', time: 6000}, {text: '请向左转头', time: 10000}]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTipsEnded() {
|
function onTipsEnded() {
|
||||||
@ -149,7 +122,7 @@ function onTipsEnded() {
|
|||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
queueMicrotask(async () => {
|
queueMicrotask(async () => {
|
||||||
try {
|
try {
|
||||||
await teardownRecorder(); // 写入 sessionStorage['step2_video_path'],压缩已移到 step3
|
await teardownRecorder(); // 内部会写 sessionStorage['step2_video_path']
|
||||||
} finally {
|
} finally {
|
||||||
router.push('/step3');
|
router.push('/step3');
|
||||||
}
|
}
|
||||||
@ -168,9 +141,7 @@ async function videoUrlToBlob(videoUrl: string): Promise<Blob> {
|
|||||||
return res.blob();
|
return res.blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
type CameraFacing = 'front' | 'rear';
|
async function setupNativeRecorder() {
|
||||||
|
|
||||||
function getCameraPreviewLayout() {
|
|
||||||
const vw = Math.round(window.innerWidth);
|
const vw = Math.round(window.innerWidth);
|
||||||
const vh = Math.round(window.innerHeight);
|
const vh = Math.round(window.innerHeight);
|
||||||
// 让预览按比例“contain”居中,避免满屏裁剪导致人脸变大
|
// 让预览按比例“contain”居中,避免满屏裁剪导致人脸变大
|
||||||
@ -185,58 +156,44 @@ function getCameraPreviewLayout() {
|
|||||||
const desiredCenterY = Math.round(vh * RETICLE_CENTER_Y);
|
const desiredCenterY = Math.round(vh * RETICLE_CENTER_Y);
|
||||||
const pyRaw = Math.round(desiredCenterY - ph / 2);
|
const pyRaw = Math.round(desiredCenterY - ph / 2);
|
||||||
const py = Math.max(0, Math.min(vh - ph, pyRaw));
|
const py = Math.max(0, Math.min(vh - ph, pyRaw));
|
||||||
return { pw, ph, px, py };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupNativeRecorder() {
|
await CameraPreview.start({
|
||||||
cameraReady.value = false;
|
parent: PREVIEW_ID,
|
||||||
const { pw, ph, px, py } = getCameraPreviewLayout();
|
toBack: true,
|
||||||
|
position: 'front',
|
||||||
const startPreviewAndRecord = async (position: CameraFacing) => {
|
enableVideoMode: true,
|
||||||
await CameraPreview.start({
|
disableAudio: false,
|
||||||
parent: PREVIEW_ID,
|
// 不铺满:按比例居中显示(可能出现上下/左右留黑边)
|
||||||
toBack: true,
|
aspectMode: 'contain',
|
||||||
position,
|
width: pw,
|
||||||
enableVideoMode: true,
|
height: ph,
|
||||||
disableAudio: true,
|
x: px,
|
||||||
// 不铺满:按比例居中显示(可能出现上下/左右留黑边)
|
y: py,
|
||||||
aspectMode: 'contain',
|
storeToFile: true,
|
||||||
width: pw,
|
rotateWhenOrientationChanged: true,
|
||||||
height: ph,
|
force: true
|
||||||
x: px,
|
});
|
||||||
y: py,
|
|
||||||
storeToFile: true,
|
|
||||||
rotateWhenOrientationChanged: true,
|
|
||||||
force: true
|
|
||||||
});
|
|
||||||
await CameraPreview.startRecordVideo({});
|
|
||||||
};
|
|
||||||
|
|
||||||
let usedFacing: CameraFacing = 'front';
|
|
||||||
try {
|
|
||||||
await startPreviewAndRecord('front');
|
|
||||||
} catch (firstErr) {
|
|
||||||
console.warn('前置摄像头不可用,尝试后置:', firstErr);
|
|
||||||
try {
|
|
||||||
await CameraPreview.stop({ force: true });
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
await startPreviewAndRecord('rear');
|
|
||||||
usedFacing = 'rear';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
await CameraPreview.startRecordVideo({});
|
||||||
started = true;
|
started = true;
|
||||||
isRecording.value = true;
|
statusText.value = '前置摄像头录制中';
|
||||||
cameraReady.value = true;
|
|
||||||
statusText.value ='前置摄像头录制中'
|
|
||||||
|
|
||||||
|
|
||||||
// 尺寸变化时同步更新原生预览,保持按比例居中
|
// 尺寸变化时同步更新原生预览,保持按比例居中
|
||||||
const onResize = async () => {
|
const onResize = async () => {
|
||||||
if (!Capacitor.isNativePlatform() || released) return;
|
if (!Capacitor.isNativePlatform() || released) return;
|
||||||
try {
|
try {
|
||||||
const { pw, ph, px, py } = getCameraPreviewLayout();
|
const vw = Math.round(window.innerWidth);
|
||||||
|
const vh = Math.round(window.innerHeight);
|
||||||
|
let pw = vw;
|
||||||
|
let ph = Math.round(pw * PREVIEW_ASPECT);
|
||||||
|
if (ph > vh) {
|
||||||
|
ph = vh;
|
||||||
|
pw = Math.round(ph / PREVIEW_ASPECT);
|
||||||
|
}
|
||||||
|
const px = Math.round((vw - pw) / 2);
|
||||||
|
const desiredCenterY = Math.round(vh * RETICLE_CENTER_Y);
|
||||||
|
const pyRaw = Math.round(desiredCenterY - ph / 2);
|
||||||
|
const py = Math.max(0, Math.min(vh - ph, pyRaw));
|
||||||
await CameraPreview.setPreviewSize({
|
await CameraPreview.setPreviewSize({
|
||||||
width: pw,
|
width: pw,
|
||||||
height: ph,
|
height: ph,
|
||||||
@ -256,8 +213,6 @@ async function teardownRecorder() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
released = true;
|
released = true;
|
||||||
isRecording.value = false;
|
|
||||||
cameraReady.value = false;
|
|
||||||
if (removeResizeListener) {
|
if (removeResizeListener) {
|
||||||
removeResizeListener();
|
removeResizeListener();
|
||||||
removeResizeListener = null;
|
removeResizeListener = null;
|
||||||
@ -287,21 +242,12 @@ async function back() {
|
|||||||
router.go(-1);
|
router.go(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBackClick() {
|
|
||||||
try {
|
|
||||||
await teardownRecorder();
|
|
||||||
} finally {
|
|
||||||
router.replace('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
released = false;
|
released = false;
|
||||||
document.documentElement.classList.add('step2-camera-active');
|
document.documentElement.classList.add('step2-camera-active');
|
||||||
progress.value = 0;
|
progress.value = 0;
|
||||||
currentRate.value = 0;
|
currentRate.value = 0;
|
||||||
|
showTipsVideo.value = false;
|
||||||
cameraReady.value = false;
|
|
||||||
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
statusText.value = '请在真机或模拟器(Capacitor)中打开以使用前置录像';
|
statusText.value = '请在真机或模拟器(Capacitor)中打开以使用前置录像';
|
||||||
@ -322,15 +268,14 @@ onMounted(async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await setupNativeRecorder();
|
await setupNativeRecorder();
|
||||||
// 相机启动成功后自动播放提示视频
|
// 相机正常启动后再显示/播放提示视频
|
||||||
|
showTipsVideo.value = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
tryPlayTips();
|
tryPlayTips();
|
||||||
//
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
statusText.value = '无法启动相机或麦克风,请检查系统权限';
|
statusText.value = '无法启动相机或麦克风,请检查系统权限';
|
||||||
|
showTipsVideo.value = false;
|
||||||
cameraReady.value = false;
|
|
||||||
const el = tipsVideoEl.value;
|
const el = tipsVideoEl.value;
|
||||||
if (el) {
|
if (el) {
|
||||||
try {
|
try {
|
||||||
@ -344,6 +289,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(async () => {
|
onBeforeUnmount(async () => {
|
||||||
|
await teardownRecorder();
|
||||||
document.documentElement.classList.remove('step2-camera-active');
|
document.documentElement.classList.remove('step2-camera-active');
|
||||||
try {
|
try {
|
||||||
await StatusBar.setOverlaysWebView({ overlay: true });
|
await StatusBar.setOverlaysWebView({ overlay: true });
|
||||||
@ -384,7 +330,7 @@ onBeforeUnmount(async () => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: rgba(0, 0, 0, 0.55);
|
||||||
/* 中间圆形镂空(iOS/WKWebView 友好) */
|
/* 中间圆形镂空(iOS/WKWebView 友好) */
|
||||||
-webkit-mask: radial-gradient(
|
-webkit-mask: radial-gradient(
|
||||||
circle at 50% 30%,
|
circle at 50% 30%,
|
||||||
@ -405,9 +351,17 @@ onBeforeUnmount(async () => {
|
|||||||
z-index: 4;
|
z-index: 4;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
img{
|
img{
|
||||||
width: 100%;
|
width: 34px;
|
||||||
height: 100%;
|
height: 34px;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: invert(1);
|
||||||
|
opacity: 0.95;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -461,88 +415,6 @@ onBeforeUnmount(async () => {
|
|||||||
border: 2px solid rgba(255, 255, 255, 0.25);
|
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reticle__hole {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: calc(var(--focus-radius) * 2);
|
|
||||||
height: calc(var(--focus-radius) * 2);
|
|
||||||
border-radius: 999px;
|
|
||||||
overflow: visible;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle__faceWrap {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle__face {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 48%;
|
|
||||||
object-fit: contain;
|
|
||||||
opacity: 0.9;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle__line {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
width: 135%;
|
|
||||||
height: 46px;
|
|
||||||
background: url('@/assets/step2/line.png') no-repeat center/cover;
|
|
||||||
opacity: 0.95;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle__line--top {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle__line--bottom {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle__hole.is-recording .reticle__line--top {
|
|
||||||
animation: reticleLineDownUp 4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle__hole.is-recording .reticle__line--bottom {
|
|
||||||
animation: reticleLineUpDown 4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes reticleLineDownUp {
|
|
||||||
0% {
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(-50%, calc(var(--focus-radius) * 2 - 46px));
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes reticleLineUpDown {
|
|
||||||
0% {
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(-50%, calc((var(--focus-radius) * 2 - 46px) * -1));
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -555,11 +427,6 @@ onBeforeUnmount(async () => {
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
padding: 0 28px 42px;
|
padding: 0 28px 42px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
.tips-text{
|
|
||||||
color: #fff;
|
|
||||||
font-size: 40px;
|
|
||||||
margin-bottom: 82px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom {
|
.zoom {
|
||||||
@ -600,7 +467,7 @@ onBeforeUnmount(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tip {
|
.tip {
|
||||||
margin: 148px auto 0;
|
margin: 0 auto;
|
||||||
width: 605px;
|
width: 605px;
|
||||||
height: 82px;
|
height: 82px;
|
||||||
img{
|
img{
|
||||||
|
|||||||
@ -5,15 +5,12 @@
|
|||||||
<div class="title">
|
<div class="title">
|
||||||
<img src="@/assets/step1/title.png" alt="">
|
<img src="@/assets/step1/title.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
<div class="r" @click="onCloseClick">
|
<div class="r" @click="router.replace('/')"><img src="@/assets/close.png" alt=""></div>
|
||||||
<img src="@/assets/close.png" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<video class="close-video" :src="closeVideoSrc" :poster="closeVideoPosterSrc" playsinline autoplay loop
|
<img class="ip" src="@/assets/step3/main.png" alt="">
|
||||||
preload="auto" @loadeddata="closeVideoReady = true" @error="closeVideoReady = false" />
|
|
||||||
<div class="tips">
|
<div class="tips">
|
||||||
<img class="tips-icon" src="@/assets/step3/loading.png" alt="">{{ tipsText }}
|
<img class="tips-icon" src="@/assets/step3/loading.png" alt="">正在分析你的身体状态…
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -22,121 +19,45 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor } from '@capacitor/core';
|
||||||
import { registerPlugin } from '@capacitor/core';
|
import { ref, onMounted } from 'vue';
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { CameraPreview } from '@capgo/camera-preview';
|
|
||||||
import closeVideoUrl from '@/assets/step3/3.mp4?url';
|
|
||||||
import closePosterUrl from '@/assets/step3/3.jpg?url';
|
|
||||||
const tipsText = ref('正在分析您的皮肤状态...');
|
|
||||||
const showHelp = ref(true);
|
const showHelp = ref(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const closeVideoSrc = closeVideoUrl;
|
const start = () => {
|
||||||
const closeVideoPosterSrc = closePosterUrl;
|
router.push('/step2');
|
||||||
const closeVideoReady = ref(false);
|
}
|
||||||
|
|
||||||
const VideoCompress = Capacitor.isNativePlatform()
|
|
||||||
? registerPlugin<any>('VideoCompress')
|
|
||||||
: null;
|
|
||||||
onMounted(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
tipsText.value = '正在分析您的衣着及面部微表情....';
|
|
||||||
setTimeout(() => {
|
|
||||||
tipsText.value = '正在分析您的面色....';
|
|
||||||
setTimeout(() => {
|
|
||||||
tipsText.value = '正在为您生成报告....';
|
|
||||||
}, 2000);
|
|
||||||
}, 2000);
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const ARK_API_URL =
|
const ARK_API_URL =
|
||||||
(import.meta.env.VITE_ARK_API_URL as string | undefined) ||
|
(import.meta.env.VITE_ARK_API_URL as string | undefined) ||
|
||||||
'https://ark.cn-shanghai.volces.com/api/v3/responses';
|
'https://ark.cn-beijing.volces.com/api/v3/responses';
|
||||||
const ARK_API_KEY =
|
const ARK_API_KEY =
|
||||||
(import.meta.env.VITE_ARK_API_KEY as string | undefined) ||
|
(import.meta.env.VITE_ARK_API_KEY as string | undefined) ||
|
||||||
'3496e327-0454-426c-8e69-13e905a1e756';
|
'3496e327-0454-426c-8e69-13e905a1e756';
|
||||||
|
|
||||||
function createAbortError() {
|
async function videoUrlToBlob(videoUrl: string): Promise<Blob> {
|
||||||
// 统一用和 fetch abort 一致的错误形态
|
|
||||||
const err = new Error('Aborted');
|
|
||||||
(err as any).name = 'AbortError';
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function videoUrlToBlob(videoUrl: string, signal?: AbortSignal): Promise<Blob> {
|
|
||||||
const src = Capacitor.convertFileSrc(videoUrl.trim());
|
const src = Capacitor.convertFileSrc(videoUrl.trim());
|
||||||
const res = await fetch(src, signal ? { signal } : undefined);
|
const res = await fetch(src);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`读取本地视频失败: ${res.status}`);
|
throw new Error(`读取本地视频失败: ${res.status}`);
|
||||||
}
|
}
|
||||||
return res.blob();
|
return res.blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function blobToDataUrl(blob: Blob, signal?: AbortSignal): Promise<string> {
|
async function blobToDataUrl(blob: Blob): Promise<string> {
|
||||||
return await new Promise<string>((resolve, reject) => {
|
return await new Promise<string>((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
let settled = false;
|
reader.onerror = () => reject(new Error('读取视频失败'));
|
||||||
const cleanup = () => {
|
reader.onload = () => resolve(String(reader.result || ''));
|
||||||
if (signal) signal.removeEventListener('abort', onAbort);
|
|
||||||
};
|
|
||||||
const onAbort = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
cleanup();
|
|
||||||
try {
|
|
||||||
reader.abort();
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
reject(createAbortError());
|
|
||||||
};
|
|
||||||
if (signal) {
|
|
||||||
if (signal.aborted) return onAbort();
|
|
||||||
signal.addEventListener('abort', onAbort, { once: true });
|
|
||||||
}
|
|
||||||
reader.onerror = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
cleanup();
|
|
||||||
reject(new Error('读取视频失败'));
|
|
||||||
};
|
|
||||||
reader.onload = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
cleanup();
|
|
||||||
resolve(String(reader.result || ''));
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pipelineAbortController = ref<AbortController | null>(null);
|
async function analyzeVideoWithArk(videoDataUrl: string) {
|
||||||
const isLeaving = ref(false);
|
|
||||||
|
|
||||||
function cancelInFlightRequest() {
|
|
||||||
const c = pipelineAbortController.value;
|
|
||||||
if (c) {
|
|
||||||
try {
|
|
||||||
c.abort();
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pipelineAbortController.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCloseClick() {
|
|
||||||
isLeaving.value = true;
|
|
||||||
cancelInFlightRequest();
|
|
||||||
router.replace('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function analyzeVideoWithArk(videoDataUrl: string, signal?: AbortSignal) {
|
|
||||||
if (!ARK_API_KEY) throw new Error('未设置 VITE_ARK_API_KEY');
|
if (!ARK_API_KEY) throw new Error('未设置 VITE_ARK_API_KEY');
|
||||||
|
|
||||||
const requestBody: any = {
|
const requestBody: any = {
|
||||||
model: 'doubao-seed-2-0-mini-260215',
|
model: 'doubao-seed-2-0-pro-260215',
|
||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@ -145,212 +66,89 @@ async function analyzeVideoWithArk(videoDataUrl: string, signal?: AbortSignal) {
|
|||||||
{
|
{
|
||||||
type: 'input_text',
|
type: 'input_text',
|
||||||
text: `角色设定
|
text: `角色设定
|
||||||
你是一位基于多模态感知(计算机视觉 + 音频分析)的医疗级 AI 健康顾问。你的核心能力是通过分析面部微细血管的颜色变化(rPPG 技术原理)、皮肤纹理细节、衣着穿搭风格以及语音语调的情感特征,来推断用户的生理与心理健康数据。
|
你是一位基于计算机视觉的医疗级AI分析师。你的核心能力是通过分析面部微细血管的颜色变化(rPPG技术原理)、皮肤纹理细节、微表情特征来推断生理数据。
|
||||||
核心原则
|
|
||||||
动态场景模拟(关键):
|
核心原则:拒绝凭空捏造
|
||||||
拒绝固定剧本: 不要每次都生成一样的数据。请在后台随机构思一个“用户画像场景”(例如:昨晚熬夜加班的程序员、刚喝完咖啡的销售、长期节食的减肥者、刚运动完的学生等)。
|
- 基于证据:每一个数据结论必须基于视频中的视觉特征。
|
||||||
基于场景生成数据: 所有的数值和状态必须服务于你构思的这个场景。
|
- 异常检测:如光线过暗/人脸模糊/遮挡严重/帧率过低导致无法提取有效信号,必须标记为 invalid,严禁编造数据。
|
||||||
强制差异化: 严禁直接复制提示词中示例的数值(如心率 101、压力 7.2 等)。必须生成全新的、符合当前模拟场景的数值。
|
- 逻辑自洽:数据必须符合生理常识。
|
||||||
真实世界的“亚健康”模拟:
|
|
||||||
拒绝完美数据: 严禁生成所有指标都完美的“教科书式”数据。
|
分析步骤
|
||||||
倾向轻微异常: 真实用户通常处于“亚健康”状态。请倾向于生成轻度偏高/偏低的数据(如:熬夜导致的心率稍快、压力导致的皮肤出油、饮食不规律导致的血糖波动),避免生成严重的病理数据,除非视觉特征极度明显。
|
1) 视觉特征提取:前额/脸颊颜色波动(心率/血压)、眼周嘴角纹理(皮肤年龄)、眉间/眨眼(压力)。
|
||||||
基于证据的生理推断:
|
2) 数值估算与校验:对照绝对极限表,超出范围直接 invalid。
|
||||||
面色潮红/充血 -> 对应:心率/血压轻度偏高、压力、饮酒。
|
3) 报告生成:输出 JSON。
|
||||||
面色苍白/无血色 -> 对应:血红蛋白偏低、疲劳、低血糖。
|
|
||||||
皮肤暗沉/油光重 -> 对应:代谢负担、皮质醇高(压力)、缺水。
|
指标参考与极限表(超出即无效)
|
||||||
关怀式反馈机制:
|
心率 40-180 bpm;呼吸 8-40 rpm;收缩压 80-200;舒张压 50-120;血糖 3.0-15.0;血红蛋白 90-180;甘油三酯 0.4-5.0;皮肤年龄 5-90;压力/焦虑 0-10。
|
||||||
模块级分析: 如果指标异常,analysis 字段必须解释视觉成因,suggestion 字段给出具体改善建议。
|
|
||||||
全局总结: summary_text 必须用温暖、关怀的口吻,串联所有异常点,并针对每个问题给出生活化的解决方案。
|
输出格式
|
||||||
分析步骤
|
请只返回 JSON(不要 Markdown/不要多余文字),格式如下:
|
||||||
场景构思: 随机模拟一个用户当前的生理状态(如:疲劳、兴奋、压力、缺水)。
|
{
|
||||||
数值生成与状态判定:
|
"visual_quality_check": {
|
||||||
构思数值:模拟真实波动,允许轻度偏离标准值。
|
"lighting": "good",
|
||||||
比对范围:确定 status ("正常"、"偏高"、"偏低")。
|
"face_clarity": "high",
|
||||||
注意: skin_status 模块必须包含 analysis(异常分析)和 suggestion(改善建议)两个独立字段。
|
"signal_reliability": "valid"
|
||||||
报告撰写: 输出 JSON。
|
},
|
||||||
指标参考范围(严格判定标准)
|
"metrics": {
|
||||||
常规生命体征:心率 (60-100)、呼吸 (12-20)、收缩压 (90-139)、舒张压 (60-90)
|
"vital_signs": {
|
||||||
血液健康:血糖 (3.9-6.1)、血红蛋白 (110-165)、甘油三酯 (0.565-1.69)
|
"heart_rate": { "value": 78, "unit": "bpm", "status": "normal", "desc": "心率" },
|
||||||
心理健康:心理健康 (0-100, 越高越好)、压力 (0-10)、抑郁 (0-10)、焦虑 (0-10)
|
"respiratory_rate": { "value": 16, "unit": "rpm", "status": "normal", "desc": "呼吸频率" },
|
||||||
皮肤健康:含水量 (10-20%)
|
"systolic_bp": { "value": 125, "unit": "mmHg", "status": "normal", "desc": "收缩压" },
|
||||||
注意:请严格依据上述数值范围判定 status。例如心率 92 在 60-100 范围内,应判定为“正常”。只有当数值超出上述范围时,才标记为“偏高”或“偏低”。
|
"diastolic_bp": { "value": 82, "unit": "mmHg", "status": "normal", "desc": "舒张压" }
|
||||||
输出格式
|
},
|
||||||
状态(status)字段只能返回以下三个枚举值之一:'正常'、'偏高'、'偏低'。严禁使用‘正常高值’、‘临界值’、‘轻微异常’等其他描述性词汇。
|
"blood_health": {
|
||||||
请只返回 JSON(不要 Markdown/不要多余文字),格式如下(注意:以下数值仅为格式示例,请务必生成与示例完全不同的新数据):
|
"glucose": { "value": 5.4, "unit": "mmol/L", "status": "normal", "desc": "血糖" },
|
||||||
{
|
"hemoglobin": { "value": 135, "unit": "g/L", "status": "normal", "desc": "血红蛋白" },
|
||||||
"visual_quality_check": {
|
"triglycerides": { "value": 1.2, "unit": "mmol/L", "status": "normal", "desc": "甘油三酯" }
|
||||||
"lighting": "good",
|
},
|
||||||
"face_clarity": "high",
|
"skin_status": {
|
||||||
"signal_reliability": "valid"
|
"skin_age": { "value": 26, "unit": "years", "status": "normal", "desc": "皮肤年龄" }
|
||||||
},
|
},
|
||||||
"metrics": {
|
"mental_health": {
|
||||||
"vital_signs": {
|
"mental_score": { "value": 80, "unit": "score", "status": "normal", "desc": "心理健康指数" },
|
||||||
"heart_rate": { "value": 82, "unit": "bpm", "status": "正常", "desc": "心率" },
|
"stress": { "value": 4, "unit": "score", "status": "normal", "desc": "压力指数" },
|
||||||
"respiratory_rate": { "value": 18, "unit": "rpm", "status": "正常", "desc": "呼吸频率" },
|
"depression": { "value": 2, "unit": "score", "status": "normal", "desc": "抑郁指数" },
|
||||||
"systolic_bp": { "value": 128, "unit": "mmHg", "status": "正常", "desc": "收缩压" },
|
"anxiety": { "value": 3, "unit": "score", "status": "normal", "desc": "焦虑指数" }
|
||||||
"diastolic_bp": { "value": 82, "unit": "mmHg", "status": "正常", "desc": "舒张压" },
|
}
|
||||||
"analysis": "面部微血管分布均匀,生命体征平稳。心率处于正常区间,显示心血管系统负荷正常。"
|
},
|
||||||
},
|
"brief_report": {
|
||||||
"blood_health": {
|
"personality": "阳光自信",
|
||||||
"glucose": { "value": 5.1, "unit": "mmol/L", "status": "正常", "desc": "血糖" },
|
"emotion": "高兴",
|
||||||
"hemoglobin": { "value": 135, "unit": "g/L", "status": "正常", "desc": "血红蛋白" },
|
"overall_status": "优秀",
|
||||||
"triglycerides": { "value": 1.4, "unit": "mmol/L", "status": "正常", "desc": "甘油三酯" },
|
"abnormal_items": [],
|
||||||
"analysis": "唇色红润,面部血色充盈,推测血液携氧能力及代谢指标均在健康区间。"
|
"summary_text": "检测显示您的生理机能处于极佳状态,皮肤状况良好,心理压力较低,整体呈现出阳光自信的状态。"
|
||||||
},
|
}
|
||||||
"skin_status": {
|
}`
|
||||||
"skin_age": { "value": 26, "unit": "years", "status": "正常", "desc": "皮肤年龄" },
|
|
||||||
"skin_type": { "value": "混合偏干", "unit": "", "status": "正常", "desc": "肤质类型" },
|
|
||||||
"hydration": { "value": 13, "unit": "%", "status": "偏低", "desc": "皮肤含水量" },
|
|
||||||
"dark_circles": { "value": "轻度", "unit": "", "status": "正常", "desc": "黑眼圈状态" },
|
|
||||||
"acne": { "value": "无", "unit": "", "status": "正常", "desc": "痤疮" },
|
|
||||||
"oil_control": { "value": "轻度", "unit": "", "status": "正常", "desc": "出油状态" },
|
|
||||||
"analysis": "皮肤纹理分析显示眼下及脸颊区域略显干燥,角质层含水量偏低,这可能与近期环境湿度低或饮水不足有关。",
|
|
||||||
"suggestion": "1. 每日饮水量增加至 2000ml;2. 使用含有角鲨烷或维生素 B5 的修复面霜加强保湿。"
|
|
||||||
},
|
|
||||||
"mental_health": {
|
|
||||||
"mental_score": { "value": 88.0, "unit": "score", "status": "正常", "desc": "心理健康指数" },
|
|
||||||
"stress": { "value": 3.0, "unit": "score", "status": "正常", "desc": "压力指数" },
|
|
||||||
"depression": { "value": 1.5, "unit": "score", "status": "正常", "desc": "抑郁指数" },
|
|
||||||
"anxiety": { "value": 2.0, "unit": "score", "status": "正常", "desc": "焦虑指数" },
|
|
||||||
"analysis": "语音语调平稳,面部表情自然放松,未见明显的紧张或焦虑微表情。您的心理状态非常健康。"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"brief_report": {
|
|
||||||
"personality": "温和/生活规律",
|
|
||||||
"emotion": "平静",
|
|
||||||
"clothing_style": "休闲舒适",
|
|
||||||
"overall_status": "健康",
|
|
||||||
"abnormal_items": [
|
|
||||||
"皮肤含水量偏低"
|
|
||||||
],
|
|
||||||
"summary_text": "亲爱的用户,很高兴看到您!根据本次多模态检测,您的整体健康状况非常理想。您的心血管系统强健,血液指标正常,且心理状态非常平稳,看来您最近的生活节奏把握得相当不错。唯一的小提示是您的皮肤含水量略低(13%),这可能是因为环境干燥或饮水稍少。建议您:随身携带保温杯,增加饮水频率,并在护肤时多涂抹一层保湿乳液。除此之外,请继续保持您现在的健康生活方式,您做得很好!"
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
return await new Promise<any>((resolve, reject) => {
|
const res = await fetch(ARK_API_URL, {
|
||||||
const xhr = new XMLHttpRequest();
|
method: 'POST',
|
||||||
let settled = false;
|
headers: {
|
||||||
const cleanup = () => {
|
Authorization: `Bearer ${ARK_API_KEY}`,
|
||||||
if (signal) signal.removeEventListener('abort', onAbort);
|
'Content-Type': 'application/json'
|
||||||
};
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
const onAbort = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
cleanup();
|
|
||||||
try {
|
|
||||||
xhr.abort();
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
reject(createAbortError());
|
|
||||||
};
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
if (signal.aborted) return onAbort();
|
|
||||||
signal.addEventListener('abort', onAbort, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.open('POST', ARK_API_URL, true);
|
|
||||||
xhr.setRequestHeader('Authorization', `Bearer ${ARK_API_KEY}`);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
cleanup();
|
|
||||||
const ok = xhr.status >= 200 && xhr.status < 300;
|
|
||||||
const body = xhr.response ?? null;
|
|
||||||
if (!ok) {
|
|
||||||
const message = (body as any)?.error?.message || xhr.status;
|
|
||||||
reject(new Error(`API请求失败: ${message}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(body);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
cleanup();
|
|
||||||
reject(new Error('网络错误'));
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onabort = () => {
|
|
||||||
onAbort();
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(JSON.stringify(requestBody));
|
|
||||||
});
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(`API请求失败: ${err?.error?.message || res.status}`);
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 从 step2 进入后兜底关闭原生相机预览,避免占用摄像头资源
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
|
||||||
try {
|
|
||||||
await CameraPreview.stop({ force: true });
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// step2 录制后写入:sessionStorage['step2_video_path']
|
// step2 录制后写入:sessionStorage['step2_video_path']
|
||||||
let videoPath = sessionStorage.getItem('step2_video_path') || '';
|
const videoPath = sessionStorage.getItem('step2_video_path') || '';
|
||||||
if (!videoPath) {
|
if (!videoPath) {
|
||||||
// router.push('/step4');
|
// router.push('/step4');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let pluginVideoInfo: {
|
|
||||||
input?: { durationMs?: number; fileSize?: number; uri?: string };
|
|
||||||
output?: { durationMs?: number; fileSize?: number; uri?: string };
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
// Android: 在 step3 统一做压缩/截取,降低 base64 与接口耗时
|
|
||||||
if (Capacitor.getPlatform() === 'android' && VideoCompress) {
|
|
||||||
try {
|
|
||||||
const inputPath = videoPath;
|
|
||||||
const ret = await VideoCompress.compressTo4s({
|
|
||||||
path: videoPath,
|
|
||||||
videoBitrate: 600_000,
|
|
||||||
removeAudio: true
|
|
||||||
});
|
|
||||||
const finalPath = ret?.outputFileUri || ret?.outputPath || videoPath;
|
|
||||||
try {
|
|
||||||
|
|
||||||
if (ret?.input || ret?.output) {
|
|
||||||
pluginVideoInfo = { input: ret?.input, output: ret?.output };
|
|
||||||
const inMs = Number(pluginVideoInfo?.input?.durationMs);
|
|
||||||
const inSize = Number(pluginVideoInfo?.input?.fileSize);
|
|
||||||
const outMs = Number(pluginVideoInfo?.output?.durationMs);
|
|
||||||
const outSize = Number(pluginVideoInfo?.output?.fileSize);
|
|
||||||
if (Number.isFinite(inMs)) console.log('[VideoCompress] input_duration_s:', (inMs / 1000).toFixed(2));
|
|
||||||
if (Number.isFinite(inSize)) console.log('[VideoCompress] input_size_bytes:', inSize);
|
|
||||||
if (Number.isFinite(outMs)) console.log('[VideoCompress] output_duration_s:', (outMs / 1000).toFixed(2));
|
|
||||||
if (Number.isFinite(outSize)) console.log('[VideoCompress] output_size_bytes:', outSize);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
videoPath = finalPath;
|
|
||||||
sessionStorage.setItem('step2_video_path', finalPath);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('视频压缩失败,回退使用原视频', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelInFlightRequest();
|
|
||||||
const controller = new AbortController();
|
|
||||||
pipelineAbortController.value = controller;
|
|
||||||
const { signal } = controller;
|
|
||||||
|
|
||||||
const now =
|
const now =
|
||||||
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||||
? () => performance.now()
|
? () => performance.now()
|
||||||
@ -358,19 +156,16 @@ onMounted(async () => {
|
|||||||
const t0 = now();
|
const t0 = now();
|
||||||
|
|
||||||
const tBlob0 = now();
|
const tBlob0 = now();
|
||||||
const blob = await videoUrlToBlob(videoPath, signal);
|
const blob = await videoUrlToBlob(videoPath);
|
||||||
const tBlob1 = now();
|
const tBlob1 = now();
|
||||||
|
|
||||||
const tDataUrl0 = now();
|
const tDataUrl0 = now();
|
||||||
if (signal.aborted) throw createAbortError();
|
const dataUrl = await blobToDataUrl(blob);
|
||||||
const dataUrl = await blobToDataUrl(blob, signal);
|
|
||||||
const tDataUrl1 = now();
|
const tDataUrl1 = now();
|
||||||
|
|
||||||
const tArk0 = now();
|
const tArk0 = now();
|
||||||
if (signal.aborted) throw createAbortError();
|
const result = await analyzeVideoWithArk(dataUrl);
|
||||||
const result = await analyzeVideoWithArk(dataUrl, signal);
|
|
||||||
const tArk1 = now();
|
const tArk1 = now();
|
||||||
if (isLeaving.value) return;
|
|
||||||
|
|
||||||
sessionStorage.setItem('step2_ark_result', JSON.stringify(result));
|
sessionStorage.setItem('step2_ark_result', JSON.stringify(result));
|
||||||
const tSave = now();
|
const tSave = now();
|
||||||
@ -388,8 +183,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
router.push('/step4');
|
router.push('/step4');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (isLeaving.value) return;
|
|
||||||
if (e?.name === 'AbortError') return;
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
errorMessage.value = e?.message || '识别失败';
|
errorMessage.value = e?.message || '识别失败';
|
||||||
router.push('/step1');
|
router.push('/step1');
|
||||||
@ -399,10 +192,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
cancelInFlightRequest();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.step3 {
|
.step3 {
|
||||||
@ -411,8 +200,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
// background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
||||||
background: #F3F3F3;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
@ -466,8 +254,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.tips {
|
.tips {
|
||||||
display: flex;
|
display: flex;
|
||||||
// width: 473px;
|
width: 473px;
|
||||||
text-align: center;
|
|
||||||
height: 67px;
|
height: 67px;
|
||||||
margin: 100px auto 0;
|
margin: 100px auto 0;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
|
|||||||
1795
src/views/step4.vue
@ -1,653 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="step4-shell">
|
|
||||||
<Transition :name="transitionName">
|
|
||||||
<!-- 报告页(原 step4) -->
|
|
||||||
<div v-if="view === 'report'" key="report" class="step4 step4--report">
|
|
||||||
<div class="top">
|
|
||||||
<div class="l"></div>
|
|
||||||
<div class="title">
|
|
||||||
我的检测报告
|
|
||||||
</div>
|
|
||||||
<div class="r" @click="router.replace('/')"><img src="@/assets/close.png" alt=""></div>
|
|
||||||
</div>
|
|
||||||
<div class="time"> 检测时间:<span>{{ creatTime }}</span></div>
|
|
||||||
<div class="main">
|
|
||||||
<div class="ip">
|
|
||||||
<div class="ip-top" @click="toStep5()">
|
|
||||||
<div class="top-l">
|
|
||||||
<div class="value">{{ data?.metrics?.skin_status?.skin_age?.value }}<span>岁</span></div>
|
|
||||||
<div class="label">皮肤年龄</div>
|
|
||||||
</div>
|
|
||||||
<div class="line"></div>
|
|
||||||
<div class="top-r">
|
|
||||||
<div class="value">{{ data?.metrics?.mental_health?.mental_score?.value }}<span>分</span></div>
|
|
||||||
<div class="label">心理健康指数</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="l" @click="toStep5()">
|
|
||||||
<ReportMetricCard v-for="(item, index) in list1" :key="index" v-bind="item" />
|
|
||||||
</div>
|
|
||||||
<div class="r" @click="toStep5()">
|
|
||||||
<ReportMetricCard v-for="(item, index) in list2" :key="index" v-bind="item" />
|
|
||||||
</div>
|
|
||||||
<img src="@/assets/step1/ip.png" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom" @click="toStep5()">
|
|
||||||
<ReportMetricCard v-for="(item, index) in list3" :key="index" v-bind="item" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 详情页(原 step5) -->
|
|
||||||
<div v-else key="detail" class="step5 step4--detail">
|
|
||||||
<div class="top">
|
|
||||||
<div class="l" @click="backToReport()"> <img src="@/assets/back.png" alt=""></div>
|
|
||||||
<div class="title">
|
|
||||||
心理压力指数MSI指标
|
|
||||||
</div>
|
|
||||||
<div class="r" @click="router.replace('/')"><img src="@/assets/close.png" alt=""></div>
|
|
||||||
</div>
|
|
||||||
<div v-html="summaryText" class="summary-text"> </div>
|
|
||||||
<!-- <div class="main">
|
|
||||||
<img class="tips" src="@/assets/step5/tips.png" alt="">
|
|
||||||
<img class="ip" src="@/assets/step1/ip.png" alt="">
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
<!-- <div class="info">
|
|
||||||
<div class="title">以下是你的心理压力指数MSI指标</div>
|
|
||||||
<div class="msi-card">
|
|
||||||
<div class="msi-score">
|
|
||||||
<span class="value">{{ msiScore.toFixed(1) }}</span>
|
|
||||||
<span class="total">/{{ msiMax.toFixed(1) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="msi-gauge">
|
|
||||||
<div class="track" />
|
|
||||||
<div class="pointer" :style="{ left: msiPointerLeft }" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="msi-labels">
|
|
||||||
<span>1-2</span>
|
|
||||||
<span>2-3</span>
|
|
||||||
<span>3-4</span>
|
|
||||||
<span>4-5</span>
|
|
||||||
<span>5-6</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
<p> 结合总分与各维度得分,测评对象当前压力状态呈现以下核心特征:</p>
|
|
||||||
<p> <span>1. 情绪与行为层面问题突出:</span>情绪易怒性、回避行为两个维度得分偏高,是当前压力的主要表现形式,易导致工作效率下降、人际沟通不畅等问题。</p>
|
|
||||||
<p> <span>2. 身心基础能力偏弱:</span>幸福感与适应能力得分偏低,反映出个体身心能量不足,对工作环境变化的适应力较弱,难以快速缓冲压力带来的负面影响。</p>
|
|
||||||
<p>
|
|
||||||
<span>3.社会支持存在提升空间:</span>社会支持维度处于中等水平,外部支持未被充分利用,主动求助、联结支持系统的行为不足,进一步加剧了压力应对的难度。基于测评结果,结合职场场景与个体需求,制定以下分层干预建议,可根据实际情况逐步落实:
|
|
||||||
每日开展 10-15 分钟情绪调节练习,如正念呼吸、渐进式肌肉放松或轻量冥想,每日固定时段完成,逐步提升情绪自控力。
|
|
||||||
建立 “情绪记录清单”,当出现烦躁、焦虑情绪时,及时记录情绪触发点与自身感受,分析情绪来源,针对性调整思维方式,减少冲动反应。
|
|
||||||
</p>
|
|
||||||
<p> 适度参与户外活动(如散步、慢跑、瑜伽),每周 2-3 次,每次 30 分钟以上,通过运动释放负面情绪,提升身心愉悦感。</p>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
//@ts-nocheck
|
|
||||||
import ReportMetricCard from '@/components/ReportMetricCard/index.vue';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { format } from 'silly-datetime';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const view = ref<'report' | 'detail'>('report');
|
|
||||||
const direction = ref<'forward' | 'back'>('forward');
|
|
||||||
const transitionName = ref('step4-slide-up');
|
|
||||||
const summaryText = ref('');
|
|
||||||
const creatTime = ref<Date>(new Date());
|
|
||||||
const data = ref<any>({});
|
|
||||||
// MSI 指数(详情页进度条)
|
|
||||||
const msiScore = ref(3.1);
|
|
||||||
const msiMax = ref(6.0);
|
|
||||||
const msiPointerLeft = computed(() => {
|
|
||||||
const ratio = Math.min(Math.max(msiScore.value / msiMax.value, 0), 1);
|
|
||||||
return `${ratio * 100}%`;
|
|
||||||
});
|
|
||||||
const toStep5 = () => {
|
|
||||||
direction.value = 'forward';
|
|
||||||
transitionName.value = 'step4-slide-up';
|
|
||||||
view.value = 'detail';
|
|
||||||
}
|
|
||||||
const backToReport = () => {
|
|
||||||
direction.value = 'back';
|
|
||||||
transitionName.value = 'step4-slide-down';
|
|
||||||
view.value = 'report';
|
|
||||||
};
|
|
||||||
const list1 = ref<any[]>([]);
|
|
||||||
|
|
||||||
const list2 = ref<any[]>([]);
|
|
||||||
const list3 = ref<any[]>([]);
|
|
||||||
/** 底部四个指标:`type` 决定布局,`iconType` 对应 icons/1~11.png */
|
|
||||||
|
|
||||||
|
|
||||||
//真实数据
|
|
||||||
const arkResult = sessionStorage.getItem('step2_ark_result');
|
|
||||||
if (arkResult) {
|
|
||||||
const result = JSON.parse(arkResult);
|
|
||||||
|
|
||||||
console.log(result);
|
|
||||||
|
|
||||||
// summaryText.value = result?.output[0]?.summary[0]?.text;
|
|
||||||
// console.log(summaryText.value); //文本
|
|
||||||
data.value = JSON.parse(result?.output[1]?.content[0]?.text);
|
|
||||||
creatTime.value = format(new Date(result.created_at ? result.created_at * 1000 : new Date()), 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
summaryText.value = data.value?.brief_report?.summary_text;
|
|
||||||
list1.value = [
|
|
||||||
{
|
|
||||||
type: 4 as const,
|
|
||||||
iconType: 1 as const,
|
|
||||||
title: '心率',
|
|
||||||
mainValue: data.value?.metrics?.vital_signs?.heart_rate?.value,
|
|
||||||
unit: 'bpm(次/分钟)',
|
|
||||||
}, {
|
|
||||||
type: 4 as const,
|
|
||||||
iconType: 2 as const,
|
|
||||||
title: '呼吸频率',
|
|
||||||
mainValue: data.value?.metrics?.vital_signs?.respiratory_rate?.value,
|
|
||||||
unit: '12-20次/分',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 4 as const,
|
|
||||||
iconType: 3 as const,
|
|
||||||
title: '舒张压',
|
|
||||||
mainValue: data.value?.metrics?.vital_signs?.diastolic_bp?.value,
|
|
||||||
unit: '85~89 mmHg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 4 as const,
|
|
||||||
iconType: 4 as const,
|
|
||||||
title: '收缩压',
|
|
||||||
mainValue: data.value?.metrics?.vital_signs?.systolic_bp?.value,
|
|
||||||
unit: '120–139 mmHg',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
list2.value = [
|
|
||||||
{
|
|
||||||
type: 4 as const,
|
|
||||||
iconType: 5 as const,
|
|
||||||
title: '血糖',
|
|
||||||
mainValue: data.value?.metrics?.blood_health?.glucose?.value,
|
|
||||||
unit: '≥7.0 mmol/L',
|
|
||||||
}, {
|
|
||||||
type: 4 as const,
|
|
||||||
iconType: 6 as const,
|
|
||||||
title: '血红蛋白',
|
|
||||||
mainValue: data.value?.metrics?.blood_health?.hemoglobin?.value,
|
|
||||||
unit: 'bmp(次/分钟)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 4 as const,
|
|
||||||
iconType: 7 as const,
|
|
||||||
title: '甘油三酯',
|
|
||||||
mainValue: data.value?.metrics?.blood_health?.triglycerides?.value,
|
|
||||||
unit: '12-15 mmol/L',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
list3.value = [
|
|
||||||
// {
|
|
||||||
// type: 1 as const,
|
|
||||||
// title: '心理检测指数',
|
|
||||||
// score: 3.1,
|
|
||||||
// max: 6,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
type: 8 as const,
|
|
||||||
iconType: 1 as const,
|
|
||||||
title: '心理检测指数',
|
|
||||||
mainValue: data.value?.metrics?.mental_health?.mental_score?.value,
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// type: 3 as const,
|
|
||||||
// iconType: 8 as const,
|
|
||||||
// title: '当前情绪',
|
|
||||||
// highlightText: '快乐',
|
|
||||||
// highlightColor: '#fb7185',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
type: 2 as const,
|
|
||||||
iconType: 9 as const,
|
|
||||||
title: '压力指数',
|
|
||||||
mainValue: data.value?.metrics?.mental_health?.stress?.value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2 as const,
|
|
||||||
iconType: 10 as const,
|
|
||||||
title: '抑郁指数',
|
|
||||||
mainValue: data.value?.metrics?.mental_health?.depression?.value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2 as const,
|
|
||||||
iconType: 11 as const,
|
|
||||||
title: '焦虑指数',
|
|
||||||
mainValue: data.value?.metrics?.mental_health?.anxiety?.value,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// data.value ={"visual_quality_check": {"lighting": "good", "face_clarity": "high", "signal_reliability": "valid"}, "metrics": {"vital_signs": {"heart_rate": {"value": 76, "unit": "bpm", "status": "normal", "desc": "心率"}, "respiratory_rate": {"value": 15, "unit": "rpm", "status": "normal", "desc": "呼吸频率"}, "systolic_bp": {"value": 122, "unit": "mmHg", "status": "normal", "desc": "收缩压"}, "diastolic_bp": {"value": 80, "unit": "mmHg", "status": "normal", "desc": "舒张压"}}, "blood_health": {"glucose": {"value": 5.2, "unit": "mmol/L", "status": "normal", "desc": "血糖"}, "hemoglobin": {"value": 132, "unit": "g/L", "status": "normal", "desc": "血红蛋白"}, "triglycerides": {"value": 1.1, "unit": "mmol/L", "status": "normal", "desc": "甘油三酯"}}, "skin_status": {"skin_age": {"value": 28, "unit": "years", "status": "normal", "desc": "皮肤年龄"}}, "mental_health": {"mental_score": {"value": 82, "unit": "score", "status": "normal", "desc": "心理健康指数"}, "stress": {"value": 3, "unit": "score", "status": "normal", "desc": "压力指数"}, "depression": {"value": 2, "unit": "score", "status": "normal", "desc": "抑郁指数"}, "anxiety": {"value": 2, "unit": "score", "status": "normal", "desc": "焦虑指数"}}}, "brief_report": {"personality": "沉稳内敛", "emotion": "平和", "overall_status": "良好", "abnormal_items": [], "summary_text": "检测显示您的生理机能处于正常状态,皮肤状况符合年龄特征,心理压力较低,整体状态良好。"}}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.step4-shell {
|
|
||||||
position: relative;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
/* 过渡时底层兜底背景,避免透明时露出白屏 */
|
|
||||||
// background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
|
||||||
background: #F3F3F3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step4,
|
|
||||||
.step5 {
|
|
||||||
min-height: 100vh;
|
|
||||||
// background: url('@/assets/step1/bg.png') no-repeat center center/cover ;
|
|
||||||
background: #F3F3F3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step4 {
|
|
||||||
padding-bottom: 166px;
|
|
||||||
position: relative;
|
|
||||||
padding-top: 52px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
// justify-content: space-between;
|
|
||||||
min-height: 100vh;
|
|
||||||
|
|
||||||
.top {
|
|
||||||
padding: 0 30px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 50px;
|
|
||||||
|
|
||||||
.l {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.r {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
line-height: 33px;
|
|
||||||
margin-top: 3px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #000;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #797979;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.ip {
|
|
||||||
position: absolute;
|
|
||||||
top: 258px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
width: 485.04px;
|
|
||||||
height: 1309px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ip-top {
|
|
||||||
border-radius: 24px;
|
|
||||||
position: absolute;
|
|
||||||
top: -187px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 335px;
|
|
||||||
height: 105px;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.top-l,
|
|
||||||
.top-r {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 50px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
width: 2px;
|
|
||||||
background: #D8D8D8;
|
|
||||||
height: 60%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.l {
|
|
||||||
position: absolute;
|
|
||||||
width: 168px;
|
|
||||||
top: 125px;
|
|
||||||
left: -222px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 17px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.r {
|
|
||||||
position: absolute;
|
|
||||||
width: 168px;
|
|
||||||
top: 125px;
|
|
||||||
right: -222px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 17px;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom {
|
|
||||||
z-index: 2;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 500px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 17px;
|
|
||||||
padding: 0 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 详情页(原 step5)的样式直接复用 */
|
|
||||||
.step5 {
|
|
||||||
padding-top: 52px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
// justify-content: space-between;
|
|
||||||
|
|
||||||
.top {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
padding: 0 30px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.l {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.r {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
height: 60px;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
transform: translateY(-200px);
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.ip {
|
|
||||||
margin-top: 61px;
|
|
||||||
width: 485.04px;
|
|
||||||
height: 1309px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tips {
|
|
||||||
display: block;
|
|
||||||
width: 404px;
|
|
||||||
height: 33px;
|
|
||||||
margin: 100px auto 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border-radius: 20px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 43px;
|
|
||||||
width: 956px;
|
|
||||||
height: 888px;
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
padding: 24px 26px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
.msi-card {
|
|
||||||
margin-top: 22px;
|
|
||||||
width: 452px;
|
|
||||||
height: 202px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 24px;
|
|
||||||
padding: 34px 34px 28px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.msi-score {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 63px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #111827;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total {
|
|
||||||
font-size: 43px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(17, 24, 39, 0.45);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.msi-gauge {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 26px;
|
|
||||||
padding-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.track {
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
#22c55e 0%,
|
|
||||||
#eab308 50%,
|
|
||||||
#ff7a00 78%,
|
|
||||||
#ff3b30 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pointer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 40px;
|
|
||||||
height: 28px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: url('@/components/ReportMetricCard/icons/dot.png') no-repeat center center / contain;
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.18));
|
|
||||||
}
|
|
||||||
|
|
||||||
.msi-labels {
|
|
||||||
margin-top: 18px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
span:nth-child(1),
|
|
||||||
span:nth-child(2) {
|
|
||||||
color: #22c55e;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:nth-child(3) {
|
|
||||||
color: #eab308;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:nth-child(4) {
|
|
||||||
color: #ff7a00;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:nth-child(5) {
|
|
||||||
color: #ff3b30;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-top: 25px;
|
|
||||||
font-size: 30px;
|
|
||||||
color: #000;
|
|
||||||
line-height: 48px;
|
|
||||||
|
|
||||||
p {
|
|
||||||
span {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.summary-text{
|
|
||||||
padding: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 内部切换动画:上下滑 + 淡入淡出(支持正/反向) */
|
|
||||||
.step4-slide-up-enter-active,
|
|
||||||
.step4-slide-down-enter-active {
|
|
||||||
transition: opacity 1100ms cubic-bezier(0.2, 0.9, 0.2, 1),
|
|
||||||
transform 1100ms cubic-bezier(0.2, 0.9, 0.2, 1);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
// background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
|
||||||
background: #F3F3F3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step4-slide-up-leave-active,
|
|
||||||
.step4-slide-down-leave-active {
|
|
||||||
transition: opacity 1100ms cubic-bezier(0.2, 0.9, 0.2, 1),
|
|
||||||
transform 1100ms cubic-bezier(0.2, 0.9, 0.2, 1);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
/* 叠层时也保持背景,避免透明叠加时出现白边/白屏 */
|
|
||||||
// background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
|
||||||
background: #F3F3F3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step4-slide-up-enter-to,
|
|
||||||
.step4-slide-up-leave-from,
|
|
||||||
.step4-slide-down-enter-to,
|
|
||||||
.step4-slide-down-leave-from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* forward:detail 从下往上进,report 往上出 */
|
|
||||||
.step4-slide-up-enter-from {
|
|
||||||
opacity: 0.01;
|
|
||||||
transform: translateY(100vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step4-slide-up-leave-to {
|
|
||||||
opacity: 0.01;
|
|
||||||
transform: translateY(-100vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* back:report 从上往下进,detail 往下出(反向) */
|
|
||||||
.step4-slide-down-enter-from {
|
|
||||||
opacity: 0.01;
|
|
||||||
transform: translateY(-100vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step4-slide-down-leave-to {
|
|
||||||
opacity: 0.01;
|
|
||||||
transform: translateY(100vh);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
86
src/views/step5.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="step5">
|
||||||
|
<div class="top">
|
||||||
|
<div class="l"> <img src="@/assets/back.png" alt=""></div>
|
||||||
|
<div class="title">
|
||||||
|
心理压力指数MSI指标
|
||||||
|
</div>
|
||||||
|
<div class="r"><img src="@/assets/close.png" alt=""></div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<img class="ip" src="@/assets/step3/main.png" alt="">
|
||||||
|
<img class="tips" src="@/assets/step3/tips.png" alt="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const showHelp = ref(true);
|
||||||
|
const router = useRouter();
|
||||||
|
const start = () => {
|
||||||
|
router.push('/step2');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.step5 {
|
||||||
|
padding-top: 52px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: url('@/assets/step1/bg.png') no-repeat center center/cover;
|
||||||
|
min-height: 100vh;
|
||||||
|
.top {
|
||||||
|
padding: 0 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.l {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.r {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
|
||||||
|
height: 60px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 50px;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
transform: translateY(-200px);
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.ip {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.tips {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
width: 473px;
|
||||||
|
height: 67px;
|
||||||
|
margin: 100px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
112
新建文本文档 (2)(1).txt
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
async function analyzeVideoWithArk(videoBase64: string) {
|
||||||
|
try {
|
||||||
|
const apiUrl = "https://ark.cn-beijing.volces.com/api/v3/responses";
|
||||||
|
const apiKey = "3496e327-0454-426c-8e69-13e905a1e756";
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
model: "doubao-seed-2-0-pro-260215",
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "input_video",
|
||||||
|
video_url: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input_text",
|
||||||
|
text:"角色设定\n" +
|
||||||
|
"你是一位基于计算机视觉的医疗级AI分析师。你的核心能力是通过分析面部微细血管的颜色变化(rPPG技术原理)、皮肤纹理细节、微表情特征来推断生理数据。\n" +
|
||||||
|
"核心原则:拒绝凭空捏造\n" +
|
||||||
|
"基于证据:每一个数据结论必须基于视频中的视觉特征(如:面部红润度变化推断心率,皮肤纹理推断年龄,肌肉紧张度推断压力)。\n" +
|
||||||
|
"异常检测:如果视频光线过暗、人脸模糊、遮挡严重或帧率过低导致无法提取有效信号,必须将对应指标标记为 invalid,严禁编造数据。\n" +
|
||||||
|
"逻辑自洽:数据必须符合生理常识(例如:心率与呼吸频率的比值通常在4:1左右,如果心率180且呼吸10,则数据存疑)。\n" +
|
||||||
|
"分析步骤\n" +
|
||||||
|
"视觉特征提取:\n" +
|
||||||
|
"观察前额/脸颊区域的像素颜色微小波动(用于计算心率、血压)。\n" +
|
||||||
|
"观察眼周、嘴角的纹理与下垂程度(用于计算皮肤年龄)。\n" +
|
||||||
|
"观察眉间紧锁程度、眨眼频率(用于计算心理压力)。\n" +
|
||||||
|
"数值估算与校验:\n" +
|
||||||
|
"根据特征估算数值。\n" +
|
||||||
|
"对照下方的【绝对生理极限表】,超出范围直接标记为 invalid。\n" +
|
||||||
|
"报告生成:基于有效数据生成JSON。\n" +
|
||||||
|
"指标参考与极限表\n" +
|
||||||
|
"| 指标 | 正常范围 | 绝对极限 (超出即无效) | 视觉依据 |\n" +
|
||||||
|
"| :--- | :--- | :--- | :--- |\n" +
|
||||||
|
"| 心率 | 60-100 bpm | 40-180 bpm | 面部皮下血流搏动频率 |\n" +
|
||||||
|
"| 呼吸 | 12-20 rpm | 8-40 rpm | 鼻翼/胸部起伏频率 |\n" +
|
||||||
|
"| 收缩压 | 90-139 mmHg | 80-200 mmHg | 血流搏动强度与波形 |\n" +
|
||||||
|
"| 舒张压 | 60-90 mmHg | 50-120 mmHg | 血管弹性估算 |\n" +
|
||||||
|
"| 血糖 | 3.9-6.1 mmol/L | 3.0-15.0 mmol/L | 巩膜/肤色特定光谱特征 |\n" +
|
||||||
|
"| 血红蛋白 | 110-165 g/L | 90-180 g/L | 面部血色充盈度 |\n" +
|
||||||
|
"| 甘油三酯 | 0.56-1.96 mmol/L | 0.4-5.0 mmol/L | 皮肤油脂光泽度 |\n" +
|
||||||
|
"| 皮肤年龄 | 实际年龄±5岁 | 5-90 岁 | 皱纹深度、皮肤紧致度 |\n" +
|
||||||
|
"| 压力/焦虑 | 0-10 分 | 0-10 分 | 眉间纹、咬肌紧张度 |\n" +
|
||||||
|
"\n" +
|
||||||
|
"输出格式\n" +
|
||||||
|
"请严格按照以下JSON格式返回,不要有任何其他的返回,你给我返回的内容就是一个json如下格式" +
|
||||||
|
"{\n" +
|
||||||
|
" \"visual_quality_check\": {\n" +
|
||||||
|
" \"lighting\": \"good\",\n" +
|
||||||
|
" \"face_clarity\": \"high\",\n" +
|
||||||
|
" \"signal_reliability\": \"valid\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"metrics\": {\n" +
|
||||||
|
" \"vital_signs\": {\n" +
|
||||||
|
" \"heart_rate\": { \"value\": 78, \"unit\": \"bpm\", \"status\": \"normal\", \"desc\": \"心率\" },\n" +
|
||||||
|
" \"respiratory_rate\": { \"value\": 16, \"unit\": \"rpm\", \"status\": \"normal\", \"desc\": \"呼吸频率\" },\n" +
|
||||||
|
" \"systolic_bp\": { \"value\": 125, \"unit\": \"mmHg\", \"status\": \"normal\", \"desc\": \"收缩压\" },\n" +
|
||||||
|
" \"diastolic_bp\": { \"value\": 82, \"unit\": \"mmHg\", \"status\": \"normal\", \"desc\": \"舒张压\" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"blood_health\": {\n" +
|
||||||
|
" \"glucose\": { \"value\": 5.4, \"unit\": \"mmol/L\", \"status\": \"normal\", \"desc\": \"血糖\" },\n" +
|
||||||
|
" \"hemoglobin\": { \"value\": 135, \"unit\": \"g/L\", \"status\": \"normal\", \"desc\": \"血红蛋白\" },\n" +
|
||||||
|
" \"triglycerides\": { \"value\": 1.2, \"unit\": \"mmol/L\", \"status\": \"normal\", \"desc\": \"甘油三酯\" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"skin_status\": {\n" +
|
||||||
|
" \"skin_age\": { \"value\": 26, \"unit\": \"years\", \"status\": \"normal\", \"desc\": \"皮肤年龄\" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"mental_health\": {\n" +
|
||||||
|
" \"mental_score\": { \"value\": 80, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"心理健康指数\" },\n" +
|
||||||
|
" \"stress\": { \"value\": 4, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"压力指数\" },\n" +
|
||||||
|
" \"depression\": { \"value\": 2, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"抑郁指数\" },\n" +
|
||||||
|
" \"anxiety\": { \"value\": 3, \"unit\": \"score\", \"status\": \"normal\", \"desc\": \"焦虑指数\" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"brief_report\": {\n" +
|
||||||
|
" \"personality\": \"阳光自信\",\n" +
|
||||||
|
" \"emotion\": \"高兴\",\n" +
|
||||||
|
" \"overall_status\": \"优秀\",\n" +
|
||||||
|
" \"abnormal_items\": [],\n" +
|
||||||
|
" \"summary_text\": \"检测显示您的生理机能处于极佳状态,皮肤状况良好,心理压力较低,整体呈现出阳光自信的状态。\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
requestBody.input[0].content[0].video_url = videoBase64;
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`API请求失败: ${errorData.error?.message || "未知错误"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("调用Ark API失败:", error);
|
||||||
|
errorMessage.value = `视频分析失败: ${error.message || "未知错误"}`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||