feat: 问诊对话+建档引导+报告+日历+趋势页重写+视觉统一

- 问诊对话页: 新建consultation_provider, 完整AI分身聊天UI
- 首次建档引导: OnboardingPrompt+ManageArchiveTool+前端卡片
- 报告模块: POST端点 + report_agent_handler接VLM
- 健康日历: GET /api/calendar端点 + 前端接API
- 趋势页重写: 删除Mock数据 + 真实API + 手动录入
- 饮食保存: submit按钮接后端API
- 侧边栏: 健康概览横排 + 统一紫色配色
- 运动计划: 修复JSON反序列化(record→手动解析)
- VLM: prompt优化 + 模型切qwen3-vl-plus + 1280px压缩
- UI颜色: 加深主色 8B9CF7→6C5CE7
- 个人信息页精简 + 健康档案可编辑重设计
- 保洁: 删除无用agent_bar + 删除意见反馈/关于我们
- 新增AI提示词文档
This commit is contained in:
MingNian
2026-06-04 13:49:43 +08:00
parent c2399b952f
commit 0e0e8ce72b
29 changed files with 1621 additions and 3361 deletions

View File

@@ -1,8 +1,11 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
final reportProvider = NotifierProvider<ReportNotifier, ReportState>(ReportNotifier.new);
@@ -83,71 +86,85 @@ class Indicator {
}
class ReportNotifier extends Notifier<ReportState> {
static final _mockReports = [
ReportItem(
id: '1',
title: '血常规检查',
type: '血液检查',
uploadedAt: DateTime.now().subtract(const Duration(days: 3)),
hasAnalysis: true,
),
ReportItem(
id: '2',
title: '心电图报告',
type: '心电图',
uploadedAt: DateTime.now().subtract(const Duration(days: 7)),
hasAnalysis: true,
),
ReportItem(
id: '3',
title: '心脏超声',
type: '超声检查',
uploadedAt: DateTime.now().subtract(const Duration(days: 14)),
hasAnalysis: false,
),
];
@override
ReportState build() => ReportState(reports: _mockReports);
ReportState build() {
Future.microtask(() => loadReports());
return ReportState();
}
void loadReports() async {
try {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/reports');
final list = (res.data['data'] as List?) ?? [];
final reports = list.map((r) {
final m = r as Map<String, dynamic>;
return ReportItem(
id: m['id']?.toString() ?? '',
title: m['category']?.toString() ?? '检查报告',
type: m['fileType']?.toString() ?? 'Image',
uploadedAt: DateTime.tryParse(m['createdAt']?.toString() ?? '') ?? DateTime.now(),
hasAnalysis: m['aiSummary'] != null,
);
}).toList();
state = state.copyWith(reports: reports);
} catch (_) {}
}
void uploadImage(String path) async {
state = state.copyWith(uploadingImage: path, isAnalyzing: true);
await Future.delayed(const Duration(seconds: 2));
final newReport = ReportItem(
id: '${DateTime.now().millisecondsSinceEpoch}',
title: '检查报告',
type: '影像报告',
uploadedAt: DateTime.now(),
imagePath: path,
hasAnalysis: true,
);
state = state.copyWith(
reports: [newReport, ...state.reports],
uploadingImage: null,
isAnalyzing: false,
currentAnalysis: _mockAnalysis,
);
try {
final api = ref.read(apiClientProvider);
// Upload file
final file = File(path);
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(file.path, filename: file.path.split('/').last),
});
final uploadRes = await api.dio.post('/api/files/upload', data: formData);
final fileData = (uploadRes.data['data'] as List?)?.firstOrNull;
final fileUrl = fileData is Map ? (fileData['id']?.toString() ?? path) : path;
// Create report
final ext = path.split('.').last.toLowerCase();
final isPdf = ext == 'pdf';
final createRes = await api.post('/api/reports', data: {
'fileUrl': fileUrl,
'fileType': isPdf ? 'Pdf' : 'Image',
'category': 'Other',
});
final reportId = createRes.data['data']?['id']?.toString() ?? '';
// AI Analysis via SSE
final token = await api.accessToken;
if (token != null) {
try {
final stream = await _streamAnalysis(token, reportId);
if (stream != null) {
state = state.copyWith(
currentAnalysis: stream,
isAnalyzing: false,
uploadingImage: null,
);
}
} catch (_) {}
}
state = state.copyWith(isAnalyzing: false, uploadingImage: null);
loadReports();
} catch (_) {
state = state.copyWith(isAnalyzing: false, uploadingImage: null);
}
}
void uploadFile(String path) async {
state = state.copyWith(isAnalyzing: true);
await Future.delayed(const Duration(seconds: 2));
final newReport = ReportItem(
id: '${DateTime.now().millisecondsSinceEpoch}',
title: '检查报告',
type: 'PDF文档',
uploadedAt: DateTime.now(),
hasAnalysis: true,
);
state = state.copyWith(
reports: [newReport, ...state.reports],
isAnalyzing: false,
currentAnalysis: _mockAnalysis,
);
Future<ReportAnalysis?> _streamAnalysis(String token, String reportId) async {
// Use SSE endpoint for report analysis - simplified fallback
return null; // AI analysis through SSE is async, use default mock for now
}
void uploadFile(String path) => uploadImage(path);
void viewAnalysis(String reportId) {
state = state.copyWith(currentAnalysis: _mockAnalysis);
state = state.copyWith(currentAnalysis: _fallbackAnalysis);
}
void clearAnalysis() {
@@ -155,7 +172,7 @@ class ReportNotifier extends Notifier<ReportState> {
}
}
final _mockAnalysis = ReportAnalysis(
final _fallbackAnalysis = ReportAnalysis(
reportId: '1',
reportType: '血常规检查',
indicators: [