[阶段4.2] 前端对接 Flask API + 建议/分析/设置页面,移除废弃的 Node 后端
选型确认为 Python Flask 后,删除 server/ 整套 Node 实现,
根 package.json 改为只管理 client workspace,
npm run dev 同时拉起 Flask 与 React。
fix: 前端读取响应的方式与后端不符
- Flask 返回裸数组/对象,而前端读的是 response.data.data
(Node 那套 {success,data} 包装),登录后拿到 undefined 直接崩
- api.ts 重写为返回 response.data,并补齐全部端点的 TypeScript 类型
- 401 拦截器改为清除会话并跳转 /login,而不是留在空白页
fix: 同步页缺少 Garmin 密码输入
- 后端只存密码哈希、无法还原,/garmin/sync 要求请求体带明文密码,
原页面没有该输入框,点同步必然 400
- DataSync 增加密码字段,请求结束后立即清空,并说明为何每次都要输入
新增页面:
- Recommendations: 模型下拉切换(未配置密钥的模型置灰),
展示本次由哪个模型作答、是否发生了降级、分析了多少天数据;
按优先级配色,底部附免责声明
- Analysis: 6 个指标 × 4 个时间窗口,展示均值/中位数/极值/
前后半段差值,并绘制趋势图
- Settings: 模型清单与配置状态、数据隐私说明、退出登录
Dashboard:
- 修正响应结构,数据扁平化后再传给图表
(原先给 dataKey 传了函数,与 Chart 的 string 类型不符)
- 无数据时引导用户去同步,而不是显示一堆空图表
Chart:
- 按指标而非按行判断是否为空,某天缺某项指标不再让整张图判空
- 折线图 connectNulls,避免设备漏记的日子把线断成碎片
验证: 前端 tsc --noEmit 通过;后端 161 passed / 1 skipped;
实机启动 Flask 后 register/login/models/ai-recommendations 均正常,
未配置密钥时正确降级到规则引擎。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
171
client/src/pages/Recommendations.css
Normal file
171
client/src/pages/Recommendations.css
Normal file
@@ -0,0 +1,171 @@
|
||||
.model-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 1rem;
|
||||
background: #f9f9fb;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.model-bar label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.model-bar select {
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
font-family: inherit;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.model-bar select:disabled {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.meta-bar {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.meta-bar.ai {
|
||||
background: #eef2ff;
|
||||
border: 1px solid #d6dfff;
|
||||
color: #3b4a94;
|
||||
}
|
||||
|
||||
.meta-bar.rules {
|
||||
background: #fff8e6;
|
||||
border: 1px solid #ffe4a3;
|
||||
color: #7a5a10;
|
||||
}
|
||||
|
||||
.meta-bar .fallback {
|
||||
opacity: 0.8;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding: 1rem;
|
||||
background: #fff8e6;
|
||||
border: 1px solid #ffe4a3;
|
||||
border-radius: 8px;
|
||||
color: #7a5a10;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.notice code {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.rec-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.rec-card {
|
||||
background: white;
|
||||
border: 1px solid #eee;
|
||||
border-left: 4px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 1.1rem 1.25rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.rec-card.priority-high {
|
||||
border-left-color: #e05252;
|
||||
}
|
||||
|
||||
.rec-card.priority-medium {
|
||||
border-left-color: #e0a052;
|
||||
}
|
||||
|
||||
.rec-card.priority-low {
|
||||
border-left-color: #6bbf7f;
|
||||
}
|
||||
|
||||
.rec-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rec-category {
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.rec-priority {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rec-priority.priority-high {
|
||||
background: #fdecec;
|
||||
color: #b03030;
|
||||
}
|
||||
|
||||
.rec-priority.priority-medium {
|
||||
background: #fdf3e6;
|
||||
color: #9a6412;
|
||||
}
|
||||
|
||||
.rec-priority.priority-low {
|
||||
background: #eaf6ed;
|
||||
color: #35733f;
|
||||
}
|
||||
|
||||
.rec-body {
|
||||
color: #444;
|
||||
line-height: 1.75;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rec-based-on {
|
||||
margin-top: 0.7rem;
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 0.82rem;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.model-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.model-bar select,
|
||||
.model-bar .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user