初始提交

This commit is contained in:
root 2025-07-28 11:59:56 +08:00
commit d36b16d458
49 changed files with 14000 additions and 0 deletions

367
.gitignore vendored Normal file
View File

@ -0,0 +1,367 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
PIPFILE.lock
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# poetry
poetry.lock
# pdm
.pdm.toml
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
storybook-static
# Temporary folders
tmp/
temp/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Electron
dist_electron/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Application specific
data/
!data/.gitkeep
backup_*.zip
exports/
sessions/
patients/
backups/
temp/
logs/
*.db
*.sqlite
*.sqlite3
# Media files
*.mp4
*.avi
*.mov
*.wmv
*.flv
*.webm
*.mkv
*.m4v
*.3gp
*.jpg
*.jpeg
*.png
*.gif
*.bmp
*.tiff
*.svg
*.ico
*.webp
# Configuration files with sensitive data
config.local.json
secrets.json
.env.local
.env.production
# Build artifacts
build/
dist/
out/
# Test artifacts
test-results/
coverage/
.nyc_output/
# Documentation
docs/build/
docs/_build/
# Backup files
*.bak
*.backup
*.old
*.orig
# Lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
# Cache directories
.cache/
.npm/
.yarn/
.pnpm/
# Runtime directories
runtime/
pids/
# Development tools
.vscode/
.idea/
*.sublime-*
# System files
*.tmp
*.temp
.fuse_hidden*
.directory
.Trash-*
.nfs*
# Windows
desktop.ini
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk

1
.vercel/project.json Normal file
View File

@ -0,0 +1 @@
{"projectName":"trae_c7qdkht3"}

220
CHANGELOG.md Normal file
View File

@ -0,0 +1,220 @@
# 更新日志
本文档记录了身体平衡评估系统的所有重要更改。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [未发布]
### 计划新增
- [ ] 多语言支持(英文、日文)
- [ ] 云端数据同步功能
- [ ] 移动端应用支持
- [ ] AI辅助诊断建议
- [ ] 3D姿态可视化
- [ ] 报告模板自定义
- [ ] 批量数据分析
- [ ] 设备校准向导
### 计划改进
- [ ] 优化实时数据处理性能
- [ ] 增强数据可视化效果
- [ ] 改进用户界面交互体验
- [ ] 扩展设备兼容性
## [1.0.0] - 2024-01-15
### 新增
- ✅ 完整的身体平衡评估系统架构
- ✅ 基于Vue 3 + Electron的现代化前端界面
- ✅ 基于Flask的RESTful API后端服务
- ✅ 多传感器数据融合摄像头、IMU、压力传感器
- ✅ 实时姿态检测和平衡分析
- ✅ 患者信息管理系统
- ✅ 检测会话管理和历史记录
- ✅ 数据分析和可视化图表
- ✅ PDF报告生成和导出功能
- ✅ 系统设置和设备配置
- ✅ 完整的日志记录系统
- ✅ 数据备份和恢复功能
### 技术特性
- ✅ 基于MediaPipe的实时姿态检测
- ✅ WebSocket实时数据传输
- ✅ SQLite数据库存储
- ✅ 模块化架构设计
- ✅ 跨平台支持Windows、macOS、Linux
- ✅ 响应式UI设计
- ✅ 国际化支持框架
### 核心功能模块
#### 前端界面
- ✅ 现代化的用户界面设计
- ✅ 实时数据可视化
- ✅ 响应式布局适配
- ✅ 深色/浅色主题切换
- ✅ 多语言界面支持
#### 后端服务
- ✅ RESTful API设计
- ✅ 实时数据处理引擎
- ✅ 设备管理和通信
- ✅ 数据分析算法
- ✅ 报告生成服务
#### 数据管理
- ✅ 患者信息CRUD操作
- ✅ 检测会话管理
- ✅ 历史数据查询和分析
- ✅ 数据导出和备份
- ✅ 数据安全和隐私保护
#### 设备集成
- ✅ 摄像头视频采集和处理
- ✅ IMU传感器数据采集
- ✅ 压力传感器数据采集
- ✅ 设备状态监控
- ✅ 设备校准功能
#### 分析算法
- ✅ 重心轨迹分析
- ✅ 姿态稳定性评估
- ✅ 平衡能力评分
- ✅ 异常检测和预警
- ✅ 趋势分析和对比
### 文件结构
```
BodyBalanceEvaluation/
├── backend/ # 后端服务
│ ├── app.py # 主应用入口
│ ├── database.py # 数据库管理
│ ├── device_manager.py # 设备管理
│ ├── detection_engine.py # 检测引擎
│ ├── data_processor.py # 数据处理
│ ├── utils.py # 工具函数
│ ├── requirements.txt # Python依赖
│ └── tests/ # 测试文件
├── src/
│ ├── main/ # Electron主进程
│ └── renderer/ # Vue前端应用
│ ├── src/
│ │ ├── views/ # 页面组件
│ │ ├── stores/ # 状态管理
│ │ └── services/ # API服务
│ └── package.json
├── data/ # 数据目录
├── logs/ # 日志目录
├── temp/ # 临时文件
├── main.py # 启动脚本
├── config.json # 配置文件
├── package.json # 项目配置
├── README.md # 项目说明
├── LICENSE # 许可证
├── .gitignore # Git忽略规则
├── install.bat # 安装脚本
├── start_dev.bat # 开发环境启动
├── start_prod.bat # 生产环境启动
└── CHANGELOG.md # 更新日志
```
### 系统要求
- **操作系统**: Windows 10/11, macOS 10.15+, Ubuntu 18.04+
- **Python**: 3.8 或更高版本
- **Node.js**: 16.0 或更高版本
- **内存**: 最少 4GB RAM推荐 8GB+
- **存储**: 最少 2GB 可用空间
- **摄像头**: USB摄像头或内置摄像头
- **串口设备**: IMU和压力传感器可选
### 安装和使用
1. 运行 `install.bat` 安装所有依赖
2. 运行 `start_dev.bat` 启动开发环境
3. 或运行 `start_prod.bat` 启动生产环境
4. 访问 http://localhost:5173 使用应用
### 已知问题
- 在某些低配置设备上可能出现实时处理延迟
- 部分USB摄像头可能需要额外驱动
- IMU传感器需要正确的串口配置
### 性能优化
- 实时数据处理采用多线程架构
- 图像处理使用GPU加速如可用
- 数据库查询优化和索引
- 前端虚拟滚动和懒加载
### 安全特性
- 本地数据存储,保护隐私
- 数据传输加密
- 用户会话管理
- 输入数据验证和清理
---
## 版本说明
### 版本号格式
本项目使用语义化版本号:`主版本号.次版本号.修订号`
- **主版本号**: 不兼容的API修改
- **次版本号**: 向下兼容的功能性新增
- **修订号**: 向下兼容的问题修正
### 更新类型
- **新增 (Added)**: 新功能
- **更改 (Changed)**: 对现有功能的更改
- **弃用 (Deprecated)**: 即将移除的功能
- **移除 (Removed)**: 已移除的功能
- **修复 (Fixed)**: 错误修复
- **安全 (Security)**: 安全相关的修复
### 发布周期
- **主版本**: 每年1-2次重大更新
- **次版本**: 每季度功能更新
- **修订版**: 每月bug修复和小改进
---
## 贡献指南
如果您想为本项目贡献代码,请:
1. Fork 本仓库
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 创建 Pull Request
### 提交信息格式
```
type(scope): description
[optional body]
[optional footer]
```
类型包括:
- `feat`: 新功能
- `fix`: 修复bug
- `docs`: 文档更新
- `style`: 代码格式化
- `refactor`: 代码重构
- `test`: 测试相关
- `chore`: 构建过程或辅助工具的变动
---
## 支持和反馈
- **问题报告**: [GitHub Issues](https://github.com/example/body-balance-evaluation/issues)
- **功能请求**: [GitHub Discussions](https://github.com/example/body-balance-evaluation/discussions)
- **邮件支持**: dev@example.com
- **文档**: [项目Wiki](https://github.com/example/body-balance-evaluation/wiki)
---
*最后更新: 2024-01-15*

680
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,680 @@
# 贡献指南
感谢您对身体平衡评估系统项目的关注!我们欢迎所有形式的贡献,包括但不限于代码、文档、测试、问题报告和功能建议。
## 目录
- [行为准则](#行为准则)
- [如何贡献](#如何贡献)
- [开发环境设置](#开发环境设置)
- [代码规范](#代码规范)
- [提交规范](#提交规范)
- [Pull Request 流程](#pull-request-流程)
- [问题报告](#问题报告)
- [功能请求](#功能请求)
- [文档贡献](#文档贡献)
- [测试指南](#测试指南)
## 行为准则
### 我们的承诺
为了营造一个开放和友好的环境,我们作为贡献者和维护者承诺,无论年龄、体型、残疾、种族、性别认同和表达、经验水平、国籍、个人形象、种族、宗教或性取向如何,参与我们项目和社区的每个人都能获得无骚扰的体验。
### 我们的标准
有助于创造积极环境的行为包括:
- 使用友好和包容的语言
- 尊重不同的观点和经验
- 优雅地接受建设性批评
- 关注对社区最有利的事情
- 对其他社区成员表示同理心
不可接受的行为包括:
- 使用性化的语言或图像,以及不受欢迎的性关注或性骚扰
- 恶意评论、侮辱/贬损评论,以及个人或政治攻击
- 公开或私下骚扰
- 未经明确许可,发布他人的私人信息,如物理或电子地址
- 在专业环境中可能被合理认为不适当的其他行为
## 如何贡献
### 贡献类型
我们欢迎以下类型的贡献:
1. **代码贡献**
- 新功能开发
- Bug 修复
- 性能优化
- 代码重构
2. **文档贡献**
- API 文档
- 用户指南
- 开发者文档
- 代码注释
3. **测试贡献**
- 单元测试
- 集成测试
- 端到端测试
- 性能测试
4. **设计贡献**
- UI/UX 设计
- 图标设计
- 用户体验改进
5. **其他贡献**
- 问题报告
- 功能建议
- 社区支持
- 翻译工作
## 开发环境设置
### 前置要求
- Python 3.8+
- Node.js 16.0+
- Git
- 代码编辑器(推荐 VS Code
### 环境配置
1. **克隆仓库**
```bash
git clone https://github.com/example/body-balance-evaluation.git
cd body-balance-evaluation
```
2. **安装依赖**
```bash
# Windows
install.bat
# 或手动安装
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linux
pip install -r backend/requirements.txt
cd src/renderer && npm install
```
3. **配置开发环境**
```bash
# 复制配置文件
cp config.json config.local.json
# 编辑本地配置(可选)
# 修改 config.local.json 中的设置
```
4. **启动开发服务器**
```bash
# Windows
start_dev.bat
# 或手动启动
python main.py --mode development
```
### 开发工具推荐
#### VS Code 扩展
- Python
- Pylance
- Vue Language Features (Volar)
- TypeScript Vue Plugin (Volar)
- ESLint
- Prettier
- GitLens
#### 浏览器扩展
- Vue.js devtools
- React Developer Tools如果使用
## 代码规范
### Python 代码规范
我们遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 规范:
```python
# 好的示例
class PatientManager:
"""患者管理器类"""
def __init__(self, database_path: str):
self.db_path = database_path
self.logger = logging.getLogger(__name__)
def create_patient(self, patient_data: dict) -> int:
"""创建新患者
Args:
patient_data: 患者信息字典
Returns:
患者ID
Raises:
ValueError: 当患者数据无效时
"""
if not self._validate_patient_data(patient_data):
raise ValueError("Invalid patient data")
# 实现逻辑...
return patient_id
```
#### 代码格式化
使用 Black 进行代码格式化:
```bash
black . --line-length=88
```
#### 代码检查
使用 flake8 进行代码检查:
```bash
flake8 . --max-line-length=88 --exclude=venv,__pycache__
```
### JavaScript/Vue 代码规范
我们遵循 [Vue.js 风格指南](https://vuejs.org/style-guide/)
```javascript
// 好的示例
export default {
name: 'PatientList',
props: {
patients: {
type: Array,
required: true
},
loading: {
type: Boolean,
default: false
}
},
emits: ['patient-selected', 'patient-deleted'],
setup(props, { emit }) {
const selectedPatient = ref(null)
const handlePatientClick = (patient) => {
selectedPatient.value = patient
emit('patient-selected', patient)
}
return {
selectedPatient,
handlePatientClick
}
}
}
```
#### 代码格式化
使用 Prettier 进行代码格式化:
```bash
npm run format
```
#### 代码检查
使用 ESLint 进行代码检查:
```bash
npm run lint
```
### 通用规范
1. **命名规范**
- 使用有意义的变量和函数名
- Python: snake_case
- JavaScript: camelCase
- 常量: UPPER_CASE
- 类名: PascalCase
2. **注释规范**
- 为复杂逻辑添加注释
- 使用文档字符串描述函数和类
- 注释应该解释"为什么"而不是"是什么"
3. **文件组织**
- 保持文件结构清晰
- 相关功能放在同一模块
- 避免循环导入
## 提交规范
我们使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
### 提交消息格式
```
type(scope): description
[optional body]
[optional footer(s)]
```
### 类型 (type)
- `feat`: 新功能
- `fix`: 修复 bug
- `docs`: 文档更新
- `style`: 代码格式化(不影响代码运行的变动)
- `refactor`: 重构(既不是新增功能,也不是修复 bug 的代码变动)
- `perf`: 性能优化
- `test`: 增加测试
- `chore`: 构建过程或辅助工具的变动
- `ci`: CI 配置文件和脚本的变动
- `revert`: 回滚之前的提交
### 范围 (scope)
- `frontend`: 前端相关
- `backend`: 后端相关
- `database`: 数据库相关
- `device`: 设备管理相关
- `analysis`: 数据分析相关
- `ui`: 用户界面相关
- `api`: API 相关
- `config`: 配置相关
- `docs`: 文档相关
- `test`: 测试相关
### 示例
```bash
# 新功能
git commit -m "feat(frontend): add patient search functionality"
# Bug 修复
git commit -m "fix(backend): resolve database connection timeout issue"
# 文档更新
git commit -m "docs(api): update patient management API documentation"
# 重构
git commit -m "refactor(device): simplify camera initialization logic"
```
## Pull Request 流程
### 创建 Pull Request
1. **Fork 仓库**
- 点击 GitHub 页面右上角的 "Fork" 按钮
2. **创建功能分支**
```bash
git checkout -b feature/your-feature-name
# 或
git checkout -b fix/your-bug-fix
```
3. **进行开发**
- 编写代码
- 添加测试
- 更新文档
4. **提交更改**
```bash
git add .
git commit -m "feat(scope): your commit message"
```
5. **推送分支**
```bash
git push origin feature/your-feature-name
```
6. **创建 Pull Request**
- 在 GitHub 上创建 Pull Request
- 填写详细的描述
### Pull Request 模板
```markdown
## 描述
简要描述这个 PR 的目的和内容。
## 更改类型
- [ ] Bug 修复
- [ ] 新功能
- [ ] 重构
- [ ] 文档更新
- [ ] 性能优化
- [ ] 其他(请说明)
## 测试
- [ ] 已添加单元测试
- [ ] 已添加集成测试
- [ ] 已手动测试
- [ ] 所有测试通过
## 检查清单
- [ ] 代码遵循项目规范
- [ ] 已添加必要的注释
- [ ] 已更新相关文档
- [ ] 没有引入新的警告
- [ ] 已测试在不同环境下的兼容性
## 相关问题
关闭 #issue_number
## 截图(如适用)
添加截图来帮助解释你的更改。
## 额外说明
添加任何其他相关信息。
```
### 代码审查
所有 Pull Request 都需要经过代码审查:
1. **自动检查**
- CI/CD 流水线检查
- 代码格式检查
- 测试覆盖率检查
2. **人工审查**
- 代码质量
- 设计合理性
- 测试完整性
- 文档完整性
3. **审查标准**
- 代码可读性
- 性能影响
- 安全性考虑
- 向后兼容性
## 问题报告
### 报告 Bug
使用 [Bug 报告模板](https://github.com/example/body-balance-evaluation/issues/new?template=bug_report.md)
```markdown
**描述 Bug**
清晰简洁地描述 bug 是什么。
**重现步骤**
重现行为的步骤:
1. 转到 '...'
2. 点击 '....'
3. 滚动到 '....'
4. 看到错误
**预期行为**
清晰简洁地描述你期望发生什么。
**截图**
如果适用,添加截图来帮助解释你的问题。
**环境信息**
- 操作系统: [例如 Windows 10]
- Python 版本: [例如 3.9.0]
- Node.js 版本: [例如 16.14.0]
- 应用版本: [例如 1.0.0]
**额外信息**
添加任何其他关于问题的信息。
```
### 报告安全问题
如果发现安全漏洞,请不要在公开的 issue 中报告。请发送邮件到 security@example.com。
## 功能请求
使用 [功能请求模板](https://github.com/example/body-balance-evaluation/issues/new?template=feature_request.md)
```markdown
**功能描述**
清晰简洁地描述你想要的功能。
**问题描述**
清晰简洁地描述问题是什么。例如:我总是感到沫丧当 [...]
**解决方案**
清晰简洁地描述你想要发生什么。
**替代方案**
清晰简洁地描述你考虑过的任何替代解决方案或功能。
**额外信息**
添加任何其他关于功能请求的信息或截图。
```
## 文档贡献
### 文档类型
1. **用户文档**
- 安装指南
- 使用教程
- 常见问题
- 故障排除
2. **开发者文档**
- API 文档
- 架构说明
- 贡献指南
- 代码注释
3. **项目文档**
- README
- CHANGELOG
- LICENSE
- 项目规划
### 文档规范
1. **Markdown 格式**
- 使用标准 Markdown 语法
- 保持一致的格式
- 使用有意义的标题
2. **内容要求**
- 清晰准确
- 及时更新
- 包含示例
- 考虑不同用户水平
3. **图片和媒体**
- 使用 SVG 格式(如可能)
- 优化文件大小
- 提供替代文本
## 测试指南
### 测试类型
1. **单元测试**
- 测试单个函数或方法
- 使用 pytestPython或 JestJavaScript
- 目标覆盖率 > 80%
2. **集成测试**
- 测试模块间交互
- 测试 API 端点
- 测试数据库操作
3. **端到端测试**
- 测试完整用户流程
- 使用 Playwright 或 Cypress
- 测试关键业务场景
### 测试规范
```python
# Python 测试示例
import pytest
from unittest.mock import Mock, patch
class TestPatientManager:
"""患者管理器测试类"""
@pytest.fixture
def patient_manager(self):
"""测试夹具"""
return PatientManager(":memory:")
def test_create_patient_success(self, patient_manager):
"""测试成功创建患者"""
patient_data = {
"name": "测试患者",
"age": 30,
"gender": "male"
}
patient_id = patient_manager.create_patient(patient_data)
assert patient_id is not None
assert isinstance(patient_id, int)
def test_create_patient_invalid_data(self, patient_manager):
"""测试无效数据创建患者"""
invalid_data = {"name": ""}
with pytest.raises(ValueError):
patient_manager.create_patient(invalid_data)
```
```javascript
// JavaScript 测试示例
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import PatientList from '@/components/PatientList.vue'
describe('PatientList', () => {
it('renders patient list correctly', () => {
const patients = [
{ id: 1, name: '患者1', age: 30 },
{ id: 2, name: '患者2', age: 25 }
]
const wrapper = mount(PatientList, {
props: { patients }
})
expect(wrapper.findAll('.patient-item')).toHaveLength(2)
expect(wrapper.text()).toContain('患者1')
expect(wrapper.text()).toContain('患者2')
})
it('emits patient-selected event when patient is clicked', async () => {
const patients = [{ id: 1, name: '患者1', age: 30 }]
const wrapper = mount(PatientList, {
props: { patients }
})
await wrapper.find('.patient-item').trigger('click')
expect(wrapper.emitted('patient-selected')).toBeTruthy()
expect(wrapper.emitted('patient-selected')[0]).toEqual([patients[0]])
})
})
```
### 运行测试
```bash
# Python 测试
cd backend
python -m pytest tests/ -v --cov=.
# JavaScript 测试
cd src/renderer
npm run test
# 所有测试
npm run test
```
## 发布流程
### 版本管理
我们使用语义化版本控制:
- **主版本号**: 不兼容的 API 修改
- **次版本号**: 向下兼容的功能性新增
- **修订号**: 向下兼容的问题修正
### 发布步骤
1. **更新版本号**
```bash
# 更新 package.json 中的版本号
npm version patch|minor|major
```
2. **更新 CHANGELOG**
- 记录所有重要更改
- 按类型分组(新增、更改、修复等)
3. **创建发布标签**
```bash
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
```
4. **构建和测试**
```bash
npm run build
npm run test
```
5. **发布**
- 创建 GitHub Release
- 上传构建产物
- 发布到包管理器(如需要)
## 社区支持
### 获取帮助
- **GitHub Discussions**: 一般讨论和问题
- **GitHub Issues**: Bug 报告和功能请求
- **邮件**: dev@example.com
- **文档**: 项目 Wiki
### 参与社区
- 回答其他用户的问题
- 参与功能讨论
- 分享使用经验
- 推广项目
## 致谢
感谢所有为这个项目做出贡献的开发者!
### 贡献者
- [贡献者列表](https://github.com/example/body-balance-evaluation/contributors)
### 特别感谢
- 感谢所有提供反馈和建议的用户
- 感谢开源社区提供的优秀工具和库
- 感谢医疗专业人士提供的专业指导
---
再次感谢您的贡献!如果您有任何问题,请随时联系我们。

425
README.md Normal file
View File

@ -0,0 +1,425 @@
# 身体平衡评估系统
一个基于多传感器融合技术的专业身体平衡评估与分析系统,为用户提供准确的平衡能力评估和康复指导。
## 系统特性
### 🎯 核心功能
- **实时姿态检测**: 基于MediaPipe的高精度人体姿态识别
- **多传感器融合**: 整合摄像头、IMU传感器和压力传感器数据
- **智能分析引擎**: 多维度平衡能力评估算法
- **可视化报告**: 直观的数据图表和分析报告
- **历史数据管理**: 完整的检测记录存储和对比分析
### 🔧 技术特点
- **现代化架构**: Vue 3 + Python Flask 前后端分离
- **实时通信**: WebSocket 实时数据传输
- **跨平台支持**: Windows、macOS、Linux
- **模块化设计**: 清晰的目录结构,易于扩展和维护
- **数据安全**: 本地存储,保护用户隐私
- **开发友好**: 独立的前后端开发环境,支持热重载
- **部署简化**: 一键安装和启动脚本,降低部署复杂度
## 系统架构
```
身体平衡评估系统/
├── backend/ # 后端服务
│ ├── main.py # 主启动脚本
│ ├── app.py # Flask 主应用
│ ├── database.py # 数据库管理
│ ├── device_manager.py # 设备管理
│ ├── detection_engine.py # 检测引擎
│ ├── data_processor.py # 数据处理
│ ├── utils.py # 工具函数
│ └── requirements.txt # Python 依赖
├── frontend/ # 前端应用
│ └── src/renderer/ # 前端源码
│ ├── src/
│ │ ├── views/ # 页面组件
│ │ ├── stores/ # 状态管理
│ │ ├── services/ # API 服务
│ │ └── router/ # 路由配置
│ ├── package.json # Node.js 依赖
│ └── vite.config.js # 构建配置
├── data/ # 数据目录
├── logs/ # 日志目录
├── venv/ # Python 虚拟环境
├── install.bat # 安装脚本
├── start_dev.bat # 开发模式启动脚本
└── start_prod.bat # 生产模式启动脚本
```
## 快速开始
### 环境要求
- **Python**: 3.8 或更高版本
- **Node.js**: 16.0 或更高版本 (开发模式)
- **操作系统**: Windows 10/11, macOS 10.15+, Ubuntu 18.04+
### 安装步骤
#### 方式一:一键安装 (Windows 推荐)
1. **克隆项目**
```bash
git clone <repository-url>
cd BodyBalanceEvaluation
```
2. **运行安装脚本**
```bash
install.bat
```
安装脚本会自动完成:
- 检查 Python 和 Node.js 环境
- 创建 Python 虚拟环境
- 安装后端依赖
- 安装前端依赖
- 创建必要的目录结构
#### 方式二:手动安装
1. **克隆项目**
```bash
git clone <repository-url>
cd BodyBalanceEvaluation
```
2. **创建虚拟环境**
```bash
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linux
```
3. **安装 Python 依赖**
```bash
pip install -r backend/requirements.txt
```
4. **安装前端依赖** (开发模式)
```bash
cd frontend/src/renderer
npm install
cd ../../..
```
### 启动应用程序
**Windows 用户 (推荐)**:
```bash
# 一键安装 (首次使用)
install.bat
# 开发模式
start_dev.bat
# 生产模式
start_prod.bat
```
**手动启动**:
```bash
# 激活虚拟环境
venv\Scripts\activate
# 进入后端目录
cd backend
# 开发模式
python main.py --mode development
# 生产模式
python main.py --mode production
```
### 命令行参数
```bash
cd backend
python main.py [选项]
选项:
--mode {development,production} 运行模式 (默认: development)
--host HOST 服务器主机 (默认: 127.0.0.1)
--port PORT 服务器端口 (默认: 5000)
--no-browser 不自动打开浏览器
--log-level {DEBUG,INFO,WARNING,ERROR} 日志级别 (默认: INFO)
```
## 使用指南
### 1. 系统设置
首次使用前,请进入「系统设置」页面配置:
- **设备配置**: 选择摄像头、配置串口设备
- **检测参数**: 设置默认检测时长、采样频率等
- **数据管理**: 配置数据存储路径和清理策略
### 2. 患者管理
- 添加患者基本信息(姓名、年龄、性别等)
- 记录患者病史和康复目标
- 管理患者档案和检测记录
### 3. 姿态检测
1. **选择患者**: 从患者列表中选择或新建患者
2. **设备准备**: 确保摄像头和传感器正常连接
3. **参数配置**: 设置检测时长、采样频率等参数
4. **开始检测**: 点击开始按钮进行实时检测
5. **查看结果**: 检测完成后查看分析结果和建议
### 4. 数据分析
- **单次分析**: 查看单次检测的详细数据和图表
- **对比分析**: 比较多次检测结果,观察变化趋势
- **报告生成**: 生成 PDF 格式的专业评估报告
- **数据导出**: 导出原始数据用于进一步分析
### 5. 历史记录
- 浏览所有历史检测记录
- 按患者、日期、状态等条件筛选
- 批量导出和删除操作
- 时间线视图查看检测历史
## 设备支持
### 摄像头
- USB 摄像头 (推荐 1080p 30fps)
- 内置摄像头
- 网络摄像头
### IMU 传感器
- 支持串口通信的 9 轴 IMU
- 波特率: 9600, 115200, 230400
- 数据格式: 加速度、陀螺仪、磁力计
### 压力传感器
- 多点压力传感器阵列
- 串口通信接口
- 支持 1-16 个传感器点
## 开发指南
### 后端开发
后端使用 Flask 框架,主要模块:
- `main.py`: 主启动脚本和进程管理
- `app.py`: 主应用和 API 路由
- `database.py`: SQLite 数据库操作
- `device_manager.py`: 硬件设备管理
- `detection_engine.py`: 检测算法引擎
- `data_processor.py`: 数据分析和处理
### 前端开发
前端使用 Vue 3 + Element Plus主要特性
- **组合式 API**: 使用 Vue 3 Composition API
- **状态管理**: Pinia 状态管理库
- **UI 组件**: Element Plus 组件库
- **图表库**: ECharts 数据可视化
- **构建工具**: Vite 快速构建
### 添加新功能
1. **后端 API**:
```python
@app.route('/api/new-feature', methods=['POST'])
def new_feature():
# 实现新功能逻辑
return jsonify({'status': 'success'})
```
2. **前端服务**:
```javascript
// frontend/src/renderer/src/services/api.js
export const newFeatureAPI = {
doSomething: (data) => api.post('/new-feature', data)
}
```
3. **前端组件**:
```vue
<template>
<!-- 新功能界面 -->
</template>
<script setup>
import { newFeatureAPI } from '../services/api'
</script>
```
### 项目结构优势
新的项目结构带来以下优势:
1. **清晰分离**: 前后端代码完全分离,便于团队协作开发
2. **独立部署**: 前后端可以独立部署和扩展
3. **开发效率**: 前后端可以并行开发,提高开发效率
4. **维护性**: 模块化结构便于代码维护和功能扩展
5. **版本管理**: 前后端可以独立进行版本控制
6. **技术栈**: 前后端可以选择最适合的技术栈
### 开发环境配置
**后端开发**:
```bash
# 激活虚拟环境
venv\Scripts\activate
# 进入后端目录
cd backend
# 启动开发服务器
python main.py --mode development --log-level DEBUG
```
**前端开发**:
```bash
# 进入前端目录
cd frontend/src/renderer
# 启动开发服务器
npm run dev
```
**同时开发** (推荐):
```bash
# 使用开发脚本同时启动前后端
start_dev.bat
```
## 故障排除
### 常见问题
**Q: 摄像头无法识别**
A: 检查摄像头连接,确保没有被其他应用占用,在设备设置中刷新摄像头列表。
**Q: 传感器连接失败**
A: 确认串口设置正确,检查设备驱动是否安装,尝试不同的波特率设置。
**Q: 前端页面无法加载**
A: 检查后端服务是否正常启动,确认防火墙设置,查看浏览器控制台错误信息。
**Q: 检测结果不准确**
A: 确保设备已正确校准,检查环境光线条件,调整检测参数设置。
### 日志查看
系统日志保存在 `logs/` 目录下:
- `app.log`: 应用程序主日志
- `device.log`: 设备管理日志
- `detection.log`: 检测引擎日志
- `error.log`: 错误日志
### 性能优化
1. **硬件要求**:
- CPU: Intel i5 或同等性能
- 内存: 8GB RAM
- 存储: 10GB 可用空间
2. **软件优化**:
- 关闭不必要的后台程序
- 使用 SSD 存储提高 I/O 性能
- 定期清理历史数据和日志
## 数据格式
### 检测数据结构
```json
{
"session_id": "uuid",
"patient_id": "patient_uuid",
"timestamp": "2024-01-01T12:00:00Z",
"duration": 60,
"data": {
"camera": {
"landmarks": [...],
"confidence": 0.95
},
"imu": {
"acceleration": [x, y, z],
"gyroscope": [x, y, z],
"magnetometer": [x, y, z]
},
"pressure": {
"sensors": [p1, p2, p3, p4],
"center_of_pressure": [x, y]
}
}
}
```
### 分析结果格式
```json
{
"session_id": "uuid",
"analysis_time": "2024-01-01T12:01:00Z",
"overall_assessment": "good",
"balance_score": 85,
"posture_score": 78,
"metrics": {
"sway_area": 2.5,
"sway_velocity": 1.2,
"postural_stability": 0.85
},
"recommendations": [
"建议加强核心肌群训练",
"注意保持正确站姿"
]
}
```
## 许可证
本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。
## 贡献指南
欢迎贡献代码!请遵循以下步骤:
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request
## 支持
如果您遇到问题或有建议,请:
- 查看 [FAQ](docs/FAQ.md)
- 提交 [Issue](issues)
- 发送邮件至: support@example.com
## 更新日志
### v1.1.0 (2024-01-15)
- **项目重构**: 前后端完全分离,优化项目结构
- **新增脚本**: 添加一键安装和启动脚本 (install.bat, start_dev.bat, start_prod.bat)
- **开发体验**: 改进开发环境配置,支持独立的前后端开发
- **文档更新**: 完善 README 文档,添加详细的安装和使用说明
- **路径优化**: 统一使用虚拟环境,规范化依赖管理
### v1.0.0 (2024-01-01)
- 初始版本发布
- 基础检测功能
- 患者管理系统
- 数据分析和报告生成
---
**身体平衡评估系统** - 专业的平衡能力评估解决方案

630
backend/app.py Normal file
View File

@ -0,0 +1,630 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
平衡体态检测系统 - 后端服务
基于Flask的本地API服务
"""
import os
import sys
import json
import time
import threading
from datetime import datetime
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
import sqlite3
import logging
from pathlib import Path
# 添加当前目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 导入自定义模块
from database import DatabaseManager
from device_manager import DeviceManager
from detection_engine import DetectionEngine
from data_processor import DataProcessor
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/backend.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 创建Flask应用
app = Flask(__name__)
app.config['SECRET_KEY'] = 'body-balance-detection-system-2024'
# 启用CORS支持
CORS(app, origins=['http://localhost:3000', 'http://localhost:3001', 'file://*'])
# 全局变量
db_manager = None
device_manager = None
detection_engine = None
data_processor = None
current_detection = None
detection_thread = None
def init_app():
"""初始化应用"""
global db_manager, device_manager, detection_engine, data_processor
try:
# 创建必要的目录
os.makedirs('logs', exist_ok=True)
os.makedirs('data', exist_ok=True)
os.makedirs('exports', exist_ok=True)
os.makedirs('videos', exist_ok=True)
# 初始化数据库
db_manager = DatabaseManager('data/body_balance.db')
db_manager.init_database()
# 初始化设备管理器
device_manager = DeviceManager()
# 初始化检测引擎
detection_engine = DetectionEngine()
# 初始化数据处理器
data_processor = DataProcessor()
logger.info('应用初始化完成')
except Exception as e:
logger.error(f'应用初始化失败: {e}')
raise
# ==================== 基础API ====================
@app.route('/health', methods=['GET'])
def health_check():
"""健康检查"""
return jsonify({
'status': 'ok',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0'
})
@app.route('/api/health', methods=['GET'])
def api_health_check():
"""API健康检查"""
return jsonify({
'status': 'ok',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0'
})
# ==================== 认证API ====================
@app.route('/api/auth/login', methods=['POST'])
def login():
"""用户登录"""
try:
data = request.get_json()
username = data.get('username')
password = data.get('password')
remember = data.get('remember', False)
# 简单的模拟登录验证
if username and password:
# 这里可以添加真实的用户验证逻辑
# 目前使用模拟数据
user_data = {
'id': 1,
'username': username,
'name': '医生',
'role': 'doctor',
'avatar': ''
}
# 生成简单的token实际项目中应使用JWT等安全token
token = f"token_{username}_{int(time.time())}"
return jsonify({
'success': True,
'data': {
'token': token,
'user': user_data
},
'message': '登录成功'
})
else:
return jsonify({
'success': False,
'message': '用户名或密码不能为空'
}), 400
except Exception as e:
logger.error(f'登录失败: {e}')
return jsonify({'success': False, 'message': '登录失败'}), 500
@app.route('/api/auth/register', methods=['POST'])
def register():
"""用户注册"""
try:
data = request.get_json()
username = data.get('username')
password = data.get('password')
email = data.get('email')
# 简单的模拟注册
if username and password:
return jsonify({
'success': True,
'message': '注册成功,请登录'
})
else:
return jsonify({
'success': False,
'message': '用户名和密码不能为空'
}), 400
except Exception as e:
logger.error(f'注册失败: {e}')
return jsonify({'success': False, 'message': '注册失败'}), 500
@app.route('/api/auth/logout', methods=['POST'])
def logout():
"""用户退出"""
try:
return jsonify({
'success': True,
'message': '退出成功'
})
except Exception as e:
logger.error(f'退出失败: {e}')
return jsonify({'success': False, 'message': '退出失败'}), 500
@app.route('/api/auth/verify', methods=['GET'])
def verify_token():
"""验证token"""
try:
# 简单的token验证实际项目中应验证JWT等
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
# 这里可以添加真实的token验证逻辑
return jsonify({
'success': True,
'data': {'valid': True}
})
else:
return jsonify({
'success': True,
'data': {'valid': True} # 暂时总是返回有效
})
except Exception as e:
logger.error(f'验证token失败: {e}')
return jsonify({'success': False, 'message': '验证失败'}), 500
@app.route('/api/auth/forgot-password', methods=['POST'])
def forgot_password():
"""忘记密码"""
try:
data = request.get_json()
email = data.get('email')
if email:
return jsonify({
'success': True,
'message': '重置密码邮件已发送'
})
else:
return jsonify({
'success': False,
'message': '邮箱不能为空'
}), 400
except Exception as e:
logger.error(f'忘记密码处理失败: {e}')
return jsonify({'success': False, 'message': '处理失败'}), 500
@app.route('/api/auth/reset-password', methods=['POST'])
def reset_password():
"""重置密码"""
try:
data = request.get_json()
token = data.get('token')
password = data.get('password')
if token and password:
return jsonify({
'success': True,
'message': '密码重置成功'
})
else:
return jsonify({
'success': False,
'message': '参数不完整'
}), 400
except Exception as e:
logger.error(f'重置密码失败: {e}')
return jsonify({'success': False, 'message': '重置失败'}), 500
@app.route('/api/system/info', methods=['GET'])
def get_system_info():
"""获取系统信息"""
try:
info = {
'version': '1.0.0',
'start_time': datetime.now().isoformat(),
'database_status': 'connected' if db_manager else 'disconnected',
'device_count': len(device_manager.get_connected_devices()) if device_manager else 0
}
return jsonify({'success': True, 'data': info})
except Exception as e:
logger.error(f'获取系统信息失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 设备管理API ====================
@app.route('/api/devices/status', methods=['GET'])
def get_device_status():
"""获取设备状态"""
try:
if not device_manager:
return jsonify({'camera': False, 'imu': False, 'pressure': False})
status = device_manager.get_device_status()
return jsonify(status)
except Exception as e:
logger.error(f'获取设备状态失败: {e}')
return jsonify({'camera': False, 'imu': False, 'pressure': False})
@app.route('/api/devices/refresh', methods=['POST'])
def refresh_devices():
"""刷新设备连接"""
try:
if device_manager:
device_manager.refresh_devices()
return jsonify({'success': True, 'message': '设备已刷新'})
except Exception as e:
logger.error(f'刷新设备失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/devices/calibrate', methods=['POST'])
def calibrate_devices():
"""校准设备"""
try:
if device_manager:
result = device_manager.calibrate_devices()
return jsonify({'success': True, 'data': result})
return jsonify({'success': False, 'error': '设备管理器未初始化'}), 500
except Exception as e:
logger.error(f'设备校准失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 患者管理API ====================
@app.route('/api/patients', methods=['GET'])
def get_patients():
"""获取患者列表"""
try:
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
keyword = request.args.get('keyword', '')
patients = db_manager.get_patients(page, size, keyword)
total = db_manager.get_patients_count(keyword)
return jsonify({
'success': True,
'data': {
'patients': patients,
'total': total,
'page': page,
'size': size
}
})
except Exception as e:
logger.error(f'获取患者列表失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/patients', methods=['POST'])
def create_patient():
"""创建患者"""
try:
data = request.get_json()
patient_id = db_manager.create_patient(data)
return jsonify({
'success': True,
'data': {'patient_id': patient_id},
'message': '患者创建成功'
})
except Exception as e:
logger.error(f'创建患者失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/patients/<patient_id>', methods=['PUT'])
def update_patient(patient_id):
"""更新患者信息"""
try:
data = request.get_json()
db_manager.update_patient(patient_id, data)
return jsonify({'success': True, 'message': '患者信息更新成功'})
except Exception as e:
logger.error(f'更新患者信息失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/patients/<patient_id>', methods=['DELETE'])
def delete_patient(patient_id):
"""删除患者"""
try:
db_manager.delete_patient(patient_id)
return jsonify({'success': True, 'message': '患者删除成功'})
except Exception as e:
logger.error(f'删除患者失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 检测管理API ====================
@app.route('/api/detection/start', methods=['POST'])
def start_detection():
"""开始检测"""
global current_detection, detection_thread
try:
if current_detection and current_detection.get('status') == 'running':
return jsonify({'success': False, 'error': '检测已在进行中'}), 400
data = request.get_json()
patient_id = data.get('patientId')
settings = data.get('settings', {})
# 创建检测会话
session_id = db_manager.create_detection_session(patient_id, settings)
# 初始化检测状态
current_detection = {
'session_id': session_id,
'patient_id': patient_id,
'status': 'running',
'start_time': datetime.now(),
'settings': settings,
'data_points': 0
}
# 启动检测线程
detection_thread = threading.Thread(
target=run_detection,
args=(session_id, settings)
)
detection_thread.start()
return jsonify({
'success': True,
'data': {'session_id': session_id},
'message': '检测已开始'
})
except Exception as e:
logger.error(f'开始检测失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/detection/stop', methods=['POST'])
def stop_detection():
"""停止检测"""
global current_detection
try:
if not current_detection or current_detection.get('status') != 'running':
return jsonify({'success': False, 'error': '没有正在进行的检测'}), 400
# 更新检测状态
current_detection['status'] = 'stopped'
current_detection['end_time'] = datetime.now()
# 等待检测线程结束
if detection_thread and detection_thread.is_alive():
detection_thread.join(timeout=5)
return jsonify({'success': True, 'message': '检测已停止'})
except Exception as e:
logger.error(f'停止检测失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/detection/status', methods=['GET'])
def get_detection_status():
"""获取检测状态"""
try:
if not current_detection:
return jsonify({
'success': True,
'data': {'status': 'idle'}
})
# 计算运行时间
if current_detection.get('status') == 'running':
elapsed = (datetime.now() - current_detection['start_time']).total_seconds()
current_detection['elapsed_time'] = int(elapsed)
return jsonify({
'success': True,
'data': current_detection
})
except Exception as e:
logger.error(f'获取检测状态失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/detection/data', methods=['GET'])
def get_realtime_data():
"""获取实时检测数据"""
try:
if not current_detection or current_detection.get('status') != 'running':
return jsonify({'success': False, 'error': '没有正在进行的检测'})
# 获取最新的检测数据
session_id = current_detection['session_id']
data = detection_engine.get_latest_data(session_id)
return jsonify({
'success': True,
'data': data
})
except Exception as e:
logger.error(f'获取实时数据失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 数据分析API ====================
@app.route('/api/analysis/session/<session_id>', methods=['GET'])
def analyze_session(session_id):
"""分析检测会话数据"""
try:
# 获取会话数据
session_data = db_manager.get_session_data(session_id)
if not session_data:
return jsonify({'success': False, 'error': '会话不存在'}), 404
# 进行数据分析
analysis_result = data_processor.analyze_session(session_data)
# 保存分析结果
db_manager.save_analysis_result(session_id, analysis_result)
return jsonify({
'success': True,
'data': analysis_result
})
except Exception as e:
logger.error(f'分析会话数据失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/export/report/<session_id>', methods=['GET'])
def export_report(session_id):
"""导出检测报告"""
try:
# 生成报告
report_path = data_processor.generate_report(session_id)
if not os.path.exists(report_path):
return jsonify({'success': False, 'error': '报告生成失败'}), 500
return send_file(
report_path,
as_attachment=True,
download_name=f'detection_report_{session_id}.pdf'
)
except Exception as e:
logger.error(f'导出报告失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 历史记录API ====================
@app.route('/api/history/sessions', methods=['GET'])
def get_detection_sessions():
"""获取检测会话历史"""
try:
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
patient_id = request.args.get('patient_id')
sessions = db_manager.get_detection_sessions(page, size, patient_id)
total = db_manager.get_sessions_count(patient_id)
return jsonify({
'success': True,
'data': {
'sessions': sessions,
'total': total,
'page': page,
'size': size
}
})
except Exception as e:
logger.error(f'获取检测历史失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
def run_detection(session_id, settings):
"""运行检测的后台线程"""
global current_detection
try:
logger.info(f'开始检测会话: {session_id}')
# 检测循环
while (current_detection and
current_detection.get('status') == 'running'):
# 采集数据
if device_manager:
data = device_manager.collect_data()
if data:
# 保存数据到数据库
db_manager.save_detection_data(session_id, data)
current_detection['data_points'] += 1
# 根据采样频率控制循环间隔
frequency = settings.get('frequency', 60)
time.sleep(1.0 / frequency)
# 检查是否达到设定时长
duration = settings.get('duration', 0)
if duration > 0:
elapsed = (datetime.now() - current_detection['start_time']).total_seconds()
if elapsed >= duration:
current_detection['status'] = 'completed'
break
# 更新会话状态
if current_detection:
db_manager.update_session_status(
session_id,
current_detection['status'],
current_detection.get('data_points', 0)
)
logger.info(f'检测会话完成: {session_id}')
except Exception as e:
logger.error(f'检测线程异常: {e}')
if current_detection:
current_detection['status'] = 'error'
current_detection['error'] = str(e)
# ==================== 错误处理 ====================
@app.errorhandler(404)
def not_found(error):
return jsonify({'success': False, 'error': 'API接口不存在'}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({'success': False, 'error': '服务器内部错误'}), 500
if __name__ == '__main__':
try:
# 初始化应用
init_app()
# 启动Flask服务
logger.info('启动后端服务...')
app.run(
host='127.0.0.1',
port=5000,
debug=False,
threaded=True
)
except KeyboardInterrupt:
logger.info('服务被用户中断')
except Exception as e:
logger.error(f'服务启动失败: {e}')
sys.exit(1)
finally:
logger.info('后端服务已停止')

41
backend/config.ini Normal file
View File

@ -0,0 +1,41 @@
[APP]
name = Body Balance Evaluation System
version = 1.0.0
debug = false
log_level = INFO
[SERVER]
host = 127.0.0.1
port = 5000
cors_origins = *
[DATABASE]
path = data/balance_system.db
backup_interval = 24
max_backups = 7
[DEVICES]
camera_index = 0
camera_width = 640
camera_height = 480
camera_fps = 30
imu_port = COM3
pressure_port = COM4
[DETECTION]
default_duration = 60
sampling_rate = 30
balance_threshold = 0.2
posture_threshold = 5.0
[DATA_PROCESSING]
filter_window = 5
outlier_threshold = 2.0
chart_dpi = 300
export_format = csv
[SECURITY]
secret_key = 026efbf83a2fe101f168780740da86bf1c9260625458e6782738aa9cf18f8e37
session_timeout = 3600
max_login_attempts = 5

730
backend/data_processor.py Normal file
View File

@ -0,0 +1,730 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据处理模块
负责数据分析报告生成和数据导出
"""
import os
import json
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Tuple
import logging
from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.backends.backend_pdf import PdfPages
import seaborn as sns
from reportlab.lib.pagesizes import letter, A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
from io import BytesIO
logger = logging.getLogger(__name__)
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
class DataProcessor:
"""数据处理器"""
def __init__(self):
self.export_dir = Path('exports')
self.charts_dir = Path('charts')
# 创建必要的目录
self.export_dir.mkdir(exist_ok=True)
self.charts_dir.mkdir(exist_ok=True)
logger.info('数据处理器初始化完成')
def analyze_session(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
"""分析会话数据"""
try:
if not session_data or 'data' not in session_data:
return {'error': '没有可分析的数据'}
data_points = session_data['data']
if not data_points:
return {'error': '数据为空'}
# 数据预处理
processed_data = self._preprocess_session_data(data_points)
# 统计分析
statistical_analysis = self._statistical_analysis(processed_data)
# 趋势分析
trend_analysis = self._trend_analysis(processed_data)
# 异常检测
anomaly_analysis = self._anomaly_detection(processed_data)
# 生成图表
charts = self._generate_charts(processed_data, session_data['id'])
analysis_result = {
'session_info': {
'session_id': session_data['id'],
'patient_name': session_data.get('patient_name', '未知'),
'start_time': session_data.get('start_time'),
'end_time': session_data.get('end_time'),
'duration': session_data.get('duration'),
'data_points': len(data_points)
},
'statistical': statistical_analysis,
'trends': trend_analysis,
'anomalies': anomaly_analysis,
'charts': charts,
'summary': self._generate_summary(statistical_analysis, trend_analysis, anomaly_analysis)
}
return analysis_result
except Exception as e:
logger.error(f'会话数据分析失败: {e}')
return {'error': str(e)}
def _preprocess_session_data(self, data_points: List[Dict]) -> Dict[str, List]:
"""预处理会话数据"""
processed = {
'timestamps': [],
'pressure_left': [],
'pressure_right': [],
'pressure_total': [],
'balance_index': [],
'center_of_pressure_x': [],
'center_of_pressure_y': [],
'imu_pitch': [],
'imu_roll': [],
'imu_accel_total': []
}
for point in data_points:
try:
# 解析时间戳
timestamp = datetime.fromisoformat(point['timestamp'].replace('Z', '+00:00'))
processed['timestamps'].append(timestamp)
# 解析数据值
data_value = point['data_value']
if point['data_type'] == 'pressure':
processed['pressure_left'].append(data_value.get('left_foot', 0))
processed['pressure_right'].append(data_value.get('right_foot', 0))
processed['pressure_total'].append(data_value.get('total_pressure', 0))
processed['balance_index'].append(data_value.get('balance_index', 0))
cop = data_value.get('center_of_pressure', {'x': 0, 'y': 0})
processed['center_of_pressure_x'].append(cop.get('x', 0))
processed['center_of_pressure_y'].append(cop.get('y', 0))
elif point['data_type'] == 'imu':
processed['imu_pitch'].append(data_value.get('pitch', 0))
processed['imu_roll'].append(data_value.get('roll', 0))
processed['imu_accel_total'].append(data_value.get('total_accel', 0))
except Exception as e:
logger.warning(f'数据点处理失败: {e}')
continue
return processed
def _statistical_analysis(self, data: Dict[str, List]) -> Dict[str, Any]:
"""统计分析"""
try:
stats = {}
# 压力数据统计
if data['pressure_total']:
pressure_total = np.array(data['pressure_total'])
stats['pressure'] = {
'mean': float(np.mean(pressure_total)),
'std': float(np.std(pressure_total)),
'min': float(np.min(pressure_total)),
'max': float(np.max(pressure_total)),
'median': float(np.median(pressure_total))
}
# 左右脚压力分析
if data['pressure_left'] and data['pressure_right']:
left_pressure = np.array(data['pressure_left'])
right_pressure = np.array(data['pressure_right'])
stats['pressure_distribution'] = {
'left_mean': float(np.mean(left_pressure)),
'right_mean': float(np.mean(right_pressure)),
'left_ratio': float(np.mean(left_pressure) / np.mean(pressure_total)) if np.mean(pressure_total) > 0 else 0,
'right_ratio': float(np.mean(right_pressure) / np.mean(pressure_total)) if np.mean(pressure_total) > 0 else 0,
'asymmetry': float(abs(np.mean(left_pressure) - np.mean(right_pressure)) / np.mean(pressure_total)) if np.mean(pressure_total) > 0 else 0
}
# 平衡指数统计
if data['balance_index']:
balance_index = np.array(data['balance_index'])
stats['balance'] = {
'mean': float(np.mean(balance_index)),
'std': float(np.std(balance_index)),
'min': float(np.min(balance_index)),
'max': float(np.max(balance_index)),
'stability_score': float(1.0 - np.std(balance_index)) # 稳定性评分
}
# 重心位置统计
if data['center_of_pressure_x'] and data['center_of_pressure_y']:
cop_x = np.array(data['center_of_pressure_x'])
cop_y = np.array(data['center_of_pressure_y'])
stats['center_of_pressure'] = {
'mean_x': float(np.mean(cop_x)),
'mean_y': float(np.mean(cop_y)),
'std_x': float(np.std(cop_x)),
'std_y': float(np.std(cop_y)),
'range_x': float(np.max(cop_x) - np.min(cop_x)),
'range_y': float(np.max(cop_y) - np.min(cop_y)),
'total_displacement': float(np.sqrt(np.std(cop_x)**2 + np.std(cop_y)**2))
}
# IMU数据统计
if data['imu_pitch'] and data['imu_roll']:
pitch = np.array(data['imu_pitch'])
roll = np.array(data['imu_roll'])
stats['posture'] = {
'mean_pitch': float(np.mean(pitch)),
'mean_roll': float(np.mean(roll)),
'std_pitch': float(np.std(pitch)),
'std_roll': float(np.std(roll)),
'max_pitch': float(np.max(np.abs(pitch))),
'max_roll': float(np.max(np.abs(roll))),
'posture_stability': float(1.0 - (np.std(pitch) + np.std(roll)) / 20) # 姿态稳定性
}
return stats
except Exception as e:
logger.error(f'统计分析失败: {e}')
return {}
def _trend_analysis(self, data: Dict[str, List]) -> Dict[str, Any]:
"""趋势分析"""
try:
trends = {}
# 平衡指数趋势
if data['balance_index'] and len(data['balance_index']) > 10:
balance_trend = self._calculate_trend(data['balance_index'])
trends['balance_trend'] = {
'slope': balance_trend['slope'],
'direction': 'improving' if balance_trend['slope'] < 0 else 'deteriorating' if balance_trend['slope'] > 0 else 'stable',
'correlation': balance_trend['correlation']
}
# 压力分布趋势
if data['pressure_left'] and data['pressure_right'] and len(data['pressure_left']) > 10:
left_trend = self._calculate_trend(data['pressure_left'])
right_trend = self._calculate_trend(data['pressure_right'])
trends['pressure_trend'] = {
'left_slope': left_trend['slope'],
'right_slope': right_trend['slope'],
'asymmetry_trend': 'increasing' if abs(left_trend['slope'] - right_trend['slope']) > 0.1 else 'stable'
}
# 姿态趋势
if data['imu_pitch'] and data['imu_roll'] and len(data['imu_pitch']) > 10:
pitch_trend = self._calculate_trend(data['imu_pitch'])
roll_trend = self._calculate_trend(data['imu_roll'])
trends['posture_trend'] = {
'pitch_slope': pitch_trend['slope'],
'roll_slope': roll_trend['slope'],
'stability_trend': 'improving' if abs(pitch_trend['slope']) + abs(roll_trend['slope']) < 0.1 else 'deteriorating'
}
return trends
except Exception as e:
logger.error(f'趋势分析失败: {e}')
return {}
def _calculate_trend(self, values: List[float]) -> Dict[str, float]:
"""计算趋势"""
try:
x = np.arange(len(values))
y = np.array(values)
# 线性回归
slope, intercept = np.polyfit(x, y, 1)
correlation = np.corrcoef(x, y)[0, 1]
return {
'slope': float(slope),
'intercept': float(intercept),
'correlation': float(correlation)
}
except Exception as e:
logger.error(f'趋势计算失败: {e}')
return {'slope': 0, 'intercept': 0, 'correlation': 0}
def _anomaly_detection(self, data: Dict[str, List]) -> Dict[str, Any]:
"""异常检测"""
try:
anomalies = {}
# 检测平衡指数异常
if data['balance_index']:
balance_anomalies = self._detect_outliers(data['balance_index'])
anomalies['balance_anomalies'] = {
'count': len(balance_anomalies),
'percentage': len(balance_anomalies) / len(data['balance_index']) * 100,
'indices': balance_anomalies
}
# 检测压力异常
if data['pressure_total']:
pressure_anomalies = self._detect_outliers(data['pressure_total'])
anomalies['pressure_anomalies'] = {
'count': len(pressure_anomalies),
'percentage': len(pressure_anomalies) / len(data['pressure_total']) * 100,
'indices': pressure_anomalies
}
# 检测姿态异常
if data['imu_pitch'] and data['imu_roll']:
pitch_anomalies = self._detect_outliers(data['imu_pitch'])
roll_anomalies = self._detect_outliers(data['imu_roll'])
anomalies['posture_anomalies'] = {
'pitch_count': len(pitch_anomalies),
'roll_count': len(roll_anomalies),
'total_percentage': (len(pitch_anomalies) + len(roll_anomalies)) / (len(data['imu_pitch']) + len(data['imu_roll'])) * 100
}
return anomalies
except Exception as e:
logger.error(f'异常检测失败: {e}')
return {}
def _detect_outliers(self, values: List[float], threshold: float = 2.0) -> List[int]:
"""检测异常值"""
try:
data = np.array(values)
mean = np.mean(data)
std = np.std(data)
# 使用Z-score方法检测异常值
z_scores = np.abs((data - mean) / std)
outlier_indices = np.where(z_scores > threshold)[0].tolist()
return outlier_indices
except Exception as e:
logger.error(f'异常值检测失败: {e}')
return []
def _generate_charts(self, data: Dict[str, List], session_id: str) -> Dict[str, str]:
"""生成图表"""
try:
charts = {}
# 创建会话专用目录
session_charts_dir = self.charts_dir / session_id
session_charts_dir.mkdir(exist_ok=True)
# 生成平衡指数时间序列图
if data['timestamps'] and data['balance_index']:
chart_path = self._create_balance_chart(data, session_charts_dir)
if chart_path:
charts['balance_chart'] = str(chart_path)
# 生成压力分布图
if data['timestamps'] and data['pressure_left'] and data['pressure_right']:
chart_path = self._create_pressure_chart(data, session_charts_dir)
if chart_path:
charts['pressure_chart'] = str(chart_path)
# 生成重心轨迹图
if data['center_of_pressure_x'] and data['center_of_pressure_y']:
chart_path = self._create_cop_trajectory_chart(data, session_charts_dir)
if chart_path:
charts['cop_trajectory_chart'] = str(chart_path)
# 生成姿态角度图
if data['timestamps'] and data['imu_pitch'] and data['imu_roll']:
chart_path = self._create_posture_chart(data, session_charts_dir)
if chart_path:
charts['posture_chart'] = str(chart_path)
return charts
except Exception as e:
logger.error(f'图表生成失败: {e}')
return {}
def _create_balance_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]:
"""创建平衡指数图表"""
try:
plt.figure(figsize=(12, 6))
timestamps = data['timestamps'][:len(data['balance_index'])]
plt.plot(timestamps, data['balance_index'], 'b-', linewidth=1.5, label='平衡指数')
# 添加平均线
mean_balance = np.mean(data['balance_index'])
plt.axhline(y=mean_balance, color='r', linestyle='--', alpha=0.7, label=f'平均值: {mean_balance:.3f}')
plt.title('平衡指数时间序列', fontsize=14, fontweight='bold')
plt.xlabel('时间', fontsize=12)
plt.ylabel('平衡指数', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
# 格式化时间轴
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
plt.xticks(rotation=45)
plt.tight_layout()
chart_path = output_dir / 'balance_chart.png'
plt.savefig(chart_path, dpi=300, bbox_inches='tight')
plt.close()
return chart_path
except Exception as e:
logger.error(f'平衡图表创建失败: {e}')
plt.close()
return None
def _create_pressure_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]:
"""创建压力分布图表"""
try:
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
timestamps = data['timestamps'][:min(len(data['pressure_left']), len(data['pressure_right']))]
pressure_left = data['pressure_left'][:len(timestamps)]
pressure_right = data['pressure_right'][:len(timestamps)]
# 上图:左右脚压力对比
ax1.plot(timestamps, pressure_left, 'b-', linewidth=1.5, label='左脚压力')
ax1.plot(timestamps, pressure_right, 'r-', linewidth=1.5, label='右脚压力')
ax1.set_title('左右脚压力对比', fontsize=14, fontweight='bold')
ax1.set_ylabel('压力值', fontsize=12)
ax1.legend()
ax1.grid(True, alpha=0.3)
# 下图:压力比例
total_pressure = np.array(pressure_left) + np.array(pressure_right)
left_ratio = np.array(pressure_left) / total_pressure * 100
right_ratio = np.array(pressure_right) / total_pressure * 100
ax2.plot(timestamps, left_ratio, 'b-', linewidth=1.5, label='左脚比例')
ax2.plot(timestamps, right_ratio, 'r-', linewidth=1.5, label='右脚比例')
ax2.axhline(y=50, color='g', linestyle='--', alpha=0.7, label='理想平衡线')
ax2.set_title('左右脚压力比例', fontsize=14, fontweight='bold')
ax2.set_xlabel('时间', fontsize=12)
ax2.set_ylabel('压力比例 (%)', fontsize=12)
ax2.legend()
ax2.grid(True, alpha=0.3)
# 格式化时间轴
for ax in [ax1, ax2]:
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
plt.tight_layout()
chart_path = output_dir / 'pressure_chart.png'
plt.savefig(chart_path, dpi=300, bbox_inches='tight')
plt.close()
return chart_path
except Exception as e:
logger.error(f'压力图表创建失败: {e}')
plt.close()
return None
def _create_cop_trajectory_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]:
"""创建重心轨迹图表"""
try:
plt.figure(figsize=(10, 8))
x_positions = data['center_of_pressure_x']
y_positions = data['center_of_pressure_y']
# 绘制轨迹
plt.plot(x_positions, y_positions, 'b-', linewidth=1, alpha=0.7, label='重心轨迹')
plt.scatter(x_positions[0], y_positions[0], color='g', s=100, marker='o', label='起始点')
plt.scatter(x_positions[-1], y_positions[-1], color='r', s=100, marker='s', label='结束点')
# 添加中心点
center_x = np.mean(x_positions)
center_y = np.mean(y_positions)
plt.scatter(center_x, center_y, color='orange', s=150, marker='*', label='平均中心')
# 添加置信椭圆
self._add_confidence_ellipse(x_positions, y_positions, plt.gca())
plt.title('重心轨迹图', fontsize=14, fontweight='bold')
plt.xlabel('X方向位移 (mm)', fontsize=12)
plt.ylabel('Y方向位移 (mm)', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
plt.axis('equal')
plt.tight_layout()
chart_path = output_dir / 'cop_trajectory_chart.png'
plt.savefig(chart_path, dpi=300, bbox_inches='tight')
plt.close()
return chart_path
except Exception as e:
logger.error(f'重心轨迹图表创建失败: {e}')
plt.close()
return None
def _add_confidence_ellipse(self, x: List[float], y: List[float], ax, n_std: float = 2.0):
"""添加置信椭圆"""
try:
from matplotlib.patches import Ellipse
import matplotlib.transforms as transforms
x_arr = np.array(x)
y_arr = np.array(y)
cov = np.cov(x_arr, y_arr)
pearson = cov[0, 1] / np.sqrt(cov[0, 0] * cov[1, 1])
ell_radius_x = np.sqrt(1 + pearson)
ell_radius_y = np.sqrt(1 - pearson)
ellipse = Ellipse((0, 0), width=ell_radius_x * 2, height=ell_radius_y * 2,
facecolor='none', edgecolor='red', alpha=0.5, linestyle='--')
scale_x = np.sqrt(cov[0, 0]) * n_std
scale_y = np.sqrt(cov[1, 1]) * n_std
mean_x = np.mean(x_arr)
mean_y = np.mean(y_arr)
transf = transforms.Affine2D().scale(scale_x, scale_y).translate(mean_x, mean_y)
ellipse.set_transform(transf + ax.transData)
ax.add_patch(ellipse)
except Exception as e:
logger.warning(f'置信椭圆添加失败: {e}')
def _create_posture_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]:
"""创建姿态角度图表"""
try:
plt.figure(figsize=(12, 8))
timestamps = data['timestamps'][:min(len(data['imu_pitch']), len(data['imu_roll']))]
pitch = data['imu_pitch'][:len(timestamps)]
roll = data['imu_roll'][:len(timestamps)]
plt.subplot(2, 1, 1)
plt.plot(timestamps, pitch, 'b-', linewidth=1.5, label='俯仰角')
plt.axhline(y=0, color='g', linestyle='--', alpha=0.7, label='理想位置')
plt.title('俯仰角变化', fontsize=14, fontweight='bold')
plt.ylabel('角度 (度)', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
plt.subplot(2, 1, 2)
plt.plot(timestamps, roll, 'r-', linewidth=1.5, label='翻滚角')
plt.axhline(y=0, color='g', linestyle='--', alpha=0.7, label='理想位置')
plt.title('翻滚角变化', fontsize=14, fontweight='bold')
plt.xlabel('时间', fontsize=12)
plt.ylabel('角度 (度)', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
# 格式化时间轴
for i in range(1, 3):
ax = plt.subplot(2, 1, i)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
plt.tight_layout()
chart_path = output_dir / 'posture_chart.png'
plt.savefig(chart_path, dpi=300, bbox_inches='tight')
plt.close()
return chart_path
except Exception as e:
logger.error(f'姿态图表创建失败: {e}')
plt.close()
return None
def _generate_summary(self, statistical: Dict, trends: Dict, anomalies: Dict) -> Dict[str, Any]:
"""生成分析摘要"""
try:
summary = {
'overall_assessment': 'normal',
'key_findings': [],
'recommendations': []
}
# 分析平衡状况
if 'balance' in statistical:
balance_mean = statistical['balance']['mean']
if balance_mean > 0.3:
summary['key_findings'].append('平衡能力较差,重心摆动较大')
summary['recommendations'].append('建议进行平衡训练,加强核心肌群锻炼')
summary['overall_assessment'] = 'poor'
elif balance_mean > 0.15:
summary['key_findings'].append('平衡能力一般,有改善空间')
summary['recommendations'].append('建议适当增加平衡练习')
if summary['overall_assessment'] == 'normal':
summary['overall_assessment'] = 'fair'
else:
summary['key_findings'].append('平衡能力良好')
# 分析压力分布
if 'pressure_distribution' in statistical:
asymmetry = statistical['pressure_distribution']['asymmetry']
if asymmetry > 0.2:
summary['key_findings'].append('左右脚压力分布不均,存在明显偏重')
summary['recommendations'].append('注意纠正站立姿势,均匀分配体重')
if summary['overall_assessment'] in ['normal', 'fair']:
summary['overall_assessment'] = 'fair'
# 分析姿态稳定性
if 'posture' in statistical:
posture_stability = statistical['posture'].get('posture_stability', 1.0)
if posture_stability < 0.7:
summary['key_findings'].append('姿态稳定性较差,身体摆动较大')
summary['recommendations'].append('建议进行姿态矫正训练')
if summary['overall_assessment'] == 'normal':
summary['overall_assessment'] = 'fair'
# 分析异常情况
if anomalies:
total_anomalies = 0
for key, value in anomalies.items():
if isinstance(value, dict) and 'count' in value:
total_anomalies += value['count']
if total_anomalies > 10:
summary['key_findings'].append(f'检测到{total_anomalies}个异常数据点')
summary['recommendations'].append('建议重新进行检测或咨询专业医师')
# 默认建议
if not summary['recommendations']:
summary['recommendations'].append('继续保持良好的平衡训练习惯')
return summary
except Exception as e:
logger.error(f'摘要生成失败: {e}')
return {
'overall_assessment': 'unknown',
'key_findings': ['分析过程中出现错误'],
'recommendations': ['建议重新进行检测']
}
def generate_report(self, session_id: str) -> str:
"""生成PDF报告"""
try:
# 这里应该从数据库获取会话数据和分析结果
# 目前返回一个模拟的报告路径
report_path = self.export_dir / f'report_{session_id}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
# 创建简单的PDF报告
self._create_simple_pdf_report(session_id, report_path)
return str(report_path)
except Exception as e:
logger.error(f'报告生成失败: {e}')
raise
def _create_simple_pdf_report(self, session_id: str, output_path: Path):
"""创建简单的PDF报告"""
try:
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
c = canvas.Canvas(str(output_path), pagesize=A4)
width, height = A4
# 标题
c.setFont("Helvetica-Bold", 20)
c.drawString(50, height - 50, "Balance Detection Report")
# 会话信息
c.setFont("Helvetica", 12)
y_position = height - 100
c.drawString(50, y_position, f"Session ID: {session_id}")
y_position -= 20
c.drawString(50, y_position, f"Report Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
y_position -= 40
# 报告内容
c.drawString(50, y_position, "Analysis Summary:")
y_position -= 20
c.drawString(70, y_position, "• Balance assessment completed")
y_position -= 20
c.drawString(70, y_position, "• Posture analysis performed")
y_position -= 20
c.drawString(70, y_position, "• Movement patterns evaluated")
c.save()
logger.info(f'PDF报告已生成: {output_path}')
except Exception as e:
logger.error(f'PDF报告创建失败: {e}')
raise
def export_data(self, session_id: str, format: str = 'csv') -> str:
"""导出数据"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if format.lower() == 'csv':
export_path = self.export_dir / f'data_{session_id}_{timestamp}.csv'
# 这里应该从数据库获取数据并导出为CSV
# 目前创建一个示例文件
with open(export_path, 'w', encoding='utf-8') as f:
f.write('timestamp,data_type,value\n')
f.write(f'{datetime.now().isoformat()},sample,0.5\n')
elif format.lower() == 'json':
export_path = self.export_dir / f'data_{session_id}_{timestamp}.json'
# 导出为JSON格式
sample_data = {
'session_id': session_id,
'export_time': datetime.now().isoformat(),
'data': []
}
with open(export_path, 'w', encoding='utf-8') as f:
json.dump(sample_data, f, ensure_ascii=False, indent=2)
else:
raise ValueError(f'不支持的导出格式: {format}')
logger.info(f'数据已导出: {export_path}')
return str(export_path)
except Exception as e:
logger.error(f'数据导出失败: {e}')
raise

618
backend/database.py Normal file
View File

@ -0,0 +1,618 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据库管理模块
负责SQLite数据库的创建连接和数据操作
"""
import sqlite3
import json
import uuid
from datetime import datetime
from typing import List, Dict, Optional, Any
import logging
logger = logging.getLogger(__name__)
class DatabaseManager:
"""数据库管理器"""
def __init__(self, db_path: str):
self.db_path = db_path
self.connection = None
def get_connection(self) -> sqlite3.Connection:
"""获取数据库连接"""
if not self.connection:
self.connection = sqlite3.connect(
self.db_path,
check_same_thread=False,
timeout=30.0
)
self.connection.row_factory = sqlite3.Row
return self.connection
def init_database(self):
"""初始化数据库表结构"""
conn = self.get_connection()
cursor = conn.cursor()
try:
# 创建患者表
cursor.execute('''
CREATE TABLE IF NOT EXISTS patients (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
gender TEXT,
age INTEGER,
height REAL,
weight REAL,
phone TEXT,
medical_history TEXT,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建检测会话表
cursor.execute('''
CREATE TABLE IF NOT EXISTS detection_sessions (
id TEXT PRIMARY KEY,
patient_id TEXT NOT NULL,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP,
duration INTEGER,
frequency INTEGER,
status TEXT DEFAULT 'created',
settings TEXT,
data_points INTEGER DEFAULT 0,
video_path TEXT,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients (id)
)
''')
# 创建检测数据表
cursor.execute('''
CREATE TABLE IF NOT EXISTS detection_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
data_type TEXT NOT NULL,
data_value TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES detection_sessions (id)
)
''')
# 创建分析结果表
cursor.execute('''
CREATE TABLE IF NOT EXISTS analysis_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
analysis_type TEXT NOT NULL,
result_data TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES detection_sessions (id)
)
''')
# 创建系统设置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_patients_name ON patients (name)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_patient ON detection_sessions (patient_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_time ON detection_sessions (start_time)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_session ON detection_data (session_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_timestamp ON detection_data (timestamp)')
conn.commit()
logger.info('数据库初始化完成')
except Exception as e:
conn.rollback()
logger.error(f'数据库初始化失败: {e}')
raise
# ==================== 患者管理 ====================
def create_patient(self, patient_data: Dict[str, Any]) -> str:
"""创建患者记录"""
conn = self.get_connection()
cursor = conn.cursor()
try:
patient_id = str(uuid.uuid4())
cursor.execute('''
INSERT INTO patients (
id, name, gender, age, height, weight,
phone, medical_history, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
patient_id,
patient_data.get('name'),
patient_data.get('gender'),
patient_data.get('age'),
patient_data.get('height'),
patient_data.get('weight'),
patient_data.get('phone'),
patient_data.get('medical_history'),
patient_data.get('notes')
))
conn.commit()
logger.info(f'创建患者记录: {patient_id}')
return patient_id
except Exception as e:
conn.rollback()
logger.error(f'创建患者记录失败: {e}')
raise
def get_patients(self, page: int = 1, size: int = 10, keyword: str = '') -> List[Dict]:
"""获取患者列表"""
conn = self.get_connection()
cursor = conn.cursor()
try:
offset = (page - 1) * size
if keyword:
cursor.execute('''
SELECT * FROM patients
WHERE name LIKE ? OR phone LIKE ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (f'%{keyword}%', f'%{keyword}%', size, offset))
else:
cursor.execute('''
SELECT * FROM patients
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (size, offset))
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f'获取患者列表失败: {e}')
raise
def get_patients_count(self, keyword: str = '') -> int:
"""获取患者总数"""
conn = self.get_connection()
cursor = conn.cursor()
try:
if keyword:
cursor.execute('''
SELECT COUNT(*) FROM patients
WHERE name LIKE ? OR phone LIKE ?
''', (f'%{keyword}%', f'%{keyword}%'))
else:
cursor.execute('SELECT COUNT(*) FROM patients')
return cursor.fetchone()[0]
except Exception as e:
logger.error(f'获取患者总数失败: {e}')
return 0
def get_patient(self, patient_id: str) -> Optional[Dict]:
"""获取单个患者信息"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('SELECT * FROM patients WHERE id = ?', (patient_id,))
row = cursor.fetchone()
return dict(row) if row else None
except Exception as e:
logger.error(f'获取患者信息失败: {e}')
return None
def update_patient(self, patient_id: str, patient_data: Dict[str, Any]):
"""更新患者信息"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('''
UPDATE patients SET
name = ?, gender = ?, age = ?, height = ?, weight = ?,
phone = ?, medical_history = ?, notes = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (
patient_data.get('name'),
patient_data.get('gender'),
patient_data.get('age'),
patient_data.get('height'),
patient_data.get('weight'),
patient_data.get('phone'),
patient_data.get('medical_history'),
patient_data.get('notes'),
patient_id
))
conn.commit()
logger.info(f'更新患者信息: {patient_id}')
except Exception as e:
conn.rollback()
logger.error(f'更新患者信息失败: {e}')
raise
def delete_patient(self, patient_id: str):
"""删除患者记录"""
conn = self.get_connection()
cursor = conn.cursor()
try:
# 删除相关的检测数据
cursor.execute('''
DELETE FROM detection_data
WHERE session_id IN (
SELECT id FROM detection_sessions WHERE patient_id = ?
)
''', (patient_id,))
# 删除相关的分析结果
cursor.execute('''
DELETE FROM analysis_results
WHERE session_id IN (
SELECT id FROM detection_sessions WHERE patient_id = ?
)
''', (patient_id,))
# 删除检测会话
cursor.execute('DELETE FROM detection_sessions WHERE patient_id = ?', (patient_id,))
# 删除患者记录
cursor.execute('DELETE FROM patients WHERE id = ?', (patient_id,))
conn.commit()
logger.info(f'删除患者记录: {patient_id}')
except Exception as e:
conn.rollback()
logger.error(f'删除患者记录失败: {e}')
raise
# ==================== 检测会话管理 ====================
def create_detection_session(self, patient_id: str, settings: Dict[str, Any]) -> str:
"""创建检测会话"""
conn = self.get_connection()
cursor = conn.cursor()
try:
session_id = str(uuid.uuid4())
cursor.execute('''
INSERT INTO detection_sessions (
id, patient_id, duration, frequency, settings, status
) VALUES (?, ?, ?, ?, ?, ?)
''', (
session_id,
patient_id,
settings.get('duration', 60),
settings.get('frequency', 60),
json.dumps(settings),
'created'
))
conn.commit()
logger.info(f'创建检测会话: {session_id}')
return session_id
except Exception as e:
conn.rollback()
logger.error(f'创建检测会话失败: {e}')
raise
def update_session_status(self, session_id: str, status: str, data_points: int = 0):
"""更新会话状态"""
conn = self.get_connection()
cursor = conn.cursor()
try:
if status in ['completed', 'stopped', 'error']:
cursor.execute('''
UPDATE detection_sessions SET
status = ?, data_points = ?, end_time = CURRENT_TIMESTAMP
WHERE id = ?
''', (status, data_points, session_id))
else:
cursor.execute('''
UPDATE detection_sessions SET
status = ?, data_points = ?
WHERE id = ?
''', (status, data_points, session_id))
conn.commit()
logger.info(f'更新会话状态: {session_id} -> {status}')
except Exception as e:
conn.rollback()
logger.error(f'更新会话状态失败: {e}')
raise
def get_detection_sessions(self, page: int = 1, size: int = 10, patient_id: str = None) -> List[Dict]:
"""获取检测会话列表"""
conn = self.get_connection()
cursor = conn.cursor()
try:
offset = (page - 1) * size
if patient_id:
cursor.execute('''
SELECT s.*, p.name as patient_name
FROM detection_sessions s
LEFT JOIN patients p ON s.patient_id = p.id
WHERE s.patient_id = ?
ORDER BY s.start_time DESC
LIMIT ? OFFSET ?
''', (patient_id, size, offset))
else:
cursor.execute('''
SELECT s.*, p.name as patient_name
FROM detection_sessions s
LEFT JOIN patients p ON s.patient_id = p.id
ORDER BY s.start_time DESC
LIMIT ? OFFSET ?
''', (size, offset))
rows = cursor.fetchall()
sessions = []
for row in rows:
session = dict(row)
# 解析设置JSON
if session['settings']:
try:
session['settings'] = json.loads(session['settings'])
except:
session['settings'] = {}
sessions.append(session)
return sessions
except Exception as e:
logger.error(f'获取检测会话列表失败: {e}')
raise
def get_sessions_count(self, patient_id: str = None) -> int:
"""获取会话总数"""
conn = self.get_connection()
cursor = conn.cursor()
try:
if patient_id:
cursor.execute('SELECT COUNT(*) FROM detection_sessions WHERE patient_id = ?', (patient_id,))
else:
cursor.execute('SELECT COUNT(*) FROM detection_sessions')
return cursor.fetchone()[0]
except Exception as e:
logger.error(f'获取会话总数失败: {e}')
return 0
def get_session_data(self, session_id: str) -> Optional[Dict]:
"""获取会话详细数据"""
conn = self.get_connection()
cursor = conn.cursor()
try:
# 获取会话基本信息
cursor.execute('''
SELECT s.*, p.name as patient_name
FROM detection_sessions s
LEFT JOIN patients p ON s.patient_id = p.id
WHERE s.id = ?
''', (session_id,))
session_row = cursor.fetchone()
if not session_row:
return None
session = dict(session_row)
# 解析设置JSON
if session['settings']:
try:
session['settings'] = json.loads(session['settings'])
except:
session['settings'] = {}
# 获取检测数据
cursor.execute('''
SELECT * FROM detection_data
WHERE session_id = ?
ORDER BY timestamp
''', (session_id,))
data_rows = cursor.fetchall()
session['data'] = []
for row in data_rows:
data_point = dict(row)
# 解析数据JSON
try:
data_point['data_value'] = json.loads(data_point['data_value'])
except:
pass
session['data'].append(data_point)
return session
except Exception as e:
logger.error(f'获取会话数据失败: {e}')
return None
# ==================== 检测数据管理 ====================
def save_detection_data(self, session_id: str, data: Dict[str, Any]):
"""保存检测数据"""
conn = self.get_connection()
cursor = conn.cursor()
try:
# 保存不同类型的数据
for data_type, data_value in data.items():
cursor.execute('''
INSERT INTO detection_data (session_id, data_type, data_value)
VALUES (?, ?, ?)
''', (session_id, data_type, json.dumps(data_value)))
conn.commit()
except Exception as e:
conn.rollback()
logger.error(f'保存检测数据失败: {e}')
raise
def get_latest_detection_data(self, session_id: str, limit: int = 10) -> List[Dict]:
"""获取最新的检测数据"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('''
SELECT * FROM detection_data
WHERE session_id = ?
ORDER BY timestamp DESC
LIMIT ?
''', (session_id, limit))
rows = cursor.fetchall()
data_points = []
for row in rows:
data_point = dict(row)
try:
data_point['data_value'] = json.loads(data_point['data_value'])
except:
pass
data_points.append(data_point)
return data_points
except Exception as e:
logger.error(f'获取最新检测数据失败: {e}')
return []
# ==================== 分析结果管理 ====================
def save_analysis_result(self, session_id: str, analysis_result: Dict[str, Any]):
"""保存分析结果"""
conn = self.get_connection()
cursor = conn.cursor()
try:
for analysis_type, result_data in analysis_result.items():
cursor.execute('''
INSERT INTO analysis_results (session_id, analysis_type, result_data)
VALUES (?, ?, ?)
''', (session_id, analysis_type, json.dumps(result_data)))
conn.commit()
logger.info(f'保存分析结果: {session_id}')
except Exception as e:
conn.rollback()
logger.error(f'保存分析结果失败: {e}')
raise
def get_analysis_results(self, session_id: str) -> Dict[str, Any]:
"""获取分析结果"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('''
SELECT * FROM analysis_results
WHERE session_id = ?
ORDER BY created_at DESC
''', (session_id,))
rows = cursor.fetchall()
results = {}
for row in rows:
analysis_type = row['analysis_type']
try:
result_data = json.loads(row['result_data'])
results[analysis_type] = result_data
except:
results[analysis_type] = row['result_data']
return results
except Exception as e:
logger.error(f'获取分析结果失败: {e}')
return {}
# ==================== 系统设置管理 ====================
def get_setting(self, key: str, default_value: Any = None) -> Any:
"""获取系统设置"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('SELECT value FROM system_settings WHERE key = ?', (key,))
row = cursor.fetchone()
if row:
try:
return json.loads(row['value'])
except:
return row['value']
return default_value
except Exception as e:
logger.error(f'获取系统设置失败: {e}')
return default_value
def set_setting(self, key: str, value: Any, description: str = ''):
"""设置系统设置"""
conn = self.get_connection()
cursor = conn.cursor()
try:
value_str = json.dumps(value) if not isinstance(value, str) else value
cursor.execute('''
INSERT OR REPLACE INTO system_settings (key, value, description, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (key, value_str, description))
conn.commit()
logger.info(f'设置系统设置: {key}')
except Exception as e:
conn.rollback()
logger.error(f'设置系统设置失败: {e}')
raise
def close(self):
"""关闭数据库连接"""
if self.connection:
self.connection.close()
self.connection = None
logger.info('数据库连接已关闭')

615
backend/detection_engine.py Normal file
View File

@ -0,0 +1,615 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
检测引擎模块
负责实时数据处理姿态分析和平衡评估
"""
import numpy as np
import cv2
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Tuple
import logging
import threading
from collections import deque
import json
logger = logging.getLogger(__name__)
class DetectionEngine:
"""检测引擎"""
def __init__(self):
self.session_data = {} # 存储各会话的数据
self.data_lock = threading.Lock()
self.analysis_algorithms = {
'balance_analysis': BalanceAnalyzer(),
'posture_analysis': PostureAnalyzer(),
'movement_analysis': MovementAnalyzer()
}
logger.info('检测引擎初始化完成')
def start_session(self, session_id: str, settings: Dict[str, Any]):
"""开始检测会话"""
with self.data_lock:
self.session_data[session_id] = {
'settings': settings,
'start_time': datetime.now(),
'data_buffer': deque(maxlen=1000), # 保留最近1000个数据点
'analysis_results': {},
'real_time_metrics': {}
}
logger.info(f'检测会话开始: {session_id}')
def process_data(self, session_id: str, raw_data: Dict[str, Any]) -> Dict[str, Any]:
"""处理实时数据"""
if session_id not in self.session_data:
logger.warning(f'会话不存在: {session_id}')
return {}
try:
# 数据预处理
processed_data = self._preprocess_data(raw_data)
# 存储数据
with self.data_lock:
self.session_data[session_id]['data_buffer'].append(processed_data)
# 实时分析
real_time_results = self._real_time_analysis(session_id, processed_data)
# 更新实时指标
with self.data_lock:
self.session_data[session_id]['real_time_metrics'].update(real_time_results)
return real_time_results
except Exception as e:
logger.error(f'数据处理失败: {e}')
return {}
def _preprocess_data(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
"""数据预处理"""
processed = {
'timestamp': raw_data.get('timestamp', datetime.now().isoformat())
}
# 处理摄像头数据
if 'camera' in raw_data:
camera_data = raw_data['camera']
if 'pose_data' in camera_data:
processed['pose'] = self._process_pose_data(camera_data['pose_data'])
# 处理IMU数据
if 'imu' in raw_data:
processed['imu'] = self._process_imu_data(raw_data['imu'])
# 处理压力数据
if 'pressure' in raw_data:
processed['pressure'] = self._process_pressure_data(raw_data['pressure'])
return processed
def _process_pose_data(self, pose_data: Dict[str, Any]) -> Dict[str, Any]:
"""处理姿态数据"""
return {
'center_of_gravity': pose_data.get('center_of_gravity', {'x': 0, 'y': 0}),
'body_angle': pose_data.get('body_angle', {'pitch': 0, 'roll': 0, 'yaw': 0}),
'confidence': pose_data.get('confidence', 0.0)
}
def _process_imu_data(self, imu_data: Dict[str, Any]) -> Dict[str, Any]:
"""处理IMU数据"""
# 计算合成加速度
accel = imu_data.get('accel', {'x': 0, 'y': 0, 'z': 0})
total_accel = np.sqrt(accel['x']**2 + accel['y']**2 + accel['z']**2)
# 计算倾斜角度
pitch = np.arctan2(accel['y'], np.sqrt(accel['x']**2 + accel['z']**2)) * 180 / np.pi
roll = np.arctan2(-accel['x'], accel['z']) * 180 / np.pi
return {
'accel': accel,
'gyro': imu_data.get('gyro', {'x': 0, 'y': 0, 'z': 0}),
'total_accel': total_accel,
'pitch': pitch,
'roll': roll,
'temperature': imu_data.get('temperature', 0)
}
def _process_pressure_data(self, pressure_data: Dict[str, Any]) -> Dict[str, Any]:
"""处理压力数据"""
left_foot = pressure_data.get('left_foot', 0)
right_foot = pressure_data.get('right_foot', 0)
total_pressure = left_foot + right_foot
# 计算压力分布比例
if total_pressure > 0:
left_ratio = left_foot / total_pressure
right_ratio = right_foot / total_pressure
balance_index = abs(left_ratio - right_ratio) # 平衡指数,越小越平衡
else:
left_ratio = right_ratio = 0.5
balance_index = 0
return {
'left_foot': left_foot,
'right_foot': right_foot,
'total_pressure': total_pressure,
'left_ratio': left_ratio,
'right_ratio': right_ratio,
'balance_index': balance_index,
'center_of_pressure': pressure_data.get('center_of_pressure', {'x': 0, 'y': 0})
}
def _real_time_analysis(self, session_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""实时分析"""
results = {}
try:
# 获取历史数据用于趋势分析
with self.data_lock:
data_buffer = list(self.session_data[session_id]['data_buffer'])
if len(data_buffer) < 2:
return results
# 平衡分析
if 'pressure' in data:
balance_result = self.analysis_algorithms['balance_analysis'].analyze_real_time(
data['pressure'], data_buffer
)
results['balance'] = balance_result
# 姿态分析
if 'pose' in data or 'imu' in data:
posture_result = self.analysis_algorithms['posture_analysis'].analyze_real_time(
data, data_buffer
)
results['posture'] = posture_result
# 运动分析
movement_result = self.analysis_algorithms['movement_analysis'].analyze_real_time(
data, data_buffer
)
results['movement'] = movement_result
except Exception as e:
logger.error(f'实时分析失败: {e}')
return results
def get_latest_data(self, session_id: str) -> Dict[str, Any]:
"""获取最新数据"""
if session_id not in self.session_data:
return {}
with self.data_lock:
session = self.session_data[session_id]
if not session['data_buffer']:
return {}
latest_data = session['data_buffer'][-1]
real_time_metrics = session['real_time_metrics'].copy()
return {
'latest_data': latest_data,
'real_time_metrics': real_time_metrics,
'data_count': len(session['data_buffer'])
}
def analyze_session(self, session_id: str) -> Dict[str, Any]:
"""分析整个会话数据"""
if session_id not in self.session_data:
logger.warning(f'会话不存在: {session_id}')
return {}
try:
with self.data_lock:
session = self.session_data[session_id]
data_buffer = list(session['data_buffer'])
settings = session['settings']
if not data_buffer:
return {'error': '没有数据可分析'}
analysis_results = {}
# 全面平衡分析
balance_analysis = self.analysis_algorithms['balance_analysis'].analyze_full_session(
data_buffer, settings
)
analysis_results['balance'] = balance_analysis
# 全面姿态分析
posture_analysis = self.analysis_algorithms['posture_analysis'].analyze_full_session(
data_buffer, settings
)
analysis_results['posture'] = posture_analysis
# 全面运动分析
movement_analysis = self.analysis_algorithms['movement_analysis'].analyze_full_session(
data_buffer, settings
)
analysis_results['movement'] = movement_analysis
# 综合评估
overall_assessment = self._generate_overall_assessment(analysis_results)
analysis_results['overall'] = overall_assessment
# 保存分析结果
with self.data_lock:
self.session_data[session_id]['analysis_results'] = analysis_results
logger.info(f'会话分析完成: {session_id}')
return analysis_results
except Exception as e:
logger.error(f'会话分析失败: {e}')
return {'error': str(e)}
def _generate_overall_assessment(self, analysis_results: Dict[str, Any]) -> Dict[str, Any]:
"""生成综合评估"""
try:
# 提取各项评分
balance_score = analysis_results.get('balance', {}).get('score', 0)
posture_score = analysis_results.get('posture', {}).get('score', 0)
movement_score = analysis_results.get('movement', {}).get('score', 0)
# 计算综合评分(加权平均)
weights = {'balance': 0.4, 'posture': 0.3, 'movement': 0.3}
overall_score = (
balance_score * weights['balance'] +
posture_score * weights['posture'] +
movement_score * weights['movement']
)
# 评估等级
if overall_score >= 90:
grade = 'A'
description = '优秀'
elif overall_score >= 80:
grade = 'B'
description = '良好'
elif overall_score >= 70:
grade = 'C'
description = '一般'
elif overall_score >= 60:
grade = 'D'
description = '较差'
else:
grade = 'E'
description = ''
# 生成建议
recommendations = self._generate_recommendations(analysis_results)
return {
'score': round(overall_score, 1),
'grade': grade,
'description': description,
'recommendations': recommendations,
'component_scores': {
'balance': balance_score,
'posture': posture_score,
'movement': movement_score
}
}
except Exception as e:
logger.error(f'综合评估生成失败: {e}')
return {'score': 0, 'grade': 'E', 'description': '评估失败'}
def _generate_recommendations(self, analysis_results: Dict[str, Any]) -> List[str]:
"""生成改善建议"""
recommendations = []
try:
# 平衡相关建议
balance_data = analysis_results.get('balance', {})
if balance_data.get('score', 0) < 80:
if balance_data.get('left_right_imbalance', 0) > 0.2:
recommendations.append('注意左右脚压力分布,建议进行单脚站立练习')
if balance_data.get('stability_index', 0) > 0.5:
recommendations.append('重心摆动较大,建议进行静态平衡训练')
# 姿态相关建议
posture_data = analysis_results.get('posture', {})
if posture_data.get('score', 0) < 80:
if abs(posture_data.get('avg_pitch', 0)) > 5:
recommendations.append('身体前后倾斜较明显,注意保持直立姿态')
if abs(posture_data.get('avg_roll', 0)) > 5:
recommendations.append('身体左右倾斜较明显,注意身体对称性')
# 运动相关建议
movement_data = analysis_results.get('movement', {})
if movement_data.get('score', 0) < 80:
if movement_data.get('movement_variability', 0) > 0.8:
recommendations.append('身体摆动过大,建议进行核心稳定性训练')
if movement_data.get('movement_frequency', 0) > 2:
recommendations.append('身体摆动频率较高,建议放松并专注于静态平衡')
# 通用建议
if not recommendations:
recommendations.append('整体表现良好,继续保持规律的平衡训练')
except Exception as e:
logger.error(f'建议生成失败: {e}')
recommendations = ['建议咨询专业医师进行详细评估']
return recommendations
def end_session(self, session_id: str):
"""结束检测会话"""
if session_id in self.session_data:
with self.data_lock:
del self.session_data[session_id]
logger.info(f'检测会话结束: {session_id}')
class BalanceAnalyzer:
"""平衡分析器"""
def analyze_real_time(self, pressure_data: Dict[str, Any], data_buffer: List[Dict]) -> Dict[str, Any]:
"""实时平衡分析"""
try:
balance_index = pressure_data.get('balance_index', 0)
cop = pressure_data.get('center_of_pressure', {'x': 0, 'y': 0})
# 计算最近10个数据点的平衡稳定性
recent_data = data_buffer[-10:] if len(data_buffer) >= 10 else data_buffer
if recent_data and all('pressure' in d for d in recent_data):
balance_indices = [d['pressure'].get('balance_index', 0) for d in recent_data]
stability = 1.0 - np.std(balance_indices) # 标准差越小,稳定性越高
else:
stability = 0.5
return {
'balance_index': balance_index,
'stability': max(0, min(1, stability)),
'center_of_pressure': cop,
'status': 'stable' if balance_index < 0.2 else 'unstable'
}
except Exception as e:
logger.error(f'实时平衡分析失败: {e}')
return {'balance_index': 0, 'stability': 0, 'status': 'unknown'}
def analyze_full_session(self, data_buffer: List[Dict], settings: Dict) -> Dict[str, Any]:
"""全会话平衡分析"""
try:
pressure_data = [d['pressure'] for d in data_buffer if 'pressure' in d]
if not pressure_data:
return {'score': 0, 'error': '没有压力数据'}
# 提取关键指标
balance_indices = [d.get('balance_index', 0) for d in pressure_data]
left_ratios = [d.get('left_ratio', 0.5) for d in pressure_data]
right_ratios = [d.get('right_ratio', 0.5) for d in pressure_data]
# 计算统计指标
avg_balance_index = np.mean(balance_indices)
std_balance_index = np.std(balance_indices)
max_balance_index = np.max(balance_indices)
# 左右脚不平衡程度
left_right_imbalance = abs(np.mean(left_ratios) - np.mean(right_ratios))
# 稳定性指数(基于标准差)
stability_index = std_balance_index
# 计算评分0-100分
balance_score = max(0, 100 - avg_balance_index * 200) # 平衡指数越小分数越高
stability_score = max(0, 100 - stability_index * 500) # 稳定性越高分数越高
symmetry_score = max(0, 100 - left_right_imbalance * 200) # 对称性越好分数越高
overall_score = (balance_score + stability_score + symmetry_score) / 3
return {
'score': round(overall_score, 1),
'avg_balance_index': round(avg_balance_index, 3),
'stability_index': round(stability_index, 3),
'left_right_imbalance': round(left_right_imbalance, 3),
'max_imbalance': round(max_balance_index, 3),
'data_points': len(pressure_data),
'component_scores': {
'balance': round(balance_score, 1),
'stability': round(stability_score, 1),
'symmetry': round(symmetry_score, 1)
}
}
except Exception as e:
logger.error(f'全会话平衡分析失败: {e}')
return {'score': 0, 'error': str(e)}
class PostureAnalyzer:
"""姿态分析器"""
def analyze_real_time(self, data: Dict[str, Any], data_buffer: List[Dict]) -> Dict[str, Any]:
"""实时姿态分析"""
try:
result = {}
# 分析IMU数据
if 'imu' in data:
imu_data = data['imu']
result['pitch'] = imu_data.get('pitch', 0)
result['roll'] = imu_data.get('roll', 0)
result['total_accel'] = imu_data.get('total_accel', 0)
# 分析姿态数据
if 'pose' in data:
pose_data = data['pose']
result['body_angle'] = pose_data.get('body_angle', {})
result['confidence'] = pose_data.get('confidence', 0)
# 计算姿态稳定性
recent_data = data_buffer[-5:] if len(data_buffer) >= 5 else data_buffer
if recent_data:
if 'imu' in data:
pitches = [d.get('imu', {}).get('pitch', 0) for d in recent_data if 'imu' in d]
rolls = [d.get('imu', {}).get('roll', 0) for d in recent_data if 'imu' in d]
if pitches and rolls:
pitch_stability = 1.0 - min(1.0, np.std(pitches) / 10)
roll_stability = 1.0 - min(1.0, np.std(rolls) / 10)
result['stability'] = (pitch_stability + roll_stability) / 2
return result
except Exception as e:
logger.error(f'实时姿态分析失败: {e}')
return {}
def analyze_full_session(self, data_buffer: List[Dict], settings: Dict) -> Dict[str, Any]:
"""全会话姿态分析"""
try:
imu_data = [d['imu'] for d in data_buffer if 'imu' in d]
if not imu_data:
return {'score': 0, 'error': '没有IMU数据'}
# 提取角度数据
pitches = [d.get('pitch', 0) for d in imu_data]
rolls = [d.get('roll', 0) for d in imu_data]
# 计算统计指标
avg_pitch = np.mean(pitches)
avg_roll = np.mean(rolls)
std_pitch = np.std(pitches)
std_roll = np.std(rolls)
max_pitch = np.max(np.abs(pitches))
max_roll = np.max(np.abs(rolls))
# 计算评分
pitch_score = max(0, 100 - abs(avg_pitch) * 5) # 平均倾斜角度越小分数越高
roll_score = max(0, 100 - abs(avg_roll) * 5)
stability_score = max(0, 100 - (std_pitch + std_roll) * 10) # 稳定性越高分数越高
overall_score = (pitch_score + roll_score + stability_score) / 3
return {
'score': round(overall_score, 1),
'avg_pitch': round(avg_pitch, 2),
'avg_roll': round(avg_roll, 2),
'std_pitch': round(std_pitch, 2),
'std_roll': round(std_roll, 2),
'max_pitch': round(max_pitch, 2),
'max_roll': round(max_roll, 2),
'data_points': len(imu_data),
'component_scores': {
'pitch': round(pitch_score, 1),
'roll': round(roll_score, 1),
'stability': round(stability_score, 1)
}
}
except Exception as e:
logger.error(f'全会话姿态分析失败: {e}')
return {'score': 0, 'error': str(e)}
class MovementAnalyzer:
"""运动分析器"""
def analyze_real_time(self, data: Dict[str, Any], data_buffer: List[Dict]) -> Dict[str, Any]:
"""实时运动分析"""
try:
if len(data_buffer) < 5:
return {'movement_detected': False}
# 分析最近的运动模式
recent_data = data_buffer[-10:]
# 计算重心位置变化
if 'pressure' in data:
cop_positions = []
for d in recent_data:
if 'pressure' in d:
cop = d['pressure'].get('center_of_pressure', {'x': 0, 'y': 0})
cop_positions.append((cop['x'], cop['y']))
if len(cop_positions) >= 2:
# 计算运动幅度
x_positions = [pos[0] for pos in cop_positions]
y_positions = [pos[1] for pos in cop_positions]
movement_range_x = np.max(x_positions) - np.min(x_positions)
movement_range_y = np.max(y_positions) - np.min(y_positions)
return {
'movement_detected': movement_range_x > 5 or movement_range_y > 5,
'movement_range_x': movement_range_x,
'movement_range_y': movement_range_y,
'total_movement': np.sqrt(movement_range_x**2 + movement_range_y**2)
}
return {'movement_detected': False}
except Exception as e:
logger.error(f'实时运动分析失败: {e}')
return {'movement_detected': False}
def analyze_full_session(self, data_buffer: List[Dict], settings: Dict) -> Dict[str, Any]:
"""全会话运动分析"""
try:
# 提取压力中心数据
cop_data = []
for d in data_buffer:
if 'pressure' in d:
cop = d['pressure'].get('center_of_pressure', {'x': 0, 'y': 0})
cop_data.append((cop['x'], cop['y']))
if len(cop_data) < 10:
return {'score': 0, 'error': '数据不足'}
# 计算运动指标
x_positions = [pos[0] for pos in cop_data]
y_positions = [pos[1] for pos in cop_data]
# 运动范围
movement_range_x = np.max(x_positions) - np.min(x_positions)
movement_range_y = np.max(y_positions) - np.min(y_positions)
total_range = np.sqrt(movement_range_x**2 + movement_range_y**2)
# 运动变异性
movement_variability = np.std(x_positions) + np.std(y_positions)
# 运动路径长度
path_length = 0
for i in range(1, len(cop_data)):
dx = cop_data[i][0] - cop_data[i-1][0]
dy = cop_data[i][1] - cop_data[i-1][1]
path_length += np.sqrt(dx**2 + dy**2)
# 运动频率分析(简化)
movement_frequency = path_length / len(cop_data) if len(cop_data) > 0 else 0
# 计算评分(运动幅度适中得分高)
range_score = max(0, 100 - total_range * 2) # 运动范围适中
variability_score = max(0, 100 - movement_variability * 10) # 变异性小
frequency_score = max(0, 100 - movement_frequency * 20) # 频率适中
overall_score = (range_score + variability_score + frequency_score) / 3
return {
'score': round(overall_score, 1),
'movement_range_x': round(movement_range_x, 2),
'movement_range_y': round(movement_range_y, 2),
'total_range': round(total_range, 2),
'movement_variability': round(movement_variability, 2),
'path_length': round(path_length, 2),
'movement_frequency': round(movement_frequency, 2),
'data_points': len(cop_data),
'component_scores': {
'range': round(range_score, 1),
'variability': round(variability_score, 1),
'frequency': round(frequency_score, 1)
}
}
except Exception as e:
logger.error(f'全会话运动分析失败: {e}')
return {'score': 0, 'error': str(e)}

479
backend/device_manager.py Normal file
View File

@ -0,0 +1,479 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
设备管理模块
负责摄像头IMU传感器和压力传感器的连接和数据采集
"""
import cv2
import numpy as np
import time
import threading
import json
from datetime import datetime
from typing import Dict, List, Optional, Any
import logging
logger = logging.getLogger(__name__)
class DeviceManager:
"""设备管理器"""
def __init__(self):
self.camera = None
self.imu_device = None
self.pressure_device = None
self.device_status = {
'camera': False,
'imu': False,
'pressure': False
}
self.calibration_data = {}
self.data_lock = threading.Lock()
self.latest_data = {}
# 初始化设备
self._init_devices()
def _init_devices(self):
"""初始化所有设备"""
try:
self._init_camera()
self._init_imu()
self._init_pressure_sensor()
logger.info('设备初始化完成')
except Exception as e:
logger.error(f'设备初始化失败: {e}')
def _init_camera(self):
"""初始化摄像头"""
try:
# 尝试连接默认摄像头
self.camera = cv2.VideoCapture(0)
if self.camera.isOpened():
# 设置摄像头参数
self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
self.camera.set(cv2.CAP_PROP_FPS, 30)
self.device_status['camera'] = True
logger.info('摄像头初始化成功')
else:
logger.warning('摄像头连接失败')
self.camera = None
except Exception as e:
logger.error(f'摄像头初始化异常: {e}')
self.camera = None
def _init_imu(self):
"""初始化IMU传感器"""
try:
# 这里应该连接实际的IMU设备
# 目前使用模拟数据
self.imu_device = MockIMUDevice()
self.device_status['imu'] = True
logger.info('IMU传感器初始化成功模拟')
except Exception as e:
logger.error(f'IMU传感器初始化失败: {e}')
self.imu_device = None
def _init_pressure_sensor(self):
"""初始化压力传感器"""
try:
# 这里应该连接实际的压力传感器
# 目前使用模拟数据
self.pressure_device = MockPressureDevice()
self.device_status['pressure'] = True
logger.info('压力传感器初始化成功(模拟)')
except Exception as e:
logger.error(f'压力传感器初始化失败: {e}')
self.pressure_device = None
def get_device_status(self) -> Dict[str, bool]:
"""获取设备状态"""
return self.device_status.copy()
def get_connected_devices(self) -> List[str]:
"""获取已连接的设备列表"""
return [device for device, status in self.device_status.items() if status]
def refresh_devices(self):
"""刷新设备连接"""
logger.info('刷新设备连接...')
# 重新初始化所有设备
if self.camera:
self.camera.release()
self._init_devices()
def calibrate_devices(self) -> Dict[str, Any]:
"""校准设备"""
calibration_result = {}
try:
# 摄像头校准
if self.device_status['camera']:
camera_calibration = self._calibrate_camera()
calibration_result['camera'] = camera_calibration
# IMU校准
if self.device_status['imu']:
imu_calibration = self._calibrate_imu()
calibration_result['imu'] = imu_calibration
# 压力传感器校准
if self.device_status['pressure']:
pressure_calibration = self._calibrate_pressure()
calibration_result['pressure'] = pressure_calibration
self.calibration_data = calibration_result
logger.info('设备校准完成')
except Exception as e:
logger.error(f'设备校准失败: {e}')
raise
return calibration_result
def _calibrate_camera(self) -> Dict[str, Any]:
"""校准摄像头"""
if not self.camera or not self.camera.isOpened():
return {'status': 'failed', 'error': '摄像头未连接'}
try:
# 获取几帧图像进行校准
frames = []
for _ in range(10):
ret, frame = self.camera.read()
if ret:
frames.append(frame)
time.sleep(0.1)
if not frames:
return {'status': 'failed', 'error': '无法获取图像'}
# 计算平均亮度和对比度
avg_brightness = np.mean([np.mean(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) for frame in frames])
calibration = {
'status': 'success',
'brightness': float(avg_brightness),
'resolution': (int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT))),
'fps': float(self.camera.get(cv2.CAP_PROP_FPS)),
'timestamp': datetime.now().isoformat()
}
return calibration
except Exception as e:
return {'status': 'failed', 'error': str(e)}
def _calibrate_imu(self) -> Dict[str, Any]:
"""校准IMU传感器"""
if not self.imu_device:
return {'status': 'failed', 'error': 'IMU设备未连接'}
try:
# 收集静态数据进行零点校准
samples = []
for _ in range(100):
data = self.imu_device.read_data()
samples.append(data)
time.sleep(0.01)
# 计算零点偏移
accel_offset = {
'x': np.mean([s['accel']['x'] for s in samples]),
'y': np.mean([s['accel']['y'] for s in samples]),
'z': np.mean([s['accel']['z'] for s in samples]) - 9.8 # 重力补偿
}
gyro_offset = {
'x': np.mean([s['gyro']['x'] for s in samples]),
'y': np.mean([s['gyro']['y'] for s in samples]),
'z': np.mean([s['gyro']['z'] for s in samples])
}
calibration = {
'status': 'success',
'accel_offset': accel_offset,
'gyro_offset': gyro_offset,
'timestamp': datetime.now().isoformat()
}
return calibration
except Exception as e:
return {'status': 'failed', 'error': str(e)}
def _calibrate_pressure(self) -> Dict[str, Any]:
"""校准压力传感器"""
if not self.pressure_device:
return {'status': 'failed', 'error': '压力传感器未连接'}
try:
# 收集零压力数据
samples = []
for _ in range(50):
data = self.pressure_device.read_data()
samples.append(data)
time.sleep(0.02)
# 计算零点偏移
zero_offset = {
'left_foot': np.mean([s['left_foot'] for s in samples]),
'right_foot': np.mean([s['right_foot'] for s in samples])
}
calibration = {
'status': 'success',
'zero_offset': zero_offset,
'timestamp': datetime.now().isoformat()
}
return calibration
except Exception as e:
return {'status': 'failed', 'error': str(e)}
def collect_data(self) -> Dict[str, Any]:
"""采集所有设备数据"""
data = {}
timestamp = datetime.now().isoformat()
try:
# 采集摄像头数据
if self.device_status['camera']:
camera_data = self._collect_camera_data()
if camera_data:
data['camera'] = camera_data
# 采集IMU数据
if self.device_status['imu']:
imu_data = self._collect_imu_data()
if imu_data:
data['imu'] = imu_data
# 采集压力传感器数据
if self.device_status['pressure']:
pressure_data = self._collect_pressure_data()
if pressure_data:
data['pressure'] = pressure_data
# 添加时间戳
data['timestamp'] = timestamp
# 更新最新数据
with self.data_lock:
self.latest_data = data.copy()
except Exception as e:
logger.error(f'数据采集失败: {e}')
return data
def _collect_camera_data(self) -> Optional[Dict[str, Any]]:
"""采集摄像头数据"""
if not self.camera or not self.camera.isOpened():
return None
try:
ret, frame = self.camera.read()
if not ret:
return None
# 进行姿态检测(这里应该集成实际的姿态检测算法)
pose_data = self._detect_pose(frame)
return {
'frame_available': True,
'frame_size': frame.shape,
'pose_data': pose_data,
'timestamp': datetime.now().isoformat()
}
except Exception as e:
logger.error(f'摄像头数据采集失败: {e}')
return None
def _detect_pose(self, frame: np.ndarray) -> Dict[str, Any]:
"""姿态检测(模拟实现)"""
# 这里应该集成实际的姿态检测算法如MediaPipe或OpenPose
# 目前返回模拟数据
return {
'center_of_gravity': {
'x': np.random.normal(0, 5),
'y': np.random.normal(0, 5)
},
'body_angle': {
'pitch': np.random.normal(0, 2),
'roll': np.random.normal(0, 2),
'yaw': np.random.normal(0, 1)
},
'confidence': np.random.uniform(0.8, 1.0)
}
def _collect_imu_data(self) -> Optional[Dict[str, Any]]:
"""采集IMU数据"""
if not self.imu_device:
return None
try:
raw_data = self.imu_device.read_data()
# 应用校准偏移
if 'imu' in self.calibration_data:
calibration = self.calibration_data['imu']
if calibration.get('status') == 'success':
accel_offset = calibration.get('accel_offset', {})
gyro_offset = calibration.get('gyro_offset', {})
# 校正加速度数据
raw_data['accel']['x'] -= accel_offset.get('x', 0)
raw_data['accel']['y'] -= accel_offset.get('y', 0)
raw_data['accel']['z'] -= accel_offset.get('z', 0)
# 校正陀螺仪数据
raw_data['gyro']['x'] -= gyro_offset.get('x', 0)
raw_data['gyro']['y'] -= gyro_offset.get('y', 0)
raw_data['gyro']['z'] -= gyro_offset.get('z', 0)
return raw_data
except Exception as e:
logger.error(f'IMU数据采集失败: {e}')
return None
def _collect_pressure_data(self) -> Optional[Dict[str, Any]]:
"""采集压力传感器数据"""
if not self.pressure_device:
return None
try:
raw_data = self.pressure_device.read_data()
# 应用校准偏移
if 'pressure' in self.calibration_data:
calibration = self.calibration_data['pressure']
if calibration.get('status') == 'success':
zero_offset = calibration.get('zero_offset', {})
raw_data['left_foot'] -= zero_offset.get('left_foot', 0)
raw_data['right_foot'] -= zero_offset.get('right_foot', 0)
# 计算重心位置
total_pressure = raw_data['left_foot'] + raw_data['right_foot']
if total_pressure > 0:
center_of_pressure = {
'x': (raw_data['right_foot'] - raw_data['left_foot']) / total_pressure * 100,
'y': 0 # 简化模型
}
else:
center_of_pressure = {'x': 0, 'y': 0}
raw_data['center_of_pressure'] = center_of_pressure
raw_data['total_pressure'] = total_pressure
return raw_data
except Exception as e:
logger.error(f'压力传感器数据采集失败: {e}')
return None
def get_latest_data(self) -> Dict[str, Any]:
"""获取最新采集的数据"""
with self.data_lock:
return self.latest_data.copy()
def start_video_recording(self, output_path: str) -> bool:
"""开始视频录制"""
if not self.camera or not self.camera.isOpened():
return False
try:
# 获取摄像头参数
width = int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(self.camera.get(cv2.CAP_PROP_FPS))
# 创建视频写入器
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
self.video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
if self.video_writer.isOpened():
logger.info(f'开始视频录制: {output_path}')
return True
else:
logger.error('视频写入器创建失败')
return False
except Exception as e:
logger.error(f'开始视频录制失败: {e}')
return False
def stop_video_recording(self):
"""停止视频录制"""
if hasattr(self, 'video_writer') and self.video_writer:
self.video_writer.release()
self.video_writer = None
logger.info('视频录制已停止')
def cleanup(self):
"""清理资源"""
try:
if self.camera:
self.camera.release()
if hasattr(self, 'video_writer') and self.video_writer:
self.video_writer.release()
logger.info('设备资源已清理')
except Exception as e:
logger.error(f'清理设备资源失败: {e}')
class MockIMUDevice:
"""模拟IMU设备"""
def __init__(self):
self.noise_level = 0.1
def read_data(self) -> Dict[str, Any]:
"""读取IMU数据"""
return {
'accel': {
'x': np.random.normal(0, self.noise_level),
'y': np.random.normal(0, self.noise_level),
'z': np.random.normal(9.8, self.noise_level) # 重力加速度
},
'gyro': {
'x': np.random.normal(0, self.noise_level),
'y': np.random.normal(0, self.noise_level),
'z': np.random.normal(0, self.noise_level)
},
'temperature': np.random.normal(25, 2),
'timestamp': datetime.now().isoformat()
}
class MockPressureDevice:
"""模拟压力传感器设备"""
def __init__(self):
self.base_pressure = 500 # 基础压力值
self.noise_level = 10
def read_data(self) -> Dict[str, Any]:
"""读取压力数据"""
# 模拟轻微的左右脚压力差异
left_pressure = self.base_pressure + np.random.normal(0, self.noise_level)
right_pressure = self.base_pressure + np.random.normal(0, self.noise_level)
return {
'left_foot': max(0, left_pressure),
'right_foot': max(0, right_pressure),
'timestamp': datetime.now().isoformat()
}

412
backend/main.py Normal file
View File

@ -0,0 +1,412 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
身体平衡评估系统 - 主启动脚本
这个脚本负责启动整个应用程序包括后端服务和前端界面
支持开发模式和生产模式
"""
import os
import sys
import time
import signal
import subprocess
import threading
import webbrowser
import logging
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(project_root / 'backend'))
from utils import Config, Logger
class ApplicationLauncher:
"""应用程序启动器"""
def __init__(self):
self.config = Config()
# 设置日志
Logger.setup_logging('INFO', 'logs/app.log')
self.logger = logging.getLogger('main')
self.backend_process = None
self.frontend_process = None
self.running = False
# 设置信号处理
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum, frame):
"""信号处理器"""
self.logger.info(f"接收到信号 {signum},正在关闭应用程序...")
self.stop()
def check_dependencies(self):
"""检查依赖项"""
self.logger.info("检查系统依赖项...")
# 检查Python版本
if sys.version_info < (3, 8):
self.logger.error("需要Python 3.8或更高版本")
return False
# 检查必要的Python包
required_packages = [
'flask', 'flask_cors', 'flask_socketio',
'numpy', 'pandas', 'opencv-python',
'sqlite3'
]
missing_packages = []
for package in required_packages:
try:
if package == 'sqlite3':
import sqlite3
elif package == 'opencv-python':
import cv2
elif package == 'flask_cors':
import flask_cors
elif package == 'flask_socketio':
import flask_socketio
else:
__import__(package)
except ImportError:
missing_packages.append(package)
if missing_packages:
self.logger.error(f"缺少必要的Python包: {', '.join(missing_packages)}")
self.logger.info("请运行: pip install -r backend/requirements.txt")
return False
# 检查Node.js和npm用于前端开发
if self.config.get('APP', 'mode', 'development') == 'development':
try:
result = subprocess.run(['node', '--version'],
capture_output=True, text=True)
if result.returncode != 0:
self.logger.warning("未找到Node.js将跳过前端开发服务器")
else:
self.logger.info(f"Node.js版本: {result.stdout.strip()}")
except FileNotFoundError:
self.logger.warning("未找到Node.js将跳过前端开发服务器")
self.logger.info("依赖项检查完成")
return True
def setup_directories(self):
"""设置必要的目录"""
self.logger.info("设置应用程序目录...")
directories = [
'data',
'data/patients',
'data/sessions',
'data/exports',
'data/backups',
'logs',
'temp'
]
for directory in directories:
dir_path = project_root / directory
dir_path.mkdir(parents=True, exist_ok=True)
self.logger.debug(f"创建目录: {dir_path}")
self.logger.info("目录设置完成")
def start_backend(self):
"""启动后端服务"""
self.logger.info("启动后端服务...")
backend_script = project_root / 'backend' / 'app.py'
if not backend_script.exists():
self.logger.error(f"后端脚本不存在: {backend_script}")
return False
try:
# 设置环境变量
env = os.environ.copy()
env['PYTHONPATH'] = str(project_root)
env['FLASK_APP'] = str(backend_script)
if self.config.get('APP', 'mode', 'development') == 'development':
env['FLASK_ENV'] = 'development'
env['FLASK_DEBUG'] = '1'
else:
env['FLASK_ENV'] = 'production'
env['FLASK_DEBUG'] = '0'
# 启动后端进程
cmd = [
sys.executable,
str(backend_script),
'--host', self.config.get('SERVER', 'host', '127.0.0.1'),
'--port', self.config.get('SERVER', 'port', '5000')
]
self.backend_process = subprocess.Popen(
cmd,
env=env,
cwd=str(project_root),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.logger.info(f"后端服务已启动 (PID: {self.backend_process.pid})")
# 等待后端服务启动
self._wait_for_backend()
return True
except Exception as e:
self.logger.error(f"启动后端服务失败: {e}")
return False
def _wait_for_backend(self, timeout=30):
"""等待后端服务启动"""
import requests
backend_url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}"
health_url = f"{backend_url}/api/health"
self.logger.info("等待后端服务启动...")
start_time = time.time()
while time.time() - start_time < timeout:
try:
response = requests.get(health_url, timeout=2)
if response.status_code == 200:
self.logger.info("后端服务已就绪")
return True
except requests.exceptions.RequestException:
pass
time.sleep(1)
self.logger.warning("后端服务启动超时")
return False
def start_frontend_dev(self):
"""启动前端开发服务器"""
if self.config.get('APP', 'mode', 'development') != 'development':
return True
self.logger.info("启动前端开发服务器...")
frontend_dir = project_root / 'frontend' / 'src' / 'renderer'
if not frontend_dir.exists():
self.logger.warning("前端目录不存在,跳过前端开发服务器")
return True
package_json = frontend_dir / 'package.json'
if not package_json.exists():
self.logger.warning("package.json不存在跳过前端开发服务器")
return True
try:
# 检查是否已安装依赖
node_modules = frontend_dir / 'node_modules'
if not node_modules.exists():
self.logger.info("安装前端依赖...")
install_process = subprocess.run(
['npm', 'install'],
cwd=str(frontend_dir),
capture_output=True,
text=True,
shell=True
)
if install_process.returncode != 0:
self.logger.error(f"安装前端依赖失败: {install_process.stderr}")
return False
# 启动开发服务器
self.frontend_process = subprocess.Popen(
['npm', 'run', 'dev'],
cwd=str(frontend_dir),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True
)
self.logger.info(f"前端开发服务器已启动 (PID: {self.frontend_process.pid})")
return True
except Exception as e:
self.logger.error(f"启动前端开发服务器失败: {e}")
return False
def open_browser(self):
"""打开浏览器"""
if not self.config.getboolean('APP', 'auto_open_browser', True):
return
if self.config.get('APP', 'mode', 'development') == 'development':
# 开发模式下打开前端开发服务器
url = "http://localhost:3000" # Vite配置端口
else:
# 生产模式下打开后端服务
url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}"
def delayed_open():
time.sleep(3) # 等待服务启动
try:
webbrowser.open(url)
self.logger.info(f"已打开浏览器: {url}")
except Exception as e:
self.logger.warning(f"打开浏览器失败: {e}")
threading.Thread(target=delayed_open, daemon=True).start()
def monitor_processes(self):
"""监控子进程"""
while self.running:
try:
# 检查后端进程
if self.backend_process and self.backend_process.poll() is not None:
self.logger.error("后端进程意外退出")
if self.backend_process.returncode != 0:
stderr = self.backend_process.stderr.read()
if stderr:
self.logger.error(f"后端错误: {stderr}")
self.running = False
break
# 检查前端进程
if (self.frontend_process and
self.frontend_process.poll() is not None and
self.config.get('APP', 'mode', 'development') == 'development'):
self.logger.warning("前端开发服务器意外退出")
time.sleep(5)
except Exception as e:
self.logger.error(f"进程监控错误: {e}")
break
def start(self):
"""启动应用程序"""
self.logger.info("=" * 50)
self.logger.info("身体平衡评估系统启动中...")
self.logger.info(f"模式: {self.config.get('APP', 'mode', 'development')}")
self.logger.info("=" * 50)
# 检查依赖项
if not self.check_dependencies():
return False
# 设置目录
self.setup_directories()
# 启动后端服务
if not self.start_backend():
return False
# 启动前端开发服务器(仅开发模式)
if not self.start_frontend_dev():
self.logger.warning("前端开发服务器启动失败,但继续运行")
self.running = True
# 打开浏览器
self.open_browser()
# 启动进程监控
monitor_thread = threading.Thread(target=self.monitor_processes, daemon=True)
monitor_thread.start()
self.logger.info("应用程序启动完成")
self.logger.info(f"后端服务: http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}")
if self.config.get('APP', 'mode', 'development') == 'development':
self.logger.info("前端开发服务器: http://localhost:3000")
self.logger.info("按 Ctrl+C 退出应用程序")
# 主循环
try:
while self.running:
time.sleep(1)
except KeyboardInterrupt:
self.logger.info("接收到中断信号")
self.stop()
return True
def stop(self):
"""停止应用程序"""
if not self.running:
return
self.logger.info("正在停止应用程序...")
self.running = False
# 停止前端进程
if self.frontend_process:
try:
self.frontend_process.terminate()
self.frontend_process.wait(timeout=5)
self.logger.info("前端开发服务器已停止")
except subprocess.TimeoutExpired:
self.frontend_process.kill()
self.logger.warning("强制终止前端开发服务器")
except Exception as e:
self.logger.error(f"停止前端服务器失败: {e}")
# 停止后端进程
if self.backend_process:
try:
self.backend_process.terminate()
self.backend_process.wait(timeout=10)
self.logger.info("后端服务已停止")
except subprocess.TimeoutExpired:
self.backend_process.kill()
self.logger.warning("强制终止后端服务")
except Exception as e:
self.logger.error(f"停止后端服务失败: {e}")
self.logger.info("应用程序已停止")
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='身体平衡评估系统')
parser.add_argument('--mode', choices=['development', 'production'],
default='development', help='运行模式')
parser.add_argument('--host', default='127.0.0.1', help='服务器主机')
parser.add_argument('--port', type=int, default=5000, help='服务器端口')
parser.add_argument('--no-browser', action='store_true', help='不自动打开浏览器')
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
default='INFO', help='日志级别')
args = parser.parse_args()
# 更新配置
config = Config()
config.set('APP', 'mode', args.mode)
config.set('APP', 'auto_open_browser', str(not args.no_browser))
config.set('SERVER', 'host', args.host)
config.set('SERVER', 'port', str(args.port))
# 设置日志级别
import logging
logging.getLogger().setLevel(getattr(logging, args.log_level))
# 启动应用程序
launcher = ApplicationLauncher()
try:
success = launcher.start()
sys.exit(0 if success else 1)
except Exception as e:
print(f"启动失败: {e}")
sys.exit(1)
if __name__ == '__main__':
main()

78
backend/pytest.ini Normal file
View File

@ -0,0 +1,78 @@
[tool:pytest]
# pytest 配置文件
# 测试发现
testpaths = tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# 输出配置
addopts =
-v
--tb=short
--strict-markers
--disable-warnings
--color=yes
--durations=10
--cov=.
--cov-report=html:htmlcov
--cov-report=term-missing
--cov-fail-under=80
# 标记定义
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
api: marks tests as API tests
database: marks tests as database tests
device: marks tests as device tests
analysis: marks tests as analysis tests
# 过滤警告
filterwarnings =
ignore::UserWarning
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
ignore:.*:pytest.PytestUnraisableExceptionWarning
# 最小版本要求
minversion = 6.0
# 测试目录
testmon_datafile = .testmondata
# 日志配置
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S
# 覆盖率配置
[coverage:run]
source = .
omit =
*/tests/*
*/venv/*
*/env/*
*/__pycache__/*
*/migrations/*
setup.py
conftest.py
[coverage:report]
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
if settings.DEBUG
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
class .*\bProtocol\):
@(abc\.)?abstractmethod
[coverage:html]
directory = htmlcov

48
backend/requirements.txt Normal file
View File

@ -0,0 +1,48 @@
# Web framework and API
Flask
Flask-CORS
Flask-SocketIO
# Data processing and scientific computing
numpy
pandas
scipy
# Computer vision and machine learning
opencv-python
# mediapipe # Not compatible with Python 3.13 yet
# torch # May have compatibility issues with Python 3.13
# torchvision # May have compatibility issues with Python 3.13
scikit-learn
# Image processing and visualization
matplotlib
seaborn
Pillow
# Data visualization and report generation
reportlab
# Serial communication
pyserial
# Audio/video processing
ffmpeg-python
# Network requests and utilities
requests
python-dateutil
psutil
watchdog
# Development and testing tools
pytest
pytest-cov
flake8
black
# Other dependencies
pyyaml
click
colorama
tqdm

View File

@ -0,0 +1 @@
# 测试模块初始化文件

295
backend/tests/test_api.py Normal file
View File

@ -0,0 +1,295 @@
import pytest
import json
import sys
import os
from unittest.mock import patch, MagicMock
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app import create_app
from database import DatabaseManager
from utils import Config
@pytest.fixture
def app():
"""创建测试应用实例"""
config = Config()
config.DATABASE['path'] = ':memory:' # 使用内存数据库进行测试
app = create_app(config)
app.config['TESTING'] = True
return app
@pytest.fixture
def client(app):
"""创建测试客户端"""
return app.test_client()
@pytest.fixture
def db():
"""创建测试数据库"""
db_manager = DatabaseManager(':memory:')
db_manager.init_database()
return db_manager
class TestHealthAPI:
"""健康检查API测试"""
def test_health_check(self, client):
"""测试健康检查端点"""
response = client.get('/api/health')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'success'
assert 'timestamp' in data['data']
assert 'uptime' in data['data']
class TestSystemAPI:
"""系统信息API测试"""
def test_system_info(self, client):
"""测试系统信息端点"""
response = client.get('/api/system/info')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'success'
assert 'system' in data['data']
assert 'python' in data['data']
assert 'memory' in data['data']
assert 'disk' in data['data']
class TestPatientAPI:
"""患者管理API测试"""
def test_create_patient(self, client):
"""测试创建患者"""
patient_data = {
'name': '测试患者',
'age': 30,
'gender': 'male',
'height': 175,
'weight': 70,
'phone': '13800138000',
'email': 'test@example.com',
'medical_history': '',
'notes': '测试患者'
}
response = client.post('/api/patients',
data=json.dumps(patient_data),
content_type='application/json')
assert response.status_code == 201
data = json.loads(response.data)
assert data['status'] == 'success'
assert 'patient_id' in data['data']
def test_get_patients(self, client):
"""测试获取患者列表"""
response = client.get('/api/patients')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'success'
assert 'patients' in data['data']
assert 'total' in data['data']
assert 'page' in data['data']
assert 'per_page' in data['data']
def test_get_patient_invalid_id(self, client):
"""测试获取不存在的患者"""
response = client.get('/api/patients/99999')
assert response.status_code == 404
data = json.loads(response.data)
assert data['status'] == 'error'
def test_create_patient_invalid_data(self, client):
"""测试创建患者时提供无效数据"""
invalid_data = {
'name': '', # 空名称
'age': -1, # 无效年龄
'gender': 'invalid' # 无效性别
}
response = client.post('/api/patients',
data=json.dumps(invalid_data),
content_type='application/json')
assert response.status_code == 400
data = json.loads(response.data)
assert data['status'] == 'error'
class TestDeviceAPI:
"""设备管理API测试"""
@patch('device_manager.DeviceManager')
def test_device_status(self, mock_device_manager, client):
"""测试设备状态查询"""
# 模拟设备管理器
mock_instance = MagicMock()
mock_instance.get_device_status.return_value = {
'camera': {'connected': True, 'status': 'ready'},
'imu': {'connected': True, 'status': 'ready'},
'pressure': {'connected': True, 'status': 'ready'}
}
mock_device_manager.return_value = mock_instance
response = client.get('/api/devices/status')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'success'
assert 'devices' in data['data']
@patch('device_manager.DeviceManager')
def test_device_refresh(self, mock_device_manager, client):
"""测试设备刷新"""
mock_instance = MagicMock()
mock_instance.refresh_devices.return_value = True
mock_device_manager.return_value = mock_instance
response = client.post('/api/devices/refresh')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'success'
class TestDetectionAPI:
"""检测管理API测试"""
@patch('detection_engine.DetectionEngine')
def test_start_detection(self, mock_detection_engine, client):
"""测试开始检测"""
mock_instance = MagicMock()
mock_instance.start_session.return_value = 'session_123'
mock_detection_engine.return_value = mock_instance
detection_config = {
'patient_id': 1,
'duration': 60,
'recording': True,
'real_time_analysis': True
}
response = client.post('/api/detection/start',
data=json.dumps(detection_config),
content_type='application/json')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'success'
assert 'session_id' in data['data']
@patch('detection_engine.DetectionEngine')
def test_stop_detection(self, mock_detection_engine, client):
"""测试停止检测"""
mock_instance = MagicMock()
mock_instance.stop_session.return_value = True
mock_detection_engine.return_value = mock_instance
response = client.post('/api/detection/stop/session_123')
assert response.status_code == 200
data = json.loads(response.data)
assert data['status'] == 'success'
def test_start_detection_invalid_config(self, client):
"""测试使用无效配置开始检测"""
invalid_config = {
'patient_id': -1, # 无效患者ID
'duration': 0 # 无效持续时间
}
response = client.post('/api/detection/start',
data=json.dumps(invalid_config),
content_type='application/json')
assert response.status_code == 400
data = json.loads(response.data)
assert data['status'] == 'error'
class TestDatabaseManager:
"""数据库管理器测试"""
def test_create_patient(self, db):
"""测试创建患者"""
patient_data = {
'name': '测试患者',
'age': 30,
'gender': 'male',
'height': 175,
'weight': 70
}
patient_id = db.create_patient(patient_data)
assert patient_id is not None
assert isinstance(patient_id, int)
def test_get_patient(self, db):
"""测试获取患者信息"""
# 先创建患者
patient_data = {
'name': '测试患者',
'age': 30,
'gender': 'male'
}
patient_id = db.create_patient(patient_data)
# 获取患者信息
patient = db.get_patient(patient_id)
assert patient is not None
assert patient['name'] == '测试患者'
assert patient['age'] == 30
def test_get_nonexistent_patient(self, db):
"""测试获取不存在的患者"""
patient = db.get_patient(99999)
assert patient is None
def test_create_session(self, db):
"""测试创建检测会话"""
# 先创建患者
patient_data = {'name': '测试患者', 'age': 30, 'gender': 'male'}
patient_id = db.create_patient(patient_data)
# 创建会话
session_data = {
'patient_id': patient_id,
'duration': 60,
'recording': True,
'config': {'test': 'config'}
}
session_id = db.create_session(session_data)
assert session_id is not None
assert isinstance(session_id, str)
def test_update_session_status(self, db):
"""测试更新会话状态"""
# 创建患者和会话
patient_data = {'name': '测试患者', 'age': 30, 'gender': 'male'}
patient_id = db.create_patient(patient_data)
session_data = {
'patient_id': patient_id,
'duration': 60,
'recording': True
}
session_id = db.create_session(session_data)
# 更新状态
result = db.update_session_status(session_id, 'completed')
assert result is True
# 验证状态已更新
session = db.get_session(session_id)
assert session['status'] == 'completed'
if __name__ == '__main__':
pytest.main([__file__, '-v'])

521
backend/utils.py Normal file
View File

@ -0,0 +1,521 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工具模块
包含常用的工具函数配置管理和辅助功能
"""
import os
import json
import logging
import hashlib
import secrets
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Union
from pathlib import Path
import configparser
from functools import wraps
import time
logger = logging.getLogger(__name__)
class Config:
"""配置管理器"""
def __init__(self, config_file: str = 'config.ini'):
self.config_file = Path(config_file)
self.config = configparser.ConfigParser()
self._load_config()
def _load_config(self):
"""加载配置文件"""
try:
if self.config_file.exists():
self.config.read(self.config_file, encoding='utf-8')
logger.info(f'配置文件已加载: {self.config_file}')
else:
self._create_default_config()
logger.info('已创建默认配置文件')
except Exception as e:
logger.error(f'配置文件加载失败: {e}')
self._create_default_config()
def _create_default_config(self):
"""创建默认配置"""
try:
# 应用配置
self.config['APP'] = {
'name': 'Body Balance Evaluation System',
'version': '1.0.0',
'debug': 'false',
'log_level': 'INFO'
}
# 服务器配置
self.config['SERVER'] = {
'host': '127.0.0.1',
'port': '5000',
'cors_origins': '*'
}
# 数据库配置
self.config['DATABASE'] = {
'path': 'data/balance_system.db',
'backup_interval': '24', # 小时
'max_backups': '7'
}
# 设备配置
self.config['DEVICES'] = {
'camera_index': '0',
'camera_width': '640',
'camera_height': '480',
'camera_fps': '30',
'imu_port': 'COM3',
'pressure_port': 'COM4'
}
# 检测配置
self.config['DETECTION'] = {
'default_duration': '60', # 秒
'sampling_rate': '30', # Hz
'balance_threshold': '0.2',
'posture_threshold': '5.0' # 度
}
# 数据处理配置
self.config['DATA_PROCESSING'] = {
'filter_window': '5',
'outlier_threshold': '2.0',
'chart_dpi': '300',
'export_format': 'csv'
}
# 安全配置
self.config['SECURITY'] = {
'secret_key': secrets.token_hex(32),
'session_timeout': '3600', # 秒
'max_login_attempts': '5'
}
# 保存配置文件
self.save_config()
except Exception as e:
logger.error(f'默认配置创建失败: {e}')
def get(self, section: str, key: str, fallback: Any = None) -> str:
"""获取配置值"""
try:
return self.config.get(section, key, fallback=fallback)
except Exception as e:
logger.warning(f'配置获取失败 [{section}][{key}]: {e}')
return fallback
def getint(self, section: str, key: str, fallback: int = 0) -> int:
"""获取整数配置值"""
try:
return self.config.getint(section, key, fallback=fallback)
except Exception as e:
logger.warning(f'整数配置获取失败 [{section}][{key}]: {e}')
return fallback
def getfloat(self, section: str, key: str, fallback: float = 0.0) -> float:
"""获取浮点数配置值"""
try:
return self.config.getfloat(section, key, fallback=fallback)
except Exception as e:
logger.warning(f'浮点数配置获取失败 [{section}][{key}]: {e}')
return fallback
def getboolean(self, section: str, key: str, fallback: bool = False) -> bool:
"""获取布尔配置值"""
try:
return self.config.getboolean(section, key, fallback=fallback)
except Exception as e:
logger.warning(f'布尔配置获取失败 [{section}][{key}]: {e}')
return fallback
def set(self, section: str, key: str, value: str):
"""设置配置值"""
try:
if not self.config.has_section(section):
self.config.add_section(section)
self.config.set(section, key, str(value))
except Exception as e:
logger.error(f'配置设置失败 [{section}][{key}]: {e}')
def save_config(self):
"""保存配置文件"""
try:
# 确保目录存在
self.config_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
logger.info(f'配置文件已保存: {self.config_file}')
except Exception as e:
logger.error(f'配置文件保存失败: {e}')
class Logger:
"""日志管理器"""
@staticmethod
def setup_logging(log_level: str = 'INFO', log_file: str = None):
"""设置日志配置"""
try:
# 创建日志目录
if log_file:
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
# 配置日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 设置根日志器
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
# 清除现有处理器
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# 文件处理器
if log_file:
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
logger.info(f'日志系统已初始化,级别: {log_level}')
except Exception as e:
print(f'日志系统初始化失败: {e}')
class DataValidator:
"""数据验证器"""
@staticmethod
def validate_patient_data(data: Dict[str, Any]) -> Dict[str, Any]:
"""验证患者数据"""
errors = []
# 必填字段检查
required_fields = ['name', 'gender', 'birth_date']
for field in required_fields:
if not data.get(field):
errors.append(f'缺少必填字段: {field}')
# 姓名验证
if data.get('name'):
name = data['name'].strip()
if len(name) < 2 or len(name) > 50:
errors.append('姓名长度应在2-50个字符之间')
data['name'] = name
# 性别验证
if data.get('gender'):
if data['gender'] not in ['male', 'female', 'other']:
errors.append('性别值无效')
# 出生日期验证
if data.get('birth_date'):
try:
birth_date = datetime.fromisoformat(data['birth_date'].replace('Z', '+00:00'))
if birth_date > datetime.now():
errors.append('出生日期不能是未来时间')
if birth_date < datetime(1900, 1, 1):
errors.append('出生日期过早')
except ValueError:
errors.append('出生日期格式无效')
# 身高验证
if data.get('height'):
try:
height = float(data['height'])
if height < 50 or height > 250:
errors.append('身高应在50-250cm之间')
data['height'] = height
except (ValueError, TypeError):
errors.append('身高格式无效')
# 体重验证
if data.get('weight'):
try:
weight = float(data['weight'])
if weight < 10 or weight > 300:
errors.append('体重应在10-300kg之间')
data['weight'] = weight
except (ValueError, TypeError):
errors.append('体重格式无效')
# 电话验证
if data.get('phone'):
phone = data['phone'].strip()
if phone and not phone.replace('-', '').replace(' ', '').isdigit():
errors.append('电话号码格式无效')
data['phone'] = phone
return {
'valid': len(errors) == 0,
'errors': errors,
'data': data
}
@staticmethod
def validate_detection_config(config: Dict[str, Any]) -> Dict[str, Any]:
"""验证检测配置"""
errors = []
# 检测时长验证
if 'duration' in config:
try:
duration = int(config['duration'])
if duration < 10 or duration > 600:
errors.append('检测时长应在10-600秒之间')
config['duration'] = duration
except (ValueError, TypeError):
errors.append('检测时长格式无效')
# 采样频率验证
if 'sampling_rate' in config:
try:
rate = int(config['sampling_rate'])
if rate < 1 or rate > 100:
errors.append('采样频率应在1-100Hz之间')
config['sampling_rate'] = rate
except (ValueError, TypeError):
errors.append('采样频率格式无效')
# 录像设置验证
if 'record_video' in config:
if not isinstance(config['record_video'], bool):
errors.append('录像设置应为布尔值')
return {
'valid': len(errors) == 0,
'errors': errors,
'config': config
}
class SecurityUtils:
"""安全工具"""
@staticmethod
def generate_session_id() -> str:
"""生成会话ID"""
return secrets.token_urlsafe(32)
@staticmethod
def hash_password(password: str) -> str:
"""密码哈希"""
salt = secrets.token_hex(16)
password_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
return f"{salt}:{password_hash.hex()}"
@staticmethod
def verify_password(password: str, hashed: str) -> bool:
"""验证密码"""
try:
salt, password_hash = hashed.split(':')
return hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000).hex() == password_hash
except Exception:
return False
@staticmethod
def sanitize_filename(filename: str) -> str:
"""清理文件名"""
import re
# 移除或替换不安全的字符
filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
# 限制长度
if len(filename) > 255:
name, ext = os.path.splitext(filename)
filename = name[:255-len(ext)] + ext
return filename
class FileUtils:
"""文件工具"""
@staticmethod
def ensure_directory(path: Union[str, Path]):
"""确保目录存在"""
Path(path).mkdir(parents=True, exist_ok=True)
@staticmethod
def get_file_size(path: Union[str, Path]) -> int:
"""获取文件大小"""
try:
return Path(path).stat().st_size
except Exception:
return 0
@staticmethod
def clean_old_files(directory: Union[str, Path], max_age_days: int = 30):
"""清理旧文件"""
try:
directory = Path(directory)
if not directory.exists():
return
cutoff_time = datetime.now() - timedelta(days=max_age_days)
for file_path in directory.iterdir():
if file_path.is_file():
file_time = datetime.fromtimestamp(file_path.stat().st_mtime)
if file_time < cutoff_time:
file_path.unlink()
logger.info(f'已删除旧文件: {file_path}')
except Exception as e:
logger.error(f'清理旧文件失败: {e}')
@staticmethod
def backup_file(source: Union[str, Path], backup_dir: Union[str, Path] = None) -> Optional[Path]:
"""备份文件"""
try:
source = Path(source)
if not source.exists():
return None
if backup_dir is None:
backup_dir = source.parent / 'backups'
else:
backup_dir = Path(backup_dir)
backup_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_name = f"{source.stem}_{timestamp}{source.suffix}"
backup_path = backup_dir / backup_name
import shutil
shutil.copy2(source, backup_path)
logger.info(f'文件已备份: {source} -> {backup_path}')
return backup_path
except Exception as e:
logger.error(f'文件备份失败: {e}')
return None
class PerformanceMonitor:
"""性能监控器"""
def __init__(self):
self.metrics = {}
def start_timer(self, name: str):
"""开始计时"""
self.metrics[name] = {'start_time': time.time()}
def end_timer(self, name: str) -> float:
"""结束计时"""
if name in self.metrics and 'start_time' in self.metrics[name]:
duration = time.time() - self.metrics[name]['start_time']
self.metrics[name]['duration'] = duration
return duration
return 0.0
def get_metrics(self) -> Dict[str, Any]:
"""获取性能指标"""
return self.metrics.copy()
def reset_metrics(self):
"""重置指标"""
self.metrics.clear()
def timing_decorator(func):
"""计时装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
duration = time.time() - start_time
logger.debug(f'{func.__name__} 执行时间: {duration:.3f}')
return result
except Exception as e:
duration = time.time() - start_time
logger.error(f'{func.__name__} 执行失败 (耗时: {duration:.3f}秒): {e}')
raise
return wrapper
def retry_decorator(max_retries: int = 3, delay: float = 1.0):
"""重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
if attempt < max_retries:
logger.warning(f'{func.__name__}{attempt + 1}次尝试失败: {e}{delay}秒后重试')
time.sleep(delay)
else:
logger.error(f'{func.__name__} 所有重试均失败')
raise last_exception
return wrapper
return decorator
class ResponseFormatter:
"""响应格式化器"""
@staticmethod
def success(data: Any = None, message: str = 'Success') -> Dict[str, Any]:
"""成功响应"""
response = {
'success': True,
'message': message,
'timestamp': datetime.now().isoformat()
}
if data is not None:
response['data'] = data
return response
@staticmethod
def error(message: str, error_code: str = None, details: Any = None) -> Dict[str, Any]:
"""错误响应"""
response = {
'success': False,
'message': message,
'timestamp': datetime.now().isoformat()
}
if error_code:
response['error_code'] = error_code
if details:
response['details'] = details
return response
@staticmethod
def paginated(data: List[Any], page: int, page_size: int, total: int) -> Dict[str, Any]:
"""分页响应"""
return {
'success': True,
'data': data,
'pagination': {
'page': page,
'page_size': page_size,
'total': total,
'total_pages': (total + page_size - 1) // page_size
},
'timestamp': datetime.now().isoformat()
}
# 全局配置实例
config = Config()
# 性能监控实例
performance_monitor = PerformanceMonitor()

41
config.ini Normal file
View File

@ -0,0 +1,41 @@
[APP]
name = Body Balance Evaluation System
version = 1.0.0
debug = false
log_level = INFO
[SERVER]
host = 127.0.0.1
port = 5000
cors_origins = *
[DATABASE]
path = data/balance_system.db
backup_interval = 24
max_backups = 7
[DEVICES]
camera_index = 0
camera_width = 640
camera_height = 480
camera_fps = 30
imu_port = COM3
pressure_port = COM4
[DETECTION]
default_duration = 60
sampling_rate = 30
balance_threshold = 0.2
posture_threshold = 5.0
[DATA_PROCESSING]
filter_window = 5
outlier_threshold = 2.0
chart_dpi = 300
export_format = csv
[SECURITY]
secret_key = 79fcc4983d478c2ee672f3305d5e12c7c84fd1b58a18acb650e9f8125bfa805f
session_timeout = 3600
max_login_attempts = 5

278
config.json Normal file
View File

@ -0,0 +1,278 @@
{
"app": {
"name": "身体平衡评估系统",
"version": "1.0.0",
"description": "基于多传感器融合技术的专业平衡能力评估与分析系统",
"author": "身体平衡评估系统开发团队",
"language": "zh-CN",
"theme": "light",
"auto_start": false,
"auto_update": true,
"window": {
"width": 1200,
"height": 800,
"min_width": 1000,
"min_height": 600,
"resizable": true,
"center": true,
"show": true,
"frame": true,
"transparent": false,
"always_on_top": false
}
},
"server": {
"host": "127.0.0.1",
"port": 5000,
"debug": false,
"cors": {
"enabled": true,
"origins": ["http://localhost:5173", "http://127.0.0.1:5173"]
},
"ssl": {
"enabled": false,
"cert_file": "",
"key_file": ""
},
"rate_limit": {
"enabled": true,
"requests_per_minute": 100
}
},
"database": {
"type": "sqlite",
"path": "data/database.db",
"backup": {
"enabled": true,
"interval_hours": 24,
"max_backups": 7
},
"connection": {
"timeout": 30,
"pool_size": 10
}
},
"devices": {
"camera": {
"enabled": true,
"device_id": 0,
"resolution": {
"width": 1280,
"height": 720
},
"fps": 30,
"format": "MJPG",
"auto_exposure": true,
"brightness": 0,
"contrast": 0,
"saturation": 0,
"calibration": {
"enabled": false,
"matrix": null,
"distortion": null
}
},
"imu": {
"enabled": true,
"port": "COM3",
"baudrate": 115200,
"timeout": 1.0,
"sample_rate": 100,
"range": {
"accelerometer": 16,
"gyroscope": 2000,
"magnetometer": 4800
},
"calibration": {
"enabled": false,
"offset": {
"accel": [0, 0, 0],
"gyro": [0, 0, 0],
"mag": [0, 0, 0]
},
"scale": {
"accel": [1, 1, 1],
"gyro": [1, 1, 1],
"mag": [1, 1, 1]
}
}
},
"pressure": {
"enabled": true,
"port": "COM4",
"baudrate": 9600,
"timeout": 1.0,
"sample_rate": 50,
"sensors": 4,
"range": {
"min": 0,
"max": 1000
},
"calibration": {
"enabled": false,
"zero_offset": [0, 0, 0, 0],
"scale_factor": [1, 1, 1, 1]
}
}
},
"detection": {
"default_duration": 60,
"min_duration": 10,
"max_duration": 300,
"sample_rate": 30,
"recording": {
"enabled": true,
"format": "mp4",
"quality": "medium",
"fps": 30
},
"analysis": {
"real_time": true,
"pose_detection": {
"enabled": true,
"model": "mediapipe",
"confidence": 0.5,
"tracking": true
},
"balance_metrics": {
"cop_analysis": true,
"sway_analysis": true,
"stability_index": true,
"frequency_analysis": true
},
"filters": {
"low_pass": {
"enabled": true,
"cutoff": 10.0
},
"median": {
"enabled": true,
"window_size": 5
}
}
},
"alerts": {
"enabled": true,
"thresholds": {
"excessive_sway": 50.0,
"instability": 0.8,
"device_disconnect": 5.0
}
}
},
"data": {
"storage": {
"base_path": "data",
"patients_path": "data/patients",
"sessions_path": "data/sessions",
"exports_path": "data/exports",
"backups_path": "data/backups",
"temp_path": "temp",
"logs_path": "logs"
},
"cleanup": {
"enabled": true,
"temp_files_days": 7,
"log_files_days": 30,
"session_files_days": 365
},
"compression": {
"enabled": true,
"algorithm": "gzip",
"level": 6
},
"export": {
"formats": ["csv", "json", "pdf"],
"include_raw_data": true,
"include_analysis": true,
"include_charts": true
}
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": {
"enabled": true,
"path": "logs/app.log",
"max_size": "10MB",
"backup_count": 5,
"rotation": "daily"
},
"console": {
"enabled": true,
"level": "INFO"
},
"modules": {
"flask": "WARNING",
"werkzeug": "WARNING",
"urllib3": "WARNING",
"matplotlib": "WARNING"
}
},
"security": {
"session": {
"timeout_minutes": 60,
"secret_key": "your-secret-key-here",
"secure_cookies": false
},
"api": {
"rate_limiting": true,
"request_timeout": 30,
"max_request_size": "100MB"
},
"data": {
"encryption": false,
"backup_encryption": false,
"anonymization": {
"enabled": false,
"fields": ["name", "phone", "email"]
}
}
},
"ui": {
"language": "zh-CN",
"theme": "light",
"animations": true,
"sound_effects": true,
"notifications": {
"enabled": true,
"position": "top-right",
"duration": 5000
},
"charts": {
"default_type": "line",
"colors": {
"primary": "#409EFF",
"success": "#67C23A",
"warning": "#E6A23C",
"danger": "#F56C6C",
"info": "#909399"
},
"animation_duration": 1000
},
"table": {
"page_size": 20,
"show_pagination": true,
"sortable": true,
"filterable": true
}
},
"performance": {
"monitoring": {
"enabled": true,
"interval_seconds": 60,
"metrics": ["cpu", "memory", "disk", "network"]
},
"optimization": {
"image_compression": true,
"data_caching": true,
"lazy_loading": true,
"batch_processing": true
},
"limits": {
"max_concurrent_sessions": 5,
"max_file_size_mb": 100,
"max_session_duration_minutes": 300
}
}
}

View File

@ -0,0 +1,99 @@
# 平衡体态检测系统 UI 与交互功能设计说明
## 1. 总体设计理念
- **以用户为中心**:以康复医生和技术操作员为核心用户,界面简洁直观。
- **数据驱动展示**:所有核心模块数据实时可视化,交互响应快速。
- **操作流程明晰**:每一步操作清晰可控,避免误操作。
---
## 2. 页面结构与交互设计
### 2.1 登录与注册页面
#### 功能点
- 用户名密码登录,明文匹配;
- 注册要求:手机号、密码二次确认;
- 明文密码显示与记住密码;
- 密码找回:显示明文密码(需优化为短信验证);
- 修改密码支持原密码校验更新。
#### 交互设计
- 所有操作均本地数据库存储;
- 弹窗提示错误、成功状态;
- 安全建议:建议后续版本启用密码加密存储。
---
### 2.2 实时检测模块
#### 起始页
- 左侧:患者列表(支持搜索与滚动加载);
- 右上:基础信息展示与编辑;
- 右下:新建档案 / 查看档案 / 开始检测按钮。
#### 新建档案页
- 信息采集:*为必填项,实时校验格式;
- 按钮:
- “退出”:弹窗确认;
- “保存”:保存数据;
- “保存并开始检测”:保存+跳转检测页。
#### 患者档案页
- 展示所有检测记录,支持:
- 测量数据图文并茂;
- 操作过程视频+录屏回放;
- 查看足底垫片配置(左右足 + 多个垫片,记录颜色/编号);
- 截图列表(放大预览、删除、查看截图数据、导出/打印)。
---
### 2.3 检测页设计
#### 实时检测主界面
- 显示患者信息、时间、医生名称;
- 数据区:
- 身体姿态:深度图动态更新;
- 头部IMU实时Pitch/Yaw/Roll值 + 最大值记录 + 清零 + 抗干扰;
- 足底压力:压力云图 + 数据占比展示;
- 视频区:录制+时间戳+最大化播放。
#### 操作按钮区
- 初始状态:显示“开始”;
- 开始后:显示“截图”“结束”;
- 截图:保存界面+对应数据+截图图片;
- 结束:终止录制,保存检测记录;
- 结束后:显示“回放”;
- 支持视频同步播放、暂停、最大化、进度条拖动。
#### 分屏模式
- 提供“分屏”按钮;
- 支持全显3模块/单模块切换(滑动或左右切换按钮)。
---
## 3. 附加功能说明
### 截图管理
- 入口:截图按钮 + 截图列表;
- 功能:图片放大预览、删除、查看数据详情、导出/打印。
### 数据管理
- 所有数据按患者、检测时间分类;
- 支持历史对比分析V2.0 实现);
- 自动生成PDF报告带医生备注
---
## 4. 优化建议基于V1.2
- 密码不应明文存储应采用hash加密
- 注册/找回密码增加验证码机制;
- 支持多用户权限(医生、操作员、管理员);
- 后续建议支持远程数据云同步;
- 视频与数据录制时钟同步机制需保证精准性;
- 多检测记录对比模块应预留逻辑框架。

View File

@ -0,0 +1,331 @@
# 平衡体态检测系统 UI 与交互设计说明
## 总体设计理念
- **以用户为中心**:以康复医生和技术操作员为核心用户,界面简洁直观
- **数据驱动展示**:所有核心模块数据实时可视化,交互响应快速
- **操作流程明晰**:每一步操作清晰可控,避免误操作
---
## 1. 登录与注册页面
![登录页](登录页.png)
### 页面结构
- 顶部系统Logo与名称
- 中部:账号输入框、密码输入框、登录按钮
- 底部:忘记密码、注册入口
### 主要功能
- **登录功能**:用户名密码登录,明文匹配
- **注册功能**:手机号、密码二次确认
- **密码管理**:明文密码显示与记住密码
- **密码找回**:显示明文密码(建议后续版本优化为短信验证)
- **修改密码**:支持原密码校验更新
### 交互说明
- 输入框获得焦点高亮
- 登录按钮点击后显示加载状态
- 错误提示信息弹出或红色提示
- 所有操作均本地数据库存储
- 弹窗提示错误、成功状态
### 组件建议
- Input输入框
- Button按钮
- Message提示组件
- Modal弹窗
### 安全建议
- 建议后续版本启用密码加密存储
- 增加验证码机制
- 支持多用户权限(医生、操作员、管理员)
---
## 2. 实时检测模块
### 2.1 起始页
![登录进入的起始页](登录进入的起始页.png)
#### 页面结构
- **左侧**:功能快速入口("检测、人员、系统"
- **右侧顶部**用户信息栏、系统Logo
- **右侧中部**:患者列表、选中患者基础信息展示
- **操作按钮**:粉色鲜亮按钮("查看档案、检测、新患者建档"
#### 主要功能
- 患者列表管理(支持搜索与滚动加载)
- 基础信息展示与编辑
- 快速进入各功能模块
#### 交互说明
- 患者列表可滚动显示,可选中,可输入用户名查询
- 选中患者可显示基础信息,可点击编辑图标进行信息编辑
- 按钮高亮显示,点击跳转对应功能页面
#### 组件建议
- List列表组件
- Search搜索框
- Card卡片
- Button按钮
- Avatar头像
### 2.2 新建档案页
#### 页面结构
- 表单区域:患者基础信息录入
- 操作按钮区:退出、保存、保存并开始检测
#### 主要功能
- **信息采集***为必填项,实时校验格式
- **数据保存**:本地数据库存储
- **快速检测**:保存后直接跳转检测页
#### 交互说明
- **"退出"按钮**:弹窗确认是否保存
- **"保存"按钮**:保存数据并返回列表
- **"保存并开始检测"按钮**:保存数据并跳转检测页
- 必填项校验,错误提示
#### 组件建议
- Form表单
- Input输入框
- Select下拉选择
- DatePicker日期选择
- Button按钮
- Validation校验
### 2.3 患者档案页
#### 页面结构
- 患者基础信息展示区
- 检测记录列表
- 详细数据查看区
#### 主要功能
- **检测记录展示**:所有历史检测记录
- **数据可视化**:测量数据图文并茂展示
- **视频回放**:操作过程视频+录屏回放
- **足底垫片配置**:左右足+多个垫片,记录颜色/编号
- **截图管理**:截图列表(放大预览、删除、查看截图数据、导出/打印)
#### 交互说明
- 检测记录按时间排序,支持筛选
- 点击记录查看详细数据
- 视频支持播放控制
- 截图支持放大预览和批量操作
#### 组件建议
- Table表格
- Timeline时间轴
- Video视频播放器
- Image图片预览
- Chart图表
- Export导出功能
---
## 3. 检测页设计
### 3.1 实时检测主界面
![姿态检测页](姿态检测页.png)
#### 页面结构
- **左侧**:快捷操作入口
- **顶部信息栏**:患者信息、检测时间、医生名称
- **数据展示区**
- 身体姿态:深度图动态更新
- 头部IMU实时Pitch/Yaw/Roll值+最大值记录+清零+抗干扰
- 足底压力:压力云图+数据占比展示
- 视频区:录制+时间戳+最大化播放
#### 主要功能
- **实时数据监控**:三大模块数据同步显示
- **视频录制**:同步录制检测过程
- **数据记录**:实时保存检测数据
- **异常监测**:数据异常自动提示
#### 交互说明
- 数据实时刷新,响应速度<100ms
- 视频支持最大化播放
- 异常情况弹窗提示
- 支持数据清零重置
#### 组件建议
- Canvas画布深度图、压力云图
- Video视频组件
- Progress进度条
- Modal弹窗
- Chart实时图表
### 3.2 操作按钮区
#### 按钮状态管理
- **初始状态**:显示"开始"按钮
- **检测中状态**:显示"截图""结束"按钮
- **结束后状态**:显示"回放"按钮
#### 功能说明
- **开始按钮**:启动检测,开始数据采集和视频录制
- **截图按钮**:保存当前界面+对应数据+截图图片
- **结束按钮**:终止录制,保存检测记录
- **回放按钮**:支持视频同步播放、暂停、最大化、进度条拖动
#### 交互说明
- 按钮状态根据检测流程自动切换
- 截图操作即时响应,保存成功提示
- 结束检测需二次确认
- 回放支持全屏模式
### 3.3 分屏模式
#### 功能设计
- 提供"分屏"切换按钮
- 支持全显3模块/单模块切换
- 单模块模式支持滑动或左右切换按钮
#### 交互说明
- 分屏切换动画流畅
- 单模块模式数据更大更清晰
- 支持快速切换不同模块
#### 组件建议
- Tabs标签页
- Swiper滑动组件
- Button切换按钮
---
## 4. 检测中的录屏界面
![开始检测中的录屏界面](开始检测中的录屏界面.png)
### 页面结构
- **左侧**:快捷操作入口
- **右侧顶部**:录制状态、时长显示、结束、截图按钮
- **右侧中部**:检测结果实时展示区
- **状态栏**:录制时长、数据同步状态
### 主要功能
- **实时录制**:同步录制检测过程和数据
- **时长显示**:显示录制时长与状态
- **截图功能**:实时截图保存
- **数据同步**:视频与数据时钟同步
### 交互说明
- 录制中按钮高亮显示
- 录制时长实时更新
- 截图操作不影响录制
- 录制结束弹窗确认保存
- 支持录制过程中查看实时数据
### 组件建议
- Timer计时器
- Button按钮
- Notification通知
- StatusBar状态栏
- ProgressBar进度条
---
## 5. 附加功能模块
### 5.1 截图管理
#### 功能入口
- 检测页面截图按钮
- 患者档案截图列表
#### 主要功能
- **图片管理**:截图列表展示
- **预览功能**:图片放大预览
- **数据关联**:查看截图对应的检测数据
- **导出打印**:支持单张或批量导出/打印
- **删除管理**:支持单张或批量删除
#### 交互说明
- 缩略图网格展示
- 点击放大预览
- 支持键盘快捷键操作
- 批量操作需确认提示
### 5.2 数据管理
#### 数据分类
- 按患者分类管理
- 按检测时间排序
- 支持标签分类
#### 功能特性
- **历史对比**多次检测数据对比分析V2.0实现)
- **报告生成**自动生成PDF报告带医生备注
- **数据导出**:支持多种格式导出
- **备份恢复**:数据备份与恢复功能
#### 交互说明
- 数据列表支持多维度筛选
- 对比分析支持图表展示
- 报告生成支持模板自定义
- 导出进度实时显示
---
---
## 6. 设计规范与建议
### 6.1 视觉设计
- **配色方案**:统一黑色科技风格,突出专业感
- **布局原则**:保持页面简洁,突出核心功能区域
- **字体规范**:统一字体族,层次分明
- **图标设计**:简洁明了,符合医疗设备使用习惯
### 6.2 交互设计
- **响应速度**:界面响应时间<100ms
- **动画效果**:简洁流畅,提升用户体验
- **反馈机制**:操作即时反馈,状态清晰可见
- **容错设计**:重要操作需二次确认
### 6.3 技术建议
- **数据同步**:视频与数据录制时钟同步机制需保证精准性
- **性能优化**:大数据量处理优化,确保实时性
- **扩展性**:预留多检测记录对比模块逻辑框架
- **安全性**:密码加密存储,数据传输安全
### 6.4 优化建议基于V1.2
- 密码不应明文存储应采用hash加密
- 注册/找回密码增加验证码机制
- 支持多用户权限(医生、操作员、管理员)
- 后续建议支持远程数据云同步
- 视频与数据录制时钟同步机制需保证精准性
- 多检测记录对比模块应预留逻辑框架
---
## 7. 开发实施建议
### 7.1 开发优先级
1. **核心功能**:登录、患者管理、实时检测
2. **数据管理**:截图、录制、数据保存
3. **扩展功能**:报告生成、数据对比
4. **优化功能**:性能优化、安全加固
### 7.2 技术栈建议
- **前端框架**React/Vue.js + Electron
- **UI组件库**Ant Design / Element Plus
- **图表库**ECharts / D3.js
- **视频处理**WebRTC / FFmpeg
- **数据库**SQLite本地+ 云端备份
### 7.3 测试建议
- **功能测试**:覆盖所有用户操作流程
- **性能测试**:实时数据处理性能验证
- **兼容性测试**:多设备、多分辨率适配
- **用户体验测试**:医生和操作员实际使用反馈
如需进一步细化任何模块的详细设计,请提供具体需求。

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,150 @@
# 平衡体态检测系统 - 软件开发技术方案Electron + PythonWindows单机版
## 一、项目目标
构建一套适用于康复医学、运动健康等场景的 **单机本地运行型体态检测系统**,具备图形界面、设备数据采集、实时可视化、视频记录、数据管理与导出等能力。用户可在 Windows 系统中一键安装、双击运行,无需联网与额外配置。
---
## 二、系统架构与技术栈
### 2.1 总体架构图
```
+---------------------------+
| Electron 桌面客户端 |
| +----------------------+ |
| | Vue3/React 前端界面 | |
| +----------------------+ |
| ⇅ API调用 |
| +----------------------+ |
| | Python 后端服务Flask|
| +----------------------+ |
+---------------------------+
⇅ 硬件通信
+---------------------------+
| 深度相机 / IMU / 足底压力板 |
+---------------------------+
```
### 2.2 技术选型
| 层级 | 技术选型 | 说明 |
|------|----------|------|
| 前端界面 | Electron + Vue3 / React | 构建跨平台 GUI 桌面应用 |
| 后端服务 | Python Flask / FastAPI | 提供设备控制、数据处理、本地API |
| 本地数据库 | SQLite | 嵌入式数据库,无需独立部署 |
| 视频处理 | FFmpeg | 实现屏幕与摄像视频录制 |
| 打包工具 | electron-builder | 生成 Windows `.exe` 安装包 |
| Python打包 | PyInstaller | 将 Python 服务打包为 `.exe` 可执行程序 |
---
## 三、模块功能与实现
### 3.1 Electron 主程序
- 负责显示用户界面(患者档案、检测流程、视频回放)
- 在启动时自动调用后端 Python `.exe` 服务
- 与本地后端通信HTTP API
- 控制设备数据展示与交互逻辑
### 3.2 Python 后端服务
- 使用 Flask/FastAPI 提供本地接口127.0.0.1:5000
- 管理设备连接、数据采集、滤波处理、存储与导出
- 可扩展实现深度图处理、压力图分析、IMU姿态分析等功能
### 3.3 视频录制模块
- 利用 FFmpeg 实现屏幕+外部摄像头录制
- 支持时间戳叠加、截图保存、同步回放
- 可打包进 Electron 安装目录中统一调用
---
## 四、部署方式与启动流程
### 4.1 打包流程
1. 将 Python 服务使用 PyInstaller 打包:
```bash
pyinstaller -F -n app backend/app.py
```
2. Electron 主进程中启动 Python 服务:
```js
const { spawn } = require("child_process");
const scriptPath = path.join(__dirname, "backend", "app.exe");
const pythonProcess = spawn(scriptPath, { detached: true, stdio: 'ignore' });
pythonProcess.unref();
```
3. 使用 electron-builder 打包为安装包:
```bash
npm run build
electron-builder --win --x64
```
### 4.2 安装后运行流程(用户视角)
- 用户双击安装包 `.exe` 安装程序;
- 桌面自动生成快捷方式;
- 双击运行程序:
- 启动 Electron 界面;
- 自动后台启动 Python 服务;
- 自动检测设备连接;
- 进入检测界面,开始数据采集。
---
## 五、目录结构(部署后)
```
体态检测系统/
├─ main.exe # 主程序Electron 打包)
├─ backend/
│ ├─ app.exe # Python 后端打包文件
│ └─ model/ # AI模型文件可选
├─ ffmpeg/ # 视频录制工具
├─ resources/ # 前端界面构建产物
├─ logs/ # 日志输出目录
└─ uninstall.exe # 卸载程序
```
---
## 六、安全性与稳定性设计
| 项目 | 说明 |
|------|------|
| 端口限制 | 后端服务仅监听 127.0.0.1,防止外部访问 |
| 自动重启 | Electron 检测后端未启动可尝试自动重启 |
| 日志记录 | 所有错误输出写入 logs 目录,便于故障排查 |
| 安全存储 | 用户/患者数据保存至 SQLite本地隔离存储 |
| 进程管理 | Python 服务进程随 Electron 一起退出或独立运行 |
---
## 七、后续拓展建议
| 版本 | 升级方向 |
|------|----------|
| V2.0 | 加入关键点识别、自动评估报告导出、双次记录对比 |
| V3.0 | 云端同步、在线远程管理、多终端协同 |
| V4.0 | 移动版Android/iOS测量与数据同步 |
---
## 八、推荐工具与资源
- Electron 官网:[https://www.electronjs.org/](https://www.electronjs.org/)
- electron-builder[https://www.electron.build/](https://www.electron.build/)
- Flask[https://flask.palletsprojects.com/](https://flask.palletsprojects.com/)
- PyInstaller[https://pyinstaller.org/](https://pyinstaller.org/)
- FFmpeg[https://ffmpeg.org/](https://ffmpeg.org/)
---
> 本技术方案支持通过 AI 代码生成平台辅助开发,可作为 ChatGPT Copilot、CodeWhisperer 等平台的 Prompt 基础文件。

Binary file not shown.

168
frontend/src/main/main.js Normal file
View File

@ -0,0 +1,168 @@
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
const axios = require('axios');
const log = require('electron-log');
let mainWindow;
let pythonProcess;
const BACKEND_PORT = 5000;
const BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`;
// 配置日志
log.transports.file.resolvePathFn = () => path.join(app.getPath('userData'), 'logs', 'main.log');
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, '../../assets/icon.ico'),
show: false
});
// 开发环境加载本地服务器,生产环境加载打包后的文件
const isDev = process.env.NODE_ENV === 'development';
if (isDev) {
mainWindow.loadURL('http://localhost:3000');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/dist/index.html'));
}
mainWindow.once('ready-to-show', () => {
mainWindow.show();
startPythonBackend();
});
mainWindow.on('closed', () => {
mainWindow = null;
stopPythonBackend();
});
}
function startPythonBackend() {
try {
const isDev = process.env.NODE_ENV === 'development';
let scriptPath;
if (isDev) {
// 开发环境直接运行Python脚本
scriptPath = path.join(__dirname, '../../backend/app.py');
pythonProcess = spawn('python', [scriptPath], {
stdio: ['pipe', 'pipe', 'pipe']
});
} else {
// 生产环境运行打包后的exe
scriptPath = path.join(process.resourcesPath, 'backend', 'app.exe');
pythonProcess = spawn(scriptPath, {
stdio: ['pipe', 'pipe', 'pipe']
});
}
log.info('启动Python后端服务:', scriptPath);
pythonProcess.stdout.on('data', (data) => {
log.info('Python输出:', data.toString());
});
pythonProcess.stderr.on('data', (data) => {
log.error('Python错误:', data.toString());
});
pythonProcess.on('close', (code) => {
log.info(`Python进程退出代码: ${code}`);
});
// 等待后端启动
setTimeout(() => {
checkBackendHealth();
}, 3000);
} catch (error) {
log.error('启动Python后端失败:', error);
dialog.showErrorBox('启动失败', '无法启动后端服务,请检查安装是否完整');
}
}
function stopPythonBackend() {
if (pythonProcess) {
pythonProcess.kill();
pythonProcess = null;
log.info('Python后端服务已停止');
}
}
async function checkBackendHealth() {
try {
const response = await axios.get(`${BACKEND_URL}/health`, { timeout: 5000 });
if (response.status === 200) {
log.info('后端服务健康检查通过');
mainWindow.webContents.send('backend-ready', true);
}
} catch (error) {
log.error('后端服务健康检查失败:', error.message);
mainWindow.webContents.send('backend-ready', false);
// 尝试重启后端
setTimeout(() => {
log.info('尝试重启后端服务');
stopPythonBackend();
startPythonBackend();
}, 5000);
}
}
// IPC 通信处理
ipcMain.handle('get-backend-url', () => {
return BACKEND_URL;
});
ipcMain.handle('check-backend-health', async () => {
try {
const response = await axios.get(`${BACKEND_URL}/health`, { timeout: 3000 });
return response.status === 200;
} catch (error) {
return false;
}
});
ipcMain.handle('restart-backend', () => {
stopPythonBackend();
setTimeout(() => {
startPythonBackend();
}, 1000);
});
// 应用事件处理
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
stopPythonBackend();
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.on('before-quit', () => {
stopPythonBackend();
});
// 处理未捕获的异常
process.on('uncaughtException', (error) => {
log.error('未捕获的异常:', error);
});
process.on('unhandledRejection', (reason, promise) => {
log.error('未处理的Promise拒绝:', reason);
});

View File

@ -0,0 +1,32 @@
const { contextBridge, ipcRenderer } = require('electron');
// 向渲染进程暴露安全的API
contextBridge.exposeInMainWorld('electronAPI', {
// 获取后端服务URL
getBackendUrl: () => ipcRenderer.invoke('get-backend-url'),
// 检查后端服务健康状态
checkBackendHealth: () => ipcRenderer.invoke('check-backend-health'),
// 重启后端服务
restartBackend: () => ipcRenderer.invoke('restart-backend'),
// 监听后端就绪状态
onBackendReady: (callback) => {
ipcRenderer.on('backend-ready', (event, isReady) => {
callback(isReady);
});
},
// 移除监听器
removeAllListeners: (channel) => {
ipcRenderer.removeAllListeners(channel);
}
});
// 暴露日志API
contextBridge.exposeInMainWorld('logAPI', {
info: (message) => console.log('[INFO]', message),
error: (message) => console.error('[ERROR]', message),
warn: (message) => console.warn('[WARN]', message)
});

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>平衡体态检测系统</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
#app {
height: 100vh;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
font-size: 18px;
}
</style>
</head>
<body>
<div id="app">
<div class="loading">
<div>正在加载平衡体态检测系统...</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
{
"name": "body-balance-renderer",
"version": "1.0.0",
"description": "平衡体态检测系统前端界面",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"pinia": "^2.1.6",
"element-plus": "^2.3.9",
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.5.0",
"echarts": "^5.4.3",
"vue-echarts": "^6.6.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.4",
"vite": "^4.4.9",
"sass": "^1.66.1"
}
}

View File

@ -0,0 +1,14 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup>
</script>
<style scoped>
#app {
height: 100vh;
}
</style>

View File

@ -0,0 +1,22 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import App from './App.vue';
import router from './router';
import './style.css';
const app = createApp(App);
const pinia = createPinia();
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(pinia);
app.use(router);
app.use(ElementPlus);
app.mount('#app');

View File

@ -0,0 +1,94 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/index'
import Login from '../views/Login.vue'
import Dashboard from '../views/Dashboard.vue'
import PatientCreate from '../views/PatientCreate.vue'
import PatientProfile from '../views/PatientProfile.vue'
import Detection from '../views/Detection.vue'
import Recording from '../views/Recording.vue'
const routes = [
{
path: '/login',
name: 'Login',
component: Login,
meta: { title: '登录 - 平衡体态检测系统' }
},
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: { title: '仪表板 - 平衡体态检测系统', requiresAuth: true }
},
{
path: '/patient/create',
name: 'PatientCreate',
component: PatientCreate,
meta: { title: '新建患者档案 - 平衡体态检测系统', requiresAuth: true }
},
{
path: '/patient/:id',
name: 'PatientProfile',
component: PatientProfile,
meta: { title: '患者档案 - 平衡体态检测系统', requiresAuth: true }
},
{
path: '/detection/:id',
name: 'Detection',
component: Detection,
meta: { title: '姿态检测 - 平衡体态检测系统', requiresAuth: true }
},
{
path: '/recording/:id',
name: 'Recording',
component: Recording,
meta: { title: '录屏回放 - 平衡体态检测系统', requiresAuth: true }
},
{
path: '/detection/replay/:id',
name: 'DetectionReplay',
component: Recording,
meta: { title: '检测回放 - 平衡体态检测系统', requiresAuth: true }
}
];
const router = createRouter({
history: createWebHistory(),
routes
})
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = to.meta.title
}
const authStore = useAuthStore()
// 检查是否需要认证
if (to.meta.requiresAuth) {
if (!authStore.isAuthenticated) {
// 未登录,重定向到登录页
next({ name: 'Login' })
return
}
// 验证token有效性
const isValid = await authStore.verifyToken()
if (!isValid) {
next({ name: 'Login' })
return
}
}
// 如果已登录用户访问登录页,重定向到仪表板
if (to.name === 'Login' && authStore.isAuthenticated) {
next({ name: 'Dashboard' })
return
}
next()
})
export default router

View File

@ -0,0 +1,592 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const api = axios.create({
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
api.interceptors.request.use(
config => {
// 动态设置baseURL
if (window.electronAPI) {
config.baseURL = window.electronAPI.getBackendUrl()
} else {
config.baseURL = 'http://127.0.0.1:5000'
}
// 添加时间戳防止缓存
if (config.method === 'get') {
config.params = {
...config.params,
_t: Date.now()
}
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
response => {
return response.data
},
error => {
let message = '请求失败'
if (error.response) {
// 服务器响应错误
const { status, data } = error.response
switch (status) {
case 400:
message = data.message || '请求参数错误'
break
case 401:
message = '未授权访问'
break
case 403:
message = '禁止访问'
break
case 404:
message = '请求的资源不存在'
break
case 500:
message = '服务器内部错误'
break
default:
message = data.message || `请求失败 (${status})`
}
} else if (error.request) {
// 网络错误
message = '网络连接失败,请检查后端服务是否启动'
} else {
// 其他错误
message = error.message || '未知错误'
}
ElMessage.error(message)
return Promise.reject(error)
}
)
// 系统API
export const systemAPI = {
// 健康检查
health() {
return api.get('/health')
},
// 获取系统信息
getSystemInfo() {
return api.get('/api/system/info')
},
// 获取系统状态
getSystemStatus() {
return api.get('/api/system/status')
}
}
// 设备API
export const deviceAPI = {
// 获取设备状态
getDeviceStatus() {
return api.get('/api/devices/status')
},
// 刷新设备
refreshDevices() {
return api.post('/api/devices/refresh')
},
// 校准设备
calibrateDevice(deviceType) {
return api.post(`/api/devices/${deviceType}/calibrate`)
},
// 测试设备
testDevice(deviceType) {
return api.post(`/api/devices/${deviceType}/test`)
}
}
// 用户认证API
export const authAPI = {
// 用户登录
login(credentials) {
return api.post('/api/auth/login', credentials)
},
// 用户注册
register(userData) {
return api.post('/api/auth/register', userData)
},
// 忘记密码
forgotPassword(email) {
return api.post('/api/auth/forgot-password', { email })
},
// 重置密码
resetPassword(token, newPassword) {
return api.post('/api/auth/reset-password', { token, password: newPassword })
},
// 退出登录
logout() {
return api.post('/api/auth/logout')
},
// 验证token
verifyToken() {
return api.get('/api/auth/verify')
}
}
// 患者API
export const patientAPI = {
// 获取患者列表
getPatients(params = {}) {
return api.get('/api/patients', { params })
},
// 获取患者详情
getPatient(id) {
return api.get(`/api/patients/${id}`)
},
// 根据ID获取患者别名方法
getById(id) {
return this.getPatient(id)
},
// 创建患者
createPatient(data) {
return api.post('/api/patients', data)
},
// 创建患者(别名方法)
create(data) {
return this.createPatient(data)
},
// 更新患者
updatePatient(id, data) {
return api.put(`/api/patients/${id}`, data)
},
// 更新患者(别名方法)
update(id, data) {
return this.updatePatient(id, data)
},
// 删除患者
deletePatient(id) {
return api.delete(`/api/patients/${id}`)
},
// 搜索患者
searchPatients(query) {
return api.get('/api/patients/search', { params: { q: query } })
},
// 获取患者统计信息
getStatistics() {
return api.get('/api/patients/statistics')
}
}
// 检测API
export const detectionAPI = {
// 开始检测
startDetection(config) {
return api.post('/api/detection/start', config)
},
// 停止检测
stopDetection(sessionId) {
return api.post(`/api/detection/${sessionId}/stop`)
},
// 获取检测状态
getDetectionStatus(sessionId) {
return api.get(`/api/detection/${sessionId}/status`)
},
// 获取实时数据
getRealTimeData(sessionId) {
return api.get(`/api/detection/${sessionId}/realtime`)
},
// 获取检测结果
getDetectionResults(sessionId) {
return api.get(`/api/detection/${sessionId}/results`)
},
// 保存检测结果
saveDetectionResults(sessionId, data) {
return api.post(`/api/detection/${sessionId}/save`, data)
},
// 创建检测记录
create(data) {
return api.post('/api/detection', data)
},
// 获取检测记录列表
getList(params = {}) {
return api.get('/api/detection', { params })
},
// 根据患者ID获取检测记录
getByPatient(patientId) {
return api.get(`/api/patients/${patientId}/detections`)
}
}
// 录制API
export const recordingAPI = {
// 创建录制
create(data) {
return api.post('/api/recordings', data)
},
// 获取录制列表
getList(params = {}) {
return api.get('/api/recordings', { params })
},
// 根据患者ID获取录制
getByPatient(patientId) {
return api.get(`/api/patients/${patientId}/recordings`)
},
// 获取录制详情
getById(id) {
return api.get(`/api/recordings/${id}`)
},
// 更新录制
update(id, data) {
return api.put(`/api/recordings/${id}`, data)
},
// 删除录制
delete(id) {
return api.delete(`/api/recordings/${id}`)
},
// 上传录制文件
upload(file, onProgress) {
const formData = new FormData()
formData.append('file', file)
return api.post('/api/recordings/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
if (onProgress) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
onProgress(percentCompleted)
}
}
})
}
}
// 会话API
export const sessionAPI = {
// 获取会话列表
getSessions(params = {}) {
return api.get('/api/sessions', { params })
},
// 获取会话详情
getSession(id) {
return api.get(`/api/sessions/${id}`)
},
// 删除会话
deleteSession(id) {
return api.delete(`/api/sessions/${id}`)
},
// 获取患者的会话历史
getPatientSessions(patientId, params = {}) {
return api.get(`/api/patients/${patientId}/sessions`, { params })
}
}
// 数据分析API
export const analysisAPI = {
// 分析会话数据
analyzeSession(sessionId) {
return api.post(`/api/analysis/session/${sessionId}`)
},
// 获取分析结果
getAnalysisResult(sessionId) {
return api.get(`/api/analysis/session/${sessionId}/result`)
},
// 比较多个会话
compareSessions(sessionIds) {
return api.post('/api/analysis/compare', { session_ids: sessionIds })
},
// 获取统计数据
getStatistics(params = {}) {
return api.get('/api/analysis/statistics', { params })
},
// 生成趋势报告
generateTrendReport(patientId, params = {}) {
return api.post(`/api/analysis/trend/${patientId}`, params)
}
}
// 报告API
export const reportAPI = {
// 生成PDF报告
generatePDFReport(sessionId) {
return api.post(`/api/reports/pdf/${sessionId}`, {}, {
responseType: 'blob'
})
},
// 导出数据
exportData(sessionId, format = 'csv') {
return api.get(`/api/reports/export/${sessionId}`, {
params: { format },
responseType: 'blob'
})
},
// 批量导出
batchExport(sessionIds, format = 'csv') {
return api.post('/api/reports/batch-export', {
session_ids: sessionIds,
format
}, {
responseType: 'blob'
})
},
// 获取报告列表
getReports(params = {}) {
return api.get('/api/reports', { params })
},
// 删除报告
deleteReport(reportId) {
return api.delete(`/api/reports/${reportId}`)
}
}
// 设置API
export const settingsAPI = {
// 获取系统设置
getSettings() {
return api.get('/api/settings')
},
// 更新系统设置
updateSettings(settings) {
return api.put('/api/settings', settings)
},
// 重置设置
resetSettings() {
return api.post('/api/settings/reset')
},
// 备份设置
backupSettings() {
return api.post('/api/settings/backup', {}, {
responseType: 'blob'
})
},
// 恢复设置
restoreSettings(file) {
const formData = new FormData()
formData.append('file', file)
return api.post('/api/settings/restore', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
}
// WebSocket连接管理
export class WebSocketManager {
constructor() {
this.socket = null
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
this.reconnectInterval = 3000
this.listeners = new Map()
}
connect(url) {
try {
this.socket = new WebSocket(url)
this.socket.onopen = () => {
console.log('WebSocket连接已建立')
this.reconnectAttempts = 0
this.emit('connected')
}
this.socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.emit('message', data)
// 根据消息类型分发
if (data.type) {
this.emit(data.type, data.payload)
}
} catch (error) {
console.error('WebSocket消息解析失败:', error)
}
}
this.socket.onclose = () => {
console.log('WebSocket连接已关闭')
this.emit('disconnected')
this.attemptReconnect()
}
this.socket.onerror = (error) => {
console.error('WebSocket错误:', error)
this.emit('error', error)
}
} catch (error) {
console.error('WebSocket连接失败:', error)
this.emit('error', error)
}
}
disconnect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data))
} else {
console.warn('WebSocket未连接无法发送消息')
}
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event).push(callback)
}
off(event, callback) {
if (this.listeners.has(event)) {
const callbacks = this.listeners.get(event)
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => {
try {
callback(data)
} catch (error) {
console.error(`WebSocket事件处理错误 [${event}]:`, error)
}
})
}
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`)
setTimeout(() => {
if (window.electronAPI) {
const backendUrl = window.electronAPI.getBackendUrl()
const wsUrl = backendUrl.replace('http', 'ws') + '/ws'
this.connect(wsUrl)
}
}, this.reconnectInterval)
} else {
console.error('WebSocket重连失败已达到最大重试次数')
this.emit('reconnect_failed')
}
}
}
// 创建WebSocket管理器实例
export const wsManager = new WebSocketManager()
// 文件上传工具
export const uploadFile = (file, onProgress) => {
return new Promise((resolve, reject) => {
const formData = new FormData()
formData.append('file', file)
const config = {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
if (onProgress) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
onProgress(percentCompleted)
}
}
}
api.post('/api/upload', formData, config)
.then(resolve)
.catch(reject)
})
}
// 文件下载工具
export const downloadFile = (url, filename) => {
return api.get(url, { responseType: 'blob' })
.then(response => {
const blob = new Blob([response])
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
})
}
export default api

View File

@ -0,0 +1,569 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { authAPI } from '../services/api'
// 认证状态管理
export const useAuthStore = defineStore('auth', () => {
// 状态
const authToken = ref(localStorage.getItem('authToken') || null)
const currentUser = ref(JSON.parse(localStorage.getItem('currentUser') || 'null'))
const isLoading = ref(false)
const error = ref(null)
// 计算属性
const isAuthenticated = computed(() => !!authToken.value && !!currentUser.value)
// 方法
const login = async (credentials) => {
try {
isLoading.value = true
error.value = null
const response = await authAPI.login(credentials)
const { token, user } = response.data
authToken.value = token
currentUser.value = user
// 保存到本地存储
localStorage.setItem('authToken', token)
localStorage.setItem('currentUser', JSON.stringify(user))
return { success: true, data: response.data }
} catch (err) {
error.value = err.response?.data?.message || '登录失败'
return { success: false, error: error.value }
} finally {
isLoading.value = false
}
}
const register = async (userData) => {
try {
isLoading.value = true
error.value = null
const response = await authAPI.register(userData)
return { success: true, data: response.data }
} catch (err) {
error.value = err.response?.data?.message || '注册失败'
return { success: false, error: error.value }
} finally {
isLoading.value = false
}
}
const forgotPassword = async (email) => {
try {
isLoading.value = true
error.value = null
const response = await authAPI.forgotPassword(email)
return { success: true, data: response.data }
} catch (err) {
error.value = err.response?.data?.message || '发送重置邮件失败'
return { success: false, error: error.value }
} finally {
isLoading.value = false
}
}
const logout = async () => {
try {
if (authToken.value) {
await authAPI.logout()
}
} catch (err) {
console.warn('Logout API call failed:', err)
} finally {
// 清除本地状态
authToken.value = null
currentUser.value = null
localStorage.removeItem('authToken')
localStorage.removeItem('currentUser')
}
}
const verifyToken = async () => {
if (!authToken.value) return false
try {
const response = await authAPI.verifyToken()
return response.data.valid
} catch (err) {
// Token无效清除本地状态
await logout()
return false
}
}
const clearError = () => {
error.value = null
}
return {
// 状态
authToken,
currentUser,
isLoading,
error,
// 计算属性
isAuthenticated,
// 方法
login,
register,
forgotPassword,
logout,
verifyToken,
clearError
}
})
// 系统状态管理
export const useSystemStore = defineStore('system', () => {
// 状态
const backendUrl = ref('http://127.0.0.1:5000')
const isBackendConnected = ref(false)
const systemInfo = ref({})
const currentUser = ref(null)
const isLoading = ref(false)
const error = ref(null)
// 计算属性
const isReady = computed(() => isBackendConnected.value && !isLoading.value)
// 方法
const setBackendConnection = (connected) => {
isBackendConnected.value = connected
}
const setSystemInfo = (info) => {
systemInfo.value = info
}
const setCurrentUser = (user) => {
currentUser.value = user
}
const setLoading = (loading) => {
isLoading.value = loading
}
const setError = (errorMsg) => {
error.value = errorMsg
}
const clearError = () => {
error.value = null
}
return {
// 状态
backendUrl,
isBackendConnected,
systemInfo,
currentUser,
isLoading,
error,
// 计算属性
isReady,
// 方法
setBackendConnection,
setSystemInfo,
setCurrentUser,
setLoading,
setError,
clearError
}
})
// 患者管理状态
export const usePatientStore = defineStore('patient', () => {
// 状态
const patients = ref([])
const currentPatient = ref(null)
const searchQuery = ref('')
const filters = ref({
gender: '',
ageRange: '',
dateRange: []
})
const pagination = ref({
page: 1,
pageSize: 10,
total: 0
})
// 计算属性
const filteredPatients = computed(() => {
let result = patients.value
// 搜索过滤
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(patient =>
patient.name.toLowerCase().includes(query) ||
patient.phone?.toLowerCase().includes(query) ||
patient.medical_history?.toLowerCase().includes(query)
)
}
// 性别过滤
if (filters.value.gender) {
result = result.filter(patient => patient.gender === filters.value.gender)
}
// 年龄范围过滤
if (filters.value.ageRange) {
const [minAge, maxAge] = filters.value.ageRange.split('-').map(Number)
result = result.filter(patient => {
const age = calculateAge(patient.birth_date)
return age >= minAge && age <= maxAge
})
}
return result
})
// 方法
const setPatients = (patientList) => {
patients.value = patientList
}
const addPatient = (patient) => {
patients.value.unshift(patient)
}
const updatePatient = (updatedPatient) => {
const index = patients.value.findIndex(p => p.id === updatedPatient.id)
if (index !== -1) {
patients.value[index] = updatedPatient
}
}
const removePatient = (patientId) => {
const index = patients.value.findIndex(p => p.id === patientId)
if (index !== -1) {
patients.value.splice(index, 1)
}
}
const setCurrentPatient = (patient) => {
currentPatient.value = patient
}
const setSearchQuery = (query) => {
searchQuery.value = query
}
const setFilters = (newFilters) => {
filters.value = { ...filters.value, ...newFilters }
}
const setPagination = (newPagination) => {
pagination.value = { ...pagination.value, ...newPagination }
}
const calculateAge = (birthDate) => {
const today = new Date()
const birth = new Date(birthDate)
let age = today.getFullYear() - birth.getFullYear()
const monthDiff = today.getMonth() - birth.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--
}
return age
}
return {
// 状态
patients,
currentPatient,
searchQuery,
filters,
pagination,
// 计算属性
filteredPatients,
// 方法
setPatients,
addPatient,
updatePatient,
removePatient,
setCurrentPatient,
setSearchQuery,
setFilters,
setPagination,
calculateAge
}
})
// 检测状态管理
export const useDetectionStore = defineStore('detection', () => {
// 状态
const isDetecting = ref(false)
const currentSession = ref(null)
const detectionConfig = ref({
duration: 60,
samplingRate: 30,
recordVideo: true
})
const realTimeData = ref({
pressure: { left: 0, right: 0, total: 0 },
balance: { index: 0, centerX: 0, centerY: 0 },
posture: { pitch: 0, roll: 0, yaw: 0 },
timestamp: null
})
const detectionResults = ref(null)
const deviceStatus = ref({
camera: { connected: false, status: 'disconnected' },
imu: { connected: false, status: 'disconnected' },
pressure: { connected: false, status: 'disconnected' }
})
// 计算属性
const allDevicesReady = computed(() => {
return Object.values(deviceStatus.value).every(device => device.connected)
})
const detectionProgress = computed(() => {
if (!currentSession.value || !currentSession.value.start_time) return 0
const startTime = new Date(currentSession.value.start_time)
const now = new Date()
const elapsed = (now - startTime) / 1000
const progress = Math.min((elapsed / detectionConfig.value.duration) * 100, 100)
return Math.round(progress)
})
// 方法
const setDetecting = (detecting) => {
isDetecting.value = detecting
}
const setCurrentSession = (session) => {
currentSession.value = session
}
const setDetectionConfig = (config) => {
detectionConfig.value = { ...detectionConfig.value, ...config }
}
const updateRealTimeData = (data) => {
realTimeData.value = { ...realTimeData.value, ...data, timestamp: new Date() }
}
const setDetectionResults = (results) => {
detectionResults.value = results
}
const updateDeviceStatus = (device, status) => {
if (deviceStatus.value[device]) {
deviceStatus.value[device] = { ...deviceStatus.value[device], ...status }
}
}
const resetDetection = () => {
isDetecting.value = false
currentSession.value = null
detectionResults.value = null
realTimeData.value = {
pressure: { left: 0, right: 0, total: 0 },
balance: { index: 0, centerX: 0, centerY: 0 },
posture: { pitch: 0, roll: 0, yaw: 0 },
timestamp: null
}
}
return {
// 状态
isDetecting,
currentSession,
detectionConfig,
realTimeData,
detectionResults,
deviceStatus,
// 计算属性
allDevicesReady,
detectionProgress,
// 方法
setDetecting,
setCurrentSession,
setDetectionConfig,
updateRealTimeData,
setDetectionResults,
updateDeviceStatus,
resetDetection
}
})
// 数据分析状态管理
export const useAnalysisStore = defineStore('analysis', () => {
// 状态
const sessions = ref([])
const currentAnalysis = ref(null)
const analysisFilters = ref({
patientId: null,
dateRange: [],
sessionType: ''
})
const comparisonData = ref([])
const reports = ref([])
// 计算属性
const filteredSessions = computed(() => {
let result = sessions.value
// 患者过滤
if (analysisFilters.value.patientId) {
result = result.filter(session => session.patient_id === analysisFilters.value.patientId)
}
// 日期范围过滤
if (analysisFilters.value.dateRange.length === 2) {
const [startDate, endDate] = analysisFilters.value.dateRange
result = result.filter(session => {
const sessionDate = new Date(session.start_time)
return sessionDate >= startDate && sessionDate <= endDate
})
}
return result
})
// 方法
const setSessions = (sessionList) => {
sessions.value = sessionList
}
const addSession = (session) => {
sessions.value.unshift(session)
}
const setCurrentAnalysis = (analysis) => {
currentAnalysis.value = analysis
}
const setAnalysisFilters = (filters) => {
analysisFilters.value = { ...analysisFilters.value, ...filters }
}
const setComparisonData = (data) => {
comparisonData.value = data
}
const addReport = (report) => {
reports.value.unshift(report)
}
const removeReport = (reportId) => {
const index = reports.value.findIndex(r => r.id === reportId)
if (index !== -1) {
reports.value.splice(index, 1)
}
}
return {
// 状态
sessions,
currentAnalysis,
analysisFilters,
comparisonData,
reports,
// 计算属性
filteredSessions,
// 方法
setSessions,
addSession,
setCurrentAnalysis,
setAnalysisFilters,
setComparisonData,
addReport,
removeReport
}
})
// 设置状态管理
export const useSettingsStore = defineStore('settings', () => {
// 状态
const appSettings = ref({
theme: 'light',
language: 'zh-CN',
autoSave: true,
notifications: true
})
const deviceSettings = ref({
camera: {
resolution: '640x480',
fps: 30,
brightness: 50,
contrast: 50
},
sensors: {
samplingRate: 30,
filterEnabled: true,
calibrationInterval: 24
}
})
const detectionSettings = ref({
defaultDuration: 60,
balanceThreshold: 0.2,
postureThreshold: 5.0,
autoAnalysis: true
})
// 方法
const updateAppSettings = (settings) => {
appSettings.value = { ...appSettings.value, ...settings }
}
const updateDeviceSettings = (settings) => {
deviceSettings.value = { ...deviceSettings.value, ...settings }
}
const updateDetectionSettings = (settings) => {
detectionSettings.value = { ...detectionSettings.value, ...settings }
}
const resetToDefaults = () => {
appSettings.value = {
theme: 'light',
language: 'zh-CN',
autoSave: true,
notifications: true
}
deviceSettings.value = {
camera: {
resolution: '640x480',
fps: 30,
brightness: 50,
contrast: 50
},
sensors: {
samplingRate: 30,
filterEnabled: true,
calibrationInterval: 24
}
}
detectionSettings.value = {
defaultDuration: 60,
balanceThreshold: 0.2,
postureThreshold: 5.0,
autoAnalysis: true
}
}
return {
// 状态
appSettings,
deviceSettings,
detectionSettings,
// 方法
updateAppSettings,
updateDeviceSettings,
updateDetectionSettings,
resetToDefaults
}
})

View File

@ -0,0 +1,285 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', Arial, sans-serif;
background-color: #f5f7fa;
color: #303133;
line-height: 1.6;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 布局容器 */
.app-container {
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: 250px;
background: #304156;
color: white;
overflow-y: auto;
transition: width 0.3s;
}
.sidebar.collapsed {
width: 64px;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
height: 60px;
background: white;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
}
.content {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f5f7fa;
}
/* 卡片样式 */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #ebeef5;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
/* 状态指示器 */
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-online {
background-color: #67c23a;
}
.status-offline {
background-color: #f56c6c;
}
.status-warning {
background-color: #e6a23c;
}
/* 数据展示 */
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.data-item {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
text-align: center;
}
.data-value {
font-size: 32px;
font-weight: bold;
color: #409eff;
margin-bottom: 8px;
}
.data-label {
font-size: 14px;
color: #909399;
}
/* 按钮组 */
.button-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
/* 表单样式 */
.form-container {
max-width: 600px;
margin: 0 auto;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.form-item {
flex: 1;
}
/* 图表容器 */
.chart-container {
height: 400px;
margin: 20px 0;
}
/* 视频容器 */
.video-container {
position: relative;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.video-player {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: fixed;
top: 0;
left: -100%;
z-index: 1000;
transition: left 0.3s;
}
.sidebar.open {
left: 0;
}
.main-content {
margin-left: 0;
}
.data-grid {
grid-template-columns: 1fr;
}
.form-row {
flex-direction: column;
}
}
/* 动画效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s;
}
.slide-enter-from {
transform: translateX(-100%);
}
.slide-leave-to {
transform: translateX(100%);
}
/* 工具类 */
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.mb-20 {
margin-bottom: 20px;
}
.mt-20 {
margin-top: 20px;
}
.p-20 {
padding: 20px;
}
.full-width {
width: 100%;
}
.full-height {
height: 100%;
}

View File

@ -0,0 +1,618 @@
<template>
<div class="dashboard-container">
<!-- 顶部导航栏 -->
<div class="header">
<div class="header-left">
<img src="/logo.png" alt="Logo" class="logo" />
<h1 class="system-title">平衡体态检测系统</h1>
</div>
<div class="header-right">
<div class="user-info">
<el-avatar :size="40" :src="userInfo.avatar">
<el-icon><User /></el-icon>
</el-avatar>
<span class="username">{{ userInfo.username }}</span>
<el-dropdown @command="handleUserCommand">
<el-button type="text" class="user-dropdown">
<el-icon><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人信息</el-dropdown-item>
<el-dropdown-item command="settings">系统设置</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div class="main-content">
<!-- 左侧功能导航 -->
<div class="sidebar">
<div class="nav-menu">
<div class="nav-item" :class="{ active: activeNav === 'detection' }" @click="activeNav = 'detection'">
<el-icon class="nav-icon"><Monitor /></el-icon>
<span>检测</span>
</div>
<div class="nav-item" :class="{ active: activeNav === 'patient' }" @click="activeNav = 'patient'">
<el-icon class="nav-icon"><User /></el-icon>
<span>人员</span>
</div>
<div class="nav-item" :class="{ active: activeNav === 'system' }" @click="activeNav = 'system'">
<el-icon class="nav-icon"><Setting /></el-icon>
<span>系统</span>
</div>
</div>
</div>
<!-- 右侧内容区域 -->
<div class="content-area">
<!-- 患者列表区域 -->
<div class="patient-section">
<div class="section-header">
<h2>患者列表</h2>
<div class="search-box">
<el-input
v-model="searchKeyword"
placeholder="搜索患者姓名"
prefix-icon="Search"
clearable
@input="handleSearch"
/>
</div>
</div>
<div class="patient-list">
<div
v-for="patient in filteredPatients"
:key="patient.id"
class="patient-item"
:class="{ selected: selectedPatient?.id === patient.id }"
@click="selectPatient(patient)"
>
<el-avatar :size="50" :src="patient.avatar">
{{ patient.name.charAt(0) }}
</el-avatar>
<div class="patient-info">
<div class="patient-name">{{ patient.name }}</div>
<div class="patient-details">
<span>{{ patient.gender }} | {{ patient.age }}</span>
<span class="patient-id">ID: {{ patient.id }}</span>
</div>
</div>
<div class="patient-status">
<el-tag :type="getStatusType(patient.lastDetection)">{{ getStatusText(patient.lastDetection) }}</el-tag>
</div>
</div>
</div>
</div>
<!-- 患者详情区域 -->
<div class="patient-detail" v-if="selectedPatient">
<div class="detail-header">
<h3>患者信息</h3>
<el-button type="text" @click="editPatient">
<el-icon><Edit /></el-icon>
</el-button>
</div>
<div class="detail-content">
<div class="info-grid">
<div class="info-item">
<label>姓名</label>
<span>{{ selectedPatient.name }}</span>
</div>
<div class="info-item">
<label>性别</label>
<span>{{ selectedPatient.gender }}</span>
</div>
<div class="info-item">
<label>年龄</label>
<span>{{ selectedPatient.age }}</span>
</div>
<div class="info-item">
<label>身高</label>
<span>{{ selectedPatient.height }}cm</span>
</div>
<div class="info-item">
<label>体重</label>
<span>{{ selectedPatient.weight }}kg</span>
</div>
<div class="info-item">
<label>联系电话</label>
<span>{{ selectedPatient.phone }}</span>
</div>
<div class="info-item">
<label>创建时间</label>
<span>{{ formatDate(selectedPatient.createdAt) }}</span>
</div>
<div class="info-item">
<label>最后检测</label>
<span>{{ selectedPatient.lastDetection ? formatDate(selectedPatient.lastDetection) : '暂无' }}</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button type="primary" size="large" class="action-btn view-btn" @click="viewPatientProfile">
查看档案
</el-button>
<el-button type="success" size="large" class="action-btn detect-btn" @click="startDetection">
开始检测
</el-button>
<el-button type="warning" size="large" class="action-btn new-btn" @click="createNewPatient">
新患者建档
</el-button>
</div>
</div>
<!-- 无选中患者时的提示 -->
<div class="no-selection" v-else>
<el-empty description="请选择一个患者查看详情">
<el-button type="primary" @click="createNewPatient">新建患者档案</el-button>
</el-empty>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { patientAPI } from '../services/api.js'
const router = useRouter()
//
const activeNav = ref('detection')
const searchKeyword = ref('')
const selectedPatient = ref(null)
const patients = ref([])
const userInfo = reactive({
username: '医生',
avatar: ''
})
//
const filteredPatients = computed(() => {
if (!searchKeyword.value) {
return patients.value
}
return patients.value.filter(patient =>
patient.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
//
const handleSearch = () => {
//
}
const selectPatient = (patient) => {
selectedPatient.value = patient
}
const editPatient = () => {
router.push(`/patient/edit/${selectedPatient.value.id}`)
}
const viewPatientProfile = () => {
router.push(`/patient/profile/${selectedPatient.value.id}`)
}
const startDetection = () => {
if (!selectedPatient.value) {
ElMessage.warning('请先选择患者')
return
}
router.push(`/detection/${selectedPatient.value.id}`)
}
const createNewPatient = () => {
router.push('/patient/create')
}
const handleUserCommand = (command) => {
switch (command) {
case 'profile':
//
break
case 'settings':
//
break
case 'logout':
handleLogout()
break
}
}
const handleLogout = async () => {
try {
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
localStorage.removeItem('userInfo')
localStorage.removeItem('rememberedUser')
ElMessage.success('已退出登录')
router.push('/login')
} catch {
//
}
}
const getStatusType = (lastDetection) => {
if (!lastDetection) return 'info'
const daysDiff = Math.floor((Date.now() - new Date(lastDetection).getTime()) / (1000 * 60 * 60 * 24))
if (daysDiff <= 7) return 'success'
if (daysDiff <= 30) return 'warning'
return 'danger'
}
const getStatusText = (lastDetection) => {
if (!lastDetection) return '未检测'
const daysDiff = Math.floor((Date.now() - new Date(lastDetection).getTime()) / (1000 * 60 * 60 * 24))
if (daysDiff === 0) return '今日检测'
if (daysDiff <= 7) return `${daysDiff}天前`
if (daysDiff <= 30) return `${daysDiff}天前`
return '超过30天'
}
const formatDate = (date) => {
return new Date(date).toLocaleString('zh-CN')
}
const loadPatients = async () => {
try {
const response = await patientAPI.getList()
if (response.success) {
patients.value = response.data
}
} catch (error) {
console.error('加载患者列表失败:', error)
//
patients.value = [
{
id: 1,
name: '张三',
gender: '男',
age: 45,
height: 175,
weight: 70,
phone: '13800138001',
createdAt: '2024-01-15T10:30:00Z',
lastDetection: '2024-01-20T14:20:00Z'
},
{
id: 2,
name: '李四',
gender: '女',
age: 38,
height: 165,
weight: 55,
phone: '13800138002',
createdAt: '2024-01-10T09:15:00Z',
lastDetection: null
},
{
id: 3,
name: '王五',
gender: '男',
age: 52,
height: 180,
weight: 75,
phone: '13800138003',
createdAt: '2024-01-05T16:45:00Z',
lastDetection: '2024-01-18T11:30:00Z'
}
]
}
}
//
onMounted(() => {
//
const savedUserInfo = localStorage.getItem('userInfo')
if (savedUserInfo) {
Object.assign(userInfo, JSON.parse(savedUserInfo))
}
//
loadPatients()
})
</script>
<style scoped>
.dashboard-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f7fa;
}
.header {
height: 60px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 40px;
height: 40px;
border-radius: 50%;
}
.system-title {
font-size: 20px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.header-right {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.username {
font-size: 14px;
color: #606266;
}
.user-dropdown {
padding: 0;
color: #909399;
}
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 200px;
background: #2c3e50;
padding: 20px 0;
}
.nav-menu {
display: flex;
flex-direction: column;
gap: 8px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 15px 20px;
color: #bdc3c7;
cursor: pointer;
transition: all 0.3s;
}
.nav-item:hover {
background: #34495e;
color: #fff;
}
.nav-item.active {
background: #3498db;
color: #fff;
border-right: 3px solid #2980b9;
}
.nav-icon {
font-size: 18px;
}
.content-area {
flex: 1;
display: flex;
gap: 20px;
padding: 20px;
overflow: hidden;
}
.patient-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-header h2 {
margin: 0;
color: #2c3e50;
font-size: 18px;
}
.search-box {
width: 250px;
}
.patient-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.patient-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
border: 1px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.patient-item:hover {
border-color: #3498db;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2);
}
.patient-item.selected {
border-color: #3498db;
background: #f0f8ff;
}
.patient-info {
flex: 1;
}
.patient-name {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 4px;
}
.patient-details {
font-size: 14px;
color: #606266;
display: flex;
justify-content: space-between;
}
.patient-id {
color: #909399;
}
.patient-status {
flex-shrink: 0;
}
.patient-detail {
width: 400px;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e4e7ed;
}
.detail-header h3 {
margin: 0;
color: #2c3e50;
font-size: 18px;
}
.detail-content {
flex: 1;
margin-bottom: 20px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-item label {
font-size: 14px;
color: #909399;
font-weight: 500;
}
.info-item span {
font-size: 14px;
color: #2c3e50;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
.action-btn {
height: 45px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
}
.view-btn {
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
border: none;
}
.detect-btn {
background: linear-gradient(135deg, #e91e63 0%, #c2185b 100%);
border: none;
}
.new-btn {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
border: none;
}
.no-selection {
width: 400px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
:deep(.el-empty__description) {
color: #909399;
}
</style>

View File

@ -0,0 +1,602 @@
<template>
<div class="dashboard-container">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<div class="sidebar-item active">
<el-icon class="sidebar-icon"><UserFilled /></el-icon>
<span class="sidebar-text">检测</span>
</div>
</aside>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 顶部工具栏 -->
<header class="top-bar">
<div style="display: flex;align-items: center;">
<div class="top-bar-left">
<el-icon class="back-icon" @click="handleBack"><ArrowLeft /></el-icon>
<span class="page-title">实时检测</span>
</div>
<el-button
type="primary"
class="start-btn"
style="--el-button-bg-color: #409EFF; --el-button-border-color: #409EFF"
>
开始
</el-button>
</div>
<div class="top-bar-right">
<span class="info-item">测试时间2025/5/28 下午14:38</span>
<span class="info-item">测试医生李四</span>
<el-icon class="top-icon"><Clock /></el-icon>
<el-icon class="top-icon"><Grid /></el-icon>
</div>
</header>
<!-- 核心内容区网格布局 -->
<div class="content-grid">
<!-- 身体姿态模块 -->
<el-card class="module-card body-posture">
<template #header>
<div class="module-header">
<span class="module-title">身体姿态</span>
</div>
</template>
<div class="posture-container">
<div class="grid-background"></div>
<img
src="https://via.placeholder.com/400x600?text=身体热力图"
alt="身体姿态热力图"
class="posture-heatmap"
>
</div>
</el-card>
<el-card class="">
<!-- 头部姿态模块 -->
<el-card class="module-card head-posture">
<template #header>
<div class="module-header">
<img
src="https://via.placeholder.com/24x24?text=头部图标"
alt="头部姿态图标"
class="header-icon"
>
<span class="module-title">头部姿态</span>
</div>
</template>
<!-- 仪表盘区域 -->
<div class="gauge-group">
<!-- 旋转角 -->
<div class="gauge-item">
<el-gauge
:value="55.2"
:min="-90"
:max="90"
:range="[{ start: -90, end: -30, color: '#1E88E5' }, { start: -30, end: 30, color: '#4CAF50' }, { start: 30, end: 90, color: '#E53935' }]"
class="gauge"
/>
<p class="gauge-desc">最大旋转角 -55.2°<br> 54.2°</p>
</div>
<!-- 倾斜角 -->
<div class="gauge-item">
<el-gauge
:value="7.7"
:min="-90"
:max="90"
:range="[{ start: -90, end: -20, color: '#409EFF' }, { start: -20, end: 20, color: '#FADB14' }, { start: 20, end: 90, color: '#F56C6C' }]"
class="gauge"
/>
<p class="gauge-desc">最大倾斜角 -7.7°<br> 8.7°</p>
</div>
<!-- 俯仰角 -->
<div class="gauge-item">
<el-gauge
:value="-10.5"
:min="-90"
:max="90"
:range="[{ start: -90, end: -20, color: '#409EFF' }, { start: -20, end: 20, color: '#FADB14' }, { start: 20, end: 90, color: '#F56C6C' }]"
class="gauge"
/>
<p class="gauge-desc">最大俯仰角 -10.5°<br> 11.5°</p>
</div>
</div>
<!-- 历史数据表格 -->
<el-table :data="historyData" border class="history-table">
<el-table-column prop="id" label="ID" align="center" />
<el-table-column prop="rotLeft" label="最大旋转角-左" align="center" />
<el-table-column prop="rotRight" label="最大旋转角-右" align="center" />
<el-table-column prop="tiltLeft" label="最大倾斜角-左" align="center" />
<el-table-column prop="tiltRight" label="最大倾斜角-右" align="center" />
<el-table-column prop="pitchDown" label="最大俯仰角-俯" align="center" />
<el-table-column prop="pitchUp" label="最大俯仰角-仰" align="center" />
</el-table>
<el-button
type="primary"
class="zero-btn"
style="--el-button-bg-color: #C22ED0; --el-button-border-color: #C22ED0"
>
清零
</el-button>
</el-card>
<!-- 足部压力模块 -->
<el-card class="module-card foot-pressure">
<template #header>
<div class="module-header">
<span class="module-title">足部压力</span>
</div>
</template>
<div class="foot-container">
<div class="foot-stats">
<div class="stat-item">左前足 54%</div>
<div class="stat-item">左后足 46%</div>
</div>
<div class="foot-graph">
<img
src="https://via.placeholder.com/120x150?text=左足热力图"
alt="左足压力"
class="foot-img"
>
<div class="foot-divider"></div>
<img
src="https://via.placeholder.com/120x150?text=右足热力图"
alt="右足压力"
class="foot-img"
>
</div>
<div class="foot-stats">
<div class="stat-item">右前足 56%</div>
<div class="stat-item">右后足 44%</div>
</div>
<div class="total-pressure">
左足总压力47% &nbsp;&nbsp; 右足总压力53%
</div>
</div>
</el-card>
</el-card>
<el-card class="">
<!-- 基础信息模块 -->
<el-card class="module-card basic-info">
<template #header>
<div class="module-header">
<span class="module-title">基础信息</span>
<el-icon class="edit-icon"><Edit /></el-icon>
</div>
</template>
<div class="info-grid">
<div class="info-item">
<label>测试者ID</label>
<span>2101</span>
</div>
<div class="info-item">
<label>姓名</label>
<span>张三</span>
</div>
<div class="info-item">
<label>性别</label>
<span></span>
</div>
<div class="info-item">
<label>出生日期</label>
<span>2011/03/07</span>
</div>
<div class="info-item">
<label>年龄</label>
<span>14</span>
</div>
<div class="info-item">
<label>民族</label>
<span></span>
</div>
<div class="info-item">
<label>身高 cm</label>
<span>167</span>
</div>
<div class="info-item">
<label>体重 kg</label>
<span>51</span>
</div>
<div class="info-item">
<label>鞋码</label>
<span>38</span>
</div>
<div class="info-item">
<label>电话号码</label>
<span>18011110000</span>
</div>
<div class="info-item">
<label>建档时间</label>
<span>2024/12/13</span>
</div>
</div>
</el-card>
<!-- 视频模块 -->
<el-card class="module-card video-module">
<template #header>
<div class="module-header">
<span class="module-title">视频</span>
</div>
</template>
<div class="video-container">
<img
src="https://via.placeholder.com/300x200?text=视频预览"
alt="视频预览"
class="video-preview"
>
<span class="video-time">2025/06/11 15:11:33</span>
</div>
</el-card>
</el-card>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
ArrowLeft,
UserFilled,
Clock,
Grid,
Edit
} from '@element-plus/icons-vue'
//
const historyData = ref([
{ id: 3, rotLeft: '-55.2°', rotRight: '54.2°', tiltLeft: '-17.7°', tiltRight: '18.2°', pitchDown: '-20.2°', pitchUp: '10.5°' },
{ id: 2, rotLeft: '-55.8°', rotRight: '56.2°', tiltLeft: '-17.5°', tiltRight: '17.9°', pitchDown: '-21.2°', pitchUp: '12.1°' },
{ id: 1, rotLeft: '-56.1°', rotRight: '55.7°', tiltLeft: '-17.5°', tiltRight: '18.5°', pitchDown: '-22.2°', pitchUp: '11.5°' }
])
//
const handleBack = () => {
//
console.log('返回上一页')
}
</script>
<style scoped>
/* 全局容器 */
.dashboard-container {
display: flex;
height: 100vh;
background-color: #1E1E1E;
color: #FFFFFF;
}
/* 左侧导航栏 */
.sidebar {
width: 60px;
background-color: #2C2C2C;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 20px;
}
.sidebar-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 10px 0;
}
.sidebar-item.active {
color: #C22ED0;
}
.sidebar-icon {
font-size: 24px;
margin-bottom: 4px;
}
.sidebar-text {
font-size: 12px;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
}
/* 顶部工具栏 */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #2C2C2C;
}
.top-bar-left {
display: flex;
align-items: center;
}
.back-icon {
font-size: 20px;
cursor: pointer;
margin-right: 10px;
}
.page-title {
font-size: 18px;
font-weight: bold;
}
.start-btn {
font-size: 16px;
padding: 8px 24px;
margin-left: 40px;
}
.top-bar-right {
display: flex;
align-items: center;
}
.info-item {
font-size: 14px;
margin-right: 20px;
color: #CCCCCC;
}
.top-icon {
font-size: 20px;
margin-left: 20px;
cursor: pointer;
}
/* 核心内容网格布局 */
.content-grid {
flex: 1;
padding: 20px;
display: grid;
grid-template-columns: 3fr 2fr;
grid-template-rows: auto 1fr;
gap: 20px;
}
/* 通用模块样式 */
.module-card {
background-color: #2C2C2C !important;
border: none !important;
}
.module-header {
width: 120px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
border: 1px solid red;
border-radius: 20px;
padding: 10px ;
}
.module-title {
font-size: 16px;
font-weight: bold;
color: #ffffff;
}
.header-icon {
width: 24px;
height: 24px;
margin-right: 8px;
}
.edit-icon {
font-size: 18px;
cursor: pointer;
color: #CCCCCC;
}
/* 身体姿态模块 */
.body-posture {
/* grid-row: 1 / 3; */
width:30% ;
}
.posture-container {
position: relative;
height: calc(100% - 40px);
}
.grid-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(to right, #333333 1px, transparent 1px), linear-gradient(to bottom, #333333 1px, transparent 1px);
background-size: 50px 50px;
}
.posture-heatmap {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 90%;
max-height: 90%;
}
/* 头部姿态模块 */
.head-posture {
grid-column: 2 / 3;
grid-row: 1 / 2;
}
.gauge-group {
display: flex;
justify-content: space-around;
margin-bottom: 20px;
}
.gauge-item {
display: flex;
flex-direction: column;
align-items: center;
}
.gauge {
width: 140px;
height: 140px;
--el-gauge-label-color: #FFFFFF;
}
.gauge-desc {
font-size: 14px;
text-align: center;
margin-top: 8px;
color: #CCCCCC;
}
.history-table {
background-color: #333333 !important;
margin-bottom: 16px;
}
.el-table th {
background-color: #333333 !important;
color: #FFFFFF !important;
border-bottom: 1px solid #444444 !important;
}
.el-table td {
background-color: #333333 !important;
color: #FFFFFF !important;
border-bottom: 1px solid #444444 !important;
}
.zero-btn {
float: right;
padding: 6px 16px;
}
/* 基础信息模块 */
.basic-info {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
font-size: 14px;
}
.info-item label {
color: #409EFF;
margin-right: 8px;
font-weight: 500;
}
/* 足部压力模块 */
.foot-pressure {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
.foot-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.foot-stats {
display: flex;
justify-content: space-around;
width: 100%;
font-size: 14px;
color: #CCCCCC;
}
.foot-graph {
display: flex;
align-items: center;
position: relative;
margin: 10px 0;
}
.foot-img {
width: 140px;
height: 180px;
margin: 0 15px;
border-radius: 8px;
}
.foot-divider {
width: 2px;
height: 100%;
background-color: #444444;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.total-pressure {
font-size: 14px;
color: #CCCCCC;
}
/* 视频模块 */
.video-module {
grid-column: 1 / 2;
grid-row: 2 / 3;
margin-top: 20px;
}
.video-container {
position: relative;
height: calc(100% - 40px);
}
.video-preview {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-time {
position: absolute;
bottom: 8px;
right: 8px;
background-color: rgba(0, 0, 0, 0.6);
padding: 4px 8px;
font-size: 12px;
color: #FFFFFF;
}
/* Element Plus 样式覆盖 */
.el-card__header {
border-bottom: 1px solid #444444 !important;
padding: 12px 20px !important;
}
.el-card__body {
padding: 20px !important;
}
.el-button--primary:hover {
--el-button-bg-color: #A325B0 !important;
--el-button-border-color: #A325B0 !important;
}
</style>

View File

@ -0,0 +1,307 @@
<template>
<div class="login-page">
<!-- 科幻网格地板3D效果 -->
<div class="grid-floor"></div>
<!-- 两侧光柱特效 -->
<div class="light-pillar left"></div>
<div class="light-pillar right"></div>
<!-- 页面主内容 -->
<div class="login-content">
<!-- 系统标题 -->
<h1 class="system-title">平衡体态检测系统</h1>
<!-- 登录卡片 -->
<el-card class="login-card">
<div class="card-header">登录</div>
<el-form class="login-form">
<!-- 账号输入框 -->
<el-form-item>
<el-input
v-model="form.account"
placeholder="请输入账号"
prefix-icon="User"
class="custom-input"
/>
</el-form-item>
<!-- 密码输入框带显示切换 -->
<el-form-item>
<el-input
v-model="form.password"
:type="passwordVisible ? 'text' : 'password'"
placeholder="请输入密码"
prefix-icon="Lock"
class="custom-input"
>
<template #suffix>
<el-icon
:icon="passwordVisible ? Hide : View"
class="password-icon"
@click="passwordVisible = !passwordVisible"
/>
</template>
</el-input>
</el-form-item>
<!-- 记住密码 & 忘记密码 -->
<div class="form-footer">
<el-checkbox v-model="form.remember" class="remember-checkbox">记住密码</el-checkbox>
<a href="#" class="forgot-link" @click="handleForgotPassword">忘记密码</a>
</div>
<!-- 操作按钮 -->
<div class="button-group">
<el-button type="primary" class="login-btn" @click="handleLogin" :loading="isLoading">登录</el-button>
<el-button class="register-btn" @click="handleRegister">注册</el-button>
</div>
</el-form>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '../stores'
import { User, Lock, View, Hide } from '@element-plus/icons-vue'
const router = useRouter()
const authStore = useAuthStore()
//
const form = ref({
account: '',
password: '',
remember: false
})
//
const passwordVisible = ref(false)
//
const isLoading = ref(false)
//
const handleLogin = async () => {
if (!form.value.account || !form.value.password) {
ElMessage.warning('请输入账号和密码')
return
}
try {
isLoading.value = true
const result = await authStore.login({
username: form.value.account,
password: form.value.password,
remember: form.value.remember
})
if (result.success) {
ElMessage.success('登录成功')
router.push('/detection/1')
} else {
ElMessage.error(result.error || '登录失败')
}
} catch (error) {
ElMessage.error('登录失败:' + (error.message || '未知错误'))
} finally {
isLoading.value = false
}
}
//
const handleRegister = () => {
router.push('/register')
}
//
const handleForgotPassword = () => {
router.push('/forgot-password')
}
</script>
<style scoped>
/* 页面全局样式 */
.login-page {
position: relative;
width: 100vw;
height: 100vh;
background-color: #000;
overflow: hidden;
}
/* 3D网格地板核心科幻效果 */
.grid-floor {
position: absolute;
bottom: 0;
left: 0;
width: 200%;
height: 60vh;
background-image:
linear-gradient(to right, #00ffff 1px, transparent 1px),
linear-gradient(to bottom, #00ffff 1px, transparent 1px);
background-size: 40px 40px;
transform:
perspective(1200px)
rotateX(65deg)
translate(-25%, 0);
transform-origin: bottom center;
opacity: 0.15;
pointer-events: none;
}
/* 两侧光柱特效 */
.light-pillar {
position: absolute;
bottom: 0;
width: 2px;
height: 220px;
background: linear-gradient(to top, #00ffff, transparent);
box-shadow: 0 0 12px rgba(0, 255, 255, 0.8);
animation: pulse 3s infinite ease-in-out;
}
.light-pillar.left { left: 32%; }
.light-pillar.right { right: 32%; }
/* 光柱呼吸动画 */
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
/* 主内容容器(垂直居中) */
.login-content {
position: relative;
z-index: 10;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 20px;
}
/* 系统标题 */
.system-title {
font-size: 2rem;
color: #00ffff;
text-shadow: 0 0 15px rgba(0, 255, 255, 0.8);
margin-bottom: 2.5rem;
letter-spacing: 2px;
}
/* 登录卡片 */
.login-card {
width: 100%;
max-width: 420px;
background-color: #003366 !important;
border: none !important;
border-radius: 12px !important;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2);
padding: 30px 25px !important;
}
/* 卡片头部标题 */
.card-header {
font-size: 1.4rem;
color: #00ffff;
text-align: center;
margin-bottom: 25px;
font-weight: 500;
}
/* 登录表单 */
.login-form {
width: 100%;
}
/* 自定义输入框 */
.custom-input {
background-color: #004080 !important;
border-color: #00ffff !important;
color: #fff !important;
border-radius: 6px !important;
}
.custom-input::placeholder {
color: #aaa !important;
}
.custom-input .el-input__icon {
color: #00ffff !important;
}
/* 密码显示图标 */
.password-icon {
color: #00ffff !important;
cursor: pointer;
padding: 0 8px;
}
/* 表单底部(记住密码+忘记密码) */
.form-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0 25px;
font-size: 0.9rem;
}
/* 记住密码复选框 */
.remember-checkbox {
color: #fff !important;
}
.remember-checkbox .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: #00ffff !important;
border-color: #00ffff !important;
}
.remember-checkbox .el-checkbox__inner {
border-color: #00ffff !important;
}
/* 忘记密码链接 */
.forgot-link {
color: #00ffff;
text-decoration: none;
}
.forgot-link:hover {
text-decoration: underline;
}
/* 按钮组 */
.button-group {
display: flex;
gap: 15px;
}
/* 登录按钮 */
.login-btn {
flex: 1;
background-color: #00ffff !important;
border-color: #00ffff !important;
color: #003366 !important;
font-weight: 500 !important;
border-radius: 6px !important;
}
.login-btn:hover {
background-color: #00e6e6 !important;
border-color: #00e6e6 !important;
}
/* 注册按钮 */
.register-btn {
flex: 1;
background-color: transparent !important;
border-color: #00ffff !important;
color: #00ffff !important;
font-weight: 500 !important;
border-radius: 6px !important;
}
.register-btn:hover {
background-color: #004080 !important;
}
</style>

View File

@ -0,0 +1,514 @@
<template>
<div class="patient-create-container">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<el-button type="text" @click="goBack" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
<h1 class="page-title">新建患者档案</h1>
</div>
</div>
<!-- 表单内容 -->
<div class="form-container">
<el-form
ref="patientFormRef"
:model="patientForm"
:rules="formRules"
label-width="120px"
class="patient-form"
>
<div class="form-section">
<h3 class="section-title">基本信息</h3>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="name" required>
<el-input
v-model="patientForm.name"
placeholder="请输入患者姓名"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender" required>
<el-radio-group v-model="patientForm.gender">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="出生日期" prop="birthDate" required>
<el-date-picker
v-model="patientForm.birthDate"
type="date"
placeholder="选择出生日期"
style="width: 100%"
@change="calculateAge"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年龄">
<el-input
v-model="calculatedAge"
placeholder="自动计算"
readonly
suffix-icon="Calendar"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="身高" prop="height" required>
<el-input
v-model="patientForm.height"
placeholder="请输入身高"
clearable
>
<template #suffix>cm</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="体重" prop="weight" required>
<el-input
v-model="patientForm.weight"
placeholder="请输入体重"
clearable
>
<template #suffix>kg</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="form-section">
<h3 class="section-title">联系信息</h3>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系电话" prop="phone" required>
<el-input
v-model="patientForm.phone"
placeholder="请输入联系电话"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="紧急联系人">
<el-input
v-model="patientForm.emergencyContact"
placeholder="请输入紧急联系人"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="紧急联系电话">
<el-input
v-model="patientForm.emergencyPhone"
placeholder="请输入紧急联系电话"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号">
<el-input
v-model="patientForm.idCard"
placeholder="请输入身份证号"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="地址">
<el-input
v-model="patientForm.address"
placeholder="请输入详细地址"
clearable
/>
</el-form-item>
</div>
<div class="form-section">
<h3 class="section-title">医疗信息</h3>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="主治医生">
<el-input
v-model="patientForm.doctor"
placeholder="请输入主治医生"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病历号">
<el-input
v-model="patientForm.medicalRecordNumber"
placeholder="请输入病历号"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="既往病史">
<el-input
v-model="patientForm.medicalHistory"
type="textarea"
:rows="3"
placeholder="请输入既往病史"
/>
</el-form-item>
<el-form-item label="过敏史">
<el-input
v-model="patientForm.allergies"
type="textarea"
:rows="2"
placeholder="请输入过敏史"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="patientForm.notes"
type="textarea"
:rows="3"
placeholder="请输入其他备注信息"
/>
</el-form-item>
</div>
</el-form>
</div>
<!-- 底部操作按钮 -->
<div class="footer-actions">
<el-button size="large" @click="handleCancel">退出</el-button>
<el-button type="primary" size="large" :loading="saveLoading" @click="handleSave">
保存
</el-button>
<el-button type="success" size="large" :loading="saveAndDetectLoading" @click="handleSaveAndDetect">
保存并开始检测
</el-button>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { patientAPI } from '../services/api.js'
const router = useRouter()
//
const patientFormRef = ref()
//
const saveLoading = ref(false)
const saveAndDetectLoading = ref(false)
//
const patientForm = reactive({
name: '',
gender: '',
birthDate: '',
height: '',
weight: '',
phone: '',
emergencyContact: '',
emergencyPhone: '',
idCard: '',
address: '',
doctor: '',
medicalRecordNumber: '',
medicalHistory: '',
allergies: '',
notes: ''
})
//
const calculatedAge = computed(() => {
if (!patientForm.birthDate) return ''
const today = new Date()
const birthDate = new Date(patientForm.birthDate)
let age = today.getFullYear() - birthDate.getFullYear()
const monthDiff = today.getMonth() - birthDate.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--
}
return age + '岁'
})
//
const formRules = {
name: [
{ required: true, message: '请输入患者姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
birthDate: [
{ required: true, message: '请选择出生日期', trigger: 'change' }
],
height: [
{ required: true, message: '请输入身高', trigger: 'blur' },
{ pattern: /^\d+(\.\d+)?$/, message: '请输入有效的身高', trigger: 'blur' }
],
weight: [
{ required: true, message: '请输入体重', trigger: 'blur' },
{ pattern: /^\d+(\.\d+)?$/, message: '请输入有效的体重', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
]
}
//
const goBack = () => {
router.go(-1)
}
const calculateAge = () => {
//
}
const handleCancel = async () => {
//
const hasData = Object.values(patientForm).some(value => value !== '')
if (hasData) {
try {
await ElMessageBox.confirm(
'您有未保存的数据,确定要退出吗?',
'提示',
{
confirmButtonText: '确定退出',
cancelButtonText: '取消',
type: 'warning'
}
)
router.go(-1)
} catch {
//
}
} else {
router.go(-1)
}
}
const validateForm = async () => {
try {
await patientFormRef.value.validate()
return true
} catch (error) {
ElMessage.error('请完善必填信息')
return false
}
}
const savePatient = async () => {
const patientData = {
...patientForm,
age: calculatedAge.value.replace('岁', ''),
createdAt: new Date().toISOString()
}
try {
const response = await patientAPI.create(patientData)
if (response.success) {
return response.data
} else {
throw new Error(response.message || '保存失败')
}
} catch (error) {
console.error('保存患者信息失败:', error)
throw error
}
}
const handleSave = async () => {
if (!(await validateForm())) return
saveLoading.value = true
try {
await savePatient()
ElMessage.success('患者档案保存成功')
router.push('/dashboard')
} catch (error) {
ElMessage.error('保存失败:' + error.message)
} finally {
saveLoading.value = false
}
}
const handleSaveAndDetect = async () => {
if (!(await validateForm())) return
saveAndDetectLoading.value = true
try {
const patient = await savePatient()
ElMessage.success('患者档案保存成功,即将开始检测')
router.push(`/detection/${patient.id}`)
} catch (error) {
ElMessage.error('保存失败:' + error.message)
} finally {
saveAndDetectLoading.value = false
}
}
</script>
<style scoped>
.patient-create-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f7fa;
}
.header {
height: 60px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.back-btn {
display: flex;
align-items: center;
gap: 5px;
color: #606266;
font-size: 14px;
}
.back-btn:hover {
color: #409eff;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.form-container {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.patient-form {
max-width: 800px;
margin: 0 auto;
background: #fff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-section {
margin-bottom: 30px;
}
.form-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
}
.footer-actions {
height: 80px;
background: #fff;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
padding: 0 20px;
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
}
.footer-actions .el-button {
min-width: 120px;
height: 40px;
font-size: 16px;
font-weight: 600;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
:deep(.el-form-item__content) {
line-height: 32px;
}
:deep(.el-input__wrapper) {
border-radius: 6px;
}
:deep(.el-textarea__inner) {
border-radius: 6px;
}
:deep(.el-date-editor) {
width: 100%;
}
:deep(.el-radio-group) {
display: flex;
gap: 20px;
}
:deep(.el-radio) {
margin-right: 0;
}
/* 必填项标识 */
:deep(.el-form-item.is-required .el-form-item__label::before) {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
</style>

View File

@ -0,0 +1,763 @@
<template>
<div class="patient-profile-container">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<el-button type="text" @click="goBack" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
<h1 class="page-title">患者档案</h1>
</div>
<div class="header-right">
<el-button type="primary" @click="startDetection">
<el-icon><Monitor /></el-icon>
开始检测
</el-button>
</div>
</div>
<div class="main-content" v-if="patient">
<!-- 患者基本信息 -->
<div class="patient-info-card">
<div class="card-header">
<h2>基本信息</h2>
<el-button type="text" @click="editPatient">
<el-icon><Edit /></el-icon>
编辑
</el-button>
</div>
<div class="info-grid">
<div class="info-item">
<label>姓名</label>
<span>{{ patient.name }}</span>
</div>
<div class="info-item">
<label>性别</label>
<span>{{ patient.gender }}</span>
</div>
<div class="info-item">
<label>年龄</label>
<span>{{ patient.age }}</span>
</div>
<div class="info-item">
<label>身高</label>
<span>{{ patient.height }}cm</span>
</div>
<div class="info-item">
<label>体重</label>
<span>{{ patient.weight }}kg</span>
</div>
<div class="info-item">
<label>联系电话</label>
<span>{{ patient.phone }}</span>
</div>
<div class="info-item">
<label>主治医生</label>
<span>{{ patient.doctor || '未设置' }}</span>
</div>
<div class="info-item">
<label>创建时间</label>
<span>{{ formatDate(patient.createdAt) }}</span>
</div>
</div>
</div>
<!-- 检测记录 -->
<div class="detection-records-card">
<div class="card-header">
<h2>检测记录</h2>
<div class="header-actions">
<el-input
v-model="searchKeyword"
placeholder="搜索记录"
prefix-icon="Search"
clearable
style="width: 200px; margin-right: 10px;"
/>
<el-button type="primary" @click="startDetection">
新建检测
</el-button>
</div>
</div>
<div class="records-list" v-if="filteredRecords.length > 0">
<div
v-for="record in filteredRecords"
:key="record.id"
class="record-item"
@click="viewRecord(record)"
>
<div class="record-header">
<div class="record-title">
<h4>检测记录 #{{ record.id }}</h4>
<el-tag :type="getRecordStatusType(record.status)">{{ record.status }}</el-tag>
</div>
<div class="record-time">{{ formatDate(record.createdAt) }}</div>
</div>
<div class="record-content">
<div class="record-stats">
<div class="stat-item">
<span class="stat-label">检测时长</span>
<span class="stat-value">{{ formatDuration(record.duration) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">截图数量</span>
<span class="stat-value">{{ record.screenshots?.length || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">检测医生</span>
<span class="stat-value">{{ record.doctor || '未记录' }}</span>
</div>
</div>
<div class="record-actions">
<el-button type="text" @click.stop="playVideo(record)">
<el-icon><VideoPlay /></el-icon>
回放视频
</el-button>
<el-button type="text" @click.stop="viewScreenshots(record)">
<el-icon><Picture /></el-icon>
查看截图
</el-button>
<el-button type="text" @click.stop="exportReport(record)">
<el-icon><Download /></el-icon>
导出报告
</el-button>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无检测记录">
<el-button type="primary" @click="startDetection">开始首次检测</el-button>
</el-empty>
</div>
</div>
<!-- 加载状态 -->
<div v-else class="loading-container">
<el-loading-service lock="true" text="加载患者信息中..." />
</div>
<!-- 视频播放对话框 -->
<el-dialog
v-model="videoDialogVisible"
title="检测视频回放"
width="80%"
:before-close="closeVideoDialog"
>
<div class="video-container" v-if="currentVideo">
<video
ref="videoPlayerRef"
:src="currentVideo.url"
controls
width="100%"
height="400"
>
您的浏览器不支持视频播放
</video>
<div class="video-info">
<p><strong>检测时间</strong>{{ formatDate(currentVideo.createdAt) }}</p>
<p><strong>检测时长</strong>{{ formatDuration(currentVideo.duration) }}</p>
<p><strong>文件大小</strong>{{ formatFileSize(currentVideo.fileSize) }}</p>
</div>
</div>
</el-dialog>
<!-- 截图查看对话框 -->
<el-dialog
v-model="screenshotDialogVisible"
title="检测截图"
width="90%"
:before-close="closeScreenshotDialog"
>
<div class="screenshot-container" v-if="currentScreenshots.length > 0">
<div class="screenshot-grid">
<div
v-for="(screenshot, index) in currentScreenshots"
:key="screenshot.id"
class="screenshot-item"
@click="previewScreenshotHandler(screenshot, index)"
>
<img :src="screenshot.thumbnail" :alt="`截图${index + 1}`" />
<div class="screenshot-overlay">
<div class="screenshot-time">{{ formatTime(screenshot.timestamp) }}</div>
<div class="screenshot-actions">
<el-button type="primary" size="small" @click.stop="downloadScreenshot(screenshot)">
<el-icon><Download /></el-icon>
</el-button>
<el-button type="danger" size="small" @click.stop="deleteScreenshot(screenshot)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无截图" />
</el-dialog>
<!-- 截图预览对话框 -->
<el-dialog
v-model="previewDialogVisible"
title="截图预览"
width="70%"
:before-close="closePreviewDialog"
>
<div class="preview-container" v-if="previewScreenshot">
<img :src="previewScreenshot.url" alt="截图预览" class="preview-image" />
<div class="preview-info">
<p><strong>截图时间</strong>{{ formatDate(previewScreenshot.timestamp) }}</p>
<p><strong>检测数据</strong></p>
<div class="data-display" v-if="previewScreenshot.data">
<div class="data-item">
<span>头部姿态</span>
<span>Pitch: {{ previewScreenshot.data.pitch }}°, Yaw: {{ previewScreenshot.data.yaw }}°, Roll: {{ previewScreenshot.data.roll }}°</span>
</div>
<div class="data-item">
<span>足底压力</span>
<span>左足: {{ previewScreenshot.data.leftPressure }}%, 右足: {{ previewScreenshot.data.rightPressure }}%</span>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { patientAPI, detectionAPI } from '../services/api.js'
const router = useRouter()
const route = useRoute()
//
const patient = ref(null)
const detectionRecords = ref([])
const searchKeyword = ref('')
const videoDialogVisible = ref(false)
const screenshotDialogVisible = ref(false)
const previewDialogVisible = ref(false)
const currentVideo = ref(null)
const currentScreenshots = ref([])
const previewScreenshot = ref(null)
const videoPlayerRef = ref()
//
const filteredRecords = computed(() => {
if (!searchKeyword.value) {
return detectionRecords.value
}
return detectionRecords.value.filter(record =>
record.id.toString().includes(searchKeyword.value) ||
record.doctor?.includes(searchKeyword.value) ||
formatDate(record.createdAt).includes(searchKeyword.value)
)
})
//
const goBack = () => {
router.go(-1)
}
const editPatient = () => {
router.push(`/patient/edit/${patient.value.id}`)
}
const startDetection = () => {
router.push(`/detection/${patient.value.id}`)
}
const viewRecord = (record) => {
router.push(`/detection/record/${record.id}`)
}
const playVideo = (record) => {
currentVideo.value = {
url: record.videoUrl,
createdAt: record.createdAt,
duration: record.duration,
fileSize: record.videoFileSize
}
videoDialogVisible.value = true
}
const viewScreenshots = (record) => {
currentScreenshots.value = record.screenshots || []
screenshotDialogVisible.value = true
}
const previewScreenshotHandler = (screenshot, index) => {
previewScreenshot.value = screenshot
previewDialogVisible.value = true
}
const downloadScreenshot = (screenshot) => {
//
const link = document.createElement('a')
link.href = screenshot.url
link.download = `screenshot_${screenshot.id}.png`
link.click()
}
const deleteScreenshot = async (screenshot) => {
try {
await ElMessageBox.confirm('确定要删除这张截图吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
//
ElMessage.success('截图已删除')
//
loadDetectionRecords()
} catch {
//
}
}
const exportReport = async (record) => {
try {
ElMessage.info('正在生成报告...')
//
const response = await detectionAPI.exportReport(record.id)
if (response.success) {
ElMessage.success('报告导出成功')
}
} catch (error) {
ElMessage.error('报告导出失败')
}
}
const closeVideoDialog = () => {
if (videoPlayerRef.value) {
videoPlayerRef.value.pause()
}
videoDialogVisible.value = false
currentVideo.value = null
}
const closeScreenshotDialog = () => {
screenshotDialogVisible.value = false
currentScreenshots.value = []
}
const closePreviewDialog = () => {
previewDialogVisible.value = false
previewScreenshot.value = null
}
const getRecordStatusType = (status) => {
switch (status) {
case '已完成': return 'success'
case '进行中': return 'warning'
case '已中断': return 'danger'
default: return 'info'
}
}
const formatDate = (date) => {
return new Date(date).toLocaleString('zh-CN')
}
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString('zh-CN')
}
const formatDuration = (seconds) => {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
return `${minutes}${remainingSeconds}`
}
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const loadPatientInfo = async () => {
try {
const patientId = route.params.id
const response = await patientAPI.getById(patientId)
if (response.success) {
patient.value = response.data
} else {
throw new Error(response.message)
}
} catch (error) {
console.error('加载患者信息失败:', error)
//
patient.value = {
id: route.params.id,
name: '张三',
gender: '男',
age: 45,
height: 175,
weight: 70,
phone: '13800138001',
doctor: '李医生',
createdAt: '2024-01-15T10:30:00Z'
}
}
}
const loadDetectionRecords = async () => {
try {
const patientId = route.params.id
const response = await detectionAPI.getByPatientId(patientId)
if (response.success) {
detectionRecords.value = response.data
} else {
throw new Error(response.message)
}
} catch (error) {
console.error('加载检测记录失败:', error)
//
detectionRecords.value = [
{
id: 1,
patientId: route.params.id,
status: '已完成',
duration: 300,
doctor: '李医生',
createdAt: '2024-01-20T14:20:00Z',
videoUrl: '/videos/detection_1.mp4',
videoFileSize: 52428800,
screenshots: [
{
id: 1,
timestamp: '2024-01-20T14:22:30Z',
thumbnail: '/screenshots/thumb_1.jpg',
url: '/screenshots/screenshot_1.png',
data: {
pitch: 2.5,
yaw: -1.2,
roll: 0.8,
leftPressure: 45,
rightPressure: 55
}
},
{
id: 2,
timestamp: '2024-01-20T14:24:15Z',
thumbnail: '/screenshots/thumb_2.jpg',
url: '/screenshots/screenshot_2.png',
data: {
pitch: 3.1,
yaw: -0.8,
roll: 1.2,
leftPressure: 48,
rightPressure: 52
}
}
]
},
{
id: 2,
patientId: route.params.id,
status: '已完成',
duration: 240,
doctor: '王医生',
createdAt: '2024-01-18T11:30:00Z',
videoUrl: '/videos/detection_2.mp4',
videoFileSize: 41943040,
screenshots: []
}
]
}
}
//
onMounted(() => {
loadPatientInfo()
loadDetectionRecords()
})
</script>
<style scoped>
.patient-profile-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f7fa;
}
.header {
height: 60px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.back-btn {
display: flex;
align-items: center;
gap: 5px;
color: #606266;
font-size: 14px;
}
.back-btn:hover {
color: #409eff;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.patient-info-card,
.detection-records-card {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e4e7ed;
}
.card-header h2 {
margin: 0;
color: #2c3e50;
font-size: 18px;
}
.header-actions {
display: flex;
align-items: center;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
}
.info-item label {
font-weight: 500;
color: #909399;
min-width: 80px;
}
.info-item span {
color: #2c3e50;
}
.records-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.record-item {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.3s;
}
.record-item:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.record-title {
display: flex;
align-items: center;
gap: 10px;
}
.record-title h4 {
margin: 0;
color: #2c3e50;
font-size: 16px;
}
.record-time {
color: #909399;
font-size: 14px;
}
.record-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.record-stats {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: #909399;
}
.stat-value {
font-size: 14px;
color: #2c3e50;
font-weight: 500;
}
.record-actions {
display: flex;
gap: 10px;
}
.loading-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.video-container {
text-align: center;
}
.video-info {
margin-top: 15px;
text-align: left;
color: #606266;
}
.screenshot-container {
max-height: 60vh;
overflow-y: auto;
}
.screenshot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.screenshot-item {
position: relative;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s;
}
.screenshot-item:hover {
transform: scale(1.05);
}
.screenshot-item img {
width: 100%;
height: 150px;
object-fit: cover;
}
.screenshot-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: white;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.screenshot-time {
font-size: 12px;
}
.screenshot-actions {
display: flex;
gap: 5px;
}
.preview-container {
text-align: center;
}
.preview-image {
max-width: 100%;
max-height: 60vh;
border-radius: 8px;
}
.preview-info {
margin-top: 20px;
text-align: left;
color: #606266;
}
.data-display {
margin-top: 10px;
padding: 15px;
background: #f5f7fa;
border-radius: 6px;
}
.data-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.data-item:last-child {
margin-bottom: 0;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
base: './',
build: {
outDir: 'dist',
assetsDir: 'assets',
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
}
}
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
host: '0.0.0.0'
}
});

205
install.bat Normal file
View File

@ -0,0 +1,205 @@
@echo off
chcp 65001 >nul
echo ====================================
echo 身体平衡评估系统 - 安装脚本
echo ====================================
echo.
echo 此脚本将帮助您安装所有必要的依赖和配置环境
echo.
:: 检查管理员权限(可选)
net session >nul 2>&1
if %errorlevel% == 0 (
echo [信息] 检测到管理员权限
) else (
echo [警告] 未检测到管理员权限,某些操作可能失败
)
echo.
:: 检查Python
echo [步骤 1/6] 检查Python环境...
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到Python
echo [提示] 请从 https://www.python.org/downloads/ 下载并安装Python 3.8或更高版本
echo [提示] 安装时请勾选 "Add Python to PATH"
pause
exit /b 1
) else (
for /f "tokens=*" %%i in ('python --version') do echo [成功] 找到 %%i
)
:: 检查Python版本
for /f "tokens=2 delims= " %%i in ('python --version') do set python_version=%%i
for /f "tokens=1,2 delims=." %%a in ("%python_version%") do (
set major=%%a
set minor=%%b
)
if %major% lss 3 (
echo [错误] Python版本过低需要3.8或更高版本
pause
exit /b 1
)
if %major% equ 3 if %minor% lss 8 (
echo [错误] Python版本过低需要3.8或更高版本
pause
exit /b 1
)
echo.
:: 检查Node.js
echo [步骤 2/6] 检查Node.js环境...
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到Node.js
echo [提示] 请从 https://nodejs.org/ 下载并安装Node.js 16.0或更高版本
pause
exit /b 1
) else (
for /f "tokens=*" %%i in ('node --version') do echo [成功] 找到Node.js %%i
for /f "tokens=*" %%i in ('npm --version') do echo [成功] 找到npm %%i
)
echo.
:: 创建虚拟环境
echo [步骤 3/6] 创建Python虚拟环境...
if exist "backend\venv" (
echo [信息] 虚拟环境已存在,跳过创建
else (
echo [信息] 正在创建虚拟环境...
python -m venv backend\venv
if %errorlevel% neq 0 (
echo [错误] 创建虚拟环境失败
pause
exit /b 1
)
echo [成功] 虚拟环境创建完成
)
:: 激活虚拟环境
echo [信息] 激活虚拟环境...
call backend\venv\Scripts\activate.bat
if %errorlevel% neq 0 (
echo [错误] 激活虚拟环境失败
pause
exit /b 1
)
echo [成功] 虚拟环境已激活
echo.
:: 升级pip
echo [步骤 4/6] 升级pip...
python -m pip install --upgrade pip
if %errorlevel% neq 0 (
echo [警告] pip升级失败继续安装
) else (
echo [成功] pip升级完成
)
echo.
:: 安装Python依赖
echo [步骤 5/6] 安装Python依赖...
if not exist "backend\requirements.txt" (
echo [错误] 未找到backend\requirements.txt文件
pause
exit /b 1
)
echo [信息] 正在安装Python包这可能需要几分钟...
pip install -r backend\requirements.txt
if %errorlevel% neq 0 (
echo [错误] 安装Python依赖失败
echo [提示] 请检查网络连接和requirements.txt文件
pause
exit /b 1
)
echo [成功] Python依赖安装完成
echo.
:: 安装前端依赖
echo [步骤 6/6] 安装前端依赖...
if exist "frontend\src\renderer\package.json" (
cd frontend\src\renderer
echo [信息] 正在安装前端包,这可能需要几分钟...
npm install
if %errorlevel% neq 0 (
echo [错误] 安装前端依赖失败
echo [提示] 请检查网络连接和package.json文件
cd ..\..\..
pause
exit /b 1
)
echo [成功] 前端依赖安装完成
cd ..\..\..
) else (
echo [警告] 未找到前端package.json文件跳过前端依赖安装
)
echo.
:: 创建目录结构
echo [信息] 创建目录结构...
if not exist "data" mkdir data
if not exist "data\patients" mkdir data\patients
if not exist "data\sessions" mkdir data\sessions
if not exist "data\exports" mkdir data\exports
if not exist "data\backups" mkdir data\backups
if not exist "logs" mkdir logs
if not exist "temp" mkdir temp
echo [成功] 目录结构创建完成
echo.
:: 检查配置文件
echo [信息] 检查配置文件...
if exist "config.json" (
echo [成功] 配置文件已存在
) else (
echo [信息] 配置文件已创建,使用默认配置
)
echo.
:: 运行测试(可选)
echo [信息] 运行基础测试...
echo [测试] 检查Python导入...
python -c "import sys; print('Python路径:', sys.executable)" 2>nul
if %errorlevel% neq 0 (
echo [警告] Python测试失败
) else (
echo [成功] Python测试通过
)
echo [测试] 检查主要依赖...
python -c "import flask, numpy, opencv-python, mediapipe; print('主要依赖检查通过')" 2>nul
if %errorlevel% neq 0 (
echo [警告] 依赖测试失败,某些功能可能不可用
) else (
echo [成功] 依赖测试通过
)
echo.
:: 安装完成
echo ====================================
echo 安装完成!
echo ====================================
echo.
echo [成功] 所有依赖已安装完成
echo [信息] 您现在可以使用以下命令启动应用:
echo.
echo 开发环境: start_dev.bat
echo 生产环境: start_prod.bat
echo 或直接运行: python main.py
echo.
echo [提示] 首次运行建议使用开发环境进行测试
echo [提示] 如遇到问题,请查看 README.md 文件
echo.
set /p choice="是否现在启动开发环境?(y/n): "
if /i "%choice%"=="y" (
echo.
echo [信息] 启动开发环境...
call start_dev.bat
) else (
echo.
echo [信息] 安装完成,您可以稍后手动启动应用
)
pause

119
package.json Normal file
View File

@ -0,0 +1,119 @@
{
"name": "body-balance-evaluation",
"version": "1.0.0",
"description": "身体平衡评估系统 - 基于多传感器融合技术的专业平衡能力评估与分析系统",
"main": "src/main/main.js",
"scripts": {
"dev": "electron .",
"build": "npm run build:renderer && npm run build:main",
"build:renderer": "cd src/renderer && npm run build",
"build:main": "electron-builder",
"pack": "electron-builder --dir",
"dist": "electron-builder",
"postinstall": "electron-builder install-app-deps",
"start": "electron .",
"start:dev": "electron . --mode development",
"start:prod": "electron . --mode production",
"install:backend": "pip install -r backend/requirements.txt",
"install:frontend": "cd src/renderer && npm install",
"install:all": "npm run install:backend && npm run install:frontend",
"test:backend": "cd backend && python -m pytest tests/ -v",
"test:frontend": "cd src/renderer && npm run test",
"lint:backend": "cd backend && flake8 . --max-line-length=88 --exclude=venv,__pycache__",
"lint:frontend": "cd src/renderer && npm run lint",
"format:backend": "cd backend && black . --line-length=88",
"format:frontend": "cd src/renderer && npm run format",
"clean": "node -e \"const fs = require('fs'); const path = require('path'); ['logs', 'temp', 'node_modules/.cache'].forEach(d => { try { fs.rmSync(d, {recursive: true, force: true}); } catch(e) {} });\"",
"clean:data": "node -e \"const fs = require('fs'); try { fs.rmSync('data', {recursive: true, force: true}); } catch(e) {}\"",
"backup": "node -e \"const fs = require('fs'); const path = require('path'); const archiver = require('archiver'); const date = new Date().toISOString().slice(0,19).replace(/:/g,'-'); const output = fs.createWriteStream(`backup_${date}_data.zip`); const archive = archiver('zip'); archive.pipe(output); archive.directory('data/', false); archive.finalize();\"",
"setup": "npm run install:all && node -e \"const fs = require('fs'); ['data', 'data/patients', 'data/sessions', 'data/exports', 'data/backups', 'logs', 'temp'].forEach(d => fs.mkdirSync(d, {recursive: true}));\"",
"check": "node --version && npm --version && electron --version",
"test": "npm run test:backend && npm run test:frontend"
},
"keywords": [
"balance",
"posture",
"assessment",
"healthcare",
"rehabilitation",
"sensors",
"computer-vision",
"medical-device",
"electron",
"vue",
"mediapipe"
],
"author": {
"name": "zheng shunli",
"email": "dev@example.com"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/example/body-balance-evaluation.git"
},
"bugs": {
"url": "https://github.com/example/body-balance-evaluation/issues"
},
"homepage": "https://github.com/example/body-balance-evaluation#readme",
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"devDependencies": {
"electron": "^27.0.0",
"electron-builder": "latest",
"electron-packager": "^17.1.2",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3"
},
"dependencies": {
"axios": "^1.5.0",
"electron-log": "^4.4.8"
},
"config": {
"backend_host": "127.0.0.1",
"backend_port": "5000",
"frontend_port": "5173"
},
"build": {
"appId": "com.bodybalance.evaluation",
"productName": "平衡体态检测系统",
"electronDist": "D:\\electron-v36.4.0-win32-x64",
"electronVersion": "36.4.0",
"directories": {
"output": "dist"
},
"files": [
"src/main/**/*",
"src/renderer/dist/**/*",
"backend/dist/**/*",
"ffmpeg/**/*",
"node_modules/**/*"
],
"extraResources": [
{
"from": "backend/dist",
"to": "backend"
},
{
"from": "ffmpeg",
"to": "ffmpeg"
}
],
"win": {
"target": "nsis",
"icon": "assets/icon.ico",
"arch": [
"x64"
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

126
start_dev.bat Normal file
View File

@ -0,0 +1,126 @@
@echo off
echo ====================================
echo Body Balance Evaluation System - Development Mode
echo ====================================
echo.
:: Check Python
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [Error] Python not found, please install Python 3.8 or higher
pause
exit /b 1
)
:: Check Node.js
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo [Error] Node.js not found, please install Node.js 16.0 or higher
pause
exit /b 1
)
:: Show version info
echo [Info] Checking environment versions...
for /f "tokens=*" %%i in ('python --version') do echo Python: %%i
for /f "tokens=*" %%i in ('node --version') do echo Node.js: %%i
for /f "tokens=*" %%i in ('npm --version') do echo npm: %%i
echo.
:: Check virtual environment
if not exist "backend\venv" (
echo [Info] Creating Python virtual environment...
python -m venv backend\venv
if %errorlevel% neq 0 (
echo [Error] Failed to create virtual environment
pause
exit /b 1
)
)
:: Activate virtual environment
echo [Info] Activating virtual environment...
call backend\venv\Scripts\activate.bat
if %errorlevel% neq 0 (
echo [Error] Failed to activate virtual environment
pause
exit /b 1
)
:: Install Python dependencies
echo [Info] Checking and installing Python dependencies...
if not exist "backend\requirements.txt" (
echo [Error] requirements.txt file not found
pause
exit /b 1
)
pip install -r backend\requirements.txt
if %errorlevel% neq 0 (
echo [Error] Failed to install Python dependencies
pause
exit /b 1
)
:: Install frontend dependencies
echo [Info] Checking and installing frontend dependencies...
if exist "frontend\src\renderer\package.json" (
cd frontend\src\renderer
if not exist "node_modules" (
echo [Info] Installing frontend dependencies...
npm install
if %errorlevel% neq 0 (
echo [Error] Failed to install frontend dependencies
cd ..\..\..
pause
exit /b 1
)
) else (
echo [Info] Frontend dependencies already exist, skipping installation
)
cd ..\..\..
) else (
echo [Warning] Frontend package.json file not found
)
:: Create necessary directories
echo [Info] Creating necessary directories...
if not exist "data" mkdir data
if not exist "data\patients" mkdir data\patients
if not exist "data\sessions" mkdir data\sessions
if not exist "data\exports" mkdir data\exports
if not exist "data\backups" mkdir data\backups
if not exist "logs" mkdir logs
if not exist "temp" mkdir temp
:: Check config file
if not exist "config.json" (
echo [Warning] config.json configuration file not found
echo [Info] Will use default configuration
)
:: Start application
echo.
echo ====================================
echo Starting Development Environment
echo ====================================
echo [Info] Starting backend server...
echo [Info] Backend address: http://127.0.0.1:5000
echo [Info] Frontend address: http://127.0.0.1:5173
echo [Info] Press Ctrl+C to stop service
echo.
:: Start main program
python backend\main.py --mode development --log-level DEBUG
if %errorlevel% neq 0 (
echo.
echo [Error] Application startup failed
echo [Tip] Please check error messages and fix issues
pause
exit /b 1
)
echo.
echo [Info] Application stopped
pause

126
start_dev_new.bat Normal file
View File

@ -0,0 +1,126 @@
@echo off
echo ====================================
echo 身体平衡评估系统 - 开发环境启动脚本
echo ====================================
echo.
:: 检查Python是否安装
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到Python请先安装Python 3.8或更高版本
pause
exit /b 1
)
:: 检查Node.js是否安装
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到Node.js请先安装Node.js 16.0或更高版本
pause
exit /b 1
)
:: 显示版本信息
echo [信息] 检查环境版本...
for /f "tokens=*" %%i in ('python --version') do echo Python: %%i
for /f "tokens=*" %%i in ('node --version') do echo Node.js: %%i
for /f "tokens=*" %%i in ('npm --version') do echo npm: %%i
echo.
:: 检查虚拟环境
if not exist "backend\venv" (
echo [信息] 创建Python虚拟环境...
python -m venv backend\venv
if %errorlevel% neq 0 (
echo [错误] 创建虚拟环境失败
pause
exit /b 1
)
)
:: 激活虚拟环境
echo [信息] 激活虚拟环境...
call backend\venv\Scripts\activate.bat
if %errorlevel% neq 0 (
echo [错误] 激活虚拟环境失败
pause
exit /b 1
)
:: 安装Python依赖
echo [信息] 检查并安装Python依赖...
if not exist "backend\requirements.txt" (
echo [错误] 未找到requirements.txt文件
pause
exit /b 1
)
pip install -r backend\requirements.txt
if %errorlevel% neq 0 (
echo [错误] 安装Python依赖失败
pause
exit /b 1
)
:: 安装前端依赖
echo [信息] 检查并安装前端依赖...
if exist "frontend\src\renderer\package.json" (
cd frontend\src\renderer
if not exist "node_modules" (
echo [信息] 安装前端依赖...
npm install
if %errorlevel% neq 0 (
echo [错误] 安装前端依赖失败
cd ..\..\..
pause
exit /b 1
)
) else (
echo [信息] 前端依赖已存在,跳过安装
)
cd ..\..\..
) else (
echo [警告] 未找到前端package.json文件
)
:: 创建必要的目录
echo [信息] 创建必要的目录...
if not exist "data" mkdir data
if not exist "data\patients" mkdir data\patients
if not exist "data\sessions" mkdir data\sessions
if not exist "data\exports" mkdir data\exports
if not exist "data\backups" mkdir data\backups
if not exist "logs" mkdir logs
if not exist "temp" mkdir temp
:: 检查配置文件
if not exist "config.json" (
echo [警告] 未找到config.json配置文件
echo [信息] 将使用默认配置
)
:: 启动应用
echo.
echo ====================================
echo 启动开发环境
echo ====================================
echo [信息] 启动后端服务器...
echo [信息] 后端地址: http://127.0.0.1:5000
echo [信息] 前端地址: http://127.0.0.1:5173
echo [信息] 按 Ctrl+C 停止服务
echo.
:: 启动主程序
python backend\main.py --mode development --log-level DEBUG
if %errorlevel% neq 0 (
echo.
echo [错误] 应用启动失败
echo [提示] 请检查错误信息并修复问题
pause
exit /b 1
)
echo.
echo [信息] 应用已停止
pause

53
start_dev_simple.bat Normal file
View File

@ -0,0 +1,53 @@
@echo off
echo Starting Body Balance Evaluation System - Development Mode
echo.
:: Check Python
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo Error: Python not found
pause
exit /b 1
)
:: Check Node.js
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo Error: Node.js not found
pause
exit /b 1
)
echo Environment check passed
echo.
:: Activate virtual environment
if exist "backend\venv\Scripts\activate.bat" (
echo Activating virtual environment...
call backend\venv\Scripts\activate.bat
) else (
echo Warning: Virtual environment not found
)
:: Install dependencies if needed
if exist "backend\requirements.txt" (
echo Installing Python dependencies...
pip install -r backend\requirements.txt
)
:: Create directories
if not exist "data" mkdir data
if not exist "logs" mkdir logs
:: Start application
echo.
echo Starting application...
echo Backend: http://127.0.0.1:5000
echo Frontend: http://127.0.0.1:5173
echo.
python backend\main.py --mode development --log-level DEBUG
echo.
echo Application stopped
pause

121
start_prod.bat Normal file
View File

@ -0,0 +1,121 @@
@echo off
chcp 65001 >nul
echo ====================================
echo 身体平衡评估系统 - 生产环境启动脚本
echo ====================================
echo.
:: 检查Python是否安装
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到Python请先安装Python 3.8或更高版本
pause
exit /b 1
)
:: 显示版本信息
echo [信息] 检查环境版本...
for /f "tokens=*" %%i in ('python --version') do echo Python: %%i
echo.
:: 检查虚拟环境
if not exist "backend\venv" (
echo [错误] 未找到虚拟环境,请先运行 start_dev.bat 进行初始化
pause
exit /b 1
)
:: 激活虚拟环境
echo [信息] 激活虚拟环境...
call backend\venv\Scripts\activate.bat
if %errorlevel% neq 0 (
echo [错误] 激活虚拟环境失败
pause
exit /b 1
)
:: 检查依赖
echo [信息] 检查依赖安装状态...
pip check >nul 2>&1
if %errorlevel% neq 0 (
echo [警告] 依赖检查失败,建议重新安装依赖
)
:: 检查前端构建
if exist "frontend\src\renderer\dist" (
echo [信息] 前端已构建
else (
echo [信息] 前端未构建,正在构建...
if exist "frontend\src\renderer\package.json" (
cd frontend\src\renderer
npm run build
if %errorlevel% neq 0 (
echo [错误] 前端构建失败
cd ..\..\..
pause
exit /b 1
)
cd ..\..\..
) else (
echo [警告] 未找到前端项目
)
)
:: 检查必要目录
echo [信息] 检查目录结构...
if not exist "data" mkdir data
if not exist "data\patients" mkdir data\patients
if not exist "data\sessions" mkdir data\sessions
if not exist "data\exports" mkdir data\exports
if not exist "data\backups" mkdir data\backups
if not exist "logs" mkdir logs
if not exist "temp" mkdir temp
:: 检查配置文件
if not exist "config.json" (
echo [错误] 未找到config.json配置文件
echo [提示] 请确保配置文件存在
pause
exit /b 1
)
:: 清理临时文件
echo [信息] 清理临时文件...
if exist "temp\*" del /q temp\*
if exist "logs\*.tmp" del /q logs\*.tmp
:: 备份数据(可选)
if exist "data\database.db" (
echo [信息] 备份数据库...
for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a"
set "YY=%dt:~2,2%" & set "YYYY=%dt:~0,4%" & set "MM=%dt:~4,2%" & set "DD=%dt:~6,2%"
set "HH=%dt:~8,2%" & set "Min=%dt:~10,2%" & set "Sec=%dt:~12,2%"
set "datestamp=%YYYY%%MM%%DD%_%HH%%Min%%Sec%"
copy "data\database.db" "data\backups\database_backup_%datestamp%.db" >nul 2>&1
)
:: 启动应用
echo.
echo ====================================
echo 启动生产环境
echo ====================================
echo [信息] 启动应用服务器...
echo [信息] 服务地址: http://127.0.0.1:5000
echo [信息] 按 Ctrl+C 停止服务
echo [信息] 日志文件: logs\app.log
echo.
:: 启动主程序
python backend\main.py --mode production
if %errorlevel% neq 0 (
echo.
echo [错误] 应用启动失败
echo [提示] 请检查日志文件: logs\app.log
pause
exit /b 1
)
echo.
echo [信息] 应用已停止
pause

41
test_start.bat Normal file
View File

@ -0,0 +1,41 @@
@echo off
echo Testing start_dev.bat functionality
echo.
:: Check if virtual environment exists
if exist "backend\venv\Scripts\activate.bat" (
echo Virtual environment found
call backend\venv\Scripts\activate.bat
echo Virtual environment activated
) else (
echo Virtual environment not found
)
:: Check if main.py exists
if exist "backend\main.py" (
echo main.py found
) else (
echo main.py not found
)
:: Test basic Python execution
echo Testing Python execution...
python --version
if %errorlevel% equ 0 (
echo Python is working
) else (
echo Python test failed
)
:: Test Node.js
echo Testing Node.js...
node --version
if %errorlevel% equ 0 (
echo Node.js is working
) else (
echo Node.js test failed
)
echo.
echo Test completed
pause