[阶段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:
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import {
|
||||
LineChart, Line, BarChart, Bar, XAxis, YAxis,
|
||||
CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
interface ChartProps {
|
||||
title: string;
|
||||
data: any[];
|
||||
data: Array<Record<string, any>>;
|
||||
type: 'line' | 'bar';
|
||||
dataKey: string;
|
||||
stroke?: string;
|
||||
@@ -18,9 +20,13 @@ function Chart({
|
||||
dataKey,
|
||||
stroke = '#667eea',
|
||||
fill = '#667eea',
|
||||
height = 300,
|
||||
height = 260,
|
||||
}: ChartProps) {
|
||||
if (!data || data.length === 0) {
|
||||
// A row may carry some metrics and not others, so emptiness is decided per
|
||||
// metric rather than by the length of `data`.
|
||||
const hasValues = data.some((row) => row[dataKey] != null);
|
||||
|
||||
if (!hasValues) {
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<h4>{title}</h4>
|
||||
@@ -29,27 +35,37 @@ function Chart({
|
||||
);
|
||||
}
|
||||
|
||||
const axisProps = { fontSize: 12, stroke: '#999' };
|
||||
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<h4>{title}</h4>
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
{type === 'line' ? (
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" fontSize={12} />
|
||||
<YAxis fontSize={12} />
|
||||
<LineChart data={data} margin={{ top: 8, right: 8, bottom: 0, left: -16 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#eee" />
|
||||
<XAxis dataKey="date" {...axisProps} />
|
||||
<YAxis {...axisProps} domain={['auto', 'auto']} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey={dataKey} stroke={stroke} dot={{ r: 4 }} />
|
||||
{/* connectNulls keeps the line continuous across days the device
|
||||
did not record, instead of breaking it into fragments. */}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={stroke}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3 }}
|
||||
connectNulls
|
||||
name={title}
|
||||
/>
|
||||
</LineChart>
|
||||
) : (
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" fontSize={12} />
|
||||
<YAxis fontSize={12} />
|
||||
<BarChart data={data} margin={{ top: 8, right: 8, bottom: 0, left: -16 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#eee" />
|
||||
<XAxis dataKey="date" {...axisProps} />
|
||||
<YAxis {...axisProps} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey={dataKey} fill={fill} />
|
||||
<Bar dataKey={dataKey} fill={fill} radius={[3, 3, 0, 0]} name={title} />
|
||||
</BarChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
|
||||
Reference in New Issue
Block a user