|
@@ -0,0 +1,577 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <meta charset="utf-8">
|
|
|
+ <title>AI身体测量</title>
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
|
+ <link rel="stylesheet" href="/assets/css/layui.css">
|
|
|
+ <link rel="stylesheet" href="/assets/css/admin.css">
|
|
|
+ <style>
|
|
|
+ .measurement-container {
|
|
|
+ display: flex;
|
|
|
+ gap: 30px;
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+ .figure-container {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .body-figure {
|
|
|
+ position: relative;
|
|
|
+ width: 300px;
|
|
|
+ height: 600px;
|
|
|
+ margin: 0 auto;
|
|
|
+ background: url('/assets/images/body_figure_male.png') no-repeat center center;
|
|
|
+ background-size: contain;
|
|
|
+ }
|
|
|
+ .body-figure.female {
|
|
|
+ background-image: url('/assets/images/body_figure_female.png');
|
|
|
+ }
|
|
|
+ .measurement-point {
|
|
|
+ position: absolute;
|
|
|
+ background: #1E9FFF;
|
|
|
+ color: white;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 15px;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+ cursor: pointer;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+ .measurement-point::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ width: 2px;
|
|
|
+ height: 20px;
|
|
|
+ background: #1E9FFF;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ }
|
|
|
+ .measurement-point.left::before {
|
|
|
+ right: 100%;
|
|
|
+ }
|
|
|
+ .measurement-point.right::before {
|
|
|
+ left: 100%;
|
|
|
+ }
|
|
|
+ .measurement-line {
|
|
|
+ position: absolute;
|
|
|
+ height: 1px;
|
|
|
+ background: #1E9FFF;
|
|
|
+ z-index: 5;
|
|
|
+ }
|
|
|
+ .data-container {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ .basic-info {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
+ }
|
|
|
+ .measurement-table {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
+ }
|
|
|
+ .measurement-table table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ }
|
|
|
+ .measurement-table th,
|
|
|
+ .measurement-table td {
|
|
|
+ padding: 12px;
|
|
|
+ text-align: left;
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
+ }
|
|
|
+ .measurement-table th {
|
|
|
+ background: #f8f9fa;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+ .measurement-value {
|
|
|
+ font-weight: bold;
|
|
|
+ color: #1E9FFF;
|
|
|
+ }
|
|
|
+ .measurement-value.empty {
|
|
|
+ color: #ccc;
|
|
|
+ }
|
|
|
+ .confidence-badge {
|
|
|
+ display: inline-block;
|
|
|
+ background: #52c41a;
|
|
|
+ color: white;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-left: 10px;
|
|
|
+ }
|
|
|
+ .confidence-badge.medium {
|
|
|
+ background: #fa8c16;
|
|
|
+ }
|
|
|
+ .confidence-badge.low {
|
|
|
+ background: #f5222d;
|
|
|
+ }
|
|
|
+ .ai-controls {
|
|
|
+ text-align: center;
|
|
|
+ margin: 20px 0;
|
|
|
+ }
|
|
|
+ .loading-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(255,255,255,0.8);
|
|
|
+ z-index: 1000;
|
|
|
+ display: none;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+ .loading-content {
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .loading-spinner {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ border: 4px solid #f3f3f3;
|
|
|
+ border-top: 4px solid #1E9FFF;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+ @keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+ }
|
|
|
+ .progress-bar {
|
|
|
+ width: 300px;
|
|
|
+ height: 6px;
|
|
|
+ background: #f0f0f0;
|
|
|
+ border-radius: 3px;
|
|
|
+ overflow: hidden;
|
|
|
+ margin: 10px auto;
|
|
|
+ }
|
|
|
+ .progress-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: #1E9FFF;
|
|
|
+ border-radius: 3px;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+ }
|
|
|
+ .warning-item {
|
|
|
+ background: #fff7e6;
|
|
|
+ border: 1px solid #ffd591;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #fa8c16;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="layui-fluid" style="padding: 15px;">
|
|
|
+ <!-- 头部信息 -->
|
|
|
+ <div class="layui-card">
|
|
|
+ <div class="layui-card-header">
|
|
|
+ <h3>AI身体测量 - {$profile.profile_name}</h3>
|
|
|
+ <span class="layui-badge layui-bg-blue">{$profile.gender_text}</span>
|
|
|
+ <span class="layui-badge layui-bg-green">{$profile.is_own_text}</span>
|
|
|
+ </div>
|
|
|
+ <div class="layui-card-body">
|
|
|
+ <!-- AI控制按钮 -->
|
|
|
+ <div class="ai-controls">
|
|
|
+ <button class="layui-btn layui-btn-lg" id="startAnalysis">
|
|
|
+ <i class="layui-icon layui-icon-play"></i> 开始AI分析
|
|
|
+ </button>
|
|
|
+ <button class="layui-btn layui-btn-normal layui-btn-lg" id="retryAnalysis" style="display: none;">
|
|
|
+ <i class="layui-icon layui-icon-refresh"></i> 重新分析
|
|
|
+ </button>
|
|
|
+ <button class="layui-btn layui-btn-warm layui-btn-lg" id="saveResult" style="display: none;">
|
|
|
+ <i class="layui-icon layui-icon-ok"></i> 保存结果
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 置信度和警告信息 -->
|
|
|
+ <div id="analysisInfo" style="display: none;">
|
|
|
+ <div style="text-align: center; margin: 10px 0;">
|
|
|
+ <span>分析置信度:</span>
|
|
|
+ <span id="confidenceValue">85%</span>
|
|
|
+ <span id="confidenceBadge" class="confidence-badge">高</span>
|
|
|
+ </div>
|
|
|
+ <div id="warningsContainer"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 测量结果展示 -->
|
|
|
+ <div class="measurement-container">
|
|
|
+ <!-- 人体示意图 -->
|
|
|
+ <div class="figure-container">
|
|
|
+ <h4>身体示意图</h4>
|
|
|
+ <div id="bodyFigure" class="body-figure {if $profile.gender == 2}female{/if}">
|
|
|
+ <!-- 测量点将通过JavaScript动态添加 -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <div class="data-container">
|
|
|
+ <!-- 基础信息 -->
|
|
|
+ <div class="basic-info">
|
|
|
+ <h4>身体数据</h4>
|
|
|
+ <table>
|
|
|
+ <tr>
|
|
|
+ <td><strong>身高</strong></td>
|
|
|
+ <td>{$profile.height|default='--'}cm</td>
|
|
|
+ <td><strong>体重</strong></td>
|
|
|
+ <td>{$profile.weight|default='--'}kg</td>
|
|
|
+ </tr>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 测量数据表格 -->
|
|
|
+ <div class="measurement-table">
|
|
|
+ <h4>测量数据</h4>
|
|
|
+ <table id="measurementTable">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>部位</th>
|
|
|
+ <th>数值(cm)</th>
|
|
|
+ <th>部位</th>
|
|
|
+ <th>数值(cm)</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody id="measurementTableBody">
|
|
|
+ <!-- 数据将通过JavaScript动态填充 -->
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 加载遮罩 -->
|
|
|
+ <div class="loading-overlay" id="loadingOverlay">
|
|
|
+ <div class="loading-content">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div id="loadingText">AI正在分析您的身体照片...</div>
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div class="progress-fill" id="progressFill" style="width: 0%"></div>
|
|
|
+ </div>
|
|
|
+ <div id="progressText">0%</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script src="/assets/js/layui.js"></script>
|
|
|
+ <script>
|
|
|
+ layui.use(['layer'], function(){
|
|
|
+ var layer = layui.layer;
|
|
|
+ var profileId = '{$profile.id}';
|
|
|
+ var currentTaskId = null;
|
|
|
+ var analysisInterval = null;
|
|
|
+
|
|
|
+ // 初始化页面
|
|
|
+ initializePage();
|
|
|
+
|
|
|
+ // 开始AI分析
|
|
|
+ $('#startAnalysis').click(function() {
|
|
|
+ startAiAnalysis();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 重新分析
|
|
|
+ $('#retryAnalysis').click(function() {
|
|
|
+ retryAnalysis();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存结果
|
|
|
+ $('#saveResult').click(function() {
|
|
|
+ saveAnalysisResult();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 初始化页面数据
|
|
|
+ function initializePage() {
|
|
|
+ // 加载默认的空数据表格
|
|
|
+ renderMeasurementTable({});
|
|
|
+
|
|
|
+ // 检查是否有进行中的任务
|
|
|
+ checkExistingTask();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查现有任务
|
|
|
+ function checkExistingTask() {
|
|
|
+ $.get('/api/ai_measurement/getResult', {
|
|
|
+ profile_id: profileId
|
|
|
+ }, function(response) {
|
|
|
+ if (response.code === 1 && response.data) {
|
|
|
+ switch (response.data.status) {
|
|
|
+ case 'processing':
|
|
|
+ case 'pending':
|
|
|
+ showLoadingOverlay();
|
|
|
+ startPolling();
|
|
|
+ break;
|
|
|
+ case 'completed':
|
|
|
+ renderAnalysisResult(response.data.data);
|
|
|
+ break;
|
|
|
+ case 'failed':
|
|
|
+ if (response.data.can_retry) {
|
|
|
+ $('#retryAnalysis').show();
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始AI分析
|
|
|
+ function startAiAnalysis() {
|
|
|
+ // 获取档案的身体照片
|
|
|
+ $.get('/api/body_profile/detail', {
|
|
|
+ profile_id: profileId
|
|
|
+ }, function(response) {
|
|
|
+ if (response.code === 1) {
|
|
|
+ var photos = response.data.body_photos_array;
|
|
|
+
|
|
|
+ if (!photos.front || !photos.side || !photos.back) {
|
|
|
+ layer.msg('请先上传完整的身体照片(正面、侧面、背面)');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发起AI分析请求
|
|
|
+ $.post('/api/ai_measurement/startAnalysis', {
|
|
|
+ profile_id: profileId,
|
|
|
+ photos: photos
|
|
|
+ }, function(res) {
|
|
|
+ if (res.code === 1) {
|
|
|
+ currentTaskId = res.data.task_id;
|
|
|
+ showLoadingOverlay();
|
|
|
+ startPolling();
|
|
|
+ } else {
|
|
|
+ layer.msg(res.msg);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ layer.msg(response.msg);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示加载遮罩
|
|
|
+ function showLoadingOverlay() {
|
|
|
+ $('#loadingOverlay').show();
|
|
|
+ $('#startAnalysis').hide();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 隐藏加载遮罩
|
|
|
+ function hideLoadingOverlay() {
|
|
|
+ $('#loadingOverlay').hide();
|
|
|
+ $('#startAnalysis').show();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始轮询检查结果
|
|
|
+ function startPolling() {
|
|
|
+ if (analysisInterval) {
|
|
|
+ clearInterval(analysisInterval);
|
|
|
+ }
|
|
|
+
|
|
|
+ analysisInterval = setInterval(function() {
|
|
|
+ checkAnalysisResult();
|
|
|
+ }, 2000); // 每2秒检查一次
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查分析结果
|
|
|
+ function checkAnalysisResult() {
|
|
|
+ var params = currentTaskId ? {task_id: currentTaskId} : {profile_id: profileId};
|
|
|
+
|
|
|
+ $.get('/api/ai_measurement/getResult', params, function(response) {
|
|
|
+ if (response.code === 1) {
|
|
|
+ switch (response.data.status) {
|
|
|
+ case 'pending':
|
|
|
+ updateProgress(10, '任务排队中...');
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'processing':
|
|
|
+ var progress = response.data.progress || 50;
|
|
|
+ updateProgress(progress, 'AI正在分析您的身体照片...');
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'completed':
|
|
|
+ clearInterval(analysisInterval);
|
|
|
+ hideLoadingOverlay();
|
|
|
+ renderAnalysisResult(response.data.data);
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'failed':
|
|
|
+ clearInterval(analysisInterval);
|
|
|
+ hideLoadingOverlay();
|
|
|
+ layer.msg('分析失败: ' + response.data.message);
|
|
|
+ if (response.data.can_retry) {
|
|
|
+ $('#retryAnalysis').show();
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新进度
|
|
|
+ function updateProgress(progress, text) {
|
|
|
+ $('#progressFill').css('width', progress + '%');
|
|
|
+ $('#progressText').text(progress + '%');
|
|
|
+ $('#loadingText').text(text);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染分析结果
|
|
|
+ function renderAnalysisResult(data) {
|
|
|
+ // 显示置信度
|
|
|
+ if (data.confidence) {
|
|
|
+ var confidence = Math.round(data.confidence * 100);
|
|
|
+ $('#confidenceValue').text(confidence + '%');
|
|
|
+
|
|
|
+ var badge = $('#confidenceBadge');
|
|
|
+ if (confidence >= 80) {
|
|
|
+ badge.removeClass('medium low').addClass('high').text('高');
|
|
|
+ } else if (confidence >= 60) {
|
|
|
+ badge.removeClass('high low').addClass('medium').text('中');
|
|
|
+ } else {
|
|
|
+ badge.removeClass('high medium').addClass('low').text('低');
|
|
|
+ }
|
|
|
+
|
|
|
+ $('#analysisInfo').show();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示警告信息
|
|
|
+ if (data.warnings && data.warnings.length > 0) {
|
|
|
+ var warningsHtml = '';
|
|
|
+ data.warnings.forEach(function(warning) {
|
|
|
+ warningsHtml += '<div class="warning-item">' + warning + '</div>';
|
|
|
+ });
|
|
|
+ $('#warningsContainer').html(warningsHtml);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染测量点
|
|
|
+ renderMeasurementPoints(data.measurements);
|
|
|
+
|
|
|
+ // 渲染数据表格
|
|
|
+ renderMeasurementTable(data.measurements);
|
|
|
+
|
|
|
+ // 显示保存按钮
|
|
|
+ $('#saveResult').show();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染测量点
|
|
|
+ function renderMeasurementPoints(measurements) {
|
|
|
+ var figure = $('#bodyFigure');
|
|
|
+ figure.find('.measurement-point, .measurement-line').remove();
|
|
|
+
|
|
|
+ if (!measurements) return;
|
|
|
+
|
|
|
+ Object.keys(measurements).forEach(function(field) {
|
|
|
+ var measurement = measurements[field];
|
|
|
+ if (!measurement.value || !measurement.position) return;
|
|
|
+
|
|
|
+ var point = $('<div class="measurement-point ' + measurement.side + '">')
|
|
|
+ .text(measurement.label + ': ' + measurement.value + measurement.unit)
|
|
|
+ .css({
|
|
|
+ left: (measurement.position.x * 100) + '%',
|
|
|
+ top: (measurement.position.y * 100) + '%'
|
|
|
+ });
|
|
|
+
|
|
|
+ figure.append(point);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染测量数据表格
|
|
|
+ function renderMeasurementTable(measurements) {
|
|
|
+ var tbody = $('#measurementTableBody');
|
|
|
+ tbody.empty();
|
|
|
+
|
|
|
+ // 默认字段配置
|
|
|
+ var defaultFields = [
|
|
|
+ ['胸围', 'chest'], ['腰围', 'waist'],
|
|
|
+ ['臀围', 'hip'], ['大腿围', 'thigh'],
|
|
|
+ ['小腿围', 'calf'], ['上臂围', 'upper_arm'],
|
|
|
+ ['肩宽', 'shoulder_width'], ['颈围', 'neck']
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 按两列排列
|
|
|
+ for (var i = 0; i < defaultFields.length; i += 2) {
|
|
|
+ var row = '<tr>';
|
|
|
+
|
|
|
+ for (var j = 0; j < 2; j++) {
|
|
|
+ if (i + j < defaultFields.length) {
|
|
|
+ var field = defaultFields[i + j];
|
|
|
+ var label = field[0];
|
|
|
+ var key = field[1];
|
|
|
+ var value = measurements[key] ? measurements[key].value : null;
|
|
|
+ var displayValue = value ? value + 'cm' : '--';
|
|
|
+ var cssClass = value ? 'measurement-value' : 'measurement-value empty';
|
|
|
+
|
|
|
+ row += '<td>' + label + '</td>';
|
|
|
+ row += '<td><span class="' + cssClass + '">' + displayValue + '</span></td>';
|
|
|
+ } else {
|
|
|
+ row += '<td></td><td></td>';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ row += '</tr>';
|
|
|
+ tbody.append(row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新分析
|
|
|
+ function retryAnalysis() {
|
|
|
+ if (!currentTaskId) {
|
|
|
+ layer.msg('没有可重试的任务');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $.post('/api/ai_measurement/retryAnalysis', {
|
|
|
+ task_id: currentTaskId
|
|
|
+ }, function(response) {
|
|
|
+ if (response.code === 1) {
|
|
|
+ $('#retryAnalysis').hide();
|
|
|
+ showLoadingOverlay();
|
|
|
+ startPolling();
|
|
|
+ } else {
|
|
|
+ layer.msg(response.msg);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存分析结果
|
|
|
+ function saveAnalysisResult() {
|
|
|
+ if (!currentTaskId) {
|
|
|
+ layer.msg('没有可保存的结果');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ layer.confirm('确定要保存这次AI测量结果吗?', {
|
|
|
+ icon: 3,
|
|
|
+ title: '确认保存'
|
|
|
+ }, function(index) {
|
|
|
+ $.post('/api/ai_measurement/saveResult', {
|
|
|
+ task_id: currentTaskId
|
|
|
+ }, function(response) {
|
|
|
+ if (response.code === 1) {
|
|
|
+ layer.msg('测量结果已保存', {icon: 1});
|
|
|
+ $('#saveResult').hide();
|
|
|
+
|
|
|
+ // 可以跳转到测量历史页面
|
|
|
+ setTimeout(function() {
|
|
|
+ parent.layer.close(parent.layer.getFrameIndex(window.name));
|
|
|
+ }, 1500);
|
|
|
+ } else {
|
|
|
+ layer.msg(response.msg);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ layer.close(index);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|