Initial commit: Set up Garmin Health Lab project structure
- Initialize monorepo with root workspace configuration - Set up Express.js backend with TypeScript - Set up React 18 frontend with TypeScript - Create database schema with SQLite - Implement project architecture and documentation - Add development and deployment guidelines Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 3000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
~*
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# API Keys and secrets
|
||||||
|
.env.production.local
|
||||||
|
config/secrets.json
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
server/dist/
|
||||||
|
client/build/
|
||||||
88
CLAUDE.md
Normal file
88
CLAUDE.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Garmin Health Lab - 项目指南
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
Garmin Health Lab 是一个完整的健康数据分析平台,用于:
|
||||||
|
- 获取和同步佳明(Garmin)设备数据
|
||||||
|
- 进行多维度的健康数据分析
|
||||||
|
- 提供个性化的健康建议
|
||||||
|
- 可视化健康趋势
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **前端**: React 18 + TypeScript + Recharts
|
||||||
|
- **后端**: Node.js + Express + TypeScript
|
||||||
|
- **数据库**: SQLite3
|
||||||
|
- **API 集成**: Garmin Connect API
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
```bash
|
||||||
|
cp server/.env.example server/.env
|
||||||
|
# 编辑 server/.env 填入 Garmin 凭证
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- 前端: http://localhost:3000
|
||||||
|
- 后端: http://localhost:5000
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
- `client/` - React 前端应用
|
||||||
|
- `server/` - Node.js 后端服务
|
||||||
|
- `docs/` - 文档
|
||||||
|
|
||||||
|
详见 [ARCHITECTURE.md](docs/ARCHITECTURE.md) 和 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
1. **仪表板** - 健康数据概览和最近数据展示
|
||||||
|
2. **数据同步** - 从 Garmin 同步最新健康数据
|
||||||
|
3. **数据分析** - 趋势分析和数据可视化
|
||||||
|
4. **健康建议** - 基于数据的个性化建议
|
||||||
|
5. **设置** - 用户配置和偏好设置
|
||||||
|
|
||||||
|
## 关键特性
|
||||||
|
|
||||||
|
- ✅ Garmin API 集成
|
||||||
|
- ✅ 多维度数据分析(步数、心率、睡眠等)
|
||||||
|
- ✅ 实时数据同步
|
||||||
|
- ✅ 交互式数据可视化
|
||||||
|
- ✅ JWT 认证安全
|
||||||
|
- ✅ 本地数据存储
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
- 详见 [DEVELOPMENT.md](docs/DEVELOPMENT.md)
|
||||||
|
- API 文档见 README.md
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # 开发模式
|
||||||
|
npm run build # 构建项目
|
||||||
|
npm run typecheck # 类型检查
|
||||||
|
npm start # 生产模式
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步任务
|
||||||
|
|
||||||
|
- [ ] 实现 Garmin OAuth 认证
|
||||||
|
- [ ] 完成 Garmin API 数据获取
|
||||||
|
- [ ] 实现仪表板可视化
|
||||||
|
- [ ] 添加数据分析算法
|
||||||
|
- [ ] 部署和优化
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
项目维护: ericwyuan.g@gmail.com
|
||||||
121
README.md
Normal file
121
README.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# 佳明健康数据分析平台 (Garmin Health Lab)
|
||||||
|
|
||||||
|
一个完整的健康数据分析平台,用于获取、分析和可视化你的佳明(Garmin)设备数据。
|
||||||
|
|
||||||
|
## 📊 主要功能
|
||||||
|
|
||||||
|
- **数据同步**: 通过 Garmin API 自动同步你的健康数据
|
||||||
|
- **综合分析**: 步数、心率、睡眠、运动、压力等多维度分析
|
||||||
|
- **数据可视化**: 交互式图表和仪表板展示健康数据趋势
|
||||||
|
- **智能建议**: 基于数据分析的个性化健康建议
|
||||||
|
- **数据导出**: 支持数据导出为 CSV/JSON 格式
|
||||||
|
|
||||||
|
## 🛠 技术栈
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- React 18 + TypeScript
|
||||||
|
- Recharts (数据可视化)
|
||||||
|
- Tailwind CSS (样式)
|
||||||
|
- Axios (API 请求)
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- Node.js + Express
|
||||||
|
- TypeScript
|
||||||
|
- SQLite3 (本地数据存储)
|
||||||
|
- garmin-connect (Garmin API 集成)
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
GarminHealthLab/
|
||||||
|
├── client/ # 前端应用
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React 组件
|
||||||
|
│ │ ├── pages/ # 页面
|
||||||
|
│ │ ├── services/ # API 服务
|
||||||
|
│ │ └── types/ # TypeScript 类型定义
|
||||||
|
│ └── package.json
|
||||||
|
├── server/ # 后端应用
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── routes/ # API 路由
|
||||||
|
│ │ ├── services/ # 业务逻辑
|
||||||
|
│ │ ├── models/ # 数据模型
|
||||||
|
│ │ ├── middleware/ # 中间件
|
||||||
|
│ │ └── utils/ # 工具函数
|
||||||
|
│ └── package.json
|
||||||
|
├── docs/ # 文档
|
||||||
|
├── package.json # 工作空间根配置
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
- Node.js 18+
|
||||||
|
- npm 或 yarn
|
||||||
|
- Garmin Connect 账户
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置环境变量
|
||||||
|
|
||||||
|
创建 `server/.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
PORT=5000
|
||||||
|
NODE_ENV=development
|
||||||
|
GARMIN_CONNECT_USER=your_garmin_email
|
||||||
|
GARMIN_CONNECT_PASSWORD=your_garmin_password
|
||||||
|
DATABASE_PATH=./data/health.db
|
||||||
|
JWT_SECRET=your_jwt_secret_here
|
||||||
|
CORS_ORIGIN=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- 前端: http://localhost:3000
|
||||||
|
- 后端: http://localhost:5000
|
||||||
|
|
||||||
|
## 📚 API 文档
|
||||||
|
|
||||||
|
### 认证
|
||||||
|
- `POST /api/auth/login` - 用户登录
|
||||||
|
- `POST /api/auth/logout` - 用户登出
|
||||||
|
- `POST /api/auth/refresh` - 刷新 Token
|
||||||
|
|
||||||
|
### Garmin 数据同步
|
||||||
|
- `POST /api/garmin/sync` - 同步 Garmin 数据
|
||||||
|
- `GET /api/garmin/status` - 获取同步状态
|
||||||
|
|
||||||
|
### 健康数据
|
||||||
|
- `GET /api/health/summary` - 获取健康摘要
|
||||||
|
- `GET /api/health/steps` - 获取步数数据
|
||||||
|
- `GET /api/health/heart-rate` - 获取心率数据
|
||||||
|
- `GET /api/health/sleep` - 获取睡眠数据
|
||||||
|
- `GET /api/health/activities` - 获取运动数据
|
||||||
|
|
||||||
|
### 分析与建议
|
||||||
|
- `GET /api/analysis/trends` - 获取数据趋势
|
||||||
|
- `GET /api/analysis/recommendations` - 获取健康建议
|
||||||
|
|
||||||
|
## 🔐 安全说明
|
||||||
|
|
||||||
|
- Garmin 账户密码使用加密存储
|
||||||
|
- 所有 API 请求需要 JWT 认证
|
||||||
|
- 敏感数据不在前端存储
|
||||||
|
|
||||||
|
## 📝 开发指南
|
||||||
|
|
||||||
|
详见 [DEVELOPMENT.md](./docs/DEVELOPMENT.md)
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
44
client/package.json
Normal file
44
client/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "garmin-health-lab-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"proxy": "http://localhost:5000",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.14.2",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"recharts": "^2.8.0",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"date-fns": "^2.30.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.14",
|
||||||
|
"@types/react-dom": "^18.2.6",
|
||||||
|
"typescript": "^5.1.3",
|
||||||
|
"react-scripts": "5.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
12
client/public/index.html
Normal file
12
client/public/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="佳明健康数据分析平台">
|
||||||
|
<title>Garmin Health Lab - 佳明健康数据分析</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
client/src/App.tsx
Normal file
26
client/src/App.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import Layout from './components/Layout';
|
||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import DataSync from './pages/DataSync';
|
||||||
|
import Analysis from './pages/Analysis';
|
||||||
|
import Recommendations from './pages/Recommendations';
|
||||||
|
import Settings from './pages/Settings';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
<Route path="/sync" element={<DataSync />} />
|
||||||
|
<Route path="/analysis" element={<Analysis />} />
|
||||||
|
<Route path="/recommendations" element={<Recommendations />} />
|
||||||
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
129
client/src/components/Layout.css
Normal file
129
client/src/components/Layout.css
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 1400px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
height: fit-content;
|
||||||
|
position: sticky;
|
||||||
|
top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-left-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background-color: #f0f0ff;
|
||||||
|
border-left-color: #667eea;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
client/src/components/Layout.tsx
Normal file
58
client/src/components/Layout.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import './Layout.css';
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Layout({ children }: LayoutProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const navigationItems = [
|
||||||
|
{ path: '/', label: '仪表板', icon: '📊' },
|
||||||
|
{ path: '/sync', label: '数据同步', icon: '🔄' },
|
||||||
|
{ path: '/analysis', label: '数据分析', icon: '📈' },
|
||||||
|
{ path: '/recommendations', label: '健康建议', icon: '💡' },
|
||||||
|
{ path: '/settings', label: '设置', icon: '⚙️' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="layout">
|
||||||
|
<header className="header">
|
||||||
|
<div className="header-content">
|
||||||
|
<h1 className="logo">🏃 Garmin Health Lab</h1>
|
||||||
|
<p className="tagline">佳明健康数据分析平台</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<nav className="sidebar">
|
||||||
|
<ul className="nav-list">
|
||||||
|
{navigationItems.map(item => (
|
||||||
|
<li key={item.path}>
|
||||||
|
<Link
|
||||||
|
to={item.path}
|
||||||
|
className={`nav-link ${location.pathname === item.path ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<span className="icon">{item.icon}</span>
|
||||||
|
<span className="label">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main className="content">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="footer">
|
||||||
|
<p>© 2024 Garmin Health Lab. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
24
client/src/index.css
Normal file
24
client/src/index.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
10
client/src/index.tsx
Normal file
10
client/src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
13
client/src/pages/Analysis.tsx
Normal file
13
client/src/pages/Analysis.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Pages.css';
|
||||||
|
|
||||||
|
function Analysis() {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h2>数据分析</h2>
|
||||||
|
<p className="placeholder">数据分析页面即将推出...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Analysis;
|
||||||
35
client/src/pages/Dashboard.tsx
Normal file
35
client/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { apiClient } from '../services/api';
|
||||||
|
import './Pages.css';
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [summary, setSummary] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
// TODO: Fetch health summary data
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load summary:', error);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="page-loading">加载中...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h2>健康仪表板</h2>
|
||||||
|
<p className="placeholder">仪表板内容即将推出...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
20
client/src/pages/DataSync.tsx
Normal file
20
client/src/pages/DataSync.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Pages.css';
|
||||||
|
|
||||||
|
function DataSync() {
|
||||||
|
const handleSync = async () => {
|
||||||
|
// TODO: Implement data sync
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h2>数据同步</h2>
|
||||||
|
<p className="placeholder">数据同步功能即将推出...</p>
|
||||||
|
<button onClick={handleSync} className="btn btn-primary">
|
||||||
|
同步 Garmin 数据
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataSync;
|
||||||
59
client/src/pages/Pages.css
Normal file
59
client/src/pages/Pages.css
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
.page {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #5568d3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
13
client/src/pages/Recommendations.tsx
Normal file
13
client/src/pages/Recommendations.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Pages.css';
|
||||||
|
|
||||||
|
function Recommendations() {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h2>健康建议</h2>
|
||||||
|
<p className="placeholder">健康建议页面即将推出...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Recommendations;
|
||||||
13
client/src/pages/Settings.tsx
Normal file
13
client/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Pages.css';
|
||||||
|
|
||||||
|
function Settings() {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h2>设置</h2>
|
||||||
|
<p className="placeholder">设置页面即将推出...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
78
client/src/services/api.ts
Normal file
78
client/src/services/api.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth endpoints
|
||||||
|
login(email: string, password: string) {
|
||||||
|
return this.client.post('/auth/login', { email, password });
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
return this.client.post('/auth/logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garmin endpoints
|
||||||
|
syncGarminData() {
|
||||||
|
return this.client.post('/garmin/sync');
|
||||||
|
}
|
||||||
|
|
||||||
|
getGarminSyncStatus() {
|
||||||
|
return this.client.get('/garmin/status');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health endpoints
|
||||||
|
getHealthSummary(startDate?: string, endDate?: string) {
|
||||||
|
return this.client.get('/health/summary', {
|
||||||
|
params: { startDate, endDate }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getStepsData(startDate?: string, endDate?: string) {
|
||||||
|
return this.client.get('/health/steps', {
|
||||||
|
params: { startDate, endDate }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeartRateData(startDate?: string, endDate?: string) {
|
||||||
|
return this.client.get('/health/heart-rate', {
|
||||||
|
params: { startDate, endDate }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSleepData(startDate?: string, endDate?: string) {
|
||||||
|
return this.client.get('/health/sleep', {
|
||||||
|
params: { startDate, endDate }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getActivities(startDate?: string, endDate?: string) {
|
||||||
|
return this.client.get('/health/activities', {
|
||||||
|
params: { startDate, endDate }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analysis endpoints
|
||||||
|
getTrends(metricType?: string) {
|
||||||
|
return this.client.get('/analysis/trends', {
|
||||||
|
params: { metricType }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecommendations() {
|
||||||
|
return this.client.get('/analysis/recommendations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiClient = new ApiClient();
|
||||||
37
client/src/types/index.ts
Normal file
37
client/src/types/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export interface HealthSummary {
|
||||||
|
date: string;
|
||||||
|
steps: number;
|
||||||
|
heartRate?: number;
|
||||||
|
sleep?: {
|
||||||
|
duration: number;
|
||||||
|
quality: number;
|
||||||
|
};
|
||||||
|
caloriesBurned?: number;
|
||||||
|
stress?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Activity {
|
||||||
|
id: string;
|
||||||
|
activityType: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
duration: number;
|
||||||
|
distance?: number;
|
||||||
|
calories?: number;
|
||||||
|
heartRateAverage?: number;
|
||||||
|
heartRateMax?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Recommendation {
|
||||||
|
id: string;
|
||||||
|
category: string;
|
||||||
|
recommendation: string;
|
||||||
|
priority: 'high' | 'medium' | 'low';
|
||||||
|
basedOn: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrendData {
|
||||||
|
date: string;
|
||||||
|
value: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
21
client/tsconfig.json
Normal file
21
client/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
175
docs/ARCHITECTURE.md
Normal file
175
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# 项目架构说明
|
||||||
|
|
||||||
|
## 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ 浏览器 (React 前端) │
|
||||||
|
│ ┌────────────┬──────────────┬──────────┬──────────┐ │
|
||||||
|
│ │ 仪表板 │ 数据同步 │ 数据分析 │ 设置 │ │
|
||||||
|
│ └────────────┴──────────────┴──────────┴──────────┘ │
|
||||||
|
└──────────────────────────┬──────────────────────────┘
|
||||||
|
│ HTTP/REST
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ Node.js/Express 后端服务器 │
|
||||||
|
│ ┌──────────────────────────────────────────────┐ │
|
||||||
|
│ │ API 路由层 │ │
|
||||||
|
│ │ ├─ /auth - 用户认证 │ │
|
||||||
|
│ │ ├─ /garmin - Garmin 数据同步 │ │
|
||||||
|
│ │ ├─ /health - 健康数据查询 │ │
|
||||||
|
│ │ └─ /analysis - 数据分析 │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ ┌──────────────────────────────────────────────┐ │
|
||||||
|
│ │ 服务层 │ │
|
||||||
|
│ │ ├─ GarminService - Garmin API 集成 │ │
|
||||||
|
│ │ ├─ HealthService - 健康数据业务逻辑 │ │
|
||||||
|
│ │ ├─ AuthService - 认证授权 │ │
|
||||||
|
│ │ └─ AnalysisService - 数据分析 │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────┬───────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ SQLite 数据库 │
|
||||||
|
│ ├─ users │
|
||||||
|
│ ├─ health_data │
|
||||||
|
│ ├─ activities │
|
||||||
|
│ └─ sync_status │
|
||||||
|
└──────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ Garmin Cloud API │
|
||||||
|
│ ├─ 用户认证 │
|
||||||
|
│ ├─ 数据获取 │
|
||||||
|
│ └─ 数据同步 │
|
||||||
|
└──────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心模块
|
||||||
|
|
||||||
|
### 1. 前端 (Client)
|
||||||
|
- **框架**: React 18 + TypeScript
|
||||||
|
- **路由**: React Router
|
||||||
|
- **UI 组件**: 自定义 + CSS
|
||||||
|
- **数据可视化**: Recharts
|
||||||
|
- **API 通信**: Axios
|
||||||
|
|
||||||
|
**主要页面**:
|
||||||
|
- Dashboard (仪表板)
|
||||||
|
- DataSync (数据同步)
|
||||||
|
- Analysis (数据分析)
|
||||||
|
- Recommendations (健康建议)
|
||||||
|
- Settings (设置)
|
||||||
|
|
||||||
|
### 2. 后端 (Server)
|
||||||
|
- **框架**: Express.js
|
||||||
|
- **语言**: TypeScript
|
||||||
|
- **数据库**: SQLite3
|
||||||
|
- **认证**: JWT
|
||||||
|
|
||||||
|
**主要服务**:
|
||||||
|
- **AuthService**: 用户认证和授权
|
||||||
|
- **GarminService**: Garmin API 集成和数据获取
|
||||||
|
- **HealthService**: 健康数据管理
|
||||||
|
- **AnalysisService**: 数据分析和建议生成
|
||||||
|
|
||||||
|
### 3. 数据库 (SQLite)
|
||||||
|
**表结构**:
|
||||||
|
- `users`: 用户信息和 Garmin 凭证
|
||||||
|
- `health_data`: 每日健康数据汇总
|
||||||
|
- `activities`: 运动活动记录
|
||||||
|
- `sync_status`: 数据同步状态追踪
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
### 1. Garmin 数据同步流程
|
||||||
|
```
|
||||||
|
用户点击"同步"
|
||||||
|
↓
|
||||||
|
POST /api/garmin/sync
|
||||||
|
↓
|
||||||
|
GarminService.syncData()
|
||||||
|
↓
|
||||||
|
获取 Garmin 授权 Token
|
||||||
|
↓
|
||||||
|
调用 Garmin API 获取数据
|
||||||
|
↓
|
||||||
|
转换数据格式
|
||||||
|
↓
|
||||||
|
存储到本地 SQLite
|
||||||
|
↓
|
||||||
|
更新 sync_status
|
||||||
|
↓
|
||||||
|
返回同步结果
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据分析流程
|
||||||
|
```
|
||||||
|
GET /api/health/summary (日期范围)
|
||||||
|
↓
|
||||||
|
从数据库查询健康数据
|
||||||
|
↓
|
||||||
|
计算统计指标
|
||||||
|
↓
|
||||||
|
生成趋势分析
|
||||||
|
↓
|
||||||
|
返回分析结果
|
||||||
|
↓
|
||||||
|
前端绘制图表
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 健康建议生成流程
|
||||||
|
```
|
||||||
|
GET /api/analysis/recommendations
|
||||||
|
↓
|
||||||
|
AnalysisService.generateRecommendations()
|
||||||
|
↓
|
||||||
|
分析历史数据
|
||||||
|
↓
|
||||||
|
识别异常和趋势
|
||||||
|
↓
|
||||||
|
根据规则引擎生成建议
|
||||||
|
↓
|
||||||
|
按优先级排序
|
||||||
|
↓
|
||||||
|
返回建议列表
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键特性
|
||||||
|
|
||||||
|
### 安全性
|
||||||
|
- Garmin 密码使用加密存储
|
||||||
|
- JWT 令牌验证所有请求
|
||||||
|
- CORS 配置限制来源
|
||||||
|
- 环境变量管理敏感配置
|
||||||
|
|
||||||
|
### 扩展性
|
||||||
|
- 模块化的服务设计
|
||||||
|
- 易于添加新的分析算法
|
||||||
|
- 支持数据导出功能
|
||||||
|
- 可扩展的 API 端点
|
||||||
|
|
||||||
|
### 性能
|
||||||
|
- 数据库索引优化查询
|
||||||
|
- API 缓存策略
|
||||||
|
- 异步处理长运行任务
|
||||||
|
- 增量数据同步支持
|
||||||
|
|
||||||
|
## 部署架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Development 本地开发 │
|
||||||
|
│ └─ npm run dev (同时运行前后端) │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Production 生产环境 │
|
||||||
|
│ ├─ Docker 容器化 │
|
||||||
|
│ ├─ Nginx 反向代理 │
|
||||||
|
│ ├─ Node.js 后端服务 │
|
||||||
|
│ └─ SQLite 数据持久化 │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
278
docs/DEVELOPMENT.md
Normal file
278
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# 开发指南
|
||||||
|
|
||||||
|
## 环境设置
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
- Node.js 18+
|
||||||
|
- npm 或 yarn
|
||||||
|
- Git
|
||||||
|
- Garmin Connect 账户
|
||||||
|
|
||||||
|
### 安装步骤
|
||||||
|
|
||||||
|
1. **克隆项目**
|
||||||
|
```bash
|
||||||
|
cd ~/Desktop/Work
|
||||||
|
git clone <repository-url> GarminHealthLab
|
||||||
|
cd GarminHealthLab
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **安装依赖**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **配置环境变量**
|
||||||
|
```bash
|
||||||
|
# 复制示例文件
|
||||||
|
cp server/.env.example server/.env
|
||||||
|
|
||||||
|
# 编辑 server/.env,填入你的 Garmin 凭证
|
||||||
|
# GARMIN_CONNECT_USER=your_email@example.com
|
||||||
|
# GARMIN_CONNECT_PASSWORD=your_password
|
||||||
|
# JWT_SECRET=generate_a_random_string
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **启动开发服务器**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- 前端: http://localhost:3000
|
||||||
|
- 后端 API: http://localhost:5000/api
|
||||||
|
|
||||||
|
## 项目命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式(同时运行前后端)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 仅运行后端服务器
|
||||||
|
npm run dev:server
|
||||||
|
|
||||||
|
# 仅运行前端应用
|
||||||
|
npm run dev:client
|
||||||
|
|
||||||
|
# 构建项目
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 生产模式启动
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
npm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码结构
|
||||||
|
|
||||||
|
### 后端 (server/)
|
||||||
|
|
||||||
|
```
|
||||||
|
server/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # 入口文件
|
||||||
|
│ ├── routes/ # API 路由
|
||||||
|
│ │ ├── auth.ts
|
||||||
|
│ │ ├── garmin.ts
|
||||||
|
│ │ ├── health.ts
|
||||||
|
│ │ └── analysis.ts
|
||||||
|
│ ├── services/ # 业务逻辑服务
|
||||||
|
│ │ ├── AuthService.ts
|
||||||
|
│ │ ├── GarminService.ts
|
||||||
|
│ │ ├── HealthService.ts
|
||||||
|
│ │ └── AnalysisService.ts
|
||||||
|
│ ├── models/ # 数据模型
|
||||||
|
│ ├── middleware/ # 中间件
|
||||||
|
│ ├── utils/ # 工具函数
|
||||||
|
│ │ └── database.ts
|
||||||
|
│ └── types/ # TypeScript 类型定义
|
||||||
|
├── dist/ # 编译输出
|
||||||
|
├── tsconfig.json
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端 (client/)
|
||||||
|
|
||||||
|
```
|
||||||
|
client/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.tsx # 入口文件
|
||||||
|
│ ├── App.tsx # 主组件
|
||||||
|
│ ├── components/ # 可复用组件
|
||||||
|
│ ├── pages/ # 页面组件
|
||||||
|
│ │ ├── Dashboard.tsx
|
||||||
|
│ │ ├── DataSync.tsx
|
||||||
|
│ │ ├── Analysis.tsx
|
||||||
|
│ │ ├── Recommendations.tsx
|
||||||
|
│ │ └── Settings.tsx
|
||||||
|
│ ├── services/ # API 服务
|
||||||
|
│ │ └── api.ts
|
||||||
|
│ ├── types/ # TypeScript 类型定义
|
||||||
|
│ └── index.css # 全局样式
|
||||||
|
├── public/ # 静态资源
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发工作流
|
||||||
|
|
||||||
|
### 添加新的 API 端点
|
||||||
|
|
||||||
|
1. **创建路由处理器** (`server/src/routes/yourFeature.ts`)
|
||||||
|
```typescript
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/endpoint', (req, res) => {
|
||||||
|
// 业务逻辑
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **在主文件中注册路由** (`server/src/index.ts`)
|
||||||
|
```typescript
|
||||||
|
import yourFeatureRoutes from './routes/yourFeature';
|
||||||
|
app.use('/api/yourfeature', yourFeatureRoutes);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **创建 API 客户端方法** (`client/src/services/api.ts`)
|
||||||
|
```typescript
|
||||||
|
getYourEndpoint() {
|
||||||
|
return this.client.get('/yourfeature/endpoint');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新页面
|
||||||
|
|
||||||
|
1. **创建页面组件** (`client/src/pages/YourPage.tsx`)
|
||||||
|
```typescript
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function YourPage() {
|
||||||
|
return <div>Page content</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default YourPage;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **在 App.tsx 中添加路由**
|
||||||
|
```typescript
|
||||||
|
<Route path="/your-path" element={<YourPage />} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库操作
|
||||||
|
|
||||||
|
使用数据库工具函数简化操作:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { runAsync, getAsync, allAsync } from '../utils/database';
|
||||||
|
|
||||||
|
// 插入数据
|
||||||
|
await runAsync('INSERT INTO table (col1, col2) VALUES (?, ?)', [val1, val2]);
|
||||||
|
|
||||||
|
// 查询单行
|
||||||
|
const row = await getAsync('SELECT * FROM table WHERE id = ?', [id]);
|
||||||
|
|
||||||
|
// 查询多行
|
||||||
|
const rows = await allAsync('SELECT * FROM table WHERE status = ?', ['active']);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试
|
||||||
|
|
||||||
|
### 后端调试
|
||||||
|
```bash
|
||||||
|
# 启用详细日志
|
||||||
|
DEBUG=* npm run dev:server
|
||||||
|
|
||||||
|
# 使用 Node 调试器
|
||||||
|
node --inspect server/dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端调试
|
||||||
|
- 使用 React DevTools 浏览器扩展
|
||||||
|
- 使用浏览器开发者工具 (F12)
|
||||||
|
- 检查网络请求和响应
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
### 测试后端 API
|
||||||
|
|
||||||
|
使用 curl 或 Postman:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取健康摘要
|
||||||
|
curl http://localhost:5000/api/health/summary
|
||||||
|
|
||||||
|
# 触发 Garmin 同步
|
||||||
|
curl -X POST http://localhost:5000/api/garmin/sync
|
||||||
|
|
||||||
|
# 获取健康建议
|
||||||
|
curl http://localhost:5000/api/analysis/recommendations
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 数据库连接失败
|
||||||
|
```bash
|
||||||
|
# 检查数据库文件
|
||||||
|
ls -la ./data/health.db
|
||||||
|
|
||||||
|
# 重置数据库
|
||||||
|
rm ./data/health.db
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端无法连接后端
|
||||||
|
- 检查 CORS 配置
|
||||||
|
- 确保后端运行在 5000 端口
|
||||||
|
- 检查防火墙设置
|
||||||
|
|
||||||
|
### Garmin 认证失败
|
||||||
|
- 验证 Garmin 邮箱和密码
|
||||||
|
- 检查网络连接
|
||||||
|
- 查看后端日志
|
||||||
|
|
||||||
|
## 性能优化建议
|
||||||
|
|
||||||
|
1. **数据库索引**
|
||||||
|
- 在频繁查询的字段上添加索引
|
||||||
|
- 定期分析查询性能
|
||||||
|
|
||||||
|
2. **缓存策略**
|
||||||
|
- 实现 API 响应缓存
|
||||||
|
- 使用浏览器缓存
|
||||||
|
|
||||||
|
3. **代码分割**
|
||||||
|
- React 懒加载路由
|
||||||
|
- 按需加载 JavaScript
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
- 使用严格模式
|
||||||
|
- 为所有函数参数添加类型
|
||||||
|
- 使用接口定义复杂对象
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
- 使用 BEM 命名规范
|
||||||
|
- 响应式设计优先
|
||||||
|
- 避免内联样式
|
||||||
|
|
||||||
|
### 提交信息
|
||||||
|
```
|
||||||
|
feat: 添加新功能描述
|
||||||
|
fix: 修复 bug 描述
|
||||||
|
docs: 文档更新
|
||||||
|
style: 代码格式调整
|
||||||
|
refactor: 代码重构
|
||||||
|
test: 测试相关
|
||||||
|
```
|
||||||
|
|
||||||
|
## 资源链接
|
||||||
|
|
||||||
|
- [Express.js 文档](https://expressjs.com/)
|
||||||
|
- [React 文档](https://react.dev/)
|
||||||
|
- [TypeScript 文档](https://www.typescriptlang.org/)
|
||||||
|
- [SQLite 文档](https://www.sqlite.org/)
|
||||||
|
- [Garmin API](https://developer.garmin.com/)
|
||||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "garmin-health-lab",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "佳明健康数据分析平台",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
||||||
|
"dev:server": "cd server && npm run dev",
|
||||||
|
"dev:client": "cd client && npm start",
|
||||||
|
"build": "npm run build:server && npm run build:client",
|
||||||
|
"build:server": "cd server && npm run build",
|
||||||
|
"build:client": "cd client && npm run build",
|
||||||
|
"start": "node server/dist/index.js"
|
||||||
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"server",
|
||||||
|
"client"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^8.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
server/.env.example
Normal file
20
server/.env.example
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Server Configuration
|
||||||
|
PORT=5000
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_PATH=./data/health.db
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your_jwt_secret_key_here_change_in_production
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN=http://localhost:3000
|
||||||
|
|
||||||
|
# Garmin Configuration
|
||||||
|
# Note: Consider using OAuth instead of storing credentials
|
||||||
|
GARMIN_CONNECT_USER=your_garmin_email@example.com
|
||||||
|
GARMIN_CONNECT_PASSWORD=your_password_here
|
||||||
|
|
||||||
|
# Data Sync
|
||||||
|
SYNC_INTERVAL_HOURS=1
|
||||||
29
server/package.json
Normal file
29
server/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "garmin-health-lab-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Garmin Health Lab Backend",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "eslint src --ext .ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"ts-node": "^10.9.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/jsonwebtoken": "^9.0.2",
|
||||||
|
"typescript": "^5.1.3",
|
||||||
|
"tsx": "^3.12.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
server/src/index.ts
Normal file
42
server/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { initializeDatabase } from './utils/database';
|
||||||
|
import authRoutes from './routes/auth';
|
||||||
|
import garminRoutes from './routes/garmin';
|
||||||
|
import healthRoutes from './routes/health';
|
||||||
|
import analysisRoutes from './routes/analysis';
|
||||||
|
import { errorHandler } from './middleware/errorHandler';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cors({
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
initializeDatabase();
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api/auth', authRoutes);
|
||||||
|
app.use('/api/garmin', garminRoutes);
|
||||||
|
app.use('/api/health', healthRoutes);
|
||||||
|
app.use('/api/analysis', analysisRoutes);
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/api/health/status', (req, res) => {
|
||||||
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
31
server/src/middleware/errorHandler.ts
Normal file
31
server/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export function errorHandler(
|
||||||
|
err: any,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
|
||||||
|
const status = err.status || 500;
|
||||||
|
const message = err.message || 'Internal server error';
|
||||||
|
|
||||||
|
res.status(status).json({
|
||||||
|
error: {
|
||||||
|
status,
|
||||||
|
message,
|
||||||
|
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppError extends Error {
|
||||||
|
constructor(
|
||||||
|
public status: number,
|
||||||
|
public message: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
server/src/routes/analysis.ts
Normal file
14
server/src/routes/analysis.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// TODO: Implement analysis endpoints
|
||||||
|
router.get('/trends', (req, res) => {
|
||||||
|
res.json({ message: 'Trends analysis endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/recommendations', (req, res) => {
|
||||||
|
res.json({ message: 'Recommendations endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
18
server/src/routes/auth.ts
Normal file
18
server/src/routes/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// TODO: Implement authentication routes
|
||||||
|
router.post('/login', (req, res) => {
|
||||||
|
res.json({ message: 'Login endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/logout', (req, res) => {
|
||||||
|
res.json({ message: 'Logout endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/refresh', (req, res) => {
|
||||||
|
res.json({ message: 'Refresh token endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
14
server/src/routes/garmin.ts
Normal file
14
server/src/routes/garmin.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// TODO: Implement Garmin API integration
|
||||||
|
router.post('/sync', (req, res) => {
|
||||||
|
res.json({ message: 'Garmin sync endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/status', (req, res) => {
|
||||||
|
res.json({ message: 'Garmin sync status endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
26
server/src/routes/health.ts
Normal file
26
server/src/routes/health.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// TODO: Implement health data endpoints
|
||||||
|
router.get('/summary', (req, res) => {
|
||||||
|
res.json({ message: 'Health summary endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/steps', (req, res) => {
|
||||||
|
res.json({ message: 'Steps data endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/heart-rate', (req, res) => {
|
||||||
|
res.json({ message: 'Heart rate data endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/sleep', (req, res) => {
|
||||||
|
res.json({ message: 'Sleep data endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/activities', (req, res) => {
|
||||||
|
res.json({ message: 'Activities endpoint' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
59
server/src/types/index.ts
Normal file
59
server/src/types/index.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
garminEmail: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthData {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
date: Date;
|
||||||
|
steps: number;
|
||||||
|
heartRate?: number;
|
||||||
|
heartRateVariability?: number;
|
||||||
|
bloodPressure?: {
|
||||||
|
systolic: number;
|
||||||
|
diastolic: number;
|
||||||
|
};
|
||||||
|
sleep?: {
|
||||||
|
duration: number;
|
||||||
|
quality: number;
|
||||||
|
};
|
||||||
|
stress?: number;
|
||||||
|
caloriesBurned?: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Activity {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
activityType: string;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
duration: number;
|
||||||
|
distance?: number;
|
||||||
|
calories?: number;
|
||||||
|
heartRateAverage?: number;
|
||||||
|
heartRateMax?: number;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthRecommendation {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
category: string;
|
||||||
|
recommendation: string;
|
||||||
|
priority: 'high' | 'medium' | 'low';
|
||||||
|
basedOn: string[];
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncStatus {
|
||||||
|
lastSyncTime: Date;
|
||||||
|
status: 'idle' | 'syncing' | 'error';
|
||||||
|
lastError?: string;
|
||||||
|
recordsSynced: number;
|
||||||
|
}
|
||||||
112
server/src/utils/database.ts
Normal file
112
server/src/utils/database.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import sqlite3 from 'sqlite3';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const dbPath = process.env.DATABASE_PATH || './data/health.db';
|
||||||
|
|
||||||
|
// Ensure data directory exists
|
||||||
|
const dataDir = path.dirname(dbPath);
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = new sqlite3.Database(dbPath);
|
||||||
|
|
||||||
|
export function initializeDatabase() {
|
||||||
|
db.serialize(() => {
|
||||||
|
// Users table
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
garmin_email TEXT NOT NULL,
|
||||||
|
garmin_password_hash TEXT NOT NULL,
|
||||||
|
jwt_token TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Health data table
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS health_data (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
steps INTEGER,
|
||||||
|
heart_rate INTEGER,
|
||||||
|
heart_rate_variability REAL,
|
||||||
|
blood_pressure_systolic INTEGER,
|
||||||
|
blood_pressure_diastolic INTEGER,
|
||||||
|
sleep_duration INTEGER,
|
||||||
|
sleep_quality REAL,
|
||||||
|
stress INTEGER,
|
||||||
|
calories_burned REAL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
UNIQUE(user_id, date)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Activities table
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS activities (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
activity_type TEXT NOT NULL,
|
||||||
|
start_time DATETIME NOT NULL,
|
||||||
|
end_time DATETIME NOT NULL,
|
||||||
|
duration INTEGER,
|
||||||
|
distance REAL,
|
||||||
|
calories REAL,
|
||||||
|
heart_rate_average INTEGER,
|
||||||
|
heart_rate_max INTEGER,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Sync status table
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sync_status (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
last_sync_time DATETIME,
|
||||||
|
status TEXT DEFAULT 'idle',
|
||||||
|
last_error TEXT,
|
||||||
|
records_synced INTEGER DEFAULT 0,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('Database initialized successfully');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runAsync(sql: string, params: any[] = []): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.run(sql, params, function(err) {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve({ id: this.lastID, changes: this.changes });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAsync(sql: string, params: any[] = []): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.get(sql, params, (err, row) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allAsync(sql: string, params: any[] = []): Promise<any[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(sql, params, (err, rows) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
20
server/tsconfig.json
Normal file
20
server/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user