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,29 +1,20 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace Health.Infrastructure.AI; namespace Health.Infrastructure.AI;
/// <summary> /// <summary>
/// DeepSeek LLM 客户端(对话 + Tool Calling /// DeepSeek LLM 客户端(对话 + Tool Calling
/// </summary> /// </summary>
public sealed class DeepSeekClient public sealed class DeepSeekClient(HttpClient http, IConfiguration config)
{ {
private readonly HttpClient _http; private readonly HttpClient _http = http;
private readonly string _model; private readonly string _model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat";
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions = new()
public DeepSeekClient(HttpClient http, IConfiguration config)
{ {
_http = http; PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
_model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat"; PropertyNameCaseInsensitive = true
_jsonOptions = new() };
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
}
/// <summary> /// <summary>
/// 流式 Chat Completions /// 流式 Chat Completions
@@ -96,22 +87,15 @@ public sealed class DeepSeekClient
/// <summary> /// <summary>
/// 千问 VL 视觉客户端(食物识别 + 报告解读) /// 千问 VL 视觉客户端(食物识别 + 报告解读)
/// </summary> /// </summary>
public sealed class QwenVisionClient public sealed class QwenVisionClient(HttpClient http, IConfiguration config)
{ {
private readonly HttpClient _http; private readonly HttpClient _http = http;
private readonly string _model; private readonly string _model = config["QWEN_VISION_MODEL"] ?? "qwen-vl-max";
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions = new()
public QwenVisionClient(HttpClient http, IConfiguration config)
{ {
_http = http; PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
_model = config["QWEN_VISION_MODEL"] ?? "qwen-vl-max"; PropertyNameCaseInsensitive = true
_jsonOptions = new() };
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
}
public async Task<ChatCompletionResponse> VisionAsync( public async Task<ChatCompletionResponse> VisionAsync(
string systemPrompt, string systemPrompt,

View File

@@ -1,33 +1,30 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace Health.Infrastructure.AI; namespace Health.Infrastructure.AI;
/// <summary> /// <summary>
/// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL /// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL
/// </summary> /// </summary>
public sealed class OpenAiCompatibleClient public sealed class OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
{ {
private readonly HttpClient _http; private readonly HttpClient _http = CreateHttpClient(baseUrl, apiKey);
private readonly string _model; private readonly string _model = model;
private readonly JsonSerializerOptions _jsonOptions; private readonly JsonSerializerOptions _jsonOptions = new()
public OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
{ {
_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('/') + "/"), BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"),
Timeout = TimeSpan.FromSeconds(60) Timeout = TimeSpan.FromSeconds(60)
}; };
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_model = model; return client;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
} }
/// <summary> /// <summary>

View File

@@ -1,5 +1,3 @@
using Health.Domain.Enums;
namespace Health.Infrastructure.AI; namespace Health.Infrastructure.AI;
/// <summary> /// <summary>

View File

@@ -1,4 +1,3 @@
using Health.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -7,9 +6,8 @@ namespace Health.Infrastructure.Data;
/// <summary> /// <summary>
/// 应用程序数据库上下文 /// 应用程序数据库上下文
/// </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>(); public DbSet<User> Users => Set<User>();

View File

@@ -1,5 +1,3 @@
using Health.Domain.Entities;
namespace Health.Infrastructure.Data; namespace Health.Infrastructure.Data;
/// <summary> /// <summary>

View File

@@ -1,5 +1,3 @@
using Health.Domain.Entities;
using Health.Domain.Enums;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Health.Infrastructure.Data; namespace Health.Infrastructure.Data;

View File

@@ -0,0 +1,4 @@
global using Health.Domain.Entities;
global using Health.Domain.Enums;
global using System.Text;
global using System.Text.Json;

View File

@@ -3,25 +3,17 @@ using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
namespace Health.Infrastructure.Services; namespace Health.Infrastructure.Services;
/// <summary> /// <summary>
/// JWT Token 生成与验证服务 /// JWT Token 生成与验证服务
/// </summary> /// </summary>
public sealed class JwtProvider public sealed class JwtProvider(IConfiguration configuration)
{ {
private readonly string _secret; private readonly string _secret = configuration["JWT_SECRET"] ?? "dev-secret-key-change-in-production-min-32-chars!!";
private readonly string _issuer; private readonly string _issuer = configuration["JWT_ISSUER"] ?? "health-manager";
private readonly string _audience; private readonly string _audience = configuration["JWT_AUDIENCE"] ?? "health-manager-app";
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";
}
/// <summary> /// <summary>
/// 生成 access_token30 分钟有效) /// 生成 access_token30 分钟有效)

View File

@@ -1,21 +1,12 @@
using Health.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Health.WebApi.BackgroundServices; namespace Health.WebApi.BackgroundServices;
/// <summary> /// <summary>
/// 数据清理后台服务(每小时检查一次) /// 数据清理后台服务(每小时检查一次)
/// </summary> /// </summary>
public sealed class CleanupService : BackgroundService public sealed class CleanupService(IServiceScopeFactory scopeFactory, ILogger<CleanupService> logger) : BackgroundService
{ {
private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
private readonly ILogger<CleanupService> _logger; private readonly ILogger<CleanupService> _logger = logger;
public CleanupService(IServiceScopeFactory scopeFactory, ILogger<CleanupService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {

View File

@@ -1,23 +1,12 @@
using Health.Domain.Entities;
using Health.Domain.Enums;
using Health.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Health.WebApi.BackgroundServices; namespace Health.WebApi.BackgroundServices;
/// <summary> /// <summary>
/// 用药提醒定时扫描服务(每分钟检查一次) /// 用药提醒定时扫描服务(每分钟检查一次)
/// </summary> /// </summary>
public sealed class MedicationReminderService : BackgroundService public sealed class MedicationReminderService(IServiceScopeFactory scopeFactory, ILogger<MedicationReminderService> logger) : BackgroundService
{ {
private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
private readonly ILogger<MedicationReminderService> _logger; private readonly ILogger<MedicationReminderService> _logger = logger;
public MedicationReminderService(IServiceScopeFactory scopeFactory, ILogger<MedicationReminderService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {

View File

@@ -1,9 +1,4 @@
using System.Text.Json;
using Health.Domain.Entities;
using Health.Domain.Enums;
using Health.Infrastructure.AI; using Health.Infrastructure.AI;
using Health.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Health.WebApi.Endpoints; namespace Health.WebApi.Endpoints;
@@ -135,7 +130,7 @@ public static class AiChatEndpoints
await SseWriteAsync(http, new { action = "answer", data = content }, ct); await SseWriteAsync(http, new { action = "answer", data = content }, ct);
} }
} }
catch { /* 跳过解析失败的 chunk */ } catch (JsonException) { /* 跳过解析失败的 chunk */ }
} }
completedNormally = true; completedNormally = true;
break; break;
@@ -311,7 +306,7 @@ public static class AiChatEndpoints
var sub = jwt.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; 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; 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 private static List<ToolDefinition> GetToolsForAgent(AgentType agentType) => agentType switch

View File

@@ -1,8 +1,4 @@
using System.Text.Json;
using Health.Domain.Entities;
using Health.Infrastructure.Data;
using Health.Infrastructure.Services; using Health.Infrastructure.Services;
using Microsoft.EntityFrameworkCore;
namespace Health.WebApi.Endpoints; namespace Health.WebApi.Endpoints;

View File

@@ -1,8 +1,3 @@
using Health.Domain.Entities;
using Health.Domain.Enums;
using Health.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Health.WebApi.Endpoints; namespace Health.WebApi.Endpoints;
/// <summary> /// <summary>

View File

@@ -1,8 +1,3 @@
using Health.Domain.Entities;
using Health.Domain.Enums;
using Health.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Health.WebApi.Endpoints; namespace Health.WebApi.Endpoints;
/// <summary> /// <summary>

View File

@@ -1,6 +1,3 @@
using Health.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Health.WebApi.Endpoints; namespace Health.WebApi.Endpoints;
/// <summary> /// <summary>

View 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;

View File

@@ -1,21 +1,14 @@
using System.Net; using System.Net;
using System.Text.Json;
namespace Health.WebApi.Middleware; namespace Health.WebApi.Middleware;
/// <summary> /// <summary>
/// 全局异常处理中间件——统一返回 {code, data, message} 格式 /// 全局异常处理中间件——统一返回 {code, data, message} 格式
/// </summary> /// </summary>
public sealed class ExceptionMiddleware public sealed class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next = next;
private readonly ILogger<ExceptionMiddleware> _logger; private readonly ILogger<ExceptionMiddleware> _logger = logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {

View File

@@ -5,7 +5,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "http://localhost:5277", "applicationUrl": "http://0.0.0.0:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@@ -14,7 +14,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "https://localhost:7102;http://localhost:5277", "applicationUrl": "https://localhost:7102;http://0.0.0.0:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -1,5 +1,5 @@
pluginManagement { pluginManagement {
val flutterSdkPath = "C:/flutter_sdk" val flutterSdkPath = "D:/flutter"
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

View File

@@ -1,18 +1,39 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/app_router.dart'; import 'core/app_router.dart';
import 'core/app_theme.dart'; import 'core/app_theme.dart';
import 'core/navigation_provider.dart';
/// 健康管家 App 根组件 /// 健康管家 App 根组件
class HealthApp extends StatelessWidget { class HealthApp extends ConsumerWidget {
const HealthApp({super.key}); const HealthApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp.router( return const MaterialApp(
title: '健康管家', title: '健康管家',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme, 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 'package:dio/dio.dart';
import 'secure_storage.dart'; import 'local_database.dart';
/// API 基础地址 /// API 基础地址
const String baseUrl = 'http://10.4.172.93:5000'; const String baseUrl = 'http://10.4.185.103:5000';
/// Dio HTTP 客户端封装——带 token 注入、401 自动刷新 /// Dio HTTP 客户端封装——带 token 注入、401 自动刷新
class ApiClient { class ApiClient {
final Dio _dio; final Dio _dio;
final SecureStorage _storage; final LocalDatabase _db;
ApiClient({required SecureStorage storage}) ApiClient({required LocalDatabase db})
: _storage = storage, : _db = db,
_dio = Dio(BaseOptions( _dio = Dio(BaseOptions(
baseUrl: baseUrl, baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 15), connectTimeout: const Duration(seconds: 15),
@@ -23,16 +23,16 @@ class ApiClient {
Dio get dio => _dio; Dio get dio => _dio;
Future<String?> get accessToken => _storage.readAccessToken(); Future<String?> get accessToken => _db.read('access_token');
Future<String?> get refreshToken => _storage.readRefreshToken(); Future<String?> get refreshToken => _db.read('refresh_token');
Future<void> saveTokens(String access, String refresh) async { Future<void> saveTokens(String access, String refresh) async {
await _storage.writeAccessToken(access); await _db.write('access_token', access);
await _storage.writeRefreshToken(refresh); await _db.write('refresh_token', refresh);
} }
Future<void> clearTokens() async { Future<void> clearTokens() async {
await _storage.deleteAll(); await _db.deleteAll();
} }
/// 带 token 的 GET 请求 /// 带 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/auth/login_page.dart';
import '../pages/home/home_page.dart'; import '../pages/home/home_page.dart';
import '../pages/chart/trend_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/profile/profile_page.dart';
import '../pages/remaining_pages.dart'; import '../pages/remaining_pages.dart';
/// 应用路由配置 /// 根据路由信息返回对应页面
class AppRouter { Widget buildPage(RouteInfo route) {
AppRouter._(); final params = route.params;
switch (route.name) {
static final GoRouter router = GoRouter( case 'login':
initialLocation: '/login', return const LoginPage();
routes: [ case 'home':
GoRoute(path: '/login', builder: (_, _) => const LoginPage()), return const HomePage();
GoRoute(path: '/home', builder: (_, _) => const HomePage()), case 'trend':
GoRoute(path: '/trend/:type', builder: (_, state) => TrendPage(metricType: state.pathParameters['type']!)), return TrendPage(metricType: params['type'] ?? '');
GoRoute(path: '/calendar', builder: (_, _) => const HealthCalendarPage()), case 'calendar':
return const HealthCalendarPage();
// 用药 case 'medications':
GoRoute(path: '/medications', builder: (_, _) => const MedicationListPage()), return const MedicationListPage();
GoRoute(path: '/medications/add', builder: (_, _) => const MedicationEditPage()), case 'medicationAdd':
GoRoute(path: '/medications/:id/edit', builder: (_, state) => MedicationEditPage(id: state.pathParameters['id'])), return const MedicationEditPage();
case 'medicationEdit':
// 报告 return MedicationEditPage(id: params['id']);
GoRoute(path: '/reports', builder: (_, _) => const ReportListPage()), case 'reports':
GoRoute(path: '/reports/:id', builder: (_, state) => ReportDetailPage(id: state.pathParameters['id']!)), return const ReportListPage();
case 'reportDetail':
// 问诊 return ReportDetailPage(id: params['id']!);
GoRoute(path: '/doctors', builder: (_, _) => const DoctorListPage()), case 'doctors':
GoRoute(path: '/consultation/:id', builder: (_, state) => DoctorChatPage(id: state.pathParameters['id']!)), return const DoctorListPage();
case 'consultation':
// 运动 return DoctorChatPage(id: params['id']!);
GoRoute(path: '/exercise-plan', builder: (_, _) => const ExercisePlanPage()), case 'exercisePlan':
return const ExercisePlanPage();
// 饮食 case 'dietRecords':
GoRoute(path: '/diet-records', builder: (_, _) => const DietRecordListPage()), return const DietRecordListPage();
case 'profile':
// 个人中心 return const ProfilePage();
GoRoute(path: '/profile', builder: (_, _) => const ProfilePage()), case 'profileEdit':
GoRoute(path: '/profile/edit', builder: (_, _) => const EditProfilePage()), return const EditProfilePage();
GoRoute(path: '/health-archive', builder: (_, _) => const HealthArchivePage()), case 'healthArchive':
return const HealthArchivePage();
// 复查 case 'followups':
GoRoute(path: '/followups', builder: (_, _) => const FollowUpListPage()), return const FollowUpListPage();
case 'settings':
// 设置 return const SettingsPage();
GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()), case 'notificationPrefs':
GoRoute(path: '/settings/notifications', builder: (_, _) => const NotificationPrefsPage()), return const NotificationPrefsPage();
GoRoute(path: '/page/:type', builder: (_, state) => StaticTextPage(type: state.pathParameters['type']!)), 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/auth_provider.dart';
/// 登录页——手机号 + 验证码 /// 登录页——手机号 + 验证码
@@ -71,7 +71,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
setState(() => _error = err); setState(() => _error = err);
return; return;
} }
if (mounted) context.go('/home'); goRoute(ref, 'home');
} }
@override @override
@@ -80,7 +80,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
// 已登录直接跳转 // 已登录直接跳转
if (authState.isLoggedIn && !authState.isLoading) { if (authState.isLoggedIn && !authState.isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) => context.go('/home')); WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home'));
} }
return Scaffold( return Scaffold(

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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'; import '../../providers/data_providers.dart';
/// 用药列表页 /// 用药列表页
@@ -37,7 +37,7 @@ class MedicationListPage extends ConsumerWidget {
error: (_, _) => _empty(context), error: (_, _) => _empty(context),
), ),
floatingActionButton: FloatingActionButton.extended( 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('添加药品'), 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], 'frequency': 'Daily', 'timeOfDay': [if (_timeCtrl.text.isNotEmpty) _timeCtrl.text],
'source': 'Manual', 'startDate': DateTime.now().toIso8601String().substring(0, 10), 'source': 'Manual', 'startDate': DateTime.now().toIso8601String().substring(0, 10),
}); });
if (mounted) context.pop(); popRoute(ref);
} }
@override Widget build(BuildContext context) => Scaffold( @override Widget build(BuildContext context) => Scaffold(

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/auth_provider.dart';
/// 个人中心页面 /// 个人中心页面
@@ -38,12 +38,12 @@ class ProfilePage extends ConsumerWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_MenuItem(icon: Icons.person, title: '编辑资料', onTap: () => context.push('/profile/edit')), _MenuItem(icon: Icons.person, title: '编辑资料', onTap: () => pushRoute(ref, 'profileEdit')),
_MenuItem(icon: Icons.folder, title: '健康档案', onTap: () => context.push('/health-archive')), _MenuItem(icon: Icons.folder, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')),
_MenuItem(icon: Icons.devices, title: '设备管理', onTap: () {}), _MenuItem(icon: Icons.devices, title: '设备管理', onTap: () {}),
const Divider(), const Divider(),
_MenuItem(icon: Icons.settings, title: '设置', onTap: () => context.push('/settings')), _MenuItem(icon: Icons.settings, title: '设置', onTap: () => pushRoute(ref, 'settings')),
_MenuItem(icon: Icons.info, title: '关于', onTap: () => context.push('/page/about')), _MenuItem(icon: Icons.info, title: '关于', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})),
const Divider(), const Divider(),
_MenuItem( _MenuItem(
icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935), icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935),
@@ -52,7 +52,7 @@ class ProfilePage extends ConsumerWidget {
title: const Text('退出登录'), content: const Text('确定退出?'), title: const Text('退出登录'), content: const Text('确定退出?'),
actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/auth_provider.dart';
/// 设置页 /// 设置页
@@ -10,17 +10,17 @@ class SettingsPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) => Scaffold( Widget build(BuildContext context, WidgetRef ref) => Scaffold(
appBar: AppBar(title: const Text('设置')), appBar: AppBar(title: const Text('设置')),
body: ListView(children: [ body: ListView(children: [
_SetItem(icon: Icons.shield, title: '隐私保护中心', onTap: () => context.push('/page/privacy')), _SetItem(icon: Icons.shield, title: '隐私保护中心', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})),
_SetItem(icon: Icons.notifications, title: '通知偏好', onTap: () => context.push('/settings/notifications')), _SetItem(icon: Icons.notifications, title: '通知偏好', onTap: () => pushRoute(ref, 'notificationPrefs')),
_SetItem(icon: Icons.text_fields, title: '字体大小', trailing: _FontSlider()), _SetItem(icon: Icons.text_fields, title: '字体大小', trailing: _FontSlider()),
_SetItem(icon: Icons.article, title: '协议与公告', onTap: () => context.push('/page/terms')), _SetItem(icon: Icons.article, title: '协议与公告', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'terms'})),
_SetItem(icon: Icons.info, title: '关于', onTap: () => context.push('/page/about')), _SetItem(icon: Icons.info, title: '关于', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})),
const Divider(), const Divider(),
_SetItem(icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935), onTap: () async { _SetItem(icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935), onTap: () async {
final ok = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog( final ok = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(
title: const Text('退出登录'), content: const Text('确定退出?'), 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('确定'))])); 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../core/api_client.dart'; import '../core/api_client.dart';
import '../core/secure_storage.dart'; import '../core/local_database.dart';
/// 用户简要信息 /// 用户简要信息
class UserInfo { class UserInfo {
@@ -25,10 +25,10 @@ class AuthState {
/// 认证 Provider /// 认证 Provider
final authProvider = NotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new); 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) { final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient(storage: ref.watch(secureStorageProvider)); return ApiClient(db: ref.watch(localDbProvider));
}); });
class AuthNotifier extends Notifier<AuthState> { class AuthNotifier extends Notifier<AuthState> {
@@ -39,8 +39,8 @@ class AuthNotifier extends Notifier<AuthState> {
} }
Future<void> _checkAuth() async { Future<void> _checkAuth() async {
final storage = ref.read(secureStorageProvider); final db = ref.read(localDbProvider);
final refresh = await storage.readRefreshToken(); final refresh = await db.read('refresh_token');
if (refresh == null) { if (refresh == null) {
state = const AuthState(isLoggedIn: false, isLoading: false); state = const AuthState(isLoggedIn: false, isLoading: false);
return; return;
@@ -51,8 +51,8 @@ class AuthNotifier extends Notifier<AuthState> {
.post('/api/auth/refresh', data: {'refreshToken': refresh}); .post('/api/auth/refresh', data: {'refreshToken': refresh});
final data = response.data['data']; final data = response.data['data'];
if (data != null) { if (data != null) {
await storage.writeAccessToken(data['accessToken']); await db.write('access_token', data['accessToken']);
await storage.writeRefreshToken(data['refreshToken']); await db.write('refresh_token', data['refreshToken']);
state = AuthState( state = AuthState(
isLoggedIn: true, isLoggedIn: true,
isLoading: false, isLoading: false,
@@ -128,8 +128,8 @@ class AuthNotifier extends Notifier<AuthState> {
/// 登出 /// 登出
Future<void> logout() async { Future<void> logout() async {
final api = ref.read(apiClientProvider); final api = ref.read(apiClientProvider);
final storage = ref.read(secureStorageProvider); final db = ref.read(localDbProvider);
final refresh = await storage.readRefreshToken(); final refresh = await db.read('refresh_token');
if (refresh != null) { if (refresh != null) {
try { await api.post('/api/auth/logout', data: {'refreshToken': refresh}); } catch (_) {} 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/auth_provider.dart';
import '../providers/data_providers.dart'; import '../providers/data_providers.dart';
@@ -26,7 +26,7 @@ class HealthDrawer extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
onTap: () => context.push('/profile'), onTap: () => pushRoute(ref, 'profile'),
child: CircleAvatar( child: CircleAvatar(
radius: 28, radius: 28,
backgroundColor: const Color(0xFFEDEBFF), 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(), const Divider(),
// 健康概览——接真实数据 // 健康概览——接真实数据
@@ -50,10 +50,10 @@ class HealthDrawer extends ConsumerWidget {
), ),
latestHealth.when( latestHealth.when(
data: (data) => Column(children: [ data: (data) => Column(children: [
_HealthMetric(icon: Icons.favorite, label: '血压', value: _bpText(data['BloodPressure']), onTap: () => context.push('/trend/blood_pressure')), _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: () => context.push('/trend/heart_rate')), _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: () => context.push('/trend/glucose')), _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: () => context.push('/trend/spo2')), _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)))), loading: () => const Padding(padding: EdgeInsets.all(16), child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))),
error: (_, _) => Column(children: [ error: (_, _) => Column(children: [
@@ -76,7 +76,7 @@ class HealthDrawer extends ConsumerWidget {
final ok = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog( final ok = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(
title: const Text('退出登录'), content: const Text('确定退出?'), 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('确定'))])); 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

@@ -65,14 +65,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" 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: collection:
dependency: transitive dependency: transitive
description: description:
@@ -254,54 +246,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -328,22 +272,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" 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: http:
dependency: transitive dependency: transitive
description: description:
@@ -440,22 +368,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" 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: js:
dependency: transitive dependency: transitive
description: description:
@@ -544,14 +456,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" 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: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -561,61 +465,13 @@ packages:
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -656,14 +512,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: riverpod:
dependency: transitive dependency: transitive
description: description:
@@ -733,6 +581,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.2" 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: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -765,6 +653,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -869,14 +765,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.15.0" 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: xml:
dependency: transitive dependency: transitive
description: description:
@@ -895,4 +783,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.10.7 <4.0.0" dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.4" flutter: ">=3.38.0"

View File

@@ -16,11 +16,9 @@ dependencies:
# HTTP 网络 # HTTP 网络
dio: ^5.4.0 dio: ^5.4.0
# 安全存储 # 本地数据库
flutter_secure_storage: ^9.2.0 sqflite: ^2.4.0
path: ^1.9.0
# 路由
go_router: ^14.0.0
# 图表 # 图表
fl_chart: ^0.68.0 fl_chart: ^0.68.0