From f6af0eb1c9ec60fd5293644232b72ca9da139661 Mon Sep 17 00:00:00 2001 From: Edwin WB Li Date: Fri, 6 Feb 2026 17:37:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E9=87=8D=E6=9E=84Dio=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将HttpManager重命名为DioClient并移至core/dio目录 - 添加对UserInfo模型的支持和本地存储功能 - 新增getModel、getList、postModel方法支持模型解析 - 实现异步请求拦截器中的Token添加功能 - 添加用户信息存储和读取功能 refactor(pages): 优化页面结构和路由配置 - 将各页面组件移至对应子目录以改善项目结构 - 在RootLayout中添加路由同步功能,修复Tab选中状态问题 - 更新category页面的导入路径引用 - 删除旧的mine页面内容,迁移到新结构 feat(auth): 实现登录认证和路由保护机制 - 新增LoginPage组件,包含完整的登录表单和验证逻辑 - 实现用户登录功能,集成Token管理和用户信息存储 - 添加路由重定向逻辑,保护购物车和个人中心页面 - 创建UserInfo和LoginData数据模型 feat(utils): 添加全局日志工具类 - 创建LogUtils工具类,提供统一的日志管理功能 - 支持开发和发布环境的不同日志级别配置 - 提供带堆栈和无堆栈两种日志输出模式 - 在main.dart中初始化日志工具 --- lib/core/{ => dio}/dio_client.dart | 114 ++++++-- lib/core/{ => dio}/dio_config.dart | 0 lib/core/{ => dio}/dio_exception.dart | 0 lib/main.dart | 7 +- lib/models/user_model.dart | 52 ++++ lib/models/user_model.g.dart | 51 ++++ lib/pages/{ => cart}/cart.dart | 0 lib/pages/{ => category}/category.dart | 2 +- lib/pages/{ => goods}/goods_detail.dart | 0 lib/pages/{ => home}/home.dart | 0 lib/pages/mine.dart | 260 ----------------- lib/pages/mine/mine.dart | 26 ++ lib/pages/root_layout.dart | 26 +- lib/pages/user/login.dart | 352 ++++++++++++++++++++++++ lib/router.dart | 41 ++- lib/utils/log_utils.dart | 67 +++++ pubspec.lock | 288 +++++++++++++++++++ pubspec.yaml | 3 + 18 files changed, 1003 insertions(+), 286 deletions(-) rename lib/core/{ => dio}/dio_client.dart (71%) rename lib/core/{ => dio}/dio_config.dart (100%) rename lib/core/{ => dio}/dio_exception.dart (100%) create mode 100644 lib/models/user_model.dart create mode 100644 lib/models/user_model.g.dart rename lib/pages/{ => cart}/cart.dart (100%) rename lib/pages/{ => category}/category.dart (99%) rename lib/pages/{ => goods}/goods_detail.dart (100%) rename lib/pages/{ => home}/home.dart (100%) delete mode 100644 lib/pages/mine.dart create mode 100644 lib/pages/mine/mine.dart create mode 100644 lib/pages/user/login.dart create mode 100644 lib/utils/log_utils.dart diff --git a/lib/core/dio_client.dart b/lib/core/dio/dio_client.dart similarity index 71% rename from lib/core/dio_client.dart rename to lib/core/dio/dio_client.dart index b90369a..7f78321 100644 --- a/lib/core/dio_client.dart +++ b/lib/core/dio/dio_client.dart @@ -1,19 +1,24 @@ import 'dart:convert'; import 'package:dio/dio.dart'; +import 'package:flutter_application/models/user_model.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dio_exception.dart'; // 后续创建自定义异常 import 'package:flutter_dotenv/flutter_dotenv.dart'; -// Dio网络请求封装类 - 全局单例 -class HttpManager { - // 全局单例实例 - static final HttpManager _instance = HttpManager._internal(); - factory HttpManager() => _instance; +/// Dio 网络请求封装类 - 全局单例 +class DioClient { + /// 全局单例实例 + static final DioClient _instance = DioClient._internal(); + factory DioClient() => _instance; late Dio _dio; + // 统一Token存储键名 + static const String _tokenKey = 'auth_token'; + static const String _userInfoKey = 'userInfo'; static String baseUrl = dotenv.env['API_BASE_URL'] ?? 'http://nest-api.weibin.xyz'; + // 私有构造方法 - 初始化Dio - HttpManager._internal() { + DioClient._internal() { // 1. 初始化Dio基础配置 BaseOptions options = BaseOptions( // 从环境工具类获取基础地址(多环境自动切换) @@ -42,7 +47,7 @@ class HttpManager { void _addInterceptors() { _dio.interceptors.add(InterceptorsWrapper( // 请求拦截:发送请求前执行(加Token、打印日志、修改请求头等) - onRequest: (RequestOptions options, RequestInterceptorHandler handler) { + onRequest: (RequestOptions options, RequestInterceptorHandler handler) async { // 开发环境打印请求日志 // if (EnvUtils.isDev) { print('【请求】[${options.method}] ${options.uri}'); @@ -51,7 +56,7 @@ class HttpManager { // } // 自动添加Token(从本地存储获取,登录后才有) - _addTokenToHeader(options); + await _addTokenToHeader(options); handler.next(options); // 继续执行请求 }, @@ -151,6 +156,75 @@ class HttpManager { } } + /// 解析单个对象模型 + Future getModel( + String url, { + // 解析函数:接收Map,返回模型实例(json_serializable自动生成) + required T Function(Map) fromJson, + Map? params, + bool showLoading = true, + CancelToken? cancelToken, + }) async { + try { + if (showLoading) EasyLoading.show(status: 'Loading...'); + final response = await _dio.get( + url, + queryParameters: params, + cancelToken: cancelToken, + ); + // 核心:将响应数据转为模型对象 + return fromJson(response.data as Map); + } finally { + if (showLoading) EasyLoading.dismiss(); + } + } + + /// 解析数组列表模型 + Future> getList( + String url, { + required T Function(Map) fromJson, + Map? params, + bool showLoading = true, + CancelToken? cancelToken, + }) async { + try { + if (showLoading) EasyLoading.show(status: 'Loading...'); + final response = await _dio.get( + url, + queryParameters: params, + cancelToken: cancelToken, + ); + // 遍历数组,逐个解析为模型 + final dataList = response.data as List; + return dataList.map((item) => fromJson(item as Map)).toList(); + } finally { + if (showLoading) EasyLoading.dismiss(); + } + } + + /// POST请求(支持模型解析) + Future postModel( + String url, { + required T Function(Map) fromJson, + Map? data, + Map? params, + bool showLoading = true, + CancelToken? cancelToken, + }) async { + try { + if (showLoading) EasyLoading.show(status: 'Loading...'); + final response = await _dio.post( + url, + data: data, + queryParameters: params, + cancelToken: cancelToken, + ); + return fromJson(response.data as Map); + } finally { + if (showLoading) EasyLoading.dismiss(); + } + } + /// 封装GET请求 /// [url]:接口路径(无需加baseUrl) /// [params]:请求参数(queryParameters) @@ -170,8 +244,6 @@ class HttpManager { cancelToken: cancelToken, ); return response.data as T; - } catch (e) { - rethrow; // 抛出自定义异常,让上层处理 } finally { if (showLoading) EasyLoading.dismiss(); } @@ -206,21 +278,29 @@ class HttpManager { } } - /// 扩展:设置全局Token(登录后调用) + /// 设置全局Token(登录后调用) Future setToken(String token) async { final prefs = await SharedPreferences.getInstance(); - await prefs.setString('user_token', token); + await prefs.setString(_tokenKey, token); } - /// 扩展:移除Token(登出后调用) + ///移除Token(登出后调用) Future removeToken() async { final prefs = await SharedPreferences.getInstance(); - await prefs.remove('user_token'); + await prefs.remove(_tokenKey); + } + + ///设置 userInfo + Future setUserInfo(UserInfo userInfo) async { + final prefs = await SharedPreferences.getInstance(); + // 将对象转为 JSON 字符串 + final jsonString = jsonEncode(userInfo.toJson()); + await prefs.setString(_userInfoKey, jsonString); } - /// 扩展:获取Dio原生实例(特殊场景使用) + /// 获取Dio原生实例(特殊场景使用) Dio get dio => _dio; } -// 全局快捷实例(上层调用更简洁) -final httpManager = HttpManager(); +/// 全局快捷实例(上层调用更简洁) +final dio = DioClient(); diff --git a/lib/core/dio_config.dart b/lib/core/dio/dio_config.dart similarity index 100% rename from lib/core/dio_config.dart rename to lib/core/dio/dio_config.dart diff --git a/lib/core/dio_exception.dart b/lib/core/dio/dio_exception.dart similarity index 100% rename from lib/core/dio_exception.dart rename to lib/core/dio/dio_exception.dart diff --git a/lib/main.dart b/lib/main.dart index e3cf4ec..a5b297f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; - import 'router.dart'; +import 'package:flutter_application/utils/log_utils.dart'; + void main() async { - // 必须添加:初始化Flutter绑定(异步操作前必备) + // 初始化日志工具 + LogUtils.init(); + // 初始化Flutter绑定(异步操作前必备) WidgetsFlutterBinding.ensureInitialized(); // 加载.env文件(指定asset路径,兼容Web) diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart new file mode 100644 index 0000000..7bc6c62 --- /dev/null +++ b/lib/models/user_model.dart @@ -0,0 +1,52 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'user_model.g.dart'; + +@JsonSerializable() +class LoginData { + final String token; + final UserInfo userInfo; + + LoginData({required this.token, required this.userInfo}); + + // json_serializable 自动生成的解析方法 + factory LoginData.fromJson(Map json) => _$LoginDataFromJson(json); + Map toJson() => _$LoginDataToJson(this); +} + +@JsonSerializable() +class UserInfo { + final int id; + final String username; + final String email; + final String mobile; + final String avatar; + final String nickName; + final String role; + final int roleId; + final String roleName; + final bool status; + final String desc; + final bool isDeleted; + final DateTime createdTime; + final DateTime updatedTime; + + UserInfo({ + required this.id, + required this.username, + required this.email, + required this.mobile, + required this.avatar, + required this.nickName, + required this.role, + required this.roleId, + required this.roleName, + required this.status, + required this.desc, + required this.isDeleted, + required this.createdTime, + required this.updatedTime, + }); + + factory UserInfo.fromJson(Map json) => _$UserInfoFromJson(json); + Map toJson() => _$UserInfoToJson(this); +} diff --git a/lib/models/user_model.g.dart b/lib/models/user_model.g.dart new file mode 100644 index 0000000..c85d419 --- /dev/null +++ b/lib/models/user_model.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoginData _$LoginDataFromJson(Map json) => LoginData( + token: json['token'] as String, + userInfo: UserInfo.fromJson(json['userInfo'] as Map), + ); + +Map _$LoginDataToJson(LoginData instance) => { + 'token': instance.token, + 'userInfo': instance.userInfo, + }; + +UserInfo _$UserInfoFromJson(Map json) => UserInfo( + id: (json['id'] as num).toInt(), + username: json['username'] as String, + email: json['email'] as String, + mobile: json['mobile'] as String, + avatar: json['avatar'] as String, + nickName: json['nickName'] as String, + role: json['role'] as String, + roleId: (json['roleId'] as num).toInt(), + roleName: json['roleName'] as String, + status: json['status'] as bool, + desc: json['desc'] as String, + isDeleted: json['isDeleted'] as bool, + createdTime: DateTime.parse(json['createdTime'] as String), + updatedTime: DateTime.parse(json['updatedTime'] as String), + ); + +Map _$UserInfoToJson(UserInfo instance) => { + 'id': instance.id, + 'username': instance.username, + 'email': instance.email, + 'mobile': instance.mobile, + 'avatar': instance.avatar, + 'nickName': instance.nickName, + 'role': instance.role, + 'roleId': instance.roleId, + 'roleName': instance.roleName, + 'status': instance.status, + 'desc': instance.desc, + 'isDeleted': instance.isDeleted, + 'createdTime': instance.createdTime.toIso8601String(), + 'updatedTime': instance.updatedTime.toIso8601String(), + }; diff --git a/lib/pages/cart.dart b/lib/pages/cart/cart.dart similarity index 100% rename from lib/pages/cart.dart rename to lib/pages/cart/cart.dart diff --git a/lib/pages/category.dart b/lib/pages/category/category.dart similarity index 99% rename from lib/pages/category.dart rename to lib/pages/category/category.dart index cad93e2..1a88603 100644 --- a/lib/pages/category.dart +++ b/lib/pages/category/category.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; -import '../models/category_model.dart'; +import '../../models/category_model.dart'; class CategoryPage extends StatefulWidget { const CategoryPage({super.key}); diff --git a/lib/pages/goods_detail.dart b/lib/pages/goods/goods_detail.dart similarity index 100% rename from lib/pages/goods_detail.dart rename to lib/pages/goods/goods_detail.dart diff --git a/lib/pages/home.dart b/lib/pages/home/home.dart similarity index 100% rename from lib/pages/home.dart rename to lib/pages/home/home.dart diff --git a/lib/pages/mine.dart b/lib/pages/mine.dart deleted file mode 100644 index 979f3e5..0000000 --- a/lib/pages/mine.dart +++ /dev/null @@ -1,260 +0,0 @@ -import 'package:flutter/material.dart'; -// import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_application/core/dio_client.dart'; -import 'package:flutter_application/core/dio_exception.dart'; -import 'package:logger/logger.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:intl_phone_field/intl_phone_field.dart'; - -var logger = Logger( - printer: PrettyPrinter(), -); - -var loggerNoStack = Logger( - printer: PrettyPrinter(methodCount: 0), -); - -// 首页获取商品列表 -Future getGoodsList() async { - try { - // 调用封装的 post方法,泛型指定返回数据类型(Map/List) - Map goodsList = await httpManager.post('/hitokoto/getHitokoto', - data: { - 'page': 1, - 'size': 10, - }, - showLoading: true); - // // 处理数据(更新UI) - // setState(() { - // _goodsList = goodsList.map((e) => GoodsModel.fromJson(e)).toList(); - // }); - loggerNoStack.t(goodsList); - } on AppException catch (e) { - // 捕获自定义异常,可根据错误码做特殊处理 - print(e); - // 其他错误可自定义提示(也可依赖拦截器的全局提示) - // EasyLoading.showError(e.msg); - } -} - -// 我的页面(基础布局) -// class MinePage extends StatefulWidget { -// const MinePage({super.key}); - -// @override -// State createState() => _MinePageState(); -// } - -// class _MinePageState extends State { -// @override -// void initState() { -// super.initState(); -// WidgetsBinding.instance.addPostFrameCallback((_) { -// // EasyLoading.show(status: 'loading...'); -// // // 3秒后隐藏 -// // Future.delayed(Duration(seconds: 1)).then((value) { -// // EasyLoading.dismiss(); -// // }); -// getGoodsList(); -// }); -// } - -// @override -// Widget build(BuildContext context) { -// logger.d('Log message with 2 methods'); - -// loggerNoStack.i('Info message'); - -// loggerNoStack.w('Just a warning!'); - -// logger.e('Error! Something bad happened', error: 'Test Error'); - -// return const Center(child: Text('我的页面')); -// } -// } - -class MinePage extends StatefulWidget { - const MinePage({super.key}); - - @override - State createState() => _MinePageState(); -} - -class _MinePageState extends State { - // 表单控制器 - final _phoneController = TextEditingController(); - final _passwordController = TextEditingController(); - final _formKey = GlobalKey(); - - FocusNode focusNode = FocusNode(); - // 记住密码/密码显隐状态 - bool _rememberPwd = true; - bool _showPwd = false; - - void _submitLogin() { - // 表单验证 - if (_formKey.currentState!.validate()) { - // 校验通过 - String phone = _phoneController.text.trim(); - String password = _passwordController.text.trim(); - logger.i('手机号:$phone,密码:$password,记住密码:$_showPwd'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("登录中:$phone / $password")), - ); - } - } - - @override - void dispose() { - // 释放控制器(避免内存泄漏) - _phoneController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // return const Center(child: Text('登录页面')); - return SafeArea( - child: Scaffold( - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(12.w), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text("用户登录", style: TextStyle(fontSize: 28.sp, fontWeight: FontWeight.bold)), - SizedBox(height: 40.h), - // 手机号输入框 - TextFormField( - controller: _phoneController, - keyboardType: TextInputType.phone, - decoration: const InputDecoration( - labelText: '手机号', - hintText: '请输入手机号', - prefixIcon: Icon(Icons.phone), - border: OutlineInputBorder(), // 描边样式(符合MD3) - ), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入手机号'; - } else if (value.length != 11) { - return "请输入正确的11位手机号"; - } - return null; - }, - ), - SizedBox(height: 16.h), - IntlPhoneField( - focusNode: focusNode, - decoration: InputDecoration( - labelText: 'Phone Number', - border: OutlineInputBorder( - borderSide: BorderSide(), - ), - ), - initialCountryCode: 'CN', - languageCode: "", - onChanged: (phone) { - print(phone.completeNumber); - }, - onCountryChanged: (country) { - print('Country changed to: $country.name'); - }, - ), - SizedBox(height: 16.h), - TextFormField( - controller: _passwordController, - obscureText: !_showPwd, // 是否隐藏密码 - decoration: InputDecoration( - labelText: '密码', - hintText: "请输入密码", - prefixIcon: const Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon(_showPwd ? Icons.visibility : Icons.visibility_off), - onPressed: () => setState(() => _showPwd = !_showPwd), - ), - border: const OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入密码'; - } else if (value.length < 6 || value.length > 16) { - return "密码长度需为6-16位"; - } - return null; - }, - ), - SizedBox(height: 12.h), - Row( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Checkbox(value: _rememberPwd, onChanged: (value) => setState(() => _rememberPwd = value!)), - const Text("记住密码"), - TextButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("跳转到找回密码页面"))); - }, - child: const Text( - '忘记密码?', - style: TextStyle(color: Colors.blue), - ), - ), - TextButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("跳转到注册页面"))); - }, - child: const Text( - '注册账号?', - style: TextStyle(color: Colors.blue), - ), - ), - ], - ), - ], - ), - SizedBox(height: 20.h), - SizedBox( - width: double.infinity, - height: 50.h, - child: ElevatedButton( - onPressed: _submitLogin, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - child: Text("登录", style: TextStyle(fontSize: 18.sp)), - ), - ), - SizedBox(height: 30.h), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.wechat, color: Colors.green, size: 36.h), - onPressed: () => - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("微信登录"))), - ), - SizedBox(width: 30.h), - IconButton( - icon: Icon(Icons.tiktok, color: Colors.black, size: 36.h), - onPressed: () => ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("抖音登录"), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/mine/mine.dart b/lib/pages/mine/mine.dart new file mode 100644 index 0000000..952f5f7 --- /dev/null +++ b/lib/pages/mine/mine.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class MinePage extends StatefulWidget { + const MinePage({super.key}); + + @override + State createState() => _MinePageState(); +} + +class _MinePageState extends State { + @override + Widget build(BuildContext context) { + // return const Center(child: Text('登录页面')); + return SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(12.w), + child: const Text("我的页面"), + ), + ), + ), + ); + } +} diff --git a/lib/pages/root_layout.dart b/lib/pages/root_layout.dart index 1574aab..bbf584f 100644 --- a/lib/pages/root_layout.dart +++ b/lib/pages/root_layout.dart @@ -4,7 +4,9 @@ import 'package:go_router/go_router.dart'; // 底部 Tab 根布局(复用所有 Tab 页面的底部导航) class RootLayout extends StatefulWidget { final Widget child; - const RootLayout({super.key, required this.child}); + final String? location; + + const RootLayout({super.key, required this.child, this.location}); @override State createState() => _RootLayoutState(); @@ -25,6 +27,28 @@ class _RootLayoutState extends State { // Tab 对应的路由地址 final List _tabRoutes = ['/home', '/category', '/cart', '/mine']; + @override + void initState() { + super.initState(); + // 初始构建后根据路由同步选中索引(处理刷新场景) + WidgetsBinding.instance.addPostFrameCallback((_) => _syncIndexWithRoute()); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // 当依赖(如 GoRouter)变化时再次同步 + _syncIndexWithRoute(); + } + + void _syncIndexWithRoute() { + final String currentLocation = GoRouterState.of(context).uri.toString(); + final int idx = _tabRoutes.indexWhere((route) => currentLocation == route || currentLocation.startsWith('$route/')); + if (idx != -1 && idx != _currentIndex) { + setState(() => _currentIndex = idx); + } + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/pages/user/login.dart b/lib/pages/user/login.dart new file mode 100644 index 0000000..165c86c --- /dev/null +++ b/lib/pages/user/login.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_application/core/dio/dio_client.dart'; +import 'package:flutter_application/core/dio/dio_exception.dart'; +import 'package:flutter_application/models/user_model.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + // 表单全局Key,用于控制表单验证、重置 + final _formKey = GlobalKey(); + + late final TextEditingController _usernameController; + late final TextEditingController _passwordController; + late final FocusNode _usernameFocusNode; + late final FocusNode _pwdFocusNode; + + // 记住密码/密码显隐状态 + bool _rememberPwd = true; + bool _showPwd = false; + bool _isSubmitting = false; // 防重复提交标识 + + @override + void initState() { + super.initState(); + // 初始化控制器和焦点 + _usernameController = TextEditingController(); + _passwordController = TextEditingController(); + _usernameFocusNode = FocusNode(); + _pwdFocusNode = FocusNode(); + } + + // 登录 + Future userLogin(String username, String password) async { + try { + // 调用封装的 post方法,泛型指定返回数据类型(Map/List) + final loginData = await dio.postModel( + '/user/login', + fromJson: LoginData.fromJson, // 核心:绑定模型解析方法 + data: { + 'username': username, + 'password': password, + }, + showLoading: true, + ); + // 存储Token + await dio.setToken(loginData.token); + return loginData; + } on AppException catch (e) { + // 捕获自定义异常,可根据错误码做特殊处理 + print(e); + rethrow; + // 其他错误可自定义提示(也可依赖拦截器的全局提示) + // EasyLoading.showError(e.msg); + } + } + + Future _submitLogin() async { + if (_isSubmitting || !(_formKey.currentState?.validate() ?? false)) { + return; + } + + try { + // 去除输入空格,标准化数据 + final username = _usernameController.text.trim(); + final password = _passwordController.text.trim(); + + await userLogin(username, password); + } finally { + // 无论成功失败,重置提交状态 + if (mounted) setState(() => _isSubmitting = false); + } + } + + // 手机号校验规则 + String? _usernameValidator(String? value) { + final text = value?.trim() ?? ''; + if (text.isEmpty) return '请输入账号'; + // if (text.length != 11) return '请输入11位有效手机号'; + return null; + } + + // 密码校验规则 + String? _pwdValidator(String? value) { + final text = value?.trim() ?? ''; + if (text.isEmpty) return '请输入密码'; + if (text.length < 6 || text.length > 16) return '密码长度需为6-16位'; + return null; + } + + // 释放控制器(避免内存泄漏) + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // return const Center(child: Text('登录页面')); + return SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(12.w), + child: Form( + key: _formKey, + // 用户交互时自动校验,提升体验 + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 标题组件 + const _TitleWidget(), + SizedBox(height: 40.h), + // 账号输入框 + _UsernameInputWidget( + controller: _usernameController, + focusNode: _usernameFocusNode, + validator: _usernameValidator, + onEditingComplete: () => FocusScope.of(context).requestFocus(_usernameFocusNode), + ), + SizedBox(height: 16.h), + _PwdInputWidget( + controller: _passwordController, + focusNode: _pwdFocusNode, + showPwd: _showPwd, + onTogglePwd: () => setState(() => _showPwd = !_showPwd), + validator: _pwdValidator, + onEditingComplete: _submitLogin, + ), + SizedBox(height: 12.h), + _ActionBarWidget( + rememberPwd: _rememberPwd, + onRememberChanged: (val) => setState(() => _rememberPwd = val!), + ), + SizedBox(height: 20.h), + // 登录按钮 + _SubmitButtonWidget( + isSubmitting: _isSubmitting, + onTap: _submitLogin, + ), + SizedBox(height: 30.h), + // 第三方登录 + const _ThirdPartyLoginWidget(), + ], + ), + ), + ), + ), + ), + ); + } +} + +// 标题组件 +class _TitleWidget extends StatelessWidget { + const _TitleWidget(); + + @override + Widget build(BuildContext context) { + return Text( + "用户登录", + style: TextStyle( + fontSize: 28.sp, + fontWeight: FontWeight.bold, + ), + ); + } +} + +// 手机号输入框组件 +class _UsernameInputWidget extends StatelessWidget { + final TextEditingController controller; + final FocusNode focusNode; + final FormFieldValidator validator; + final VoidCallback onEditingComplete; + + const _UsernameInputWidget({ + required this.controller, + required this.focusNode, + required this.validator, + required this.onEditingComplete, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + focusNode: focusNode, + // 设置文本输入框键盘类型 + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + onEditingComplete: onEditingComplete, + decoration: const InputDecoration( + labelText: '账号', + hintText: '请输入账号', + prefixIcon: Icon(Icons.person), + border: OutlineInputBorder(), // 描边样式(符合MD3) + ), + validator: validator, + ); + } +} + +// 密码组件 +class _PwdInputWidget extends StatelessWidget { + final TextEditingController controller; + final FocusNode focusNode; + final bool showPwd; + final VoidCallback onTogglePwd; + final FormFieldValidator validator; + final VoidCallback onEditingComplete; + + const _PwdInputWidget({ + required this.controller, + required this.focusNode, + required this.showPwd, + required this.onTogglePwd, + required this.validator, + required this.onEditingComplete, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + focusNode: focusNode, + obscureText: !showPwd, + textInputAction: TextInputAction.done, + onEditingComplete: onEditingComplete, + decoration: InputDecoration( + labelText: '密码', + hintText: "请输入密码", + prefixIcon: const Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon(showPwd ? Icons.visibility : Icons.visibility_off), + onPressed: onTogglePwd, + ), + border: const OutlineInputBorder(), + ), + validator: validator, + ); + } +} + +class _ActionBarWidget extends StatelessWidget { + final bool rememberPwd; + final ValueChanged onRememberChanged; + + const _ActionBarWidget({ + required this.rememberPwd, + required this.onRememberChanged, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 记住密码 + Row( + children: [ + Checkbox( + value: rememberPwd, + onChanged: onRememberChanged, + ), + const Text("记住密码"), + ], + ), + // 忘记密码+注册 + Row( + children: [ + TextButton( + onPressed: () => _showSnackBar(context, "跳转到找回密码页面"), + child: const Text('忘记密码?', style: TextStyle(color: Colors.blue)), + ), + TextButton( + onPressed: () => _showSnackBar(context, "跳转到注册页面"), + child: const Text('注册账号?', style: TextStyle(color: Colors.blue)), + ), + ], + ), + ], + ); + } + + // 封装SnackBar,避免重复代码 + void _showSnackBar(BuildContext context, String msg) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(msg)), + ); + } +} + +// 登录按钮组件 +class _SubmitButtonWidget extends StatelessWidget { + final VoidCallback onTap; + final bool isSubmitting; + + const _SubmitButtonWidget({required this.onTap, required this.isSubmitting}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 50.h, + child: ElevatedButton( + onPressed: isSubmitting ? null : onTap, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: isSubmitting + ? const CircularProgressIndicator(color: Colors.white) + : Text("登录", style: TextStyle(fontSize: 18.sp)), + ), + ); + } +} + +// 第三方登录组件 +class _ThirdPartyLoginWidget extends StatelessWidget { + const _ThirdPartyLoginWidget(); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.wechat, color: Colors.green, size: 36.h), + onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("微信登录"))), + ), + SizedBox(width: 30.h), + IconButton( + icon: Icon(Icons.tiktok, color: Colors.black, size: 36.h), + onPressed: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("抖音登录"), + ), + ), + ), + ], + ); + } +} diff --git a/lib/router.dart b/lib/router.dart index 3278d93..0668b23 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,14 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_application/pages/user/login.dart'; import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -import 'pages/home.dart'; +import 'pages/home/home.dart'; import 'pages/root_layout.dart'; -import 'pages/category.dart'; -import 'pages/cart.dart'; -import 'pages/mine.dart'; -import 'pages/goods_detail.dart'; +import 'pages/category/category.dart'; +import 'pages/cart/cart.dart'; +import 'pages/mine/mine.dart'; +import 'pages/goods/goods_detail.dart'; final GoRouter router = GoRouter( + // 初始页面 initialLocation: '/home', + // 全局重定向(路由拦截核心) + redirect: (BuildContext context, GoRouterState state) async { + // 1. 获取目标路由路径 + final String targetPath = state.uri.path; + // 2. 白名单:登录/注册页面,不做拦截 + final List whiteList = ['/login', '/register', '/home', '/category']; + + if (whiteList.contains(targetPath)) return null; + + // 3. 核心逻辑:仅拦截购物车页面 /cart + if (targetPath == '/cart' || targetPath == '/mine') { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('auth_token'); + final bool isLogin = token != null && token.isNotEmpty; + // 判断登录状态 + if (!isLogin) { + // 未登录:跳转到登录页 + return '/login'; + } + } + // 其他页面/已登录:放行 + return null; + }, routes: [ // ShellRoute:实现底部 Tab 嵌套路由(核心!) ShellRoute( @@ -37,6 +64,10 @@ final GoRouter router = GoRouter( path: '/mine', builder: (context, state) => const MinePage(), ), + GoRoute( + path: '/login', + builder: (context, state) => const LoginPage(), + ), ], ), // 商品详情页(跳转路由,无 Tab) diff --git a/lib/utils/log_utils.dart b/lib/utils/log_utils.dart new file mode 100644 index 0000000..0208c5e --- /dev/null +++ b/lib/utils/log_utils.dart @@ -0,0 +1,67 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +/// 全局日志工具类 +class LogUtils { + // 私有静态实例,单例模式 + static late Logger _logger; + static late Logger _loggerNoStack; + + // 私有构造方法,禁止外部实例化 + LogUtils._internal(); + + /// 初始化日志配置(建议在 main.dart 入口调用一次) + static void init() { + // Release 环境关闭所有日志 + if (kReleaseMode) { + _logger = Logger(level: Level.off); + _loggerNoStack = Logger(level: Level.off); + return; + } + // 基础配置:带堆栈信息(默认,用于调试详情) + final basePrinter = PrettyPrinter( + methodCount: 2, // 显示的堆栈方法数 + errorMethodCount: 8, // 错误日志堆栈数 + colors: true, // 开启控制台颜色 + printEmojis: true, // 开启表情图标 + ); + + // 无堆栈配置:简洁模式,替代你原有的 loggerNoStack + final noStackPrinter = PrettyPrinter( + methodCount: 0, // 关闭堆栈 + colors: true, + printEmojis: true, + ); + + _logger = Logger(printer: basePrinter); + _loggerNoStack = Logger(printer: noStackPrinter); + } + + // ==================== 基础日志级别(带堆栈,默认) ==================== + static void d(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.d(message, error: error, stackTrace: stackTrace); + } + + static void i(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.i(message, error: error, stackTrace: stackTrace); + } + + static void w(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.w(message, error: error, stackTrace: stackTrace); + } + + static void e(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.e(message, error: error, stackTrace: stackTrace); + } + + static void t(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.t(message, error: error, stackTrace: stackTrace); + } + + // ==================== 简洁模式(无堆栈,替代原 loggerNoStack) ==================== + static void simpleD(dynamic message) => _loggerNoStack.d(message); + static void simpleI(dynamic message) => _loggerNoStack.i(message); + static void simpleW(dynamic message) => _loggerNoStack.w(message); + static void simpleE(dynamic message) => _loggerNoStack.e(message); + static void simpleT(dynamic message) => _loggerNoStack.t(message); +} diff --git a/pubspec.lock b/pubspec.lock index 70a1aa6..7de10c9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + url: "https://pub.dev" + source: hosted + version: "93.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + url: "https://pub.dev" + source: hosted + version: "10.0.1" animations: dependency: "direct main" description: @@ -9,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -25,6 +49,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: b4d854962a32fd9f8efc0b76f98214790b833af8b2e9b2df6bfc927c0415a072 + url: "https://pub.dev" + source: hosted + version: "2.10.5" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + url: "https://pub.dev" + source: hosted + version: "8.12.3" cached_network_image: dependency: "direct main" description: @@ -65,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" clock: dependency: transitive description: @@ -73,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" collection: dependency: transitive description: @@ -81,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" cookie_jar: dependency: "direct main" description: @@ -97,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" + url: "https://pub.dev" + source: hosted + version: "3.1.5" dio: dependency: "direct main" description: @@ -224,6 +328,14 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" go_router: dependency: "direct main" description: @@ -232,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "17.0.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" http: dependency: transitive description: @@ -240,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: @@ -256,6 +384,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + json_schema: + dependency: transitive + description: + name: json_schema + sha256: f37d9c3fdfe8c9aae55fdfd5af815d24ce63c3a0f6a2c1f0982c30f43643fa1a + url: "https://pub.dev" + source: hosted + version: "5.2.2" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: "93fba3ad139dab2b1ce59ecc6fdce6da46a42cdb6c4399ecda30f1e7e725760d" + url: "https://pub.dev" + source: hosted + version: "6.12.0" leak_tracker: dependency: transitive description: @@ -352,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -424,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" provider: dependency: "direct main" description: @@ -432,6 +608,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" pull_to_refresh: dependency: "direct main" description: @@ -440,6 +632,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rfc_6901: + dependency: transitive + description: + name: rfc_6901 + sha256: "6a43b1858dca2febaf93e15639aa6b0c49ccdfd7647775f15a499f872b018154" + url: "https://pub.dev" + source: hosted + version: "0.2.1" rxdart: dependency: transitive description: @@ -504,11 +712,43 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "4a85e90b50694e652075cbe4575665539d253e6ec10e46e76b45368ab5e3caae" + url: "https://pub.dev" + source: hosted + version: "1.3.10" source_span: dependency: transitive description: @@ -573,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -621,6 +869,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + uri: + dependency: transitive + description: + name: uri + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" + source: hosted + version: "1.0.0" uuid: dependency: transitive description: @@ -645,6 +901,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: transitive description: @@ -653,6 +917,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -661,6 +941,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.9.0 <4.0.0" flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8c4c87c..65f19d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,12 +28,15 @@ dependencies: shared_preferences: ^2.5.4 logger: ^2.6.2 pull_to_refresh: ^2.0.0 + json_annotation: ^4.10.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 + build_runner: ^2.10.5 + json_serializable: ^6.12.0 flutter: uses-material-design: true