| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- <div class="weight-container">
- <app-page-header [title]="'体重管理'" (back)="backToDashboard()">
- <div header-actions>
- <button class="btn btn-primary" (click)="openAddRecordDialog()">+ 录入体重</button>
- <button class="btn btn-secondary" (click)="openGoalDialog()">目标设置</button>
- </div>
- </app-page-header>
- <!-- 趋势主导视图主布局 -->
- <div class="weight-main-grid" *ngIf="stats">
- <!-- 左侧:体重趋势 + 关键数字 -->
- <div class="left-column">
- <section class="panel">
- <div class="panel-header">
- <span class="panel-title">体重趋势总览</span>
- </div>
-
- <!-- 周期切换标签 -->
- <div class="chart-period-tabs">
- <button
- class="period-tab"
- [class.active]="trendPeriod === 'week'"
- (click)="changeTrendPeriod('week')">
- 周
- </button>
- <button
- class="period-tab"
- [class.active]="trendPeriod === 'month'"
- (click)="changeTrendPeriod('month')">
- 月
- </button>
- <button
- class="period-tab"
- [class.active]="trendPeriod === '6months'"
- (click)="changeTrendPeriod('6months')">
- 6个月
- </button>
- </div>
-
- <!-- 统计信息 -->
- <div class="trend-stats" *ngIf="trendStats">
- <div class="trend-avg">
- <span class="avg-label">平均</span>
- <span class="avg-value" [ngClass]="{'positive': trendStats.avgChange > 0, 'negative': trendStats.avgChange < 0}">
- {{ trendStats.avgChange > 0 ? '+' : '' }}{{ trendStats.avgChange.toFixed(2) }}
- </span>
- <span class="avg-unit">kg(距离基线)</span>
- </div>
- <div class="trend-range">{{ trendStats.dateRange }}</div>
- </div>
-
- <div class="trend-chart-wrapper">
- <div id="trendChart" class="chart" style="width: 100%; height: 100%;"></div>
- </div>
- <div class="kpi-strip">
- <div class="kpi-item">
- <div class="kpi-label">当前体重</div>
- <div class="kpi-main">
- <span class="kpi-value">{{ stats.currentWeight.toFixed(1) }}</span>
- <span class="kpi-unit">kg</span>
- </div>
- <div class="kpi-desc" *ngIf="goal">
- 目标 {{ goal.targetWeight }}kg · 剩余
- {{ (stats.currentWeight - goal.targetWeight) | number:'1.1-1' }}kg
- </div>
- <div class="kpi-desc" *ngIf="!goal">
- 建议先设置目标体重,便于评估进度
- </div>
- </div>
- <div class="kpi-item">
- <div class="kpi-label">最近体重变化</div>
- <div class="kpi-main">
- <span
- class="kpi-value"
- [ngClass]="{ 'positive': stats.weightChange > 0, 'negative': stats.weightChange < 0 }"
- >
- {{ stats.weightChange > 0 ? '+' : '' }}{{ stats.weightChange.toFixed(1) }}
- </span>
- <span class="kpi-unit">kg</span>
- </div>
- <div class="kpi-desc">从所选时间范围起点到当前的总体变化</div>
- </div>
- <div class="kpi-item">
- <div class="kpi-label">平均周变化</div>
- <div class="kpi-main">
- <span
- class="kpi-value"
- [ngClass]="{ 'positive': stats.avgWeeklyChange > 0, 'negative': stats.avgWeeklyChange < 0 }"
- >
- {{ stats.avgWeeklyChange > 0 ? '+' : '' }}{{ stats.avgWeeklyChange.toFixed(2) }}
- </span>
- <span class="kpi-unit">kg/周</span>
- </div>
- <div class="kpi-desc">
- 已跟踪 {{ stats.daysTracked }} 天 · {{ stats.goalETA || '预计达成时间计算中' }}
- </div>
- </div>
- </div>
- </section>
- </div>
- <!-- 右侧:体脂率与最近记录 -->
- <div class="right-column" *ngIf="records.length > 0">
- <section class="panel">
- <div class="panel-header">
- <span class="panel-title">体脂率与身体成分</span>
- <span class="panel-extra">目标体脂率 {{ goal?.targetBodyFat ?? '—' }}%</span>
- </div>
- <div class="composition-body">
- <div class="composition-ring">
- <div id="progressChart" class="chart-small"></div>
- </div>
- <div class="composition-info">
- <ul class="list">
- <li class="list-item">
- <span>当前体脂率</span>
- <span><strong>{{ records[0].bodyFat.toFixed(1) }}%</strong></span>
- </li>
- <li class="list-item" *ngIf="stats">
- <span>最近 30 天变化</span>
- <span
- class="kpi-value"
- [ngClass]="{ 'negative': stats.bodyFatChange < 0, 'positive': stats.bodyFatChange > 0 }"
- >
- {{ stats.bodyFatChange > 0 ? '+' : ''}}{{ stats.bodyFatChange.toFixed(1) }}%
- </span>
- </li>
- <li class="list-item">
- <span>估算去脂体重</span>
- <span>{{ (records[0].weight * (1 - records[0].bodyFat / 100)).toFixed(1) }} kg</span>
- </li>
- <li class="list-item">
- <span>肌肉量</span>
- <span>{{ records[0].muscleMass.toFixed(1) }} kg</span>
- </li>
- </ul>
- <div class="tag-row">
- <span class="tag" *ngIf="stats.bodyFatChange <= 0">体重趋势与体脂率整体同步下降</span>
- <span class="tag" *ngIf="stats.bodyFatChange > 0">近期可能存在"掉秤不掉脂"情况</span>
- </div>
- </div>
- </div>
- </section>
- <section class="panel">
- <div class="panel-header">
- <span class="panel-title">最近关键记录</span>
- <span class="panel-extra">近 7 天</span>
- </div>
- <ul class="list">
- <li class="list-item" *ngFor="let rec of records | slice:0:3">
- <span>{{ rec.date }} · {{ rec.weight.toFixed(1) }}kg · {{ rec.bodyFat.toFixed(1) }}%</span>
- <span class="badge">
- {{ rec.measurementCondition === 'fasting' ? '空腹' : '餐后' }}
- </span>
- </li>
- </ul>
- <div class="cta-row">
- <button class="cta-btn primary" (click)="openAddRecordDialog()">+ 录入体重与体脂</button>
- <button class="cta-btn" (click)="updateTimePeriod('7days')">仅看近 7 天</button>
- </div>
- </section>
- </div>
- </div>
- <div class="filter-bar">
- <button class="filter-btn" [class.active]="filter.timePeriod === '7days'" (click)="updateTimePeriod('7days')">7天</button>
- <button class="filter-btn" [class.active]="filter.timePeriod === '30days'" (click)="updateTimePeriod('30days')">30天</button>
- <button class="filter-btn" [class.active]="filter.timePeriod === '90days'" (click)="updateTimePeriod('90days')">90天</button>
- <button class="filter-btn" [class.active]="filter.timePeriod === 'all'" (click)="updateTimePeriod('all')">全部</button>
- <button class="filter-btn" [class.active]="filter.timePeriod === 'custom'" (click)="showCustomDatePicker = !showCustomDatePicker">自定义</button>
- <select class="filter-select" [(ngModel)]="filter.condition" (change)="applyFilter(); updateCharts();">
- <option value="all">全部条件</option>
- <option value="fasting">空腹</option>
- <option value="after_meal">餐后</option>
- </select>
- </div>
- <!-- 自定义日期范围选择器 -->
- <div class="custom-date-picker" *ngIf="showCustomDatePicker">
- <div class="date-range-inputs">
- <div class="date-input-group">
- <label>开始日期</label>
- <input type="date" [(ngModel)]="filter.startDate" />
- </div>
- <div class="date-input-group">
- <label>结束日期</label>
- <input type="date" [(ngModel)]="filter.endDate" />
- </div>
- <button class="btn btn-primary" (click)="applyCustomDateRange()">应用</button>
- </div>
- </div>
- <!-- 标签筛选 -->
- <div class="tag-filter" *ngIf="availableTags.length > 0">
- <div class="tag-filter-title">按标签筛选:</div>
- <div class="tag-filter-buttons">
- <button *ngFor="let tag of availableTags" class="tag-filter-btn" [class.active]="filter.tags?.includes(tag)" (click)="toggleFilterTag(tag)">
- {{ tag }}
- </button>
- </div>
- </div>
- <!-- 体重变化与体脂率分析(横向布局) -->
- <div class="analysis-charts-container">
- <div class="change-chart-section">
- <div class="chart-header">
- <h3 class="chart-title">体重变化趋势</h3>
- <button class="toggle-btn" (click)="toggleChartPeriod()">切换至{{ chartPeriod === 'weekly' ? '月' : '周' }}视图</button>
- </div>
- <div id="changeChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
- </div>
- <div class="scatter-chart-section">
- <div class="chart-header">
- <h3 class="chart-title">体重体脂率关系</h3>
- </div>
- <div id="scatterChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
- </div>
- </div>
- <!-- 添加记录对话框 -->
- <app-modal
- [(isOpen)]="showAddRecordDialog"
- [config]="{
- title: '录入体重数据',
- icon: '⚖️',
- size: 'medium',
- theme: 'default',
- showFooter: false
- }">
- <div class="weight-record-form">
- <div class="form-group">
- <label>日期</label>
- <input type="date" [(ngModel)]="newRecord.date" max="{{ today }}" />
- </div>
- <div class="form-group">
- <label>测量时间</label>
- <input type="time" [(ngModel)]="newRecord.measurementTime" />
- </div>
- <div class="form-group">
- <label>体重 (kg)</label>
- <input type="number" [(ngModel)]="newRecord.weight" step="0.1" min="30" max="200" />
- </div>
- <div class="form-group">
- <label>体脂率 (%)</label>
- <input type="number" [(ngModel)]="newRecord.bodyFat" step="0.1" min="5" max="60" />
- </div>
- <div class="form-group">
- <label>肌肉量 (kg)</label>
- <input type="number" [(ngModel)]="newRecord.muscleMass" step="0.1" min="10" max="100" />
- </div>
- <div class="form-group">
- <label>测量条件</label>
- <select [(ngModel)]="newRecord.measurementCondition">
- <option value="fasting">空腹</option>
- <option value="after_meal">餐后</option>
- </select>
- </div>
- <div class="form-group">
- <label>备注</label>
- <textarea [(ngModel)]="newRecord.notes" rows="3"></textarea>
- </div>
- <div class="form-group">
- <label>标签(关键节点标记)</label>
- <div class="tag-selector">
- <div class="available-tags">
- <button *ngFor="let tag of availableTags" type="button" class="tag-btn" [class.selected]="newRecord.tags?.includes(tag)" (click)="addTagToRecord(tag)">
- {{ tag }}
- </button>
- </div>
- <div class="selected-tags" *ngIf="newRecord.tags && newRecord.tags.length > 0">
- <span class="selected-tag" *ngFor="let tag of newRecord.tags">
- {{ tag }}
- <button type="button" class="remove-tag" (click)="removeTagFromRecord(tag)">×</button>
- </span>
- </div>
- <div class="custom-tag-input">
- <input type="text" [(ngModel)]="newTagInput" placeholder="添加自定义标签" (keyup.enter)="addCustomTag()" />
- <button type="button" class="btn btn-secondary btn-sm" (click)="addCustomTag()">添加</button>
- </div>
- </div>
- </div>
- <div class="dialog-actions">
- <button class="btn btn-secondary" (click)="showAddRecordDialog = false">取消</button>
- <button class="btn btn-primary" (click)="addRecord()">保存</button>
- </div>
- </div>
- </app-modal>
- <!-- 目标设置对话框 -->
- <app-modal
- [(isOpen)]="showGoalDialog"
- [config]="{
- title: '设置目标',
- icon: '🎯',
- size: 'small',
- theme: 'gradient',
- showFooter: false
- }">
- <div class="goal-setting-form">
- <div class="form-group">
- <label>目标体重 (kg)</label>
- <input type="number" [(ngModel)]="newGoal.targetWeight" step="0.1" min="30" max="200" />
- </div>
- <div class="form-group">
- <label>目标体脂率 (%)</label>
- <input type="number" [(ngModel)]="newGoal.targetBodyFat" step="0.1" min="5" max="60" />
- </div>
- <div class="form-group">
- <label>目标日期</label>
- <input type="date" [(ngModel)]="newGoal.targetDate" [min]="tomorrow" />
- </div>
- <div class="dialog-actions">
- <button class="btn btn-secondary" (click)="showGoalDialog = false">取消</button>
- <button class="btn btn-primary" (click)="setGoal()">保存</button>
- </div>
- </div>
- </app-modal>
- <!-- 调试:一键生成90天模拟数据 -->
- <div style="padding: 16px; text-align: center;">
- <button class="btn btn-secondary" (click)="generateMockData()">一键生成90天模拟数据</button>
- </div>
- </div>
|