weight.component.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <div class="weight-container">
  2. <app-page-header [title]="'体重管理'" (back)="backToDashboard()">
  3. <div header-actions>
  4. <button class="btn btn-primary" (click)="openAddRecordDialog()">+ 录入体重</button>
  5. <button class="btn btn-secondary" (click)="openGoalDialog()">目标设置</button>
  6. </div>
  7. </app-page-header>
  8. <!-- 趋势主导视图主布局 -->
  9. <div class="weight-main-grid" *ngIf="stats">
  10. <!-- 左侧:体重趋势 + 关键数字 -->
  11. <div class="left-column">
  12. <section class="panel">
  13. <div class="panel-header">
  14. <span class="panel-title">体重趋势总览</span>
  15. </div>
  16. <!-- 周期切换标签 -->
  17. <div class="chart-period-tabs">
  18. <button
  19. class="period-tab"
  20. [class.active]="trendPeriod === 'week'"
  21. (click)="changeTrendPeriod('week')">
  22. 周
  23. </button>
  24. <button
  25. class="period-tab"
  26. [class.active]="trendPeriod === 'month'"
  27. (click)="changeTrendPeriod('month')">
  28. 月
  29. </button>
  30. <button
  31. class="period-tab"
  32. [class.active]="trendPeriod === '6months'"
  33. (click)="changeTrendPeriod('6months')">
  34. 6个月
  35. </button>
  36. </div>
  37. <!-- 统计信息 -->
  38. <div class="trend-stats" *ngIf="trendStats">
  39. <div class="trend-avg">
  40. <span class="avg-label">平均</span>
  41. <span class="avg-value" [ngClass]="{'positive': trendStats.avgChange > 0, 'negative': trendStats.avgChange < 0}">
  42. {{ trendStats.avgChange > 0 ? '+' : '' }}{{ trendStats.avgChange.toFixed(2) }}
  43. </span>
  44. <span class="avg-unit">kg(距离基线)</span>
  45. </div>
  46. <div class="trend-range">{{ trendStats.dateRange }}</div>
  47. </div>
  48. <div class="trend-chart-wrapper">
  49. <div id="trendChart" class="chart" style="width: 100%; height: 100%;"></div>
  50. </div>
  51. <div class="kpi-strip">
  52. <div class="kpi-item">
  53. <div class="kpi-label">当前体重</div>
  54. <div class="kpi-main">
  55. <span class="kpi-value">{{ stats.currentWeight.toFixed(1) }}</span>
  56. <span class="kpi-unit">kg</span>
  57. </div>
  58. <div class="kpi-desc" *ngIf="goal">
  59. 目标 {{ goal.targetWeight }}kg · 剩余
  60. {{ (stats.currentWeight - goal.targetWeight) | number:'1.1-1' }}kg
  61. </div>
  62. <div class="kpi-desc" *ngIf="!goal">
  63. 建议先设置目标体重,便于评估进度
  64. </div>
  65. </div>
  66. <div class="kpi-item">
  67. <div class="kpi-label">最近体重变化</div>
  68. <div class="kpi-main">
  69. <span
  70. class="kpi-value"
  71. [ngClass]="{ 'positive': stats.weightChange > 0, 'negative': stats.weightChange < 0 }"
  72. >
  73. {{ stats.weightChange > 0 ? '+' : '' }}{{ stats.weightChange.toFixed(1) }}
  74. </span>
  75. <span class="kpi-unit">kg</span>
  76. </div>
  77. <div class="kpi-desc">从所选时间范围起点到当前的总体变化</div>
  78. </div>
  79. <div class="kpi-item">
  80. <div class="kpi-label">平均周变化</div>
  81. <div class="kpi-main">
  82. <span
  83. class="kpi-value"
  84. [ngClass]="{ 'positive': stats.avgWeeklyChange > 0, 'negative': stats.avgWeeklyChange < 0 }"
  85. >
  86. {{ stats.avgWeeklyChange > 0 ? '+' : '' }}{{ stats.avgWeeklyChange.toFixed(2) }}
  87. </span>
  88. <span class="kpi-unit">kg/周</span>
  89. </div>
  90. <div class="kpi-desc">
  91. 已跟踪 {{ stats.daysTracked }} 天 · {{ stats.goalETA || '预计达成时间计算中' }}
  92. </div>
  93. </div>
  94. </div>
  95. </section>
  96. </div>
  97. <!-- 右侧:体脂率与最近记录 -->
  98. <div class="right-column" *ngIf="records.length > 0">
  99. <section class="panel">
  100. <div class="panel-header">
  101. <span class="panel-title">体脂率与身体成分</span>
  102. <span class="panel-extra">目标体脂率 {{ goal?.targetBodyFat ?? '—' }}%</span>
  103. </div>
  104. <div class="composition-body">
  105. <div class="composition-ring">
  106. <div id="progressChart" class="chart-small"></div>
  107. </div>
  108. <div class="composition-info">
  109. <ul class="list">
  110. <li class="list-item">
  111. <span>当前体脂率</span>
  112. <span><strong>{{ records[0].bodyFat.toFixed(1) }}%</strong></span>
  113. </li>
  114. <li class="list-item" *ngIf="stats">
  115. <span>最近 30 天变化</span>
  116. <span
  117. class="kpi-value"
  118. [ngClass]="{ 'negative': stats.bodyFatChange < 0, 'positive': stats.bodyFatChange > 0 }"
  119. >
  120. {{ stats.bodyFatChange > 0 ? '+' : ''}}{{ stats.bodyFatChange.toFixed(1) }}%
  121. </span>
  122. </li>
  123. <li class="list-item">
  124. <span>估算去脂体重</span>
  125. <span>{{ (records[0].weight * (1 - records[0].bodyFat / 100)).toFixed(1) }} kg</span>
  126. </li>
  127. <li class="list-item">
  128. <span>肌肉量</span>
  129. <span>{{ records[0].muscleMass.toFixed(1) }} kg</span>
  130. </li>
  131. </ul>
  132. <div class="tag-row">
  133. <span class="tag" *ngIf="stats.bodyFatChange <= 0">体重趋势与体脂率整体同步下降</span>
  134. <span class="tag" *ngIf="stats.bodyFatChange > 0">近期可能存在"掉秤不掉脂"情况</span>
  135. </div>
  136. </div>
  137. </div>
  138. </section>
  139. <section class="panel">
  140. <div class="panel-header">
  141. <span class="panel-title">最近关键记录</span>
  142. <span class="panel-extra">近 7 天</span>
  143. </div>
  144. <ul class="list">
  145. <li class="list-item" *ngFor="let rec of records | slice:0:3">
  146. <span>{{ rec.date }} · {{ rec.weight.toFixed(1) }}kg · {{ rec.bodyFat.toFixed(1) }}%</span>
  147. <span class="badge">
  148. {{ rec.measurementCondition === 'fasting' ? '空腹' : '餐后' }}
  149. </span>
  150. </li>
  151. </ul>
  152. <div class="cta-row">
  153. <button class="cta-btn primary" (click)="openAddRecordDialog()">+ 录入体重与体脂</button>
  154. <button class="cta-btn" (click)="updateTimePeriod('7days')">仅看近 7 天</button>
  155. </div>
  156. </section>
  157. </div>
  158. </div>
  159. <div class="filter-bar">
  160. <button class="filter-btn" [class.active]="filter.timePeriod === '7days'" (click)="updateTimePeriod('7days')">7天</button>
  161. <button class="filter-btn" [class.active]="filter.timePeriod === '30days'" (click)="updateTimePeriod('30days')">30天</button>
  162. <button class="filter-btn" [class.active]="filter.timePeriod === '90days'" (click)="updateTimePeriod('90days')">90天</button>
  163. <button class="filter-btn" [class.active]="filter.timePeriod === 'all'" (click)="updateTimePeriod('all')">全部</button>
  164. <button class="filter-btn" [class.active]="filter.timePeriod === 'custom'" (click)="showCustomDatePicker = !showCustomDatePicker">自定义</button>
  165. <select class="filter-select" [(ngModel)]="filter.condition" (change)="applyFilter(); updateCharts();">
  166. <option value="all">全部条件</option>
  167. <option value="fasting">空腹</option>
  168. <option value="after_meal">餐后</option>
  169. </select>
  170. </div>
  171. <!-- 自定义日期范围选择器 -->
  172. <div class="custom-date-picker" *ngIf="showCustomDatePicker">
  173. <div class="date-range-inputs">
  174. <div class="date-input-group">
  175. <label>开始日期</label>
  176. <input type="date" [(ngModel)]="filter.startDate" />
  177. </div>
  178. <div class="date-input-group">
  179. <label>结束日期</label>
  180. <input type="date" [(ngModel)]="filter.endDate" />
  181. </div>
  182. <button class="btn btn-primary" (click)="applyCustomDateRange()">应用</button>
  183. </div>
  184. </div>
  185. <!-- 标签筛选 -->
  186. <div class="tag-filter" *ngIf="availableTags.length > 0">
  187. <div class="tag-filter-title">按标签筛选:</div>
  188. <div class="tag-filter-buttons">
  189. <button *ngFor="let tag of availableTags" class="tag-filter-btn" [class.active]="filter.tags?.includes(tag)" (click)="toggleFilterTag(tag)">
  190. {{ tag }}
  191. </button>
  192. </div>
  193. </div>
  194. <!-- 体重变化与体脂率分析(横向布局) -->
  195. <div class="analysis-charts-container">
  196. <div class="change-chart-section">
  197. <div class="chart-header">
  198. <h3 class="chart-title">体重变化趋势</h3>
  199. <button class="toggle-btn" (click)="toggleChartPeriod()">切换至{{ chartPeriod === 'weekly' ? '月' : '周' }}视图</button>
  200. </div>
  201. <div id="changeChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
  202. </div>
  203. <div class="scatter-chart-section">
  204. <div class="chart-header">
  205. <h3 class="chart-title">体重体脂率关系</h3>
  206. </div>
  207. <div id="scatterChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
  208. </div>
  209. </div>
  210. <!-- 添加记录对话框 -->
  211. <app-modal
  212. [(isOpen)]="showAddRecordDialog"
  213. [config]="{
  214. title: '录入体重数据',
  215. icon: '⚖️',
  216. size: 'medium',
  217. theme: 'default',
  218. showFooter: false
  219. }">
  220. <div class="weight-record-form">
  221. <div class="form-group">
  222. <label>日期</label>
  223. <input type="date" [(ngModel)]="newRecord.date" max="{{ today }}" />
  224. </div>
  225. <div class="form-group">
  226. <label>测量时间</label>
  227. <input type="time" [(ngModel)]="newRecord.measurementTime" />
  228. </div>
  229. <div class="form-group">
  230. <label>体重 (kg)</label>
  231. <input type="number" [(ngModel)]="newRecord.weight" step="0.1" min="30" max="200" />
  232. </div>
  233. <div class="form-group">
  234. <label>体脂率 (%)</label>
  235. <input type="number" [(ngModel)]="newRecord.bodyFat" step="0.1" min="5" max="60" />
  236. </div>
  237. <div class="form-group">
  238. <label>肌肉量 (kg)</label>
  239. <input type="number" [(ngModel)]="newRecord.muscleMass" step="0.1" min="10" max="100" />
  240. </div>
  241. <div class="form-group">
  242. <label>测量条件</label>
  243. <select [(ngModel)]="newRecord.measurementCondition">
  244. <option value="fasting">空腹</option>
  245. <option value="after_meal">餐后</option>
  246. </select>
  247. </div>
  248. <div class="form-group">
  249. <label>备注</label>
  250. <textarea [(ngModel)]="newRecord.notes" rows="3"></textarea>
  251. </div>
  252. <div class="form-group">
  253. <label>标签(关键节点标记)</label>
  254. <div class="tag-selector">
  255. <div class="available-tags">
  256. <button *ngFor="let tag of availableTags" type="button" class="tag-btn" [class.selected]="newRecord.tags?.includes(tag)" (click)="addTagToRecord(tag)">
  257. {{ tag }}
  258. </button>
  259. </div>
  260. <div class="selected-tags" *ngIf="newRecord.tags && newRecord.tags.length > 0">
  261. <span class="selected-tag" *ngFor="let tag of newRecord.tags">
  262. {{ tag }}
  263. <button type="button" class="remove-tag" (click)="removeTagFromRecord(tag)">×</button>
  264. </span>
  265. </div>
  266. <div class="custom-tag-input">
  267. <input type="text" [(ngModel)]="newTagInput" placeholder="添加自定义标签" (keyup.enter)="addCustomTag()" />
  268. <button type="button" class="btn btn-secondary btn-sm" (click)="addCustomTag()">添加</button>
  269. </div>
  270. </div>
  271. </div>
  272. <div class="dialog-actions">
  273. <button class="btn btn-secondary" (click)="showAddRecordDialog = false">取消</button>
  274. <button class="btn btn-primary" (click)="addRecord()">保存</button>
  275. </div>
  276. </div>
  277. </app-modal>
  278. <!-- 目标设置对话框 -->
  279. <app-modal
  280. [(isOpen)]="showGoalDialog"
  281. [config]="{
  282. title: '设置目标',
  283. icon: '🎯',
  284. size: 'small',
  285. theme: 'gradient',
  286. showFooter: false
  287. }">
  288. <div class="goal-setting-form">
  289. <div class="form-group">
  290. <label>目标体重 (kg)</label>
  291. <input type="number" [(ngModel)]="newGoal.targetWeight" step="0.1" min="30" max="200" />
  292. </div>
  293. <div class="form-group">
  294. <label>目标体脂率 (%)</label>
  295. <input type="number" [(ngModel)]="newGoal.targetBodyFat" step="0.1" min="5" max="60" />
  296. </div>
  297. <div class="form-group">
  298. <label>目标日期</label>
  299. <input type="date" [(ngModel)]="newGoal.targetDate" [min]="tomorrow" />
  300. </div>
  301. <div class="dialog-actions">
  302. <button class="btn btn-secondary" (click)="showGoalDialog = false">取消</button>
  303. <button class="btn btn-primary" (click)="setGoal()">保存</button>
  304. </div>
  305. </div>
  306. </app-modal>
  307. <!-- 调试:一键生成90天模拟数据 -->
  308. <div style="padding: 16px; text-align: center;">
  309. <button class="btn btn-secondary" (click)="generateMockData()">一键生成90天模拟数据</button>
  310. </div>
  311. </div>