chore: 全面规范化代码,遵循 CLAUDE.md 编码规范

- C# 文件命名改为 snake_case(28 个文件重命名)
- C# 类转换为主构造函数(8 个类)
- 空 catch 添加异常类型(2 处)
- 新建 GlobalUsings.cs(Health.Infrastructure、Health.WebApi)
- Flutter 移除 go_router,改用 Riverpod 路由栈
- Flutter 移除 flutter_secure_storage,改用 sqflite 持久化
- 修复 Flutter 构建路径(Flutter SDK 迁至 D 盘)
- 后端端口改为 0.0.0.0:5000,支持局域网访问
This commit is contained in:
MingNian
2026-06-02 12:41:06 +08:00
parent 14d7c30d3d
commit 6e69f1085e
47 changed files with 342 additions and 428 deletions

View File

@@ -1,18 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/app_router.dart';
import 'core/app_theme.dart';
import 'core/navigation_provider.dart';
/// 健康管家 App 根组件
class HealthApp extends StatelessWidget {
class HealthApp extends ConsumerWidget {
const HealthApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
Widget build(BuildContext context, WidgetRef ref) {
return const MaterialApp(
title: '健康管家',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
routerConfig: AppRouter.router,
home: _RootNavigator(),
);
}
}
/// 根导航——根据 Riverpod 路由状态切换页面
class _RootNavigator extends ConsumerWidget {
const _RootNavigator();
@override
Widget build(BuildContext context, WidgetRef ref) {
final stack = ref.watch(routeStackProvider);
final current = stack.last;
return PopScope(
canPop: stack.length <= 1,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) popRoute(ref);
},
child: buildPage(current),
);
}
}

View File

@@ -1,16 +1,16 @@
import 'package:dio/dio.dart';
import 'secure_storage.dart';
import 'local_database.dart';
/// API 基础地址
const String baseUrl = 'http://10.4.172.93:5000';
const String baseUrl = 'http://10.4.185.103:5000';
/// Dio HTTP 客户端封装——带 token 注入、401 自动刷新
class ApiClient {
final Dio _dio;
final SecureStorage _storage;
final LocalDatabase _db;
ApiClient({required SecureStorage storage})
: _storage = storage,
ApiClient({required LocalDatabase db})
: _db = db,
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 15),
@@ -23,16 +23,16 @@ class ApiClient {
Dio get dio => _dio;
Future<String?> get accessToken => _storage.readAccessToken();
Future<String?> get refreshToken => _storage.readRefreshToken();
Future<String?> get accessToken => _db.read('access_token');
Future<String?> get refreshToken => _db.read('refresh_token');
Future<void> saveTokens(String access, String refresh) async {
await _storage.writeAccessToken(access);
await _storage.writeRefreshToken(refresh);
await _db.write('access_token', access);
await _db.write('refresh_token', refresh);
}
Future<void> clearTokens() async {
await _storage.deleteAll();
await _db.deleteAll();
}
/// 带 token 的 GET 请求

View File

@@ -1,4 +1,5 @@
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
import 'navigation_provider.dart';
import '../pages/auth/login_page.dart';
import '../pages/home/home_page.dart';
import '../pages/chart/trend_page.dart';
@@ -9,49 +10,51 @@ import '../pages/settings/settings_pages.dart';
import '../pages/profile/profile_page.dart';
import '../pages/remaining_pages.dart';
/// 应用路由配置
class AppRouter {
AppRouter._();
static final GoRouter router = GoRouter(
initialLocation: '/login',
routes: [
GoRoute(path: '/login', builder: (_, _) => const LoginPage()),
GoRoute(path: '/home', builder: (_, _) => const HomePage()),
GoRoute(path: '/trend/:type', builder: (_, state) => TrendPage(metricType: state.pathParameters['type']!)),
GoRoute(path: '/calendar', builder: (_, _) => const HealthCalendarPage()),
// 用药
GoRoute(path: '/medications', builder: (_, _) => const MedicationListPage()),
GoRoute(path: '/medications/add', builder: (_, _) => const MedicationEditPage()),
GoRoute(path: '/medications/:id/edit', builder: (_, state) => MedicationEditPage(id: state.pathParameters['id'])),
// 报告
GoRoute(path: '/reports', builder: (_, _) => const ReportListPage()),
GoRoute(path: '/reports/:id', builder: (_, state) => ReportDetailPage(id: state.pathParameters['id']!)),
// 问诊
GoRoute(path: '/doctors', builder: (_, _) => const DoctorListPage()),
GoRoute(path: '/consultation/:id', builder: (_, state) => DoctorChatPage(id: state.pathParameters['id']!)),
// 运动
GoRoute(path: '/exercise-plan', builder: (_, _) => const ExercisePlanPage()),
// 饮食
GoRoute(path: '/diet-records', builder: (_, _) => const DietRecordListPage()),
// 个人中心
GoRoute(path: '/profile', builder: (_, _) => const ProfilePage()),
GoRoute(path: '/profile/edit', builder: (_, _) => const EditProfilePage()),
GoRoute(path: '/health-archive', builder: (_, _) => const HealthArchivePage()),
// 复查
GoRoute(path: '/followups', builder: (_, _) => const FollowUpListPage()),
// 设置
GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()),
GoRoute(path: '/settings/notifications', builder: (_, _) => const NotificationPrefsPage()),
GoRoute(path: '/page/:type', builder: (_, state) => StaticTextPage(type: state.pathParameters['type']!)),
],
);
/// 根据路由信息返回对应页面
Widget buildPage(RouteInfo route) {
final params = route.params;
switch (route.name) {
case 'login':
return const LoginPage();
case 'home':
return const HomePage();
case 'trend':
return TrendPage(metricType: params['type'] ?? '');
case 'calendar':
return const HealthCalendarPage();
case 'medications':
return const MedicationListPage();
case 'medicationAdd':
return const MedicationEditPage();
case 'medicationEdit':
return MedicationEditPage(id: params['id']);
case 'reports':
return const ReportListPage();
case 'reportDetail':
return ReportDetailPage(id: params['id']!);
case 'doctors':
return const DoctorListPage();
case 'consultation':
return DoctorChatPage(id: params['id']!);
case 'exercisePlan':
return const ExercisePlanPage();
case 'dietRecords':
return const DietRecordListPage();
case 'profile':
return const ProfilePage();
case 'profileEdit':
return const EditProfilePage();
case 'healthArchive':
return const HealthArchivePage();
case 'followups':
return const FollowUpListPage();
case 'settings':
return const SettingsPage();
case 'notificationPrefs':
return const NotificationPrefsPage();
case 'staticText':
return StaticTextPage(type: params['type']!);
default:
return const LoginPage();
}
}

View File

@@ -0,0 +1,58 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
/// SQLite 本地数据库——存储 token 等关键信息
class LocalDatabase {
static LocalDatabase? _instance;
Database? _db;
LocalDatabase._();
static LocalDatabase get instance => _instance ??= LocalDatabase._();
Future<Database> get database async {
_db ??= await _initDb();
return _db!;
}
Future<Database> _initDb() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'health_app.db');
return openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE kv_store (key TEXT PRIMARY KEY, value TEXT)',
);
},
);
}
Future<void> write(String key, String value) async {
final db = await database;
await db.insert(
'kv_store',
{'key': key, 'value': value},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<String?> read(String key) async {
final db = await database;
final result =
await db.query('kv_store', where: 'key = ?', whereArgs: [key]);
if (result.isEmpty) return null;
return result.first['value'] as String?;
}
Future<void> delete(String key) async {
final db = await database;
await db.delete('kv_store', where: 'key = ?', whereArgs: [key]);
}
Future<void> deleteAll() async {
final db = await database;
await db.delete('kv_store');
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// 路由信息
class RouteInfo {
final String name;
final Map<String, String> params;
const RouteInfo(this.name, {this.params = const {}});
String param(String key) => params[key] ?? '';
}
/// 路由栈 Notifier
class RouteStackNotifier extends Notifier<List<RouteInfo>> {
@override
List<RouteInfo> build() => [const RouteInfo('login')];
void replace(String name, {Map<String, String> params = const {}}) {
state = [RouteInfo(name, params: params)];
}
void push(String name, {Map<String, String> params = const {}}) {
state = [...state, RouteInfo(name, params: params)];
}
void pop() {
if (state.length > 1) {
state = state.sublist(0, state.length - 1);
}
}
}
/// 路由栈 Provider
final routeStackProvider =
NotifierProvider<RouteStackNotifier, List<RouteInfo>>(RouteStackNotifier.new);
/// 当前路由
final currentRouteProvider = Provider<RouteInfo>((ref) {
final stack = ref.watch(routeStackProvider);
return stack.last;
});
/// 跳转(替换整个栈)
void goRoute(WidgetRef ref, String name, {Map<String, String> params = const {}}) {
ref.read(routeStackProvider.notifier).replace(name, params: params);
}
/// 推入新页面
void pushRoute(WidgetRef ref, String name, {Map<String, String> params = const {}}) {
ref.read(routeStackProvider.notifier).push(name, params: params);
}
/// 返回上一页
void popRoute(WidgetRef ref) {
ref.read(routeStackProvider.notifier).pop();
}

View File

@@ -1,35 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// Token 安全存储iOS Keychain / Android EncryptedSharedPreferences / Web 内存)
class SecureStorage {
final FlutterSecureStorage _storage;
static final Map<String, String> _webFallback = {};
SecureStorage() : _storage = const FlutterSecureStorage();
static const _access = 'access_token';
static const _refresh = 'refresh_token';
bool get _isWeb => kIsWeb;
Future<void> writeAccessToken(String t) async {
if (_isWeb) { _webFallback[_access] = t; return; }
await _storage.write(key: _access, value: t);
}
Future<String?> readAccessToken() async {
if (_isWeb) return _webFallback[_access];
return _storage.read(key: _access);
}
Future<void> writeRefreshToken(String t) async {
if (_isWeb) { _webFallback[_refresh] = t; return; }
await _storage.write(key: _refresh, value: t);
}
Future<String?> readRefreshToken() async {
if (_isWeb) return _webFallback[_refresh];
return _storage.read(key: _refresh);
}
Future<void> deleteAll() async {
if (_isWeb) { _webFallback.clear(); return; }
await _storage.deleteAll();
}
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
/// 登录页——手机号 + 验证码
@@ -71,7 +71,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
setState(() => _error = err);
return;
}
if (mounted) context.go('/home');
goRoute(ref, 'home');
}
@override
@@ -80,7 +80,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
// 已登录直接跳转
if (authState.isLoggedIn && !authState.isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) => context.go('/home'));
WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home'));
}
return Scaffold(

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/navigation_provider.dart';
import '../../providers/data_providers.dart';
/// 用药列表页
@@ -37,7 +37,7 @@ class MedicationListPage extends ConsumerWidget {
error: (_, _) => _empty(context),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => context.push('/medications/add').then((_) => ref.invalidate(medicationListProvider)),
onPressed: () { pushRoute(ref, 'medicationAdd'); ref.invalidate(medicationListProvider); },
icon: const Icon(Icons.add), label: const Text('添加药品'),
),
);
@@ -65,7 +65,7 @@ class _MedicationEditPageState extends ConsumerState<MedicationEditPage> {
'frequency': 'Daily', 'timeOfDay': [if (_timeCtrl.text.isNotEmpty) _timeCtrl.text],
'source': 'Manual', 'startDate': DateTime.now().toIso8601String().substring(0, 10),
});
if (mounted) context.pop();
popRoute(ref);
}
@override Widget build(BuildContext context) => Scaffold(

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
/// 个人中心页面
@@ -38,12 +38,12 @@ class ProfilePage extends ConsumerWidget {
),
),
const SizedBox(height: 8),
_MenuItem(icon: Icons.person, title: '编辑资料', onTap: () => context.push('/profile/edit')),
_MenuItem(icon: Icons.folder, title: '健康档案', onTap: () => context.push('/health-archive')),
_MenuItem(icon: Icons.person, title: '编辑资料', onTap: () => pushRoute(ref, 'profileEdit')),
_MenuItem(icon: Icons.folder, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')),
_MenuItem(icon: Icons.devices, title: '设备管理', onTap: () {}),
const Divider(),
_MenuItem(icon: Icons.settings, title: '设置', onTap: () => context.push('/settings')),
_MenuItem(icon: Icons.info, title: '关于', onTap: () => context.push('/page/about')),
_MenuItem(icon: Icons.settings, title: '设置', onTap: () => pushRoute(ref, 'settings')),
_MenuItem(icon: Icons.info, title: '关于', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})),
const Divider(),
_MenuItem(
icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935),
@@ -52,7 +52,7 @@ class ProfilePage extends ConsumerWidget {
title: const Text('退出登录'), content: const Text('确定退出?'),
actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定'))]));
if (ok == true) { await ref.read(authProvider.notifier).logout(); if (context.mounted) context.go('/login'); }
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
},
),
],

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
/// 设置页
@@ -10,17 +10,17 @@ class SettingsPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) => Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(children: [
_SetItem(icon: Icons.shield, title: '隐私保护中心', onTap: () => context.push('/page/privacy')),
_SetItem(icon: Icons.notifications, title: '通知偏好', onTap: () => context.push('/settings/notifications')),
_SetItem(icon: Icons.shield, title: '隐私保护中心', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})),
_SetItem(icon: Icons.notifications, title: '通知偏好', onTap: () => pushRoute(ref, 'notificationPrefs')),
_SetItem(icon: Icons.text_fields, title: '字体大小', trailing: _FontSlider()),
_SetItem(icon: Icons.article, title: '协议与公告', onTap: () => context.push('/page/terms')),
_SetItem(icon: Icons.info, title: '关于', onTap: () => context.push('/page/about')),
_SetItem(icon: Icons.article, title: '协议与公告', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'terms'})),
_SetItem(icon: Icons.info, title: '关于', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})),
const Divider(),
_SetItem(icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935), onTap: () async {
final ok = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(
title: const Text('退出登录'), content: const Text('确定退出?'),
actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定'))]));
if (ok == true) { await ref.read(authProvider.notifier).logout(); if (context.mounted) context.go('/login'); }
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
}),
]),
);

View File

@@ -1,7 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import '../core/api_client.dart';
import '../core/secure_storage.dart';
import '../core/local_database.dart';
/// 用户简要信息
class UserInfo {
@@ -25,10 +25,10 @@ class AuthState {
/// 认证 Provider
final authProvider = NotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);
final secureStorageProvider = Provider<SecureStorage>((ref) => SecureStorage());
final localDbProvider = Provider<LocalDatabase>((ref) => LocalDatabase.instance);
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient(storage: ref.watch(secureStorageProvider));
return ApiClient(db: ref.watch(localDbProvider));
});
class AuthNotifier extends Notifier<AuthState> {
@@ -39,8 +39,8 @@ class AuthNotifier extends Notifier<AuthState> {
}
Future<void> _checkAuth() async {
final storage = ref.read(secureStorageProvider);
final refresh = await storage.readRefreshToken();
final db = ref.read(localDbProvider);
final refresh = await db.read('refresh_token');
if (refresh == null) {
state = const AuthState(isLoggedIn: false, isLoading: false);
return;
@@ -51,8 +51,8 @@ class AuthNotifier extends Notifier<AuthState> {
.post('/api/auth/refresh', data: {'refreshToken': refresh});
final data = response.data['data'];
if (data != null) {
await storage.writeAccessToken(data['accessToken']);
await storage.writeRefreshToken(data['refreshToken']);
await db.write('access_token', data['accessToken']);
await db.write('refresh_token', data['refreshToken']);
state = AuthState(
isLoggedIn: true,
isLoading: false,
@@ -128,8 +128,8 @@ class AuthNotifier extends Notifier<AuthState> {
/// 登出
Future<void> logout() async {
final api = ref.read(apiClientProvider);
final storage = ref.read(secureStorageProvider);
final refresh = await storage.readRefreshToken();
final db = ref.read(localDbProvider);
final refresh = await db.read('refresh_token');
if (refresh != null) {
try { await api.post('/api/auth/logout', data: {'refreshToken': refresh}); } catch (_) {}
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../core/navigation_provider.dart';
import '../providers/auth_provider.dart';
import '../providers/data_providers.dart';
@@ -26,7 +26,7 @@ class HealthDrawer extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => context.push('/profile'),
onTap: () => pushRoute(ref, 'profile'),
child: CircleAvatar(
radius: 28,
backgroundColor: const Color(0xFFEDEBFF),
@@ -40,7 +40,7 @@ class HealthDrawer extends ConsumerWidget {
],
),
),
_DrawerItem(icon: Icons.settings, label: '设置', onTap: () => context.push('/settings')),
_DrawerItem(icon: Icons.settings, label: '设置', onTap: () => pushRoute(ref, 'settings')),
const Divider(),
// 健康概览——接真实数据
@@ -50,10 +50,10 @@ class HealthDrawer extends ConsumerWidget {
),
latestHealth.when(
data: (data) => Column(children: [
_HealthMetric(icon: Icons.favorite, label: '血压', value: _bpText(data['BloodPressure']), onTap: () => context.push('/trend/blood_pressure')),
_HealthMetric(icon: Icons.monitor_heart, label: '心率', value: _metricText(data['HeartRate'], '次/分'), onTap: () => context.push('/trend/heart_rate')),
_HealthMetric(icon: Icons.bloodtype, label: '血糖', value: _metricText(data['Glucose'], 'mmol/L'), onTap: () => context.push('/trend/glucose')),
_HealthMetric(icon: Icons.air, label: '血氧', value: _metricText(data['SpO2'], '%'), onTap: () => context.push('/trend/spo2')),
_HealthMetric(icon: Icons.favorite, label: '血压', value: _bpText(data['BloodPressure']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'})),
_HealthMetric(icon: Icons.monitor_heart, label: '心率', value: _metricText(data['HeartRate'], '次/分'), onTap: () => pushRoute(ref, 'trend', params: {'type': 'heart_rate'})),
_HealthMetric(icon: Icons.bloodtype, label: '血糖', value: _metricText(data['Glucose'], 'mmol/L'), onTap: () => pushRoute(ref, 'trend', params: {'type': 'glucose'})),
_HealthMetric(icon: Icons.air, label: '血氧', value: _metricText(data['SpO2'], '%'), onTap: () => pushRoute(ref, 'trend', params: {'type': 'spo2'})),
]),
loading: () => const Padding(padding: EdgeInsets.all(16), child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))),
error: (_, _) => Column(children: [
@@ -76,7 +76,7 @@ class HealthDrawer extends ConsumerWidget {
final ok = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(
title: const Text('退出登录'), content: const Text('确定退出?'),
actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定'))]));
if (ok == true) { await ref.read(authProvider.notifier).logout(); if (context.mounted) context.go('/login'); }
if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); }
}),
],
),