ai_measurement.html 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>AI身体测量</title>
  6. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  7. <link rel="stylesheet" href="/assets/css/layui.css">
  8. <link rel="stylesheet" href="/assets/css/admin.css">
  9. <style>
  10. .measurement-container {
  11. display: flex;
  12. gap: 30px;
  13. margin-top: 20px;
  14. }
  15. .figure-container {
  16. flex: 1;
  17. position: relative;
  18. background: #f8f9fa;
  19. border-radius: 10px;
  20. padding: 20px;
  21. text-align: center;
  22. }
  23. .body-figure {
  24. position: relative;
  25. width: 300px;
  26. height: 600px;
  27. margin: 0 auto;
  28. background: url('/assets/images/body_figure_male.png') no-repeat center center;
  29. background-size: contain;
  30. }
  31. .body-figure.female {
  32. background-image: url('/assets/images/body_figure_female.png');
  33. }
  34. .measurement-point {
  35. position: absolute;
  36. background: #1E9FFF;
  37. color: white;
  38. padding: 4px 8px;
  39. border-radius: 15px;
  40. font-size: 12px;
  41. white-space: nowrap;
  42. cursor: pointer;
  43. z-index: 10;
  44. }
  45. .measurement-point::before {
  46. content: '';
  47. position: absolute;
  48. width: 2px;
  49. height: 20px;
  50. background: #1E9FFF;
  51. top: 50%;
  52. transform: translateY(-50%);
  53. }
  54. .measurement-point.left::before {
  55. right: 100%;
  56. }
  57. .measurement-point.right::before {
  58. left: 100%;
  59. }
  60. .measurement-line {
  61. position: absolute;
  62. height: 1px;
  63. background: #1E9FFF;
  64. z-index: 5;
  65. }
  66. .data-container {
  67. flex: 1;
  68. }
  69. .basic-info {
  70. background: #fff;
  71. border-radius: 8px;
  72. padding: 20px;
  73. margin-bottom: 20px;
  74. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  75. }
  76. .measurement-table {
  77. background: #fff;
  78. border-radius: 8px;
  79. padding: 20px;
  80. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  81. }
  82. .measurement-table table {
  83. width: 100%;
  84. border-collapse: collapse;
  85. }
  86. .measurement-table th,
  87. .measurement-table td {
  88. padding: 12px;
  89. text-align: left;
  90. border-bottom: 1px solid #eee;
  91. }
  92. .measurement-table th {
  93. background: #f8f9fa;
  94. font-weight: bold;
  95. }
  96. .measurement-value {
  97. font-weight: bold;
  98. color: #1E9FFF;
  99. }
  100. .measurement-value.empty {
  101. color: #ccc;
  102. }
  103. .confidence-badge {
  104. display: inline-block;
  105. background: #52c41a;
  106. color: white;
  107. padding: 2px 8px;
  108. border-radius: 10px;
  109. font-size: 12px;
  110. margin-left: 10px;
  111. }
  112. .confidence-badge.medium {
  113. background: #fa8c16;
  114. }
  115. .confidence-badge.low {
  116. background: #f5222d;
  117. }
  118. .ai-controls {
  119. text-align: center;
  120. margin: 20px 0;
  121. }
  122. .loading-overlay {
  123. position: fixed;
  124. top: 0;
  125. left: 0;
  126. width: 100%;
  127. height: 100%;
  128. background: rgba(255,255,255,0.8);
  129. z-index: 1000;
  130. display: none;
  131. align-items: center;
  132. justify-content: center;
  133. flex-direction: column;
  134. }
  135. .loading-content {
  136. text-align: center;
  137. }
  138. .loading-spinner {
  139. width: 60px;
  140. height: 60px;
  141. border: 4px solid #f3f3f3;
  142. border-top: 4px solid #1E9FFF;
  143. border-radius: 50%;
  144. animation: spin 1s linear infinite;
  145. margin-bottom: 20px;
  146. }
  147. @keyframes spin {
  148. 0% { transform: rotate(0deg); }
  149. 100% { transform: rotate(360deg); }
  150. }
  151. .progress-bar {
  152. width: 300px;
  153. height: 6px;
  154. background: #f0f0f0;
  155. border-radius: 3px;
  156. overflow: hidden;
  157. margin: 10px auto;
  158. }
  159. .progress-fill {
  160. height: 100%;
  161. background: #1E9FFF;
  162. border-radius: 3px;
  163. transition: width 0.3s ease;
  164. }
  165. .warning-item {
  166. background: #fff7e6;
  167. border: 1px solid #ffd591;
  168. padding: 8px 12px;
  169. border-radius: 4px;
  170. margin-bottom: 5px;
  171. font-size: 12px;
  172. color: #fa8c16;
  173. }
  174. </style>
  175. </head>
  176. <body>
  177. <div class="layui-fluid" style="padding: 15px;">
  178. <!-- 头部信息 -->
  179. <div class="layui-card">
  180. <div class="layui-card-header">
  181. <h3>AI身体测量 - {$profile.profile_name}</h3>
  182. <span class="layui-badge layui-bg-blue">{$profile.gender_text}</span>
  183. <span class="layui-badge layui-bg-green">{$profile.is_own_text}</span>
  184. </div>
  185. <div class="layui-card-body">
  186. <!-- AI控制按钮 -->
  187. <div class="ai-controls">
  188. <button class="layui-btn layui-btn-lg" id="startAnalysis">
  189. <i class="layui-icon layui-icon-play"></i> 开始AI分析
  190. </button>
  191. <button class="layui-btn layui-btn-normal layui-btn-lg" id="retryAnalysis" style="display: none;">
  192. <i class="layui-icon layui-icon-refresh"></i> 重新分析
  193. </button>
  194. <button class="layui-btn layui-btn-warm layui-btn-lg" id="saveResult" style="display: none;">
  195. <i class="layui-icon layui-icon-ok"></i> 保存结果
  196. </button>
  197. </div>
  198. <!-- 置信度和警告信息 -->
  199. <div id="analysisInfo" style="display: none;">
  200. <div style="text-align: center; margin: 10px 0;">
  201. <span>分析置信度:</span>
  202. <span id="confidenceValue">85%</span>
  203. <span id="confidenceBadge" class="confidence-badge">高</span>
  204. </div>
  205. <div id="warningsContainer"></div>
  206. </div>
  207. <!-- 测量结果展示 -->
  208. <div class="measurement-container">
  209. <!-- 人体示意图 -->
  210. <div class="figure-container">
  211. <h4>身体示意图</h4>
  212. <div id="bodyFigure" class="body-figure {if $profile.gender == 2}female{/if}">
  213. <!-- 测量点将通过JavaScript动态添加 -->
  214. </div>
  215. </div>
  216. <!-- 数据表格 -->
  217. <div class="data-container">
  218. <!-- 基础信息 -->
  219. <div class="basic-info">
  220. <h4>身体数据</h4>
  221. <table>
  222. <tr>
  223. <td><strong>身高</strong></td>
  224. <td>{$profile.height|default='--'}cm</td>
  225. <td><strong>体重</strong></td>
  226. <td>{$profile.weight|default='--'}kg</td>
  227. </tr>
  228. </table>
  229. </div>
  230. <!-- 测量数据表格 -->
  231. <div class="measurement-table">
  232. <h4>测量数据</h4>
  233. <table id="measurementTable">
  234. <thead>
  235. <tr>
  236. <th>部位</th>
  237. <th>数值(cm)</th>
  238. <th>部位</th>
  239. <th>数值(cm)</th>
  240. </tr>
  241. </thead>
  242. <tbody id="measurementTableBody">
  243. <!-- 数据将通过JavaScript动态填充 -->
  244. </tbody>
  245. </table>
  246. </div>
  247. </div>
  248. </div>
  249. </div>
  250. </div>
  251. </div>
  252. <!-- 加载遮罩 -->
  253. <div class="loading-overlay" id="loadingOverlay">
  254. <div class="loading-content">
  255. <div class="loading-spinner"></div>
  256. <div id="loadingText">AI正在分析您的身体照片...</div>
  257. <div class="progress-bar">
  258. <div class="progress-fill" id="progressFill" style="width: 0%"></div>
  259. </div>
  260. <div id="progressText">0%</div>
  261. </div>
  262. </div>
  263. <script src="/assets/js/layui.js"></script>
  264. <script>
  265. layui.use(['layer'], function(){
  266. var layer = layui.layer;
  267. var profileId = '{$profile.id}';
  268. var currentTaskId = null;
  269. var analysisInterval = null;
  270. // 初始化页面
  271. initializePage();
  272. // 开始AI分析
  273. $('#startAnalysis').click(function() {
  274. startAiAnalysis();
  275. });
  276. // 重新分析
  277. $('#retryAnalysis').click(function() {
  278. retryAnalysis();
  279. });
  280. // 保存结果
  281. $('#saveResult').click(function() {
  282. saveAnalysisResult();
  283. });
  284. // 初始化页面数据
  285. function initializePage() {
  286. // 加载默认的空数据表格
  287. renderMeasurementTable({});
  288. // 检查是否有进行中的任务
  289. checkExistingTask();
  290. }
  291. // 检查现有任务
  292. function checkExistingTask() {
  293. $.get('/api/ai_measurement/getResult', {
  294. profile_id: profileId
  295. }, function(response) {
  296. if (response.code === 1 && response.data) {
  297. switch (response.data.status) {
  298. case 'processing':
  299. case 'pending':
  300. showLoadingOverlay();
  301. startPolling();
  302. break;
  303. case 'completed':
  304. renderAnalysisResult(response.data.data);
  305. break;
  306. case 'failed':
  307. if (response.data.can_retry) {
  308. $('#retryAnalysis').show();
  309. }
  310. break;
  311. }
  312. }
  313. });
  314. }
  315. // 开始AI分析
  316. function startAiAnalysis() {
  317. // 获取档案的身体照片
  318. $.get('/api/body_profile/detail', {
  319. profile_id: profileId
  320. }, function(response) {
  321. if (response.code === 1) {
  322. var photos = response.data.body_photos_array;
  323. if (!photos.front || !photos.side || !photos.back) {
  324. layer.msg('请先上传完整的身体照片(正面、侧面、背面)');
  325. return;
  326. }
  327. // 发起AI分析请求
  328. $.post('/api/ai_measurement/startAnalysis', {
  329. profile_id: profileId,
  330. photos: photos
  331. }, function(res) {
  332. if (res.code === 1) {
  333. currentTaskId = res.data.task_id;
  334. showLoadingOverlay();
  335. startPolling();
  336. } else {
  337. layer.msg(res.msg);
  338. }
  339. });
  340. } else {
  341. layer.msg(response.msg);
  342. }
  343. });
  344. }
  345. // 显示加载遮罩
  346. function showLoadingOverlay() {
  347. $('#loadingOverlay').show();
  348. $('#startAnalysis').hide();
  349. }
  350. // 隐藏加载遮罩
  351. function hideLoadingOverlay() {
  352. $('#loadingOverlay').hide();
  353. $('#startAnalysis').show();
  354. }
  355. // 开始轮询检查结果
  356. function startPolling() {
  357. if (analysisInterval) {
  358. clearInterval(analysisInterval);
  359. }
  360. analysisInterval = setInterval(function() {
  361. checkAnalysisResult();
  362. }, 2000); // 每2秒检查一次
  363. }
  364. // 检查分析结果
  365. function checkAnalysisResult() {
  366. var params = currentTaskId ? {task_id: currentTaskId} : {profile_id: profileId};
  367. $.get('/api/ai_measurement/getResult', params, function(response) {
  368. if (response.code === 1) {
  369. switch (response.data.status) {
  370. case 'pending':
  371. updateProgress(10, '任务排队中...');
  372. break;
  373. case 'processing':
  374. var progress = response.data.progress || 50;
  375. updateProgress(progress, 'AI正在分析您的身体照片...');
  376. break;
  377. case 'completed':
  378. clearInterval(analysisInterval);
  379. hideLoadingOverlay();
  380. renderAnalysisResult(response.data.data);
  381. break;
  382. case 'failed':
  383. clearInterval(analysisInterval);
  384. hideLoadingOverlay();
  385. layer.msg('分析失败: ' + response.data.message);
  386. if (response.data.can_retry) {
  387. $('#retryAnalysis').show();
  388. }
  389. break;
  390. }
  391. }
  392. });
  393. }
  394. // 更新进度
  395. function updateProgress(progress, text) {
  396. $('#progressFill').css('width', progress + '%');
  397. $('#progressText').text(progress + '%');
  398. $('#loadingText').text(text);
  399. }
  400. // 渲染分析结果
  401. function renderAnalysisResult(data) {
  402. // 显示置信度
  403. if (data.confidence) {
  404. var confidence = Math.round(data.confidence * 100);
  405. $('#confidenceValue').text(confidence + '%');
  406. var badge = $('#confidenceBadge');
  407. if (confidence >= 80) {
  408. badge.removeClass('medium low').addClass('high').text('高');
  409. } else if (confidence >= 60) {
  410. badge.removeClass('high low').addClass('medium').text('中');
  411. } else {
  412. badge.removeClass('high medium').addClass('low').text('低');
  413. }
  414. $('#analysisInfo').show();
  415. }
  416. // 显示警告信息
  417. if (data.warnings && data.warnings.length > 0) {
  418. var warningsHtml = '';
  419. data.warnings.forEach(function(warning) {
  420. warningsHtml += '<div class="warning-item">' + warning + '</div>';
  421. });
  422. $('#warningsContainer').html(warningsHtml);
  423. }
  424. // 渲染测量点
  425. renderMeasurementPoints(data.measurements);
  426. // 渲染数据表格
  427. renderMeasurementTable(data.measurements);
  428. // 显示保存按钮
  429. $('#saveResult').show();
  430. }
  431. // 渲染测量点
  432. function renderMeasurementPoints(measurements) {
  433. var figure = $('#bodyFigure');
  434. figure.find('.measurement-point, .measurement-line').remove();
  435. if (!measurements) return;
  436. Object.keys(measurements).forEach(function(field) {
  437. var measurement = measurements[field];
  438. if (!measurement.value || !measurement.position) return;
  439. var point = $('<div class="measurement-point ' + measurement.side + '">')
  440. .text(measurement.label + ': ' + measurement.value + measurement.unit)
  441. .css({
  442. left: (measurement.position.x * 100) + '%',
  443. top: (measurement.position.y * 100) + '%'
  444. });
  445. figure.append(point);
  446. });
  447. }
  448. // 渲染测量数据表格
  449. function renderMeasurementTable(measurements) {
  450. var tbody = $('#measurementTableBody');
  451. tbody.empty();
  452. // 默认字段配置
  453. var defaultFields = [
  454. ['胸围', 'chest'], ['腰围', 'waist'],
  455. ['臀围', 'hip'], ['大腿围', 'thigh'],
  456. ['小腿围', 'calf'], ['上臂围', 'upper_arm'],
  457. ['肩宽', 'shoulder_width'], ['颈围', 'neck']
  458. ];
  459. // 按两列排列
  460. for (var i = 0; i < defaultFields.length; i += 2) {
  461. var row = '<tr>';
  462. for (var j = 0; j < 2; j++) {
  463. if (i + j < defaultFields.length) {
  464. var field = defaultFields[i + j];
  465. var label = field[0];
  466. var key = field[1];
  467. var value = measurements[key] ? measurements[key].value : null;
  468. var displayValue = value ? value + 'cm' : '--';
  469. var cssClass = value ? 'measurement-value' : 'measurement-value empty';
  470. row += '<td>' + label + '</td>';
  471. row += '<td><span class="' + cssClass + '">' + displayValue + '</span></td>';
  472. } else {
  473. row += '<td></td><td></td>';
  474. }
  475. }
  476. row += '</tr>';
  477. tbody.append(row);
  478. }
  479. }
  480. // 重新分析
  481. function retryAnalysis() {
  482. if (!currentTaskId) {
  483. layer.msg('没有可重试的任务');
  484. return;
  485. }
  486. $.post('/api/ai_measurement/retryAnalysis', {
  487. task_id: currentTaskId
  488. }, function(response) {
  489. if (response.code === 1) {
  490. $('#retryAnalysis').hide();
  491. showLoadingOverlay();
  492. startPolling();
  493. } else {
  494. layer.msg(response.msg);
  495. }
  496. });
  497. }
  498. // 保存分析结果
  499. function saveAnalysisResult() {
  500. if (!currentTaskId) {
  501. layer.msg('没有可保存的结果');
  502. return;
  503. }
  504. layer.confirm('确定要保存这次AI测量结果吗?', {
  505. icon: 3,
  506. title: '确认保存'
  507. }, function(index) {
  508. $.post('/api/ai_measurement/saveResult', {
  509. task_id: currentTaskId
  510. }, function(response) {
  511. if (response.code === 1) {
  512. layer.msg('测量结果已保存', {icon: 1});
  513. $('#saveResult').hide();
  514. // 可以跳转到测量历史页面
  515. setTimeout(function() {
  516. parent.layer.close(parent.layer.getFrameIndex(window.name));
  517. }, 1500);
  518. } else {
  519. layer.msg(response.msg);
  520. }
  521. });
  522. layer.close(index);
  523. });
  524. }
  525. });
  526. </script>
  527. </body>
  528. </html>