Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
119 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "bilybaofu/baby-growth-curve", "files": {"baby-tracker.js": {"5b07ca027b872c8c79ccdefa063fb954b92be2ac": {"t": 1786068268.397868, "n": 112807, "h": "d90ab3df4667a9cc", "c": "// 婴幼儿体检报告智能识别系统 - 百度OCR版本\n(function() {\n 'use strict';\n \n class BabyHealthTracker {\n constructor() {\n this.apiKey = localStorage.getItem('deepseek_api_key') || '';\n this.baiduOcrConfig = {\n apiKey: localStorage.getItem('baidu_ocr_api_key') || '',\n secretKey: localStorage.getItem('baidu_ocr_secret_key') || '',\n accessToken: localStorage.getItem('baidu_ocr_access_token') || '',\n tokenExpireTime: parseInt(localStorage.getItem('baidu_ocr_token_expire') || '0')\n };\n this.checkRecords = JSON.parse(localStorage.getItem('baby_check_records') || '[]');\n this.babyInfo = JSON.parse(localStorage.getItem('baby_info') || '{}');\n this.isUploading = false;\n this.lastOCRResult = null;\n this.whoData = this.initCompleteWHOData();\n this.growthChart = null;\n this.tempExtractedData = null;\n \n this.init();\n }\n\n initCompleteWHOData() {\n return {\n maleHeight: {\n 0: [44.2, 46.1, 48.0, 49.9, 51.8, 53.7, 55.6],\n 1: [48.9, 50.8, 52.8, 54.7, 56.7, 58.6, 60.6],\n 2: [52.4, 54.4, 56.4, 58.4, 60.4, 62.4, 64.4],\n 3: [55.3, 57.3, 59.4, 61.4, 63.5, 65.5, 67.6],\n 4: [57.6, 59.7, 61.8, 63.9, 66.0, 68.0, 70.1],\n 5: [59.6, 61.7, 63.8, 65.9, 68.0, 70.1, 72.2],\n 6: [61.2, 63.3, 65.5, 67.6, 69.8, 71.9, 74.0],\n 7: [62.7, 64.8, 67.0, 69.2, 71.3, 73.5, 75.7],\n 8: [64.0, 66.2, 68.4, 70.6, 72.8, 75.0, 77.2],\n 9: [65.2, 67.5, 69.7, 72.0, 74.2, 76.5, 78.7],\n 10: [66.4, 68.7, 71.0, 73.3, 75.6, 77.9, 80.1],\n 11: [67.6, 69.9, 72.2, 74.5, 76.9, 79.2, 81.5],\n 12: [68.6, 71.0, 73.4, 75.7, 78.1, 80.5, 82.9],\n 13: [69.6, 72.1, 74.5, 76.9, 79.3, 81.8, 84.2],\n 14: [70.6, 73.1, 75.6, 78.0, 80.5, 83.0, 85.5],\n 15: [71.6, 74.1, 76.6, 79.1, 81.7, 84.2, 86.7],\n 16: [72.5, 75.0, 77.6, 80.2, 82.8, 85.4, 88.0],\n 17: [73.3, 76.0, 78.6, 81.2, 83.9, 86.5, 89.2],\n 18: [74.2, 76.9, 79.6, 82.3, 85.0, 87.7, 90.4],\n 19: [75.0, 77.7, 80.5, 83.2, 86.0, 88.8, 91.5],\n 20: [75.8, 78.6, 81.4, 84.2, 87.0, 89.8, 92.6],\n 21: [76.5, 79.4, 82.3, 85.1, 88.0, 90.9, 93.8],\n 22: [77.2, 80.2, 83.1, 86.0, 89.0, 91.9, 94.9],\n 23: [78.0, 81.0, 83.9, 86.9, 89.9, 92.9, 95.9],\n 24: [78.7, 81.7, 84.8, 87.8, 90.9, 93.9, 97.0],\n 25: [78.6, 81.7, 84.9, 88.0, 91.1, 94.2, 97.3],\n 26: [79.3, 82.5, 85.6, 88.8, 92.0, 95.2, 98.3],\n 27: [79.9, 83.1, 86.4, 89.6, 92.9, 96.1, 99.3],\n 28: [80.5, 83.8, 87.1, 90.4, 93.7, 97.0, 100.3],\n 29: [81.1, 84.5, 87.8, 91.2, 94.5, 97.9, 101.2],\n 30: [81.7, 85.1, 88.5, 91.9, 95.3, 98.7, 102.1],\n 31: [82.3, 85.7, 89.2, 92.7, 96.1, 99.6, 103.0],\n 32: [82.8, 86.4, 89.9, 93.4, 96.9, 100.4, 103.9],\n 33: [83.4, 86.9, 90.5, 94.1, 97.6, 101.2, 104.8],\n 34: [83.9, 87.5, 91.1, 94.8, 98.4, 102.0, 105.6],\n 35: [84.4, 88.1, 91.8, 95.4, 99.1, 102.7, 106.4],\n 36: [85.0, 88.7, 92.4, 96.1, 99.8, 103.5, 107.2]\n },\n femaleHeight: {\n 0: [43.6, 45.4, 47.3, 49.1, 51.0, 52.9, 54.7],\n 1: [47.8, 49.8, 51.7, 53.7, 55.6, 57.6, 59.5],\n 2: [51.0, 53.0, 55.0, 57.1, 59.1, 61.1, 63.2],\n 3: [53.5, 55.6, 57.7, 59.8, 61.9, 64.0, 66.1],\n 4: [55.6, 57.8, 59.9, 62.1, 64.3, 66.4, 68.6],\n 5: [57.4, 59.6, 61.8, 64.0, 66.2, 68.5, 70.7],\n 6: [58.9, 61.2, 63.5, 65.7, 68.0, 70.3, 72.5],\n 7: [60.3, 62.7, 65.0, 67.3, 69.6, 71.9, 74.2],\n 8: [61.7, 64.0, 66.4, 68.7, 71.1, 73.5, 75.8],\n 9: [62.9, 65.3, 67.7, 70.1, 72.6, 75.0, 77.4],\n 10: [64.1, 66.5, 69.0, 71.5, 73.9, 76.4, 78.9],\n 11: [65.2, 67.7, 70.3, 72.8, 75.3, 77.8, 80.3],\n 12: [66.3, 68.9, 71.4, 74.0, 76.6, 79.2, 81.7],\n 13: [67.3, 70.0, 72.6, 75.2, 77.8, 80.5, 83.1],\n 14: [68.3, 71.0, 73.7, 76.4, 79.1, 81.7, 84.4],\n 15: [69.3, 72.0, 74.8, 77.5, 80.2, 83.0, 85.7],\n 16: [70.2, 73.0, 75.8, 78.6, 81.4, 84.2, 87.0],\n 17: [71.1, 74.0, 76.8, 79.7, 82.5, 85.4, 88.2],\n 18: [72.0, 74.9, 77.8, 80.7, 83.6, 86.5, 89.4],\n 19: [72.8, 75.8, 78.8, 81.7, 84.7, 87.6, 90.6],\n 20: [73.7, 76.7, 79.7, 82.7, 85.7, 88.7, 91.7],\n 21: [74.5, 77.5, 80.6, 83.7, 86.7, 89.8, 92.9],\n 22: [75.2, 78.4, 81.5, 84.6, 87.7, 90.8, 94.0],\n 23: [76.0, 79.2, 82.3, 85.5, 88.7, 91.9, 95.0],\n 24: [76.7, 80.0, 83.2, 86.4, 89.6, 92.9, 96.1],\n 25: [76.8, 80.0, 83.3, 86.6, 89.9, 93.1, 96.4],\n 26: [77.5, 80.8, 84.1, 87.4, 90.8, 94.1, 97.4],\n 27: [78.1, 81.5, 84.9, 88.3, 91.7, 95.0, 98.4],\n 28: [78.8, 82.2, 85.7, 89.1, 92.5, 96.0, 99.4],\n 29: [79.5, 82.9, 86.4, 89.9, 93.4, 96.9, 100.3],\n 30: [80.1, 83.6, 87.1, 90.7, 94.2, 97.7, 101.3],\n 31: [80.7, 84.3, 87.9, 91.4, 95.0, 98.6, 102.2],\n 32: [81.3, 84.9, 88.6, 92.2, 95.8, 99.4, 103.1],\n 33: [81.9, 85.6, 89.3, 92.9, 96.6, 100.3, 103.9],\n 34: [82.5, 86.2, 89.9, 93.6, 97.4, 101.1, 104.8],\n 35: [83.1, 86.8, 90.6, 94.4, 98.1, 101.9, 105.6],\n 36: [83.6, 87.4, 91.2, 95.1, 98.9, 102.7, 106.5]\n },\n maleWeight: {\n 0: [2.1, 2.5, 2.9, 3.3, 3.9, 4.4, 5.0],\n 1: [2.9, 3.4, 3.9, 4.5, 5.1, 5.8, 6.6],\n 2: [3.8, 4.3, 4.9, 5.6, 6.3, 7.1, 8.0],\n 3: [4.4, 5.0, 5.7, 6.4, 7.2, 8.0, 9.0],\n 4: [4.9, 5.6, 6.2, 7.0, 7.8, 8.7, 9.7],\n 5: [5.3, 6.0, 6.7, 7.5, 8.4, 9.3, 10.4],\n 6: [5.7, 6.4, 7.1, 7.9, 8.8, 9.8, 10.9],\n 7: [5.9, 6.7, 7.4, 8.3, 9.2, 10.3, 11.4],\n 8: [6.2, 6.9, 7.7, 8.6, 9.6, 10.7, 11.9],\n 9: [6.4, 7.1, 8.0, 8.9, 9.9, 11.0, 12.3],\n 10: [6.6, 7.4, 8.2, 9.2, 10.2, 11.4, 12.7],\n 11: [6.8, 7.6, 8.4, 9.4, 10.5, 11.7, 13.0],\n 12: [6.9, 7.7, 8.6, 9.6, 10.8, 12.0, 13.3],\n 13: [7.1, 7.9, 8.8, 9.9, 11.0, 12.3, 13.7],\n 14: [7.2, 8.1, 9.0, 10.1, 11.3, 12.6, 14.0],\n 15: [7.4, 8.3, 9.2, 10.3, 11.5, 12.8, 14.3],\n 16: [7.5, 8.4, 9.4, 10.5, 11.7, 13.1, 14.6],\n 17: [7.7, 8.6, 9.6, 10.7, 12.0, 13.4, 14.9],\n 18: [7.8, 8.8, 9.8, 10.9, 12.2, 13.7, 15.3],\n 19: [8.0, 8.9, 10.0, 11.1, 12.5, 13.9, 15.6],\n 20: [8.1, 9.1, 10.1, 11.3, 12.7, 14.2, 15.9],\n 21: [8.2, 9.2, 10.3, 11.5, 12.9, 14.5, 16.2],\n 22: [8.4, 9.4, 10.5, 11.8, 13.2, 14.7, 16.5],\n 23: [8.5, 9.5, 10.7, 12.0, 13.4, 15.0, 16.8],\n 24: [8.6, 9.7, 10.8, 12.2, 13.6, 15.3, 17.1],\n 25: [8.8, 9.8, 11.0, 12.4, 13.9, 15.5, 17.5],\n 26: [8.9, 10.0, 11.2, 12.5, 14.1, 15.8, 17.8],\n 27: [9.0, 10.1, 11.3, 12.7, 14.3, 16.1, 18.1],\n 28: [9.1, 10.2, 11.5, 12.9, 14.5, 16.3, 18.4],\n 29: [9.2, 10.4, 11.7, 13.1, 14.8, 16.6, 18.7],\n 30: [9.4, 10.5, 11.8, 13.3, 15.0, 16.9, 19.0],\n 31: [9.5, 10.7, 12.0, 13.5, 15.2, 17.1, 19.3],\n 32: [9.6, 10.8, 12.1, 13.7, 15.4, 17.4, 19.6],\n 33: [9.7, 10.9, 12.3, 13.8, 15.6, 17.6, 19.9],\n 34: [9.8, 11.0, 12.4, 14.0, 15.8, 17.8, 20.2],\n 35: [9.9, 11.2, 12.6, 14.2, 16.0, 18.1, 20.4],\n 36: [10.0, 11.3, 12.7, 14.3, 16.2, 18.3, 20.7]\n },\n femaleWeight: {\n 0: [2.0, 2.4, 2.8, 3.2, 3.7, 4.2, 4.8],\n 1: [2.7, 3.2, 3.6, 4.2, 4.8, 5.5, 6.2],\n 2: [3.4, 3.9, 4.5, 5.1, 5.8, 6.6, 7.5],\n 3: [4.0, 4.5, 5.2, 5.8, 6.6, 7.5, 8.5],\n 4: [4.4, 5.0, 5.7, 6.4, 7.3, 8.2, 9.3],\n 5: [4.8, 5.4, 6.1, 6.9, 7.8, 8.8, 10.0],\n 6: [5.1, 5.7, 6.5, 7.3, 8.2, 9.3, 10.6],\n 7: [5.3, 6.0, 6.8, 7.6, 8.6, 9.8, 11.1],\n 8: [5.6, 6.3, 7.0, 7.9, 9.0, 10.2, 11.6],\n 9: [5.8, 6.5, 7.3, 8.2, 9.3, 10.5, 12.0],\n 10: [5.9, 6.7, 7.5, 8.5, 9.6, 10.9, 12.4],\n 11: [6.1, 6.9, 7.7, 8.7, 9.9, 11.2, 12.8],\n 12: [6.3, 7.0, 7.9, 8.9, 10.1, 11.5, 13.1],\n 13: [6.4, 7.2, 8.1, 9.2, 10.4, 11.8, 13.5],\n 14: [6.6, 7.4, 8.3, 9.4, 10.6, 12.1, 13.8],\n 15: [6.7, 7.6, 8.5, 9.6, 10.9, 12.4, 14.1],\n 16: [6.9, 7.7, 8.7, 9.8, 11.1, 12.6, 14.5],\n 17: [7.0, 7.9, 8.9, 10.0, 11.4, 12.9, 14.8],\n 18: [7.2, 8.1, 9.1, 10.2, 11.6, 13.2, 15.1],\n 19: [7.3, 8.2, 9.2, 10.4, 11.8, 13.5, 15.4],\n 20: [7.5, 8.4, 9.4, 10.6, 12.1, 13.7, 15.7],\n 21: [7.6, 8.6, 9.6, 10.9, 12.3, 14.0, 16.0],\n 22: [7.8, 8.7, 9.8, 11.1, 12.5, 14.3, 16.4],\n 23: [7.9, 8.9, 10.0, 11.3, 12.8, 14.6, 16.7],\n 24: [8.1, 9.0, 10.2, 11.5, 13.0, 14.8, 17.0],\n 25: [8.2, 9.2, 10.3, 11.7, 13.3, 15.1, 17.3],\n 26: [8.4, 9.4, 10.5, 11.9, 13.5, 15.4, 17.7],\n 27: [8.5, 9.5, 10.7, 12.1, 13.7, 15.7, 18.0],\n 28: [8.6, 9.7, 10.9, 12.3, 14.0, 16.0, 18.3],\n 29: [8.8, 9.8, 11.1, 12.5, 14.2, 16.2, 18.7],\n 30: [8.9, 10.0, 11.2, 12.7, 14.4, 16.5, 19.0],\n 31: [9.0, 10.1, 11.4, 12.9, 14.7, 16.8, 19.3],\n 32: [9.1, 10.3, 11.6, 13.1, 14.9, 17.1, 19.6],\n 33: [9.3, 10.4, 11.7, 13.3, 15.1, 17.3, 20.0],\n 34: [9.4, 10.5, 11.9, 13.5, 15.4, 17.6, 20.3],\n 35: [9.5, 10.7, 12.0, 13.7, 15.6, 17.9, 20.6],\n 36: [9.6, 10.8, 12.2, 13.9, 15.8, 18.1, 20.9]\n },\n maleHeadCircumference: {\n 0: [30.4, 31.7, 33.0, 34.3, 35.6, 36.9, 38.3],\n 1: [33.4, 34.6, 35.8, 37.0, 38.2, 39.4, 40.6],\n 2: [35.7, 36.8, 37.9, 39.1, 40.2, 41.4, 42.6],\n 3: [37.1, 38.2, 39.3, 40.5, 41.6, 42.8, 44.1],\n 4: [38.1, 39.3, 40.4, 41.6, 42.8, 44.0, 45.3],\n 5: [39.0, 40.2, 41.3, 42.5, 43.8, 45.0, 46.3],\n 6: [39.8, 41.0, 42.1, 43.4, 44.6, 45.9, 47.2],\n 7: [40.5, 41.7, 42.8, 44.0, 45.3, 46.6, 47.9],\n 8: [41.1, 42.2, 43.4, 44.6, 45.9, 47.2, 48.5],\n 9: [41.5, 42.7, 43.9, 45.1, 46.4, 47.7, 49.0],\n 10: [41.9, 43.1, 44.3, 45.5, 46.8, 48.1, 49.4],\n 11: [42.3, 43.4, 44.6, 45.8, 47.1, 48.4, 49.8],\n 12: [42.5, 43.7, 44.9, 46.1, 47.4, 48.7, 50.1],\n 13: [42.8, 44.0, 45.1, 46.4, 47.7, 49.0, 50.3],\n 14: [43.0, 44.2, 45.4, 46.6, 47.9, 49.2, 50.6],\n 15: [43.2, 44.4, 45.6, 46.8, 48.1, 49.4, 50.8],\n 16: [43.4, 44.6, 45.8, 47.0, 48.3, 49.6, 51.0],\n 17: [43.6, 44.7, 45.9, 47.2, 48.5, 49.8, 51.2],\n 18: [43.8, 44.9, 46.1, 47.4, 48.7, 50.0, 51.4],\n 19: [43.9, 45.1, 46.3, 47.5, 48.8, 50.2, 51.6],\n 20: [44.1, 45.3, 46.5, 47.7, 49.0, 50.4, 51.7],\n 21: [44.3, 45.4, 46.6, 47.9, 49.2, 50.5, 51.9],\n 22: [44.4, 45.6, 46.8, 48.1, 49.4, 50.7, 52.1],\n 23: [44.6, 45.7, 47.0, 48.2, 49.5, 50.9, 52.3],\n 24: [44.7, 45.9, 47.1, 48.3, 49.6, 51.0, 52.4],\n 25: [44.8, 46.0, 47.2, 48.4, 49.7, 51.1, 52.5],\n 26: [44.9, 46.1, 47.3, 48.5, 49.8, 51.2, 52.6],\n 27: [45.0, 46.2, 47.4, 48.7, 50.0, 51.3, 52.7],\n 28: [45.1, 46.3, 47.5, 48.8, 50.1, 51.4, 52.8],\n 29: [45.2, 46.3, 47.6, 48.8, 50.2, 51.5, 52.9],\n 30: [45.3, 46.4, 47.7, 48.9, 50.3, 51.6, 53.0],\n 31: [45.4, 46.5, 47.8, 49.0, 50.4, 51.7, 53.1],\n 32: [45.4, 46.6, 47.8, 49.1, 50.4, 51.8, 53.2],\n 33: [45.5, 46.7, 47.9, 49.2, 50.5, 51.9, 53.3],\n 34: [45.6, 46.7, 48.0, 49.2, 50.6, 52.0, 53.4],\n 35: [45.6, 46.8, 48.0, 49.3, 50.6, 52.0, 53.4],\n 36: [45.7, 46.8, 48.1, 49.3, 50.7, 52.1, 53.5]\n },\n femaleHeadCircumference: {\n 0: [30.1, 31.4, 32.7, 33.9, 35.2, 36.5, 37.7],\n 1: [32.9, 34.0, 35.2, 36.3, 37.5, 38.6, 39.8],\n 2: [34.9, 36.0, 37.1, 38.2, 39.3, 40.4, 41.6],\n 3: [36.2, 37.3, 38.4, 39.5, 40.7, 41.8, 42.9],\n 4: [37.2, 38.3, 39.4, 40.6, 41.7, 42.9, 44.1],\n 5: [38.0, 39.2, 40.3, 41.5, 42.6, 43.8, 45.0],\n 6: [38.8, 39.9, 41.1, 42.2, 43.4, 44.6, 45.9],\n 7: [39.4, 40.6, 41.7, 42.9, 44.1, 45.3, 46.6],\n 8: [40.0, 41.1, 42.3, 43.5, 44.7, 45.9, 47.2],\n 9: [40.4, 41.6, 42.8, 44.0, 45.2, 46.5, 47.7],\n 10: [40.8, 42.0, 43.2, 44.4, 45.6, 46.9, 48.2],\n 11: [41.2, 42.4, 43.6, 44.8, 46.0, 47.3, 48.6],\n 12: [41.5, 42.7, 43.9, 45.1, 46.4, 47.6, 48.9],\n 13: [41.8, 42.9, 44.2, 45.4, 46.6, 47.9, 49.2],\n 14: [42.0, 43.2, 44.4, 45.6, 46.9, 48.2, 49.5],\n 15: [42.2, 43.4, 44.6, 45.9, 47.1, 48.4, 49.7],\n 16: [42.4, 43.6, 44.8, 46.1, 47.3, 48.6, 50.0],\n 17: [42.6, 43.8, 45.0, 46.2, 47.5, 48.8, 50.1],\n 18: [42.7, 43.9, 45.2, 46.4, 47.7, 49.0, 50.3],\n 19: [42.9, 44.1, 45.3, 46.6, 47.9, 49.2, 50.5],\n 20: [43.1, 44.3, 45.5, 46.7, 48.0, 49.3, 50.7],\n 21: [43.2, 44.4, 45.6, 46.9, 48.2, 49.5, 50.9],\n 22: [43.4, 44.6, 45.8, 47.1, 48.4, 49.7, 51.0],\n 23: [43.5, 44.7, 45.9, 47.2, 48.5, 49.8, 51.2],\n 24: [43.6, 44.8, 46.1, 47.3, 48.6, 50.0, 51.3],\n 25: [43.7, 44.9, 46.2, 47.4, 48.8, 50.1, 51.5],\n 26: [43.8, 45.0, 46.3, 47.5, 48.9, 50.2, 51.6],\n 27: [43.9, 45.1, 46.4, 47.6, 49.0, 50.3, 51.7],\n 28: [44.0, 45.2, 46.5, 47.7, 49.1, 50.4, 51.8],\n 29: [44.1, 45.3, 46.6, 47.8, 49.2, 50.5, 51.9],\n 30: [44.2, 45.4, 46.7, 47.9, 49.3, 50.6, 52.0],\n 31: [44.3, 45.5, 46.8, 48.0, 49.4, 50.7, 52.1],\n 32: [44.3, 45.6, 46.8, 48.1, 49.5, 50.8, 52.2],\n 33: [44.4, 45.7, 46.9, 48.2, 49.6, 50.9, 52.3],\n 34: [44.5, 45.8, 47.0, 48.3, 49.7, 51.0, 52.4],\n 35: [44.6, 45.8, 47.1, 48.4, 49.8, 51.1, 52.5],\n 36: [44.7, 45.9, 47.2, 48.5, 49.9, 51.2, 52.7]\n }\n };\n }\n\n init() {\n this.bindEvents();\n this.loadSavedData();\n this.updateUI();\n this.initializeChart();\n }\n\n validateBasicInfo() {\n const name = this.babyInfo.name ? this.babyInfo.name.trim() : '';\n const gender = this.babyInfo.gender;\n const birthDate = this.babyInfo.birthDate;\n \n console.log('验证基本信息:', { name, gender, birthDate });\n \n if (!name || !gender || !birthDate) {\n this.showMessage('🍼 请先完整填写宝宝的基本信息(姓名、性别、出生日期)', 'error');\n this.switchToBasicInfoTab();\n return false;\n }\n return true;\n }\n\n switchToBasicInfoTab() {\n const basicInfoTab = document.querySelector('[data-tab=\"basic-info\"]');\n const basicInfoContent = document.getElementById('basic-info');\n \n if (basicInfoTab && basicInfoContent) {\n document.querySelectorAll('.tab-button').forEach(tab => tab.classList.remove('active'));\n document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));\n \n basicInfoTab.classList.add('active');\n basicInfoContent.classList.add('active');\n }\n }\n\n bindEvents() {\n const self = this;\n \n // API密钥设置\n const apiKeyInput = document.getElementById('apiKeyInput');\n if (apiKeyInput) {\n apiKeyInput.addEventListener('change', function(e) {\n self.apiKey = e.target.value.trim();\n self.updateApiStatus();\n });\n }\n\n // API密钥保存按钮\n const saveApiKeyBtn = document.getElementById('saveApiKeyBtn');\n if (saveApiKeyBtn) {\n saveApiKeyBtn.addEventListener('click', function() {\n self.saveApiKey();\n });\n }\n\n // 百度OCR配置保存\n const saveBaiduOcrBtn = document.getElementById('saveBaiduOcrBtn');\n if (saveBaiduOcrBtn) {\n saveBaiduOcrBtn.addEventListener('click', function() {\n self.saveBaiduOcrConfig();\n });\n }\n const testBaiduOcrBtn = document.getElementById('testBaiduOcrBtn');\n if (testBaiduOcrBtn) {\n testBaiduOcrBtn.addEventListener('click', function() {\n console.log('🔧 用户点击了测试连接按钮');\n self.testBaiduOcrConnection();\n });\n }\n\n // 图片上传\n const reportUpload = document.getElementById('reportUpload');\n if (reportUpload) {\n reportUpload.addEventListener('change', function(e) {\n self.handleImageUpload(e.target.files[0]);\n });\n }\n\n // 手动添加记录\n const addRecordBtn = document.getElementById('addRecordBtn');\n if (addRecordBtn) {\n addRecordBtn.addEventListener('click', function() {\n self.addManualRecord();\n });\n }\n\n // 一键清除记录\n const clearAllRecordsBtn = document.getElementById('clearAllRecordsBtn');\n if (clearAllRecordsBtn) {\n clearAllRecordsBtn.addEventListener('click', function() {\n self.clearAllRecords();\n });\n }\n\n // 导出数据\n const exportBtn = document.getElementById('exportBtn');\n if (exportBtn) {\n exportBtn.addEventListener('click', function() {\n self.exportData();\n });\n }\n\n // 导入数据\n const importBtn = document.getElementById('importBtn');\n if (importBtn) {\n importBtn.addEventListener('click', function() {\n self.importData();\n });\n }\n\n // 清空表单\n const clearFormBtn = document.getElementById('clearFormBtn');\n if (clearFormBtn) {\n clearFormBtn.addEventListener('click', function() {\n self.clearForm();\n });\n }\n\n // 拖拽上传\n const uploadArea = document.querySelector('.upload-area');\n if (uploadArea) {\n uploadArea.addEventListener('dragover', function(e) {\n e.preventDefault();\n uploadArea.classList.add('dragover');\n });\n\n uploadArea.addEventListener('dragleave', function() {\n uploadArea.classList.remove('dragover');\n });\n\n uploadArea.addEventListener('drop', function(e) {\n e.preventDefault();\n uploadArea.classList.remove('dragover');\n const files = e.dataTransfer.files;\n if (files.length > 0) {\n self.handleImageUpload(files[0]);\n }\n });\n }\n\n // 实时计算百分位\n const inputs = ['ageMonths', 'height', 'weight', 'headCircumference'];\n inputs.forEach(function(id) {\n const input = document.getElementById(id);\n if (input) {\n input.addEventListener('input', function() {\n self.updatePercentiles();\n });\n }\n });\n\n // 保存宝宝基本信息\n const babyInfoInputs = ['babyName', 'babyGender', 'babyBirthDate'];\n babyInfoInputs.forEach(function(id) {\n const input = document.getElementById(id);\n if (input) {\n input.addEventListener('change', function() {\n self.saveBabyInfo();\n self.updateCurrentStatus();\n });\n }\n });\n\n // 体检日期变化时自动计算月龄\n const checkDateInput = document.getElementById('checkDate');\n if (checkDateInput) {\n checkDateInput.addEventListener('change', function() {\n self.autoCalculateAge();\n });\n }\n\n // 生成AI喂养建议按钮\n const generateFeedingBtn = document.getElementById('generateFeedingBtn');\n if (generateFeedingBtn) {\n generateFeedingBtn.addEventListener('click', function() {\n self.generateAIFeedingGuide();\n });\n }\n }\n\n autoCalculateAge() {\n const birthDate = this.babyInfo.birthDate;\n const checkDate = document.getElementById('checkDate') ? document.getElementById('checkDate').value : '';\n \n console.log('计算月龄 - 出生日期:', birthDate, '体检日期:', checkDate);\n \n if (birthDate && checkDate) {\n const birth = new Date(birthDate);\n const check = new Date(checkDate);\n \n if (check >= birth) {\n const years = check.getFullYear() - birth.getFullYear();\n const months = check.getMonth() - birth.getMonth();\n const days = check.getDate() - birth.getDate();\n \n let totalMonths = years * 12 + months;\n if (days < 0) {\n totalMonths -= 1;\n }\n \n const exactMonths = totalMonths + (days >= 0 ? days / 30 : (30 + days) / 30);\n const roundedMonths = Math.round(exactMonths * 10) / 10;\n \n const ageInput = document.getElementById('ageMonths');\n if (ageInput) {\n ageInput.value = roundedMonths;\n this.updatePercentiles();\n console.log('自动计算月龄:', roundedMonths);\n }\n }\n } else if (!birthDate) {\n this.showMessage('🍼 请先在基本信息中填写出生日期', 'warning');\n }\n }\n\n saveApiKey() {\n const apiKeyInput = document.getElementById('apiKeyInput');\n if (apiKeyInput) {\n this.apiKey = apiKeyInput.value.trim();\n if (this.apiKey) {\n localStorage.setItem('deepseek_api_key', this.apiKey);\n this.updateApiStatus();\n this.showMessage('🔑 API密钥保存成功', 'success');\n } else {\n this.showMessage('❌ 请输入有效的API密钥', 'error');\n }\n }\n }\n\n saveBaiduOcrConfig() {\n const apiKeyInput = document.getElementById('baiduOcrApiKey');\n const secretKeyInput = document.getElementById('baiduOcrSecretKey');\n \n if (apiKeyInput && secretKeyInput) {\n const apiKey = apiKeyInput.value.trim();\n const secretKey = secretKeyInput.value.trim();\n \n if (apiKey && secretKey) {\n this.baiduOcrConfig.apiKey = apiKey;\n this.baiduOcrConfig.secretKey = secretKey;\n \n localStorage.setItem('baidu_ocr_api_key', apiKey);\n localStorage.setItem('baidu_ocr_secret_key', secretKey);\n \n this.baiduOcrConfig.accessToken = '';\n this.baiduOcrConfig.tokenExpireTime = 0;\n localStorage.removeItem('baidu_ocr_access_token');\n localStorage.removeItem('baidu_ocr_token_expire');\n \n this.updateBaiduOcrStatus();\n this.showMessage('🔑 百度OCR配置保存成功', 'success');\n } else {\n this.showMessage('❌ 请输入完整的百度OCR API Key和Secret Key', 'error');\n }\n }\n }\n\n updateBaiduOcrStatus() {\n const statusIndicator = document.getElementById('baiduOcrStatus');\n const statusText = document.getElementById('baiduOcrStatusText');\n \n if (this.baiduOcrConfig.apiKey && this.baiduOcrConfig.secretKey) {\n if (statusIndicator) statusIndicator.classList.add('connected');\n if (statusText) statusText.textContent = '🤖 百度OCR已配置';\n } else {\n if (statusIndicator) statusIndicator.classList.remove('connected');\n if (statusText) statusText.textContent = '😴 未配置';\n }\n }\n // 测试百度OCR连接\nasync testBaiduOcrConnection() {\n console.log('🔧 开始测试百度OCR连接...');\n \n try {\n this.showLoadingState('🔧 正在测试连接...');\n \n // 第一步:检查配置\n if (!this.baiduOcrConfig.apiKey || !this.baiduOcrConfig.secretKey) {\n throw new Error('❌ 请先填写百度OCR的API Key和Secret Key');\n }\n console.log('✅ 配置检查通过');\n \n // 第二步:直接测试百度OCR认证(跳过代理服务器健康检查)\n this.showLoadingState('🔑 正在测试百度OCR认证...');\n console.log('🔑 测试百度OCR认证...');\n \n // 清除旧的访问令牌,强制重新获取\n this.baiduOcrConfig.accessToken = '';\n this.baiduOcrConfig.tokenExpireTime = 0;\n \n const accessToken = await this.getBaiduOcrAccessToken();\n if (!accessToken) {\n throw new Error('❌ 获取访问令牌失败');\n }\n console.log('✅ 百度OCR认证成功,令牌长度:', accessToken.length);\n \n // 第三步:测试一个简单的OCR调用\n this.showLoadingState('🧪 正在测试OCR功能...');\n console.log('🧪 测试OCR功能可用性...');\n \n // 创建一个1x1像素的测试图片\n const testImageBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';\n \n const testResponse = await fetch('https://jumpy-foil-whimsey.glitch.me/api/baidu-ocr', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n access_token: accessToken,\n image: testImageBase64,\n language_type: 'CHN_ENG'\n })\n });\n \n if (testResponse.ok) {\n console.log('✅ OCR API调用成功');\n } else {\n console.log('⚠️ OCR API调用失败,但认证成功');\n }\n \n // 测试成功\n this.hideLoadingState();\n this.showMessage('🎉 百度OCR连接测试成功!认证正常,可以进行图片识别', 'success');\n console.log('🎉 连接测试完成,认证正常!');\n \n } catch (error) {\n this.hideLoadingState();\n console.error('❌ 连接测试失败:', error);\n \n // 提供更详细的错误信息\n let errorMessage = error.message;\n if (error.message.includes('CORS')) {\n errorMessage = '网络连接问题,但这不影响图片识别功能的使用';\n }\n \n this.showMessage('❌ 连接测试失败: ' + errorMessage, 'error');\n }\n}\n\n\n\n updateApiStatus() {\n const statusIndicator = document.getElementById('apiStatus');\n const statusText = document.getElementById('apiStatusText');\n \n if (this.apiKey && this.apiKey.trim() !== '') {\n if (statusIndicator) statusIndicator.classList.add('connected');\n if (statusText) statusText.textContent = '🤖 OCR+AI模式';\n } else {\n if (statusIndicator) statusIndicator.classList.remove('connected');\n if (statusText) statusText.textContent = '😴 未连接';\n }\n }\n\n saveBabyInfo() {\n this.babyInfo = {\n name: document.getElementById('babyName') ? document.getElementById('babyName').value : '',\n gender: document.getElementById('babyGender') ? document.getElementById('babyGender').value : '',\n birthDate: document.getElementById('babyBirthDate') ? document.getElementById('babyBirthDate').value : ''\n };\n \n this.saveData();\n console.log('宝宝信息已保存:', this.babyInfo);\n }\n\n clearAllRecords() {\n if (this.checkRecords.length === 0) {\n this.showMessage('🤔 没有记录需要清除', 'info');\n return;\n }\n \n if (confirm('🗑️ 确定要清除所有 ' + this.checkRecords.length + ' 条体检记录吗?此操作不可恢复!')) {\n this.checkRecords = [];\n this.saveData();\n this.updateRecordsTable();\n this.updateStatistics();\n this.updateCurrentStatus();\n this.updateChart();\n this.showMessage('✨ 所有记录已清除', 'success');\n }\n }\n\n async handleImageUpload(file) {\n if (!file) return;\n \n if (!this.validateBasicInfo()) {\n return;\n }\n \n if (this.isUploading) {\n this.showMessage('🤖 正在处理中,请稍候...', 'info');\n return;\n }\n \n if (!file.type.startsWith('image/')) {\n this.showMessage('🖼️ 请上传图片文件 (JPG, PNG, WEBP)', 'error');\n return;\n }\n \n if (file.size > 10 * 1024 * 1024) {\n this.showMessage('📦 图片文件太大,请选择小于10MB的图片', 'error');\n return;\n }\n \n if (!this.baiduOcrConfig.apiKey || !this.baiduOcrConfig.secretKey) {\n this.showMessage('🔧 请先配置百度OCR API密钥', 'error');\n return;\n }\n \n this.isUploading = true;\n \n try {\n this.showImagePreview(file);\n this.showLoadingState('🔍 正在初始化百度OCR识别...');\n const ocrResult = await this.performBaiduOCR(file);\n this.showOCRResult(ocrResult);\n this.hideLoadingState();\n this.showMessage('✨ OCR识别完成,请查看识别结果', 'success');\n } catch (error) {\n this.hideLoadingState();\n console.error('详细错误信息:', error);\n this.showMessage('❌ 识别失败: ' + error.message, 'error');\n } finally {\n this.isUploading = false;\n this.resetUploadInput();\n }\n }\n\n async getBaiduOcrAccessToken() {\n const now = Date.now();\n if (this.baiduOcrConfig.accessToken && now < this.baiduOcrConfig.tokenExpireTime) {\n return this.baiduOcrConfig.accessToken;\n }\n \n console.log('获取新的百度OCR访问令牌...');\n \n try {\n const response = await fetch('https://jumpy-foil-whimsey.glitch.me/api/baidu-token', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n client_id: this.baiduOcrConfig.apiKey,\n client_secret: this.baiduOcrConfig.secretKey\n })\n });\n\n \n if (!response.ok) {\n throw new Error('获取访问令牌失败: ' + response.status);\n }\n \n const data = await response.json();\n \n if (data.error) {\n throw new Error('百度API错误: ' + (data.error_description || data.error));\n }\n \n this.baiduOcrConfig.accessToken = data.access_token;\n this.baiduOcrConfig.tokenExpireTime = now + (data.expires_in - 300) * 1000;\n \n localStorage.setItem('baidu_ocr_access_token', data.access_token);\n localStorage.setItem('baidu_ocr_token_expire', this.baiduOcrConfig.tokenExpireTime.toString());\n \n console.log('百度OCR访问令牌获取成功');\n return data.access_token;\n \n } catch (error) {\n console.error('获取百度OCR访问令牌失败:', error);\n throw new Error('获取访问令牌失败: ' + error.message);\n }\n }\n\n async fileToBase64(file) {\n return new Promise(function(resolve, reject) {\n const reader = new FileReader();\n reader.onload = function() {\n const base64 = reader.result.split(',')[1];\n resolve(base64);\n };\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n }\n\n async performBaiduOCR(file) {\n try {\n console.log('开始百度OCR识别...文件大小:', file.size, '类型:', file.type);\n \n this.showLoadingState('🔑 正在获取访问令牌...');\n const accessToken = await this.getBaiduOcrAccessToken();\n \n this.showLoadingState('📷 正在处理图片...');\n const base64Image = await this.fileToBase64(file);\n \n this.showLoadingState('🔍 正在识别文字...');\n const response = await fetch('https://jumpy-foil-whimsey.glitch.me/api/baidu-ocr', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n access_token: accessToken,\n image: base64Image,\n language_type: 'CHN_ENG',\n detect_direction: 'true',\n paragraph: 'false',\n probability: 'false'\n })\n});\n\n \n if (!response.ok) {\n throw new Error('百度OCR API调用失败: ' + response.status);\n }\n \n const data = await response.json();\n \n if (data.error_code) {\n throw new Error('百度OCR错误 (' + data.error_code + '): ' + data.error_msg);\n }\n \n if (!data.words_result || data.words_result.length === 0) {\n throw new Error('未识别到任何文字,请确保图片清晰可读');\n }\n \n const recognizedText = data.words_result\n .map(function(item) { return item.words; })\n .join('\\n');\n \n console.log('百度OCR识别完成');\n console.log('识别到文字行数:', data.words_result.length);\n console.log('识别文本长度:', recognizedText.length);\n console.log('识别文本:', recognizedText);\n \n return recognizedText;\n \n } catch (error) {\n console.error('百度OCR识别失败:', error);\n throw new Error('图片文字识别失败: ' + error.message);\n }\n }\n\n showOCRResult(ocrText) {\n const self = this;\n this.lastOCRResult = ocrText;\n \n let resultDiv = document.getElementById('ocrResultDiv');\n \n if (!resultDiv) {\n resultDiv = document.createElement('div');\n resultDiv.id = 'ocrResultDiv';\n resultDiv.className = 'card cute-card';\n resultDiv.style.cssText = 'background: linear-gradient(135deg, #E5F3FF 0%, #F0FFE5 100%); border: 3px solid #4FACFE; margin: 20px 0; padding: 25px; border-radius: 20px; box-shadow: 0 8px 25px rgba(79, 172, 254, 0.3);';\n const uploadedImageDiv = document.getElementById('uploadedImage');\n if (uploadedImageDiv && uploadedImageDiv.parentNode) {\n uploadedImageDiv.parentNode.insertBefore(resultDiv, uploadedImageDiv.nextSibling);\n }\n }\n \n resultDiv.innerHTML = '<div style=\"text-align: center; margin-bottom: 20px;\"><h4 style=\"color: #4FACFE; margin-bottom: 10px; font-size: 1.4em;\">🔍 百度OCR识别结果</h4><p style=\"color: #666; font-size: 0.9em;\">✨ 文字识别完成,接下来可以进行智能解析</p></div><div style=\"background: white; padding: 20px; border-radius: 15px; border: 2px solid #B6E5FC; margin-bottom: 20px;\"><h5 style=\"color: #4FACFE; margin-bottom: 15px;\">📝 原始识别文本:</h5><pre style=\"white-space: pre-wrap; font-size: 14px; line-height: 1.4; margin: 10px 0; color: #333; background: #F8F9FA; padding: 15px; border-radius: 10px; max-height: 200px; overflow-y: auto;\">' + ocrText + '</pre></div><div style=\"background: white; padding: 20px; border-radius: 15px; border: 2px solid #B6E5FC; margin-bottom: 20px;\"><h5 style=\"color: #4FACFE; margin-bottom: 15px;\">📊 文本统计:</h5><div style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; font-size: 0.9em;\"><div style=\"background: #F0F8FF; padding: 10px; border-radius: 8px; text-align: center;\"><strong>📏 字符总数:</strong><br>' + ocrText.length + '</div><div style=\"background: #F0FFF0; padding: 10px; border-radius: 8px; text-align: center;\"><strong>📄 行数:</strong><br>' + ocrText.split('\\n').length + '</div><div style=\"background: #FFF8DC; padding: 10px; border-radius: 8px; text-align: center;\"><strong>🔢 包含数字:</strong><br>' + (/\\d/.test(ocrText) ? '✅ 是' : '❌ 否') + '</div><div style=\"background: #FFE4E1; padding: 10px; border-radius: 8px; text-align: center;\"><strong>🇨🇳 包含中文:</strong><br>' + (/[\\u4e00-\\u9fff]/.test(ocrText) ? '✅ 是' : '❌ 否') + '</div></div></div><div style=\"text-align: center; display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;\"><button onclick=\"window.babyTracker.parseOCRResultManually()\" class=\"btn btn-primary cute-btn\" style=\"background: linear-gradient(135deg, #4FACFE 0%, #00F2FE 100%); padding: 12px 25px; border-radius: 25px; font-weight: 600; box-shadow: 0 5px 15px rgba(79, 172, 254, 0.4);\">🤖 智能解析数据</button><button onclick=\"window.babyTracker.hideOCRResult()\" class=\"btn btn-secondary cute-btn\" style=\"background: linear-gradient(135deg, #DDA0DD 0%, #DA70D6 100%); padding: 12px 25px; border-radius: 25px; font-weight: 600;\">❌ 关闭结果</button></div>';\n \n resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n }\n\n async parseOCRResultManually() {\n if (!this.lastOCRResult) {\n this.showMessage('❌ 没有OCR识别结果', 'error');\n return;\n }\n \n try {\n this.showLoadingState('🤖 正在智能解析表格数据...');\n \n const localParsedData = this.parseTableStructure(this.lastOCRResult);\n console.log('本地解析结果:', localParsedData);\n \n const aiEnhancedData = await this.enhanceDataWithAI(this.lastOCRResult, localParsedData);\n console.log('AI增强结果:', aiEnhancedData);\n \n const finalData = this.mergeDataResults(localParsedData, aiEnhancedData);\n \n this.showParseResult(finalData);\n this.handleMultipleRecords(finalData);\n this.hideLoadingState();\n this.showMessage('🎉 成功解析 ' + (finalData.records ? finalData.records.length : 0) + ' 条体检记录', 'success');\n \n } catch (error) {\n this.hideLoadingState();\n this.showMessage('❌ 解析失败: ' + error.message, 'error');\n console.error('表格解析错误:', error);\n }\n }\n\n parseTableStructure(text) {\n console.log('=== 开始本地表格解析 ===');\n const result = {\n records: []\n };\n \n let cleanText = text\n .replace(/[|\\[\\]{}]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n \n console.log('清理后文本:', cleanText);\n \n const lines = cleanText.split('\\n').map(function(line) { return line.trim(); }).filter(function(line) { return line.length > 0; });\n \n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const record = this.parseTableRow(line);\n if (record) {\n console.log('第' + (i+1) + '行识别到记录:', record);\n result.records.push(record);\n }\n }\n \n result.records.sort(function(a, b) { return new Date(b.date) - new Date(a.date); });\n \n console.log('本地解析完成,共', result.records.length, '条记录');\n return result;\n }\n\n parseTableRow(line) {\n if (line.includes('体检日期') || line.includes('年龄') || line.includes('身高') || line.includes('体重')) {\n return null;\n }\n \n const record = {};\n \n const datePatterns = [\n /(\\d{4}[-\\/]\\d{1,2}[-\\/]\\d{1,2})/,\n /(\\d{4}年\\d{1,2}月\\d{1,2}日)/\n ];\n \n for (let i = 0; i < datePatterns.length; i++) {\n const pattern = datePatterns[i];\n const match = line.match(pattern);\n if (match) {\n record.date = match[1].replace(/年/g, '-').replace(/月/g, '-').replace(/日/g, '');\n break;\n }\n }\n \n if (!record.date) return null;\n \n const ageMonths = this.extractAgeInMonths(line);\n if (ageMonths !== null) {\n record.ageMonths = ageMonths;\n }\n \n const weightMatch = line.match(/(\\d+(?:\\.\\d+)?)\\s*(?:kg|公斤)/i);\n if (weightMatch) {\n record.weight = parseFloat(weightMatch[1]);\n } else {\n const numbers = line.match(/\\d+(?:\\.\\d+)?/g);\n if (numbers) {\n for (let i = 0; i < numbers.length; i++) {\n const num = numbers[i];\n const val = parseFloat(num);\n if (val >= 2 && val <= 30) {\n record.weight = val;\n break;\n }\n }\n }\n }\n \n const heightMatch = line.match(/(\\d+(?:\\.\\d+)?)\\s*(?:cm|厘米)/i);\n if (heightMatch) {\n record.height = parseFloat(heightMatch[1]);\n } else {\n const numbers = line.match(/\\d+(?:\\.\\d+)?/g);\n if (numbers) {\n for (let i = 0; i < numbers.length; i++) {\n const num = numbers[i];\n const val = parseFloat(num);\n if (val >= 40 && val <= 120) {\n record.height = val;\n break;\n }\n }\n }\n }\n \n const headMatch = line.match(/(?:头围|头周)[::\\s]*(\\d+(?:\\.\\d+)?)/);\n if (headMatch) {\n record.headCircumference = parseFloat(headMatch[1]);\n } else {\n const numbers = line.match(/\\d+(?:\\.\\d+)?/g);\n if (numbers) {\n for (let i = 0; i < numbers.length; i++) {\n const num = numbers[i];\n const val = parseFloat(num);\n if (val >= 30 && val <= 60) {\n record.headCircumference = val;\n break;\n }\n }\n }\n }\n \n if (record.weight || record.height || record.headCircumference) {\n return record;\n }\n \n return null;\n }\n\n extractAgeInMonths(text) {\n const ageYearMonthMatch = text.match(/(\\d+)岁(\\d+)月/);\n if (ageYearMonthMatch) {\n const years = parseInt(ageYearMonthMatch[1]);\n const months = parseInt(ageYearMonthMatch[2]);\n return years * 12 + months;\n }\n \n const ageMonthDayMatch = text.match(/(\\d+)月(\\d+)天/);\n if (ageMonthDayMatch) {\n const months = parseInt(ageMonthDayMatch[1]);\n const days = parseInt(ageMonthDayMatch[2]);\n return months + (days / 30);\n }\n \n const ageDayMatch = text.match(/^(\\d+)天$/);\n if (ageDayMatch) {\n const days = parseInt(ageDayMatch[1]);\n return days / 30;\n }\n \n const ageMonthMatch = text.match(/(\\d+)个月/);\n if (ageMonthMatch) {\n return parseInt(ageMonthMatch[1]);\n }\n \n return null;\n }\n\n async enhanceDataWithAI(ocrText, localData) {\n if (!this.apiKey) {\n console.log('无API密钥,跳过AI增强');\n return { records: [] };\n }\n \n const prompt = '你是体检报告分析专家。请分析以下OCR识别的体检报告文本,提取所有体检记录。\\n\\nOCR文本:\\n' + ocrText + '\\n\\n本地解析已识别到 ' + localData.records.length + ' 条记录。\\n\\n请按以下JSON格式返回所有体检记录,特别注意:\\n1. 年龄格式转换:\\n \"1岁2月\" = 14个月\\n \"8月0天\" = 8个月 \\n \"6月1天\" = 6.03个月\\n2. 数据验证:体重(2-30kg)、身高(40-120cm)、头围(30-60cm)\\n3. 只返回JSON,不要其他说明\\n\\n{\\n \"records\": [\\n {\\n \"date\": \"2025-04-03\",\\n \"ageMonths\": 14.7,\\n \"weight\": 11.4,\\n \"height\": 81.5,\\n \"headCircumference\": 47.5\\n }\\n ]\\n}';\n \n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + this.apiKey\n },\n body: JSON.stringify({\n model: \"deepseek-chat\",\n messages: [{\n role: \"user\",\n content: prompt\n }],\n max_tokens: 1500,\n temperature: 0.1\n })\n });\n \n if (!response.ok) {\n throw new Error('AI API调用失败: ' + response.status);\n }\n \n const data = await response.json();\n const aiResponse = data.choices[0].message.content;\n \n console.log('AI原始响应:', aiResponse);\n \n const jsonMatch = aiResponse.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n return JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('AI返回格式错误');\n }\n \n } catch (error) {\n console.error('AI增强失败:', error);\n return { records: [] };\n }\n }\n\n mergeDataResults(localData, aiData) {\n console.log('=== 合并解析结果 ===');\n const merged = {\n records: []\n };\n \n const allRecords = localData.records.slice();\n \n if (aiData.records && Array.isArray(aiData.records)) {\n for (let i = 0; i < aiData.records.length; i++) {\n const aiRecord = aiData.records[i];\n const existing = allRecords.find(function(r) { return r.date === aiRecord.date; });\n if (!existing) {\n allRecords.push(aiRecord);\n } else {\n if (!existing.weight && aiRecord.weight) existing.weight = aiRecord.weight;\n if (!existing.height && aiRecord.height) existing.height = aiRecord.height;\n if (!existing.headCircumference && aiRecord.headCircumference) existing.headCircumference = aiRecord.headCircumference;\n if (!existing.ageMonths && aiRecord.ageMonths) existing.ageMonths = aiRecord.ageMonths;\n }\n }\n }\n \n merged.records = allRecords.filter(function(record) {\n if (!record.date) return false;\n \n if (record.weight && (record.weight < 1 || record.weight > 50)) return false;\n if (record.height && (record.height < 30 || record.height > 150)) return false;\n if (record.headCircumference && (record.headCircumference < 25 || record.headCircumference > 65)) return false;\n \n return record.weight || record.height || record.headCircumference;\n });\n \n merged.records.sort(function(a, b) { return new Date(b.date) - new Date(a.date); });\n \n console.log('合并完成,最终记录数:', merged.records.length);\n return merged;\n }\n\n showParseResult(extractedData) {\n let parseDiv = document.getElementById('parseResultDiv');\n \n if (!parseDiv) {\n parseDiv = document.createElement('div');\n parseDiv.id = 'parseResultDiv';\n parseDiv.className = 'card cute-card';\n parseDiv.style.cssText = 'background: linear-gradient(135deg, #FFE5F1 0%, #FFE5E5 100%); border: 3px solid #FF69B4; margin: 20px 0; padding: 25px; border-radius: 20px; box-shadow: 0 8px 25px rgba(255, 105, 180, 0.3);';\n const ocrDiv = document.getElementById('ocrResultDiv');\n if (ocrDiv && ocrDiv.parentNode) {\n ocrDiv.parentNode.insertBefore(parseDiv, ocrDiv.nextSibling);\n }\n }\n \n const recordsCount = extractedData.records ? extractedData.records.length : 0;\n \n parseDiv.innerHTML = '<div style=\"text-align: center; margin-bottom: 20px;\"><h4 style=\"color: #FF1493; margin-bottom: 10px; font-size: 1.4em;\">🎯 智能识别结果<span style=\"background: #FF69B4; color: white; padding: 5px 12px; border-radius: 15px; font-size: 0.8em;\">' + recordsCount + '条记录</span></h4><p style=\"color: #666; font-size: 0.9em;\">💡 可以编辑下方数据,确保准确性后再添加到记录中</p></div><div style=\"background: white; padding: 20px; border-radius: 15px; border: 2px solid #FFB6C1; margin-bottom: 20px;\"><h5 style=\"color: #FF1493; margin-bottom: 15px; display: flex; align-items: center;\"><span>📊 识别概况</span></h5><div style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; font-size: 0.9em;\"><div style=\"background: #FFF0F5; padding: 10px; border-radius: 10px;\"><strong>📈 识别记录数:</strong> ' + recordsCount + '</div><div style=\"background: #F0F8FF; padding: 10px; border-radius: 10px;\"><strong>🤖 解析方式:</strong> 百度OCR + AI增强</div><div style=\"background: #F0FFF0; padding: 10px; border-radius: 10px;\"><strong>✅ 数据完整性:</strong> ' + this.getDataCompleteness(extractedData.records) + '</div></div></div>' + this.generateEditableRecordsHtml(extractedData.records) + '<div style=\"text-align: center; margin-top: 20px; display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;\"><button onclick=\"window.babyTracker.saveEditedRecords()\" class=\"btn btn-success cute-btn\" style=\"background: linear-gradient(135deg, #FF69B4 0%, #FF1493 100%); padding: 12px 25px; border-radius: 25px; font-weight: 600; box-shadow: 0 5px 15px rgba(255, 105, 180, 0.4);\">✨ 保存并添加记录</button><button onclick=\"window.babyTracker.hideOCRResult()\" class=\"btn btn-secondary cute-btn\" style=\"background: linear-gradient(135deg, #DDA0DD 0%, #DA70D6 100%); padding: 12px 25px; border-radius: 25px; font-weight: 600;\">❌ 取消</button></div>';\n \n parseDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n }\n\n generateEditableRecordsHtml(records) {\n if (!records || records.length === 0) {\n return '<div style=\"background: white; padding: 20px; border-radius: 15px; border: 2px solid #FFB6C1; text-align: center;\"><p style=\"color: #888; font-size: 1.1em;\">🤔 未识别到有效记录</p><p style=\"color: #666; font-size: 0.9em;\">请检查图片是否清晰,或尝试手动添加记录</p></div>';\n }\n \n let html = '<div style=\"background: white; padding: 20px; border-radius: 15px; border: 2px solid #FFB6C1;\"><h5 style=\"color: #FF1493; margin-bottom: 15px; display: flex; align-items: center;\"><span>✏️ 编辑识别数据</span><span style=\"margin-left: auto; font-size: 0.8em; color: #888;\">点击表格单元格可编辑</span></h5><div style=\"overflow-x: auto;\"><table id=\"editableRecordsTable\" style=\"width: 100%; border-collapse: collapse; font-size: 0.9em;\"><thead><tr style=\"background: linear-gradient(135deg, #FFB6C1 0%, #FFC0CB 100%);\"><th style=\"padding: 12px 8px; border: 1px solid #FF69B4; color: #8B008B; font-weight: 600;\">📅 体检日期</th><th style=\"padding: 12px 8px; border: 1px solid #FF69B4; color: #8B008B; font-weight: 600;\">👶 月龄</th><th style=\"padding: 12px 8px; border: 1px solid #FF69B4; color: #8B008B; font-weight: 600;\">📏 身高(cm)</th><th style=\"padding: 12px 8px; border: 1px solid #FF69B4; color: #8B008B; font-weight: 600;\">⚖️ 体重(kg)</th><th style=\"padding: 12px 8px; border: 1px solid #FF69B4; color: #8B008B; font-weight: 600;\">🧠 头围(cm)</th><th style=\"padding: 12px 8px; border: 1px solid #FF69B4; color: #8B008B; font-weight: 600;\">🛠️ 操作</th></tr></thead><tbody>';\n \n for (let i = 0; i < records.length; i++) {\n const record = records[i];\n html += '<tr data-index=\"' + i + '\" style=\"background: ' + (i % 2 === 0 ? '#FFF8DC' : '#F0F8FF') + ';\"><td style=\"padding: 10px 8px; border: 1px solid #FFB6C1;\"><input type=\"date\" value=\"' + (record.date || '') + '\" onchange=\"window.babyTracker.updateEditedRecord(' + i + ', \\'date\\', this.value)\" style=\"width: 100%; border: none; background: transparent; text-align: center; font-size: 0.9em;\"></td><td style=\"padding: 10px 8px; border: 1px solid #FFB6C1;\"><input type=\"number\" step=\"0.1\" value=\"' + (record.ageMonths || '') + '\" onchange=\"window.babyTracker.updateEditedRecord(' + i + ', \\'ageMonths\\', this.value)\" style=\"width: 100%; border: none; background: transparent; text-align: center; font-size: 0.9em;\" placeholder=\"月\"></td><td style=\"padding: 10px 8px; border: 1px solid #FFB6C1;\"><input type=\"number\" step=\"0.1\" value=\"' + (record.height || '') + '\" onchange=\"window.babyTracker.updateEditedRecord(' + i + ', \\'height\\', this.value)\" style=\"width: 100%; border: none; background: transparent; text-align: center; font-size: 0.9em;\" placeholder=\"cm\"></td><td style=\"padding: 10px 8px; border: 1px solid #FFB6C1;\"><input type=\"number\" step=\"0.1\" value=\"' + (record.weight || '') + '\" onchange=\"window.babyTracker.updateEditedRecord(' + i + ', \\'weight\\', this.value)\" style=\"width: 100%; border: none; background: transparent; text-align: center; font-size: 0.9em;\" placeholder=\"kg\"></td><td style=\"padding: 10px 8px; border: 1px solid #FFB6C1;\"><input type=\"number\" step=\"0.1\" value=\"' + (record.headCircumference || '') + '\" onchange=\"window.babyTracker.updateEditedRecord(' + i + ', \\'headCircumference\\', this.value)\" style=\"width: 100%; border: none; background: transparent; text-align: center; font-size: 0.9em;\" placeholder=\"cm\"></td><td style=\"padding: 10px 8px; border: 1px solid #FFB6C1; text-align: center;\"><button onclick=\"window.babyTracker.deleteEditedRecord(' + i + ')\" style=\"background: linear-gradient(135deg, #FF6B6B 0%, #FF5252 100%); color: white; border: none; border-radius: 15px; padding: 5px 10px; font-size: 0.8em; cursor: pointer;\">🗑️ 删除</button></td></tr>';\n }\n \n html += '</tbody></table></div><div style=\"text-align: center; margin-top: 15px;\"><button onclick=\"window.babyTracker.addNewEditedRecord()\" style=\"background: linear-gradient(135deg, #98FB98 0%, #90EE90 100%); color: #006400; border: none; border-radius: 20px; padding: 10px 20px; font-weight: 600; cursor: pointer; box-shadow: 0 3px 10px rgba(152, 251, 152, 0.4);\">➕ 添加新记录</button></div></div>';\n \n return html;\n }\n\n updateEditedRecord(index, field, value) {\n if (!this.tempExtractedData || !this.tempExtractedData.records) {\n return;\n }\n \n if (this.tempExtractedData.records[index]) {\n if (field === 'ageMonths' || field === 'height' || field === 'weight' || field === 'headCircumference') {\n this.tempExtractedData.records[index][field] = value ? parseFloat(value) : null;\n } else {\n this.tempExtractedData.records[index][field] = value;\n }\n \n console.log('更新记录:', index, field, value);\n this.validateEditedRecord(index);\n }\n }\n\n validateEditedRecord(index) {\n const record = this.tempExtractedData.records[index];\n const row = document.querySelector('tr[data-index=\"' + index + '\"]');\n if (!row) return;\n \n const inputs = row.querySelectorAll('input');\n for (let i = 0; i < inputs.length; i++) {\n const input = inputs[i];\n input.style.border = 'none';\n input.style.background = 'transparent';\n }\n \n let hasError = false;\n \n if (record.weight && (record.weight < 1 || record.weight > 50)) {\n const weightInput = row.querySelector('input[onchange*=\"weight\"]');\n if (weightInput) {\n weightInput.style.border = '2px solid #FF6B6B';\n weightInput.style.background = '#FFE5E5';\n }\n hasError = true;\n }\n \n if (record.height && (record.height < 30 || record.height > 150)) {\n const heightInput = row.querySelector('input[onchange*=\"height\"]');\n if (heightInput) {\n heightInput.style.border = '2px solid #FF6B6B';\n heightInput.style.background = '#FFE5E5';\n }\n hasError = true;\n }\n \n if (record.headCircumference && (record.headCircumference < 25 || record.headCircumference > 65)) {\n const headInput = row.querySelector('input[onchange*=\"headCircumference\"]');\n if (headInput) {\n headInput.style.border = '2px solid #FF6B6B';\n headInput.style.background = '#FFE5E5';\n }\n hasError = true;\n }\n \n if (hasError) {\n this.showMessage('⚠️ 请检查数据范围:体重(1-50kg)、身高(30-150cm)、头围(25-65cm)', 'warning');\n }\n }\n\n deleteEditedRecord(index) {\n if (!this.tempExtractedData || !this.tempExtractedData.records) {\n return;\n }\n \n if (confirm('🤔 确定要删除这条记录吗?')) {\n this.tempExtractedData.records.splice(index, 1);\n this.showParseResult(this.tempExtractedData);\n this.showMessage('✅ 记录已删除', 'success');\n }\n }\n\n addNewEditedRecord() {\n if (!this.tempExtractedData) {\n this.tempExtractedData = { records: [] };\n }\n \n const newRecord = {\n date: new Date().toISOString().split('T')[0],\n ageMonths: null,\n height: null,\n weight: null,\n headCircumference: null\n };\n \n this.tempExtractedData.records.push(newRecord);\n this.showParseResult(this.tempExtractedData);\n this.showMessage('✨ 已添加新记录,请填写数据', 'info');\n }\n\n saveEditedRecords() {\n if (!this.tempExtractedData || !this.tempExtractedData.records) {\n this.showMessage('❌ 没有可保存的记录', 'error');\n return;\n }\n \n const validRecords = this.tempExtractedData.records.filter(function(record) {\n return record.date && (record.height || record.weight || record.headCircumference);\n });\n \n if (validRecords.length === 0) {\n this.showMessage('❌ 没有有效的记录数据', 'error');\n return;\n }\n \n this.tempExtractedData.records = validRecords;\n this.addAllRecords();\n }\n\n getDataCompleteness(records) {\n if (!records || records.length === 0) return '无数据';\n \n let completeCount = 0;\n for (let i = 0; i < records.length; i++) {\n const record = records[i];\n if (record.weight && record.height && record.headCircumference && record.ageMonths) {\n completeCount++;\n }\n }\n \n return completeCount + '/' + records.length + ' 条记录数据完整';\n }\n\n handleMultipleRecords(extractedData) {\n console.log('=== 处理多条记录 ===');\n this.tempExtractedData = extractedData;\n console.log('识别到体检记录数量:', extractedData.records ? extractedData.records.length : 0);\n }\n\n addAllRecords() {\n if (!this.tempExtractedData || !this.tempExtractedData.records) {\n this.showMessage('❌ 没有可添加的记录', 'error');\n return;\n }\n \n const gender = this.babyInfo.gender || 'male';\n let addedCount = 0;\n \n for (let i = 0; i < this.tempExtractedData.records.length; i++) {\n const record = this.tempExtractedData.records[i];\n const existing = this.checkRecords.find(function(r) { return r.checkDate === record.date; });\n if (!existing) {\n const recordWithPercentiles = {\n checkDate: record.date,\n ageMonths: record.ageMonths,\n height: record.height,\n weight: record.weight,\n headCircumference: record.headCircumference,\n id: Date.now() + Math.random(),\n addedAt: new Date().toISOString(),\n heightPercentile: record.height ? this.calculateWHOPercentileWithLinearInterpolation(record.ageMonths, record.height, 'height', gender) : null,\n weightPercentile: record.weight ? this.calculateWHOPercentileWithLinearInterpolation(record.ageMonths, record.weight, 'weight', gender) : null,\n headPercentile: record.headCircumference ? this.calculateWHOPercentileWithLinearInterpolation(record.ageMonths, record.headCircumference, 'headCircumference', gender) : null\n };\n \n this.checkRecords.push(recordWithPercentiles);\n addedCount++;\n }\n }\n \n if (addedCount > 0) {\n this.saveData();\n this.updateRecordsTable();\n this.updateCurrentStatus();\n this.updateChart();\n this.showMessage('🎉 成功添加 ' + addedCount + ' 条体检记录', 'success');\n } else {\n this.showMessage('ℹ️ 所有记录已存在,未添加新记录', 'info');\n }\n \n this.hideOCRResult();\n this.tempExtractedData = null;\n }\n\n hideOCRResult() {\n const ocrDiv = document.getElementById('ocrResultDiv');\n const parseDiv = document.getElementById('parseResultDiv');\n if (ocrDiv) ocrDiv.remove();\n if (parseDiv) parseDiv.remove();\n }\n\n resetUploadInput() {\n const reportUpload = document.getElementById('reportUpload');\n if (reportUpload) {\n reportUpload.value = '';\n }\n }\n\n showImagePreview(file) {\n const uploadedImageDiv = document.getElementById('uploadedImage');\n const previewImage = document.getElementById('previewImage');\n \n if (uploadedImageDiv && previewImage) {\n const reader = new FileReader();\n reader.onload = function(e) {\n previewImage.src = e.target.result;\n uploadedImageDiv.classList.remove('hidden');\n };\n reader.readAsDataURL(file);\n }\n }\n\n updatePercentiles() {\n const ageMonths = parseFloat(document.getElementById('ageMonths') ? document.getElementById('ageMonths').value : '') || null;\n const height = parseFloat(document.getElementById('height') ? document.getElementById('height').value : '') || null;\n const weight = parseFloat(document.getElementById('weight') ? document.getElementById('weight').value : '') || null;\n const headCircumference = parseFloat(document.getElementById('headCircumference') ? document.getElementById('headCircumference').value : '') || null;\n \n console.log('计算百分位 - 年龄:', ageMonths, '身高:', height, '体重:', weight, '头围:', headCircumference);\n \n if (!ageMonths) {\n this.updatePercentileDisplay({\n height: null,\n weight: null,\n headCircumference: null\n });\n return;\n }\n \n const gender = this.babyInfo.gender || 'male';\n const percentiles = {\n height: this.calculateWHOPercentileWithLinearInterpolation(ageMonths, height, 'height', gender),\n weight: this.calculateWHOPercentileWithLinearInterpolation(ageMonths, weight, 'weight', gender),\n headCircumference: this.calculateWHOPercentileWithLinearInterpolation(ageMonths, headCircumference, 'headCircumference', gender)\n };\n \n console.log('计算出的百分位:', percentiles);\n this.updatePercentileDisplay(percentiles);\n }\n\n calculateWHOPercentileWithLinearInterpolation(ageMonths, value, type, gender) {\n if (!value || !ageMonths) return null;\n \n console.log('\\n=== 计算' + type + '百分位 (线性插值) ===');\n console.log('年龄: ' + ageMonths + '月, ' + type + ': ' + value + ', 性别: ' + gender);\n \n let dataKey;\n if (type === 'height') {\n dataKey = gender === 'female' ? 'femaleHeight' : 'maleHeight';\n } else if (type === 'weight') {\n dataKey = gender === 'female' ? 'femaleWeight' : 'maleWeight';\n } else if (type === 'headCircumference') {\n dataKey = gender === 'female' ? 'femaleHeadCircumference' : 'maleHeadCircumference';\n }\n \n const whoDataSource = this.whoData[dataKey];\n \n const lowerAge = Math.floor(ageMonths);\n const upperAge = Math.ceil(ageMonths);\n \n console.log('年龄边界: ' + lowerAge + '月 到 ' + upperAge + '月');\n \n if (lowerAge === upperAge) {\n const whoRecord = whoDataSource[lowerAge];\n if (!whoRecord) {\n console.log('❌ 没有找到' + lowerAge + '月龄的WHO数据');\n return null;\n }\n return this.calculatePercentileFromWHOData(value, whoRecord, lowerAge + '月整数月龄');\n }\n \n const lowerWHOData = whoDataSource[lowerAge];\n const upperWHOData = whoDataSource[upperAge];\n \n if (!lowerWHOData || !upperWHOData) {\n console.log('❌ 缺少WHO数据: ' + lowerAge + '月=' + !!lowerWHOData + ', ' + upperAge + '月=' + !!upperWHOData);\n return null;\n }\n \n const ratio = ageMonths - lowerAge;\n console.log('插值比例: (' + ageMonths + ' - ' + lowerAge + ') = ' + ratio);\n \n const interpolatedWHOData = [];\n for (let i = 0; i < 7; i++) {\n const interpolatedValue = lowerWHOData[i] + ratio * (upperWHOData[i] - lowerWHOData[i]);\n interpolatedWHOData.push(parseFloat(interpolatedValue.toFixed(2)));\n }\n \n console.log(lowerAge + '月WHO数据:', lowerWHOData);\n console.log(upperAge + '月WHO数据:', upperWHOData);\n console.log('插值后WHO数据:', interpolatedWHOData);\n \n return this.calculatePercentileFromWHOData(value, interpolatedWHOData, ageMonths + '月插值数据');\n }\n\n calculatePercentileFromWHOData(value, whoData, description) {\n console.log('\\n--- 基于' + description + '计算百分位 ---');\n \n const sd3neg = whoData[0];\n const sd2neg = whoData[1];\n const sd1neg = whoData[2];\n const median = whoData[3];\n const sd1pos = whoData[4];\n const sd2pos = whoData[5];\n const sd3pos = whoData[6];\n \n console.log('WHO标准数据:');\n console.log(' -3SD: ' + sd3neg);\n console.log(' -2SD: ' + sd2neg);\n console.log(' -1SD: ' + sd1neg);\n console.log(' 中位数: ' + median);\n console.log(' +1SD: ' + sd1pos);\n console.log(' +2SD: ' + sd2pos);\n console.log(' +3SD: ' + sd3pos);\n \n let interval = '';\n let percentile = null;\n \n if (value <= sd3neg) {\n interval = '< -3SD (严重偏低)';\n percentile = 0.1;\n } else if (value <= sd2neg) {\n interval = '-3SD 到 -2SD 之间 (偏低)';\n const ratio = (value - sd3neg) / (sd2neg - sd3neg);\n percentile = 0.1 + ratio * (2.3 - 0.1);\n } else if (value <= sd1neg) {\n interval = '-2SD 到 -1SD 之间 (稍低)';\n const ratio = (value - sd2neg) / (sd1neg - sd2neg);\n percentile = 2.3 + ratio * (15.9 - 2.3);\n } else if (value <= median) {\n interval = '-1SD 到 中位数 之间 (正常偏低)';\n const ratio = (value - sd1neg) / (median - sd1neg);\n percentile = 15.9 + ratio * (50 - 15.9);\n } else if (value <= sd1pos) {\n interval = '中位数 到 +1SD 之间 (正常偏高)';\n const ratio = (value - median) / (sd1pos - median);\n percentile = 50 + ratio * (84.1 - 50);\n } else if (value <= sd2pos) {\n interval = '+1SD 到 +2SD 之间 (稍高)';\n const ratio = (value - sd1pos) / (sd2pos - sd1pos);\n percentile = 84.1 + ratio * (97.7 - 84.1);\n } else if (value <= sd3pos) {\n interval = '+2SD 到 +3SD 之间 (偏高)';\n const ratio = (value - sd2pos) / (sd3pos - sd2pos);\n percentile = 97.7 + ratio * (99.9 - 97.7);\n } else {\n interval = '> +3SD (严重偏高)';\n percentile = 99.9;\n } \n console.log('📊 标准差区间: ' + interval);\n console.log('📈 计算百分位: ' + (percentile ? percentile.toFixed(1) : 'null') + '%');\n \n const finalPercentile = Math.max(0.1, Math.min(99.9, Math.round(percentile * 10) / 10));\n console.log('✅ 最终百分位: ' + finalPercentile + '%');\n console.log('--- 计算完成 ---\\n');\n \n return finalPercentile;\n }\n\n updatePercentileDisplay(percentiles) {\n const heightPercentileEl = document.getElementById('heightPercentile');\n const weightPercentileEl = document.getElementById('weightPercentile');\n const headPercentileEl = document.getElementById('headPercentile');\n \n if (heightPercentileEl) {\n heightPercentileEl.textContent = percentiles.height !== null ? percentiles.height + '%' : '--';\n }\n if (weightPercentileEl) {\n weightPercentileEl.textContent = percentiles.weight !== null ? percentiles.weight + '%' : '--';\n }\n if (headPercentileEl) {\n headPercentileEl.textContent = percentiles.headCircumference !== null ? percentiles.headCircumference + '%' : '--';\n }\n }\n\n initializeChart() {\n const ctx = document.getElementById('growthChart');\n if (!ctx) return;\n \n if (typeof Chart === 'undefined') {\n console.warn('Chart.js未加载,跳过图表初始化');\n return;\n }\n \n const config = {\n type: 'line',\n data: {\n labels: [],\n datasets: []\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: {\n title: {\n display: true,\n text: '🌈 生长发育曲线图'\n },\n legend: {\n display: true,\n position: 'top'\n }\n },\n scales: {\n x: {\n title: {\n display: true,\n text: '年龄 (月)'\n },\n min: 0,\n max: 36\n },\n y: {\n title: {\n display: true,\n text: '数值'\n }\n }\n },\n interaction: {\n intersect: false,\n mode: 'index'\n }\n }\n };\n \n this.growthChart = new Chart(ctx, config);\n this.updateChart();\n }\n\n updateChart() {\n if (!this.growthChart) return;\n \n const gender = this.babyInfo.gender || 'male';\n const chartType = document.querySelector('input[name=\"chartType\"]:checked') ? document.querySelector('input[name=\"chartType\"]:checked').value : 'height';\n \n const whoKey = gender === 'female' ? \n 'female' + chartType.charAt(0).toUpperCase() + chartType.slice(1) : \n 'male' + chartType.charAt(0).toUpperCase() + chartType.slice(1);\n const whoData = this.whoData[whoKey] || this.whoData.maleHeight;\n \n const ageLabels = [];\n for (let i = 0; i <= 36; i++) {\n ageLabels.push(i);\n }\n \n const standardLines = [\n {\n label: '-2SD (3%)',\n data: [],\n borderColor: '#ff6b6b',\n backgroundColor: 'transparent',\n borderWidth: 1,\n pointRadius: 0\n },\n {\n label: '-1SD (15%)',\n data: [],\n borderColor: '#ffa726',\n backgroundColor: 'transparent',\n borderWidth: 1,\n pointRadius: 0\n },\n {\n label: '中位数 (50%)',\n data: [],\n borderColor: '#4caf50',\n backgroundColor: 'transparent',\n borderWidth: 2,\n pointRadius: 0\n },\n {\n label: '+1SD (85%)',\n data: [],\n borderColor: '#ffa726',\n backgroundColor: 'transparent',\n borderWidth: 1,\n pointRadius: 0\n },\n {\n label: '+2SD (97%)',\n data: [],\n borderColor: '#ff6b6b',\n backgroundColor: 'transparent',\n borderWidth: 1,\n pointRadius: 0\n }\n ];\n \n for (let i = 0; i < ageLabels.length; i++) {\n const age = ageLabels[i];\n const whoRecord = whoData[age];\n if (whoRecord) {\n standardLines[0].data.push(whoRecord[1]);\n standardLines[1].data.push(whoRecord[2]);\n standardLines[2].data.push(whoRecord[3]);\n standardLines[3].data.push(whoRecord[4]);\n standardLines[4].data.push(whoRecord[5]);\n } else {\n for (let j = 0; j < standardLines.length; j++) {\n standardLines[j].data.push(null);\n }\n }\n }\n \n const babyData = {\n label: '🍼 宝宝实际数据',\n data: new Array(37).fill(null),\n borderColor: '#2196f3',\n backgroundColor: '#2196f3',\n borderWidth: 3,\n pointRadius: 5,\n pointHoverRadius: 7\n };\n \n for (let i = 0; i < this.checkRecords.length; i++) {\n const record = this.checkRecords[i];\n const age = Math.round(record.ageMonths);\n let value = null;\n \n switch(chartType) {\n case 'height':\n value = record.height;\n break;\n case 'weight':\n value = record.weight;\n break;\n case 'headCircumference':\n value = record.headCircumference;\n break;\n }\n \n if (value && age >= 0 && age <= 36) {\n babyData.data[age] = value;\n }\n }\n \n this.growthChart.data.labels = ageLabels;\n this.growthChart.data.datasets = standardLines.concat([babyData]);\n \n let yAxisTitle = '';\n switch(chartType) {\n case 'height':\n yAxisTitle = '身高 (cm)';\n break;\n case 'weight':\n yAxisTitle = '体重 (kg)';\n break;\n case 'headCircumference':\n yAxisTitle = '头围 (cm)';\n break;\n }\n \n this.growthChart.options.scales.y.title.text = yAxisTitle;\n this.growthChart.update();\n }\n\n getLatestRecord() {\n if (this.checkRecords.length === 0) {\n return null;\n }\n \n const sortedRecords = this.checkRecords.slice().sort(function(a, b) {\n return new Date(b.checkDate) - new Date(a.checkDate);\n });\n return sortedRecords[0];\n }\n\n analyzeGrowthTrend() {\n if (this.checkRecords.length < 2) {\n return {\n trend: '数据不足',\n description: '需要至少2次体检记录才能分析生长趋势'\n };\n }\n \n const sortedRecords = this.checkRecords.slice().sort(function(a, b) {\n return new Date(a.checkDate) - new Date(b.checkDate);\n });\n const latest = sortedRecords[sortedRecords.length - 1];\n const previous = sortedRecords[sortedRecords.length - 2];\n \n const trends = [];\n \n if (latest.height && previous.height && latest.heightPercentile && previous.heightPercentile) {\n const heightChange = latest.heightPercentile - previous.heightPercentile;\n if (heightChange > 5) {\n trends.push('身高增长良好');\n } else if (heightChange < -5) {\n trends.push('身高增长放缓');\n } else {\n trends.push('身高稳步增长');\n }\n }\n \n if (latest.weight && previous.weight && latest.weightPercentile && previous.weightPercentile) {\n const weightChange = latest.weightPercentile - previous.weightPercentile;\n if (weightChange > 5) {\n trends.push('体重增长良好');\n } else if (weightChange < -5) {\n trends.push('体重增长放缓');\n } else {\n trends.push('体重稳步增长');\n }\n }\n \n if (latest.headCircumference && previous.headCircumference && latest.headPercentile && previous.headPercentile) {\n const headChange = latest.headPercentile - previous.headPercentile;\n if (headChange > 5) {\n trends.push('头围增长良好');\n } else if (headChange < -5) {\n trends.push('头围增长放缓');\n } else {\n trends.push('头围稳步增长');\n }\n }\n \n return {\n trend: trends.length > 0 ? trends.join(',') : '生长正常',\n description: '基于最近两次体检记录的对比分析(' + previous.checkDate + ' vs ' + latest.checkDate + ')'\n };\n }\n\n async generateAIFeedingGuide() {\n console.log('=== 开始生成AI喂养指导 ===');\n console.log('调试信息:');\n console.log('- 基本信息:', this.babyInfo);\n console.log('- 体检记录数量:', this.checkRecords.length);\n console.log('- API密钥状态:', !!this.apiKey);\n \n if (!this.validateBasicInfo()) {\n return;\n }\n \n if (this.checkRecords.length === 0) {\n this.showMessage('🍼 请先添加体检记录', 'error');\n return;\n }\n \n if (!this.apiKey || this.apiKey.trim() === '') {\n this.showMessage('🔑 请先设置Deepseek API密钥', 'error');\n return;\n }\n \n const latestRecord = this.getLatestRecord();\n if (!latestRecord) {\n this.showMessage('❌ 无法获取最新体检记录', 'error');\n return;\n }\n \n console.log('最新体检记录:', latestRecord);\n \n const growthTrend = this.analyzeGrowthTrend();\n console.log('生长趋势分析:', growthTrend);\n \n const concernsInput = document.getElementById('feedingConcerns');\n const concerns = concernsInput ? concernsInput.value.trim() : '';\n \n const feedingLoading = document.getElementById('feedingLoading');\n const feedingGuide = document.getElementById('feedingGuide');\n \n console.log('DOM元素检查:');\n console.log('- feedingLoading:', !!feedingLoading);\n console.log('- feedingGuide:', !!feedingGuide);\n \n if (feedingLoading) feedingLoading.classList.remove('hidden');\n if (feedingGuide) feedingGuide.classList.add('hidden');\n \n try {\n const healthAnalysis = this.analyzeHealthStatus(latestRecord);\n console.log('健康分析:', healthAnalysis);\n \n this.showLoadingState('🤖 正在生成AI喂养建议...');\n const aiGuide = await this.callDeepseekForFeedingGuide(latestRecord, healthAnalysis, growthTrend, concerns);\n console.log('AI建议生成完成,字符数:', aiGuide.length);\n \n this.displayAIFeedingGuide(aiGuide, healthAnalysis, growthTrend, concerns);\n \n if (feedingLoading) feedingLoading.classList.add('hidden');\n if (feedingGuide) feedingGuide.classList.remove('hidden');\n this.hideLoadingState();\n this.showMessage('✨ AI喂养建议生成完成', 'success');\n \n } catch (error) {\n if (feedingLoading) feedingLoading.classList.add('hidden');\n this.hideLoadingState();\n this.showMessage('❌ 生成AI喂养建议失败: ' + error.message, 'error');\n console.error('AI指导生成错误:', error);\n }\n }\n\n analyzeHealthStatus(record) {\n const issues = [];\n const recommendations = [];\n \n if (record.height && record.heightPercentile) {\n if (record.heightPercentile < 10) {\n issues.push('身高偏低');\n recommendations.push('注意营养补充,建议咨询儿科医生');\n } else if (record.heightPercentile > 90) {\n issues.push('身高偏高');\n recommendations.push('继续保持良好的营养状态');\n }\n }\n \n if (record.weight && record.weightPercentile) {\n if (record.weightPercentile < 10) {\n issues.push('体重偏轻');\n recommendations.push('需要增加营养密度,增加喂养频次');\n } else if (record.weightPercentile > 90) {\n issues.push('体重超重');\n recommendations.push('控制高热量食物,增加活动量');\n }\n }\n \n if (record.headCircumference && record.headPercentile) {\n if (record.headPercentile < 10) {\n issues.push('头围偏小');\n recommendations.push('建议咨询儿科医生,关注神经系统发育');\n } else if (record.headPercentile > 90) {\n issues.push('头围偏大');\n recommendations.push('建议咨询儿科医生,排除相关疾病');\n }\n }\n \n if (record.height && record.weight) {\n const bmi = record.weight / Math.pow(record.height / 100, 2);\n if (bmi > 18) {\n issues.push('BMI偏高');\n recommendations.push('注意饮食结构,避免过度喂养');\n } else if (bmi < 14) {\n issues.push('BMI偏低');\n recommendations.push('增加营养摄入,关注生长发育');\n }\n }\n \n return {\n issues: issues.length > 0 ? issues : ['生长发育正常'],\n recommendations: recommendations,\n riskLevel: issues.length > 0 ? '需要关注' : '正常'\n };\n }\n\n async callDeepseekForFeedingGuide(record, healthAnalysis, growthTrend, concerns) {\n console.log('开始调用Deepseek API生成喂养建议');\n \n let prompt = '作为专业的儿科营养师,请为以下婴幼儿生成个性化的喂养指导建议:\\n\\n# 宝宝基本信息\\n- 姓名:' + (this.babyInfo.name || '未知') + '\\n- 性别:' + (this.babyInfo.gender === 'female' ? '女' : '男') + '\\n- 出生日期:' + (this.babyInfo.birthDate || '未知') + '\\n\\n# 最新体检数据(' + record.checkDate + ')\\n- 年龄:' + record.ageMonths + '个月\\n- 身高:' + (record.height || '未知') + 'cm (百分位:' + (record.heightPercentile || '未知') + '%)\\n- 体重:' + (record.weight || '未知') + 'kg (百分位:' + (record.weightPercentile || '未知') + '%)\\n- 头围:' + (record.headCircumference || '未知') + 'cm (百分位:' + (record.headPercentile || '未知') + '%)\\n\\n# 健康状况分析\\n主要问题:' + healthAnalysis.issues.join('、') + '\\n风险等级:' + healthAnalysis.riskLevel + '\\n\\n# 生长趋势分析\\n趋势:' + growthTrend.trend + '\\n分析基础:' + growthTrend.description;\n \n if (concerns) {\n prompt += '\\n\\n# 家长重点关注\\n' + concerns + '\\n\\n请特别针对以上关注点提供详细的解决方案和建议。';\n }\n \n prompt += '\\n\\n请提供以下三个方面的详细建议,要求专业、实用、针对性强:\\n\\n### 1. 每日营养需求\\n根据年龄、性别、当前生长状况和趋势,提供具体的营养摄入建议,包括热量、蛋白质、维生素、矿物质等需求。\\n\\n### 2. 食物推荐\\n推荐适合的食物种类、制作方法和注意事项,考虑年龄特点和个体差异。\\n\\n### 3. 喂养技巧\\n提供实用的喂养方法、时间安排和注意事项,结合生长趋势给出调整建议。\\n\\n如果存在健康问题或生长趋势异常,请重点关注并提供针对性的改善建议。回复请用中文,条理清晰,内容详实。';\n \n console.log('发送的提示词长度:', prompt.length);\n \n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + this.apiKey\n },\n body: JSON.stringify({\n model: \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content: prompt\n }\n ],\n max_tokens: 2500,\n temperature: 0.7,\n stream: false\n })\n });\n \n console.log('API响应状态:', response.status);\n \n if (!response.ok) {\n const errorText = await response.text();\n console.error('API错误响应:', errorText);\n throw new Error('API调用失败: ' + response.status + ' - ' + errorText);\n }\n \n const data = await response.json();\n console.log('API响应数据结构:', Object.keys(data));\n \n if (!data.choices || !data.choices[0] || !data.choices[0].message) {\n throw new Error('API返回数据格式错误');\n }\n \n return data.choices[0].message.content;\n \n } catch (error) {\n console.error('Deepseek API调用失败:', error);\n if (error.message.includes('401')) {\n throw new Error('API密钥无效,请检查密钥是否正确');\n } else if (error.message.includes('429')) {\n throw new Error('请求过于频繁,请稍后重试');\n } else if (error.message.includes('500')) {\n throw new Error('AI服务暂时不可用,请稍后重试');\n } else {\n throw new Error('网络错误: ' + error.message);\n }\n }\n }\n\n displayAIFeedingGuide(aiGuide, healthAnalysis, growthTrend, concerns) {\n console.log('显示AI喂养建议,内容长度:', aiGuide.length);\n \n const nutritionContent = document.getElementById('nutritionContent');\n const foodContent = document.getElementById('foodContent');\n const tipsContent = document.getElementById('tipsContent');\n \n console.log('内容容器检查:');\n console.log('- nutritionContent:', !!nutritionContent);\n console.log('- foodContent:', !!foodContent);\n console.log('- tipsContent:', !!tipsContent);\n \n const sections = this.parseAIGuide(aiGuide);\n \n if (nutritionContent) {\n let nutritionHTML = '';\n \n const latestRecord = this.getLatestRecord();\n nutritionHTML += '<div style=\"background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%); padding: 15px; border-radius: 15px; margin-bottom: 15px; border: 2px solid #2196F3;\"><strong>📊 最新体检数据(' + latestRecord.checkDate + '):</strong><br>年龄:' + latestRecord.ageMonths + '个月 | 身高:' + (latestRecord.height || '--') + 'cm (' + (latestRecord.heightPercentile || '--') + '%) | 体重:' + (latestRecord.weight || '--') + 'kg (' + (latestRecord.weightPercentile || '--') + '%) | 头围:' + (latestRecord.headCircumference || '--') + 'cm (' + (latestRecord.headPercentile || '--') + '%)</div>';\n \n nutritionHTML += '<div style=\"background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%); padding: 15px; border-radius: 15px; margin-bottom: 15px; border: 2px solid #9C27B0;\"><strong>📈 生长趋势:</strong>' + growthTrend.trend + '<br><small>' + growthTrend.description + '</small></div>';\n \n nutritionHTML += '<div style=\"background: linear-gradient(135deg, #FFF3CD 0%, #FFEAA7 100%); padding: 15px; border-radius: 15px; margin-bottom: 15px; border: 2px solid #FFC107;\"><strong>⚠️ 健康状况:</strong>' + healthAnalysis.issues.join('、') + '</div>';\n \n if (concerns) {\n nutritionHTML += '<div style=\"background: linear-gradient(135deg, #D1ECF1 0%, #B6E3EC 100%); padding: 15px; border-radius: 15px; margin-bottom: 15px; border: 2px solid #17A2B8;\"><strong>🎯 重点关注:</strong>' + concerns + '</div>';\n }\n \n nutritionHTML += sections.nutrition || '<div style=\"white-space: pre-wrap; background: white; padding: 20px; border-radius: 15px; border: 2px solid #FFB6C1;\">' + aiGuide + '</div>';\n nutritionContent.innerHTML = nutritionHTML;\n }\n \n if (foodContent) {\n foodContent.innerHTML = sections.food || '<p style=\"text-align: center; color: #888;\">请查看上方营养需求部分的完整建议</p>';\n }\n \n if (tipsContent) {\n tipsContent.innerHTML = sections.tips || '<p style=\"text-align: center; color: #888;\">请查看上方营养需求部分的完整建议</p>';\n }\n }\n\n parseAIGuide(content) {\n const sections = {};\n \n const nutritionMatch = content.match(/(?:每日营养需求|### 1\\. 每日营养需求|1\\. 每日营养需求)[::]?\\s*([\\s\\S]*?)(?=(?:食物推荐|### 2\\. 食物推荐|2\\. 食物推荐|喂养技巧|### 3\\. 喂养技巧|3\\. 喂养技巧)|$)/);\n const foodMatch = content.match(/(?:食物推荐|### 2\\. 食物推荐|2\\. 食物推荐)[::]?\\s*([\\s\\S]*?)(?=(?:喂养技巧|### 3\\. 喂养技巧|3\\. 喂养技巧|每日营养|### 1\\. 每日营养需求|1\\. 每日营养需求)|$)/);\n const tipsMatch = content.match(/(?:喂养技巧|### 3\\. 喂养技巧|3\\. 喂养技巧)[::]?\\s*([\\s\\S]*?)(?=(?:每日营养|### 1\\. 每日营养需求|1\\. 每日营养需求|食物推荐|### 2\\. 食物推荐|2\\. 食物推荐)|$)/);\n \n if (nutritionMatch) {\n sections.nutrition = '<div style=\"white-space: pre-wrap; background: white; padding: 20px; border-radius: 15px; border: 2px solid #FFB6C1;\">' + nutritionMatch[1].trim() + '</div>';\n }\n \n if (foodMatch) {\n sections.food = '<div style=\"white-space: pre-wrap; background: white; padding: 20px; border-radius: 15px; border: 2px solid #FFB6C1;\">' + foodMatch[1].trim() + '</div>';\n }\n \n if (tipsMatch) {\n sections.tips = '<div style=\"white-space: pre-wrap; background: white; padding: 20px; border-radius: 15px; border: 2px solid #FFB6C1;\">' + tipsMatch[1].trim() + '</div>';\n }\n \n return sections;\n }\n\n showLoadingState(message) {\n const loadingEl = document.getElementById('loadingMessage');\n if (loadingEl) {\n const span = loadingEl.querySelector('span');\n if (span) span.textContent = message;\n loadingEl.classList.remove('hidden');\n }\n }\n\n hideLoadingState() {\n const loadingEl = document.getElementById('loadingMessage');\n if (loadingEl) {\n loadingEl.classList.add('hidden');\n }\n }\n\n showMessage(message, type) {\n type = type || 'info';\n const existingMessage = document.querySelector('.toast-message');\n if (existingMessage) {\n existingMessage.remove();\n }\n \n const messageEl = document.createElement('div');\n messageEl.className = 'toast-message';\n messageEl.textContent = message;\n \n let backgroundColor;\n switch(type) {\n case 'success':\n backgroundColor = 'linear-gradient(135deg, #4CAF50 0%, #45A049 100%)';\n break;\n case 'error':\n backgroundColor = 'linear-gradient(135deg, #f44336 0%, #d32f2f 100%)';\n break;\n case 'warning':\n backgroundColor = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';\n break;\n default:\n backgroundColor = 'linear-gradient(135deg, #2196F3 0%, #1976D2 100%)';\n }\n \n messageEl.style.cssText = 'position: fixed; top: 20px; right: 20px; padding: 15px 25px; border-radius: 25px; color: white; z-index: 1000; font-size: 14px; font-weight: 600; max-width: 350px; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); background: ' + backgroundColor + '; transition: all 0.3s ease; animation: slideInRight 0.5s ease-out;';\n \n const style = document.createElement('style');\n style.textContent = '@keyframes slideInRight { from { transform: translateX(100px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }';\n document.head.appendChild(style);\n \n document.body.appendChild(messageEl);\n \n setTimeout(function() {\n if (messageEl.parentNode) {\n messageEl.style.opacity = '0';\n messageEl.style.transform = 'translateX(100px)';\n setTimeout(function() {\n if (messageEl.parentNode) {\n messageEl.remove();\n }\n }, 300);\n }\n }, 3000);\n }\n\n addManualRecord() {\n if (!this.validateBasicInfo()) {\n return;\n }\n \n const formData = this.getFormData();\n if (this.validateFormData(formData)) {\n const gender = this.babyInfo.gender || 'male';\n const recordWithPercentiles = {\n checkDate: formData.checkDate,\n ageMonths: formData.ageMonths,\n height: formData.height,\n weight: formData.weight,\n headCircumference: formData.headCircumference,\n id: Date.now(),\n addedAt: new Date().toISOString(),\n heightPercentile: this.calculateWHOPercentileWithLinearInterpolation(formData.ageMonths, formData.height, 'height', gender),\n weightPercentile: this.calculateWHOPercentileWithLinearInterpolation(formData.ageMonths, formData.weight, 'weight', gender),\n headPercentile: this.calculateWHOPercentileWithLinearInterpolation(formData.ageMonths, formData.headCircumference, 'headCircumference', gender)\n };\n \n this.checkRecords.push(recordWithPercentiles);\n this.saveData();\n this.updateRecordsTable();\n this.updateCurrentStatus();\n this.updateChart();\n this.clearForm();\n this.showMessage('🎉 记录添加成功', 'success');\n }\n }\n\n getFormData() {\n return {\n checkDate: document.getElementById('checkDate') ? document.getElementById('checkDate').value : '',\n ageMonths: parseFloat(document.getElementById('ageMonths') ? document.getElementById('ageMonths').value : '') || null,\n height: parseFloat(document.getElementById('height') ? document.getElementById('height').value : '') || null,\n weight: parseFloat(document.getElementById('weight') ? document.getElementById('weight').value : '') || null,\n headCircumference: parseFloat(document.getElementById('headCircumference') ? document.getElementById('headCircumference').value : '') || null\n };\n }\n\n validateFormData(data) {\n if (!data.checkDate) {\n this.showMessage('📅 请填写体检日期', 'error');\n return false;\n }\n if (!data.ageMonths || data.ageMonths <= 0) {\n this.showMessage('👶 请填写有效的年龄(月)', 'error');\n return false;\n }\n if (!data.height && !data.weight && !data.headCircumference) {\n this.showMessage('📏 请至少填写身高、体重或头围中的一项', 'error');\n return false;\n }\n return true;\n }\n\n saveData() {\n localStorage.setItem('baby_check_records', JSON.stringify(this.checkRecords));\n localStorage.setItem('baby_info', JSON.stringify(this.babyInfo));\n }\n\n loadSavedData() {\n this.updateApiStatus();\n this.updateBaiduOcrStatus();\n \n const apiKeyInput = document.getElementById('apiKeyInput');\n if (apiKeyInput && this.apiKey) {\n apiKeyInput.value = this.apiKey;\n }\n \n const baiduApiKeyInput = document.getElementById('baiduOcrApiKey');\n const baiduSecretKeyInput = document.getElementById('baiduOcrSecretKey');\n if (baiduApiKeyInput && this.baiduOcrConfig.apiKey) {\n baiduApiKeyInput.value = this.baiduOcrConfig.apiKey;\n }\n if (baiduSecretKeyInput && this.baiduOcrConfig.secretKey) {\n baiduSecretKeyInput.value = this.baiduOcrConfig.secretKey;\n }\n \n if (this.babyInfo.name) {\n const babyNameInput = document.getElementById('babyName');\n if (babyNameInput) babyNameInput.value = this.babyInfo.name;\n }\n if (this.babyInfo.gender) {\n const babyGenderSelect = document.getElementById('babyGender');\n if (babyGenderSelect) babyGenderSelect.value = this.babyInfo.gender;\n }\n if (this.babyInfo.birthDate) {\n const babyBirthDateInput = document.getElementById('babyBirthDate');\n if (babyBirthDateInput) babyBirthDateInput.value = this.babyInfo.birthDate;\n }\n }\n\n updateUI() {\n this.updateRecordsTable();\n this.updateStatistics();\n this.updateCurrentStatus();\n }\n\n updateCurrentStatus() {\n const latestRecord = this.getLatestRecord();\n if (latestRecord) {\n const currentAgeEl = document.getElementById('currentAge');\n const currentHeightEl = document.getElementById('currentHeight');\n const currentWeightEl = document.getElementById('currentWeight');\n \n if (currentAgeEl) currentAgeEl.textContent = latestRecord.ageMonths + '月';\n if (currentHeightEl) currentHeightEl.textContent = (latestRecord.height || '--') + 'cm';\n if (currentWeightEl) currentWeightEl.textContent = (latestRecord.weight || '--') + 'kg';\n \n const feedingAgeEl = document.getElementById('feedingAge');\n const feedingHeightEl = document.getElementById('feedingHeight');\n const feedingWeightEl = document.getElementById('feedingWeight');\n \n if (feedingAgeEl) feedingAgeEl.textContent = latestRecord.ageMonths + '月';\n if (feedingHeightEl) feedingHeightEl.textContent = (latestRecord.height || '--') + 'cm';\n if (feedingWeightEl) feedingWeightEl.textContent = (latestRecord.weight || '--') + 'kg';\n } else {\n const elementsToUpdate = [\n 'currentAge', 'currentHeight', 'currentWeight',\n 'feedingAge', 'feedingHeight', 'feedingWeight'\n ];\n \n for (let i = 0; i < elementsToUpdate.length; i++) {\n const el = document.getElementById(elementsToUpdate[i]);\n if (el) el.textContent = '--';\n }\n }\n }\n\n updateRecordsTable() {\n const tableBody = document.getElementById('recordsTableBody');\n const noRecords = document.getElementById('noRecords');\n \n if (!tableBody) return;\n \n tableBody.innerHTML = '';\n \n if (this.checkRecords.length === 0) {\n if (noRecords) noRecords.style.display = 'block';\n return;\n }\n \n if (noRecords) noRecords.style.display = 'none';\n \n const sortedRecords = this.checkRecords.slice().sort(function(a, b) {\n return new Date(b.checkDate) - new Date(a.checkDate);\n });\n \n const self = this;\n for (let i = 0; i < sortedRecords.length; i++) {\n const record = sortedRecords[i];\n const row = document.createElement('tr');\n row.innerHTML = '<td>' + record.checkDate + '</td><td>' + record.ageMonths + '个月</td><td>' + (record.height || '--') + '</td><td>' + (record.heightPercentile || '--') + '%</td><td>' + (record.weight || '--') + '</td><td>' + (record.weightPercentile || '--') + '%</td><td>' + (record.headCircumference || '--') + '</td><td>' + (record.headPercentile || '--') + '%</td><td><button onclick=\"window.babyTracker.deleteRecord(' + this.checkRecords.indexOf(record) + ')\" class=\"btn btn-danger\" style=\"padding: 8px 15px; font-size: 0.8em; border-radius: 15px; background: linear-gradient(135deg, #FF6B6B 0%, #FF5252 100%);\">🗑️ 删除</button></td>';\n tableBody.appendChild(row);\n }\n }\n\n updateStatistics() {\n const statsEl = document.getElementById('statisticsInfo');\n if (statsEl && this.checkRecords.length > 0) {\n const latestRecord = this.getLatestRecord();\n statsEl.innerHTML = '<p><strong>📊 总记录数:</strong>' + this.checkRecords.length + '</p><p><strong>🗓️ 最新记录:</strong>' + latestRecord.checkDate + '</p><p><strong>👶 当前月龄:</strong>' + latestRecord.ageMonths + '个月</p><p><strong>🤖 识别模式:</strong>百度OCR+AI增强</p>';\n }\n }\n\n deleteRecord(index) {\n if (confirm('🤔 确定要删除这条记录吗?')) {\n this.checkRecords.splice(index, 1);\n this.saveData();\n this.updateRecordsTable();\n this.updateStatistics();\n this.updateCurrentStatus();\n this.updateChart();\n this.showMessage('✅ 记录已删除', 'success');\n }\n }\n\n clearForm() {\n const inputs = ['checkDate', 'ageMonths', 'height', 'weight', 'headCircumference'];\n for (let i = 0; i < inputs.length; i++) {\n const el = document.getElementById(inputs[i]);\n if (el) el.value = '';\n }\n \n const percentileEls = ['heightPercentile', 'weightPercentile', 'headPercentile'];\n for (let i = 0; i < percentileEls.length; i++) {\n const el = document.getElementById(percentileEls[i]);\n if (el) el.textContent = '--';\n }\n \n const uploadedImageDiv = document.getElementById('uploadedImage');\n if (uploadedImageDiv) uploadedImageDiv.classList.add('hidden');\n \n this.hideOCRResult();\n this.resetUploadInput();\n }\n\n exportData() {\n const data = {\n babyInfo: this.babyInfo,\n checkRecords: this.checkRecords,\n exportDate: new Date().toISOString(),\n version: '10.0-BaiduOCR'\n };\n \n const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'baby_health_data_' + new Date().toISOString().split('T')[0] + '.json';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n \n this.showMessage('💾 数据导出成功', 'success');\n }\n\n importData() {\n const self = this;\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = '.json';\n input.onchange = function(e) {\n const file = e.target.files[0];\n if (file) {\n const reader = new FileReader();\n reader.onload = function(e) {\n try {\n const data = JSON.parse(e.target.result);\n if (data.checkRecords && Array.isArray(data.checkRecords)) {\n self.babyInfo = data.babyInfo || {};\n self.checkRecords = data.checkRecords || [];\n self.saveData();\n self.updateUI();\n self.loadSavedData();\n self.updateChart();\n self.showMessage('📥 数据导入成功', 'success');\n } else {\n self.showMessage('❌ 数据格式不正确', 'error');\n }\n } catch (error) {\n self.showMessage('⚠️ 数据格式错误,请检查文件', 'error');\n console.error('导入错误:', error);\n }\n };\n reader.readAsDataURL(file);\n }\n };\n input.click();\n }\n }\n\n // 页面加载完成后初始化\n document.addEventListener('DOMContentLoaded', function() {\n window.babyTracker = new BabyHealthTracker();\n console.log('🎀 婴幼儿健康追踪系统已初始化 (百度OCR版 v10.0)');\n \n // 绑定图表类型切换事件\n const chartTypeInputs = document.querySelectorAll('input[name=\"chartType\"]');\n for (let i = 0; i < chartTypeInputs.length; i++) {\n const input = chartTypeInputs[i];\n input.addEventListener('change', function() {\n if (window.babyTracker && window.babyTracker.updateChart) {\n window.babyTracker.updateChart();\n }\n });\n }\n \n // 绑定标签页切换功能\n const tabButtons = document.querySelectorAll('.tab-button');\n const tabContents = document.querySelectorAll('.tab-content');\n \n for (let i = 0; i < tabButtons.length; i++) {\n const button = tabButtons[i];\n button.addEventListener('click', function() {\n const targetTab = button.getAttribute('data-tab');\n \n // 移除所有活动状态\n for (let j = 0; j < tabButtons.length; j++) {\n tabButtons[j].classList.remove('active');\n }\n for (let j = 0; j < tabContents.length; j++) {\n tabContents[j].classList.remove('active');\n }\n \n // 激活当前标签页\n button.classList.add('active');\n const targetContent = document.getElementById(targetTab);\n if (targetContent) {\n targetContent.classList.add('active');\n }\n });\n }\n \n // 初始化第一个标签页为活动状态\n if (tabButtons.length > 0) {\n tabButtons[0].classList.add('active');\n const firstTabId = tabButtons[0].getAttribute('data-tab');\n const firstTabContent = document.getElementById(firstTabId);\n if (firstTabContent) {\n firstTabContent.classList.add('active');\n }\n }\n });\n\n})();\n"}}}}