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 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,
|
||||||
@@ -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>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using Health.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Health.Infrastructure.AI;
|
namespace Health.Infrastructure.AI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -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>();
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using Health.Domain.Entities;
|
|
||||||
|
|
||||||
namespace Health.Infrastructure.Data;
|
namespace Health.Infrastructure.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -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;
|
||||||
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.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_token(30 分钟有效)
|
/// 生成 access_token(30 分钟有效)
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
using Health.Infrastructure.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Health.WebApi.Endpoints;
|
namespace Health.WebApi.Endpoints;
|
||||||
|
|
||||||
/// <summary>
|
/// <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.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)
|
||||||
{
|
{
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 请求
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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/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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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'); }
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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'); }
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (_) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'); }
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user