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:
@@ -1,5 +1,5 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath = "C:/flutter_sdk"
|
||||
val flutterSdkPath = "D:/flutter"
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 请求
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
58
health_app/lib/core/local_database.dart
Normal file
58
health_app/lib/core/local_database.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
56
health_app/lib/core/navigation_provider.dart
Normal file
56
health_app/lib/core/navigation_provider.dart
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'); }
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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'); }
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
|
||||
@@ -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'); }
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -65,14 +65,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -254,54 +246,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.4"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -328,22 +272,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.8.1"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -440,22 +368,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -544,14 +456,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -561,61 +465,13 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -656,14 +512,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -733,6 +581,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2+1"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2+3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.8"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -765,6 +653,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -869,14 +765,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -895,4 +783,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.7 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
flutter: ">=3.38.0"
|
||||
|
||||
@@ -16,11 +16,9 @@ dependencies:
|
||||
# HTTP 网络
|
||||
dio: ^5.4.0
|
||||
|
||||
# 安全存储
|
||||
flutter_secure_storage: ^9.2.0
|
||||
|
||||
# 路由
|
||||
go_router: ^14.0.0
|
||||
# 本地数据库
|
||||
sqflite: ^2.4.0
|
||||
path: ^1.9.0
|
||||
|
||||
# 图表
|
||||
fl_chart: ^0.68.0
|
||||
|
||||
Reference in New Issue
Block a user