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,29 +1,20 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
/// <summary>
|
||||
/// DeepSeek LLM 客户端(对话 + Tool Calling)
|
||||
/// </summary>
|
||||
public sealed class DeepSeekClient
|
||||
public sealed class DeepSeekClient(HttpClient http, IConfiguration config)
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _model;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public DeepSeekClient(HttpClient http, IConfiguration config)
|
||||
private readonly HttpClient _http = http;
|
||||
private readonly string _model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat";
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
_http = http;
|
||||
_model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat";
|
||||
_jsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 流式 Chat Completions
|
||||
@@ -96,22 +87,15 @@ public sealed class DeepSeekClient
|
||||
/// <summary>
|
||||
/// 千问 VL 视觉客户端(食物识别 + 报告解读)
|
||||
/// </summary>
|
||||
public sealed class QwenVisionClient
|
||||
public sealed class QwenVisionClient(HttpClient http, IConfiguration config)
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _model;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public QwenVisionClient(HttpClient http, IConfiguration config)
|
||||
private readonly HttpClient _http = http;
|
||||
private readonly string _model = config["QWEN_VISION_MODEL"] ?? "qwen-vl-max";
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
_http = http;
|
||||
_model = config["QWEN_VISION_MODEL"] ?? "qwen-vl-max";
|
||||
_jsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public async Task<ChatCompletionResponse> VisionAsync(
|
||||
string systemPrompt,
|
||||
@@ -1,33 +1,30 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL
|
||||
/// </summary>
|
||||
public sealed class OpenAiCompatibleClient
|
||||
public sealed class OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _model;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
|
||||
private readonly HttpClient _http = CreateHttpClient(baseUrl, apiKey);
|
||||
private readonly string _model = model;
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
_http = new HttpClient
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private static HttpClient CreateHttpClient(string baseUrl, string apiKey)
|
||||
{
|
||||
var client = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"),
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
_model = model;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1,5 +1,3 @@
|
||||
using Health.Domain.Enums;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
/// <summary>
|
||||
@@ -1,4 +1,3 @@
|
||||
using Health.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
@@ -7,9 +6,8 @@ namespace Health.Infrastructure.Data;
|
||||
/// <summary>
|
||||
/// 应用程序数据库上下文
|
||||
/// </summary>
|
||||
public sealed class AppDbContext : DbContext
|
||||
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
// 核心业务表
|
||||
public DbSet<User> Users => Set<User>();
|
||||
@@ -1,5 +1,3 @@
|
||||
using Health.Domain.Entities;
|
||||
|
||||
namespace Health.Infrastructure.Data;
|
||||
|
||||
/// <summary>
|
||||
@@ -1,5 +1,3 @@
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Health.Infrastructure.Data;
|
||||
4
backend/src/Health.Infrastructure/GlobalUsings.cs
Normal file
4
backend/src/Health.Infrastructure/GlobalUsings.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
global using Health.Domain.Entities;
|
||||
global using Health.Domain.Enums;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
@@ -3,25 +3,17 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Health.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// JWT Token 生成与验证服务
|
||||
/// </summary>
|
||||
public sealed class JwtProvider
|
||||
public sealed class JwtProvider(IConfiguration configuration)
|
||||
{
|
||||
private readonly string _secret;
|
||||
private readonly string _issuer;
|
||||
private readonly string _audience;
|
||||
|
||||
public JwtProvider(IConfiguration configuration)
|
||||
{
|
||||
_secret = configuration["JWT_SECRET"] ?? "dev-secret-key-change-in-production-min-32-chars!!";
|
||||
_issuer = configuration["JWT_ISSUER"] ?? "health-manager";
|
||||
_audience = configuration["JWT_AUDIENCE"] ?? "health-manager-app";
|
||||
}
|
||||
private readonly string _secret = configuration["JWT_SECRET"] ?? "dev-secret-key-change-in-production-min-32-chars!!";
|
||||
private readonly string _issuer = configuration["JWT_ISSUER"] ?? "health-manager";
|
||||
private readonly string _audience = configuration["JWT_AUDIENCE"] ?? "health-manager-app";
|
||||
|
||||
/// <summary>
|
||||
/// 生成 access_token(30 分钟有效)
|
||||
@@ -1,21 +1,12 @@
|
||||
using Health.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// 数据清理后台服务(每小时检查一次)
|
||||
/// </summary>
|
||||
public sealed class CleanupService : BackgroundService
|
||||
public sealed class CleanupService(IServiceScopeFactory scopeFactory, ILogger<CleanupService> logger) : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<CleanupService> _logger;
|
||||
|
||||
public CleanupService(IServiceScopeFactory scopeFactory, ILogger<CleanupService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
|
||||
private readonly ILogger<CleanupService> _logger = logger;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
@@ -1,23 +1,12 @@
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Health.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// 用药提醒定时扫描服务(每分钟检查一次)
|
||||
/// </summary>
|
||||
public sealed class MedicationReminderService : BackgroundService
|
||||
public sealed class MedicationReminderService(IServiceScopeFactory scopeFactory, ILogger<MedicationReminderService> logger) : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<MedicationReminderService> _logger;
|
||||
|
||||
public MedicationReminderService(IServiceScopeFactory scopeFactory, ILogger<MedicationReminderService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
|
||||
private readonly ILogger<MedicationReminderService> _logger = logger;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
@@ -1,9 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Health.Infrastructure.AI;
|
||||
using Health.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
@@ -135,7 +130,7 @@ public static class AiChatEndpoints
|
||||
await SseWriteAsync(http, new { action = "answer", data = content }, ct);
|
||||
}
|
||||
}
|
||||
catch { /* 跳过解析失败的 chunk */ }
|
||||
catch (JsonException) { /* 跳过解析失败的 chunk */ }
|
||||
}
|
||||
completedNormally = true;
|
||||
break;
|
||||
@@ -311,7 +306,7 @@ public static class AiChatEndpoints
|
||||
var sub = jwt.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
return sub != null && Guid.TryParse(sub, out var id) ? id : null;
|
||||
}
|
||||
catch { return null; }
|
||||
catch (Exception) { return null; }
|
||||
}
|
||||
|
||||
private static List<ToolDefinition> GetToolsForAgent(AgentType agentType) => agentType switch
|
||||
@@ -1,8 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Health.Domain.Entities;
|
||||
using Health.Infrastructure.Data;
|
||||
using Health.Infrastructure.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Health.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
@@ -1,8 +1,3 @@
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Health.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
@@ -1,6 +1,3 @@
|
||||
using Health.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
5
backend/src/Health.WebApi/GlobalUsings.cs
Normal file
5
backend/src/Health.WebApi/GlobalUsings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
global using Health.Domain.Entities;
|
||||
global using Health.Domain.Enums;
|
||||
global using Health.Infrastructure.Data;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using System.Text.Json;
|
||||
@@ -1,21 +1,14 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Health.WebApi.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// 全局异常处理中间件——统一返回 {code, data, message} 格式
|
||||
/// </summary>
|
||||
public sealed class ExceptionMiddleware
|
||||
public sealed class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
||||
|
||||
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly RequestDelegate _next = next;
|
||||
private readonly ILogger<ExceptionMiddleware> _logger = logger;
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
@@ -5,7 +5,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5277",
|
||||
"applicationUrl": "http://0.0.0.0:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7102;http://localhost:5277",
|
||||
"applicationUrl": "https://localhost:7102;http://0.0.0.0:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -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