Browse Source

增加下拉刷新上拉加载组件, 个人中心增加下拉刷新.

maqiang 1 year ago
parent
commit
a35e13310b

+ 1 - 0
lib/base/base_lifecycle_state.dart

@@ -1,6 +1,7 @@
1 1
 import 'package:flutter/material.dart';
2 2
 import 'package:flutter/services.dart';
3 3
 import 'package:lszlgl/base/base_state.dart';
4
+export 'package:lszlgl/ext/value_notifier_ext.dart';
4 5
 
5 6
 /// 页面生命周期基类
6 7
 ///

+ 1 - 0
lib/base/base_state.dart

@@ -4,6 +4,7 @@ import 'package:lszlgl/config/pics.dart';
4 4
 export 'package:lszlgl/config/pics.dart';
5 5
 export 'package:lszlgl/router/my_navigator.dart';
6 6
 export 'package:lszlgl/router/my_router.dart';
7
+export 'package:lszlgl/ext/value_notifier_ext.dart';
7 8
 
8 9
 abstract class BaseState<T extends StatefulWidget> extends State<T> {
9 10
   /// 隐藏软键盘

+ 66 - 0
lib/ext/value_notifier_ext.dart

@@ -0,0 +1,66 @@
1
+import 'package:flutter/material.dart';
2
+
3
+/// 万物皆可Notifier
4
+extension ObjectNotifierExt on Object? {
5
+  ValueNotifier<T?> notifier<T>() => ValueNotifier(this as T?);
6
+}
7
+
8
+/// 拓展ValueNotifier
9
+/// 快捷调用builder来编写组件
10
+extension NotifierObjectExt<T> on ValueNotifier<T> {
11
+  /// 创建Builder
12
+  ValueListenableBuilder<T> builder(Widget Function(T v) build) {
13
+    return ValueListenableBuilder<T>(
14
+      valueListenable: this,
15
+      builder: (context, v, child) => build.call(v),
16
+    );
17
+  }
18
+
19
+  /// ==
20
+  bool equals(T other) => value == other;
21
+
22
+  /// 刷新数据
23
+  update(T other) => value = other;
24
+}
25
+
26
+/// num相关拓展
27
+extension NotifierNumExt on ValueNotifier<num> {
28
+  ValueListenableBuilder<num> builder(Widget Function(num v) build) {
29
+    return ValueListenableBuilder(
30
+      valueListenable: this,
31
+      builder: (context, v, child) => build.call(v),
32
+    );
33
+  }
34
+
35
+  num operator +(num other) => value += other;
36
+
37
+  num operator -(num other) => value -= other;
38
+
39
+  num operator *(num other) => value *= other;
40
+
41
+  double operator /(num other) => value = value / other;
42
+
43
+  int operator ~/(num other) => value = value ~/ other;
44
+
45
+  num operator -() => -value;
46
+
47
+  /// 取模
48
+  num operator %(num other) => value % other;
49
+
50
+  /// 取余
51
+  num remainder(num other) => value.remainder(other);
52
+
53
+  /// 是否相等
54
+  bool equals(num other) => value == other;
55
+
56
+  bool operator <(num other) => value < other;
57
+
58
+  bool operator <=(num other) => value <= other;
59
+
60
+  bool operator >(num other) => value > other;
61
+
62
+  bool operator >=(num other) => value >= other;
63
+
64
+  /// 赋值 通知更新
65
+  update(num other) => value = other;
66
+}

+ 118 - 0
lib/main.dart

@@ -1,3 +1,4 @@
1
+import 'package:easy_refresh/easy_refresh.dart';
1 2
 import 'package:flutter/material.dart';
2 3
 import 'package:flutter/services.dart';
3 4
 import 'package:flutter_localizations/flutter_localizations.dart';
@@ -8,6 +9,102 @@ import 'package:lszlgl/router/my_navigator.dart';
8 9
 import 'package:lszlgl/utils/sp_utils.dart';
9 10
 
10 11
 late Logger logger;
12
+const zhCN = {
13
+  'Sample': '示例',
14
+  'Style': '样式',
15
+  'More': '更多',
16
+  'Theme': '主题',
17
+  'System': '系统',
18
+  'Light': '明亮',
19
+  'Dark': '暗黑',
20
+  'Classic': '经典',
21
+  'Classic and default': '经典和默认',
22
+  'Direction': '方向',
23
+  'Vertical': '垂直',
24
+  'Horizontal': '水平',
25
+  'Clamping': '固定',
26
+  'Background': '背景',
27
+  'Alignment': '对齐',
28
+  'Center': '中心',
29
+  'Start': '开端',
30
+  'End': '结尾',
31
+  'Infinite': '无限',
32
+  'Message': '信息',
33
+  'Text': '文本',
34
+  'Animation': '动画',
35
+  'Bounce': '回弹',
36
+  'List Spring': '列表弹簧',
37
+  'Pull to refresh': '下拉刷新',
38
+  'Release ready': '释放开始',
39
+  'Refreshing...': '刷新中...',
40
+  'Succeeded': '成功了',
41
+  'No more': '没有更多',
42
+  'Failed': '失败了',
43
+  'Last updated at %T': '最后更新于 %T',
44
+  'Pull to load': '上拉加载',
45
+  'Loading...': '加载中...',
46
+  'Bezier curve': '贝塞尔曲线',
47
+  'Disable': '禁用',
48
+  'Display balls': '显示小球',
49
+  'Spin in center': 'Spin居中',
50
+  'Only show spin': '只显示Spin',
51
+  'Bezier circle': '弹出圆圈',
52
+  'Golden campus': '金色校园',
53
+  'Rush to the sky': '冲上云霄',
54
+  'Balloon delivery': '气球快递',
55
+  'Star track': '太空轨道',
56
+  'Lumberjack Squats': '暴力深蹲',
57
+  'Skating boy': '滑雪少年',
58
+  'Halloween horror': '万圣节惊魂',
59
+  'User profile': '个人中心',
60
+  'User personal center': '用户个人中心',
61
+  'QQ group': 'QQ群',
62
+  'Name': '名字',
63
+  'Age': '年龄',
64
+  'Not yet bald': '尚未秃顶',
65
+  'City': '城市',
66
+  'China - ChengDu': '中国 - 成都',
67
+  'Phone': '电话',
68
+  'Mail': '邮箱',
69
+  'NestedScrollView example': 'NestedScrollView示例',
70
+  'Carousel': '轮播',
71
+  'Carousel example': '轮播示例',
72
+  'Refresh on start': '启动时刷新',
73
+  'Refresh when the list is displayed and specify the Header': '列表显示时刷新并指定Header',
74
+  'Listener': '监听器',
75
+  'Use listener to respond anywhere': '使用监听器,在任意位置响应',
76
+  'Secondary': '二楼',
77
+  'Combine existing Header with secondary': '将已有的Header结合二楼',
78
+  'Open the second floor': '打开二楼',
79
+  'Chat': '聊天',
80
+  'Chat page example': '聊天页面示例',
81
+  'PageView example': 'PageView 示例',
82
+  'Join discussion': '加入讨论',
83
+  'Join the QQ group (554981921)': '加入QQ群(554981921)',
84
+  'Repository': '项目地址',
85
+  'Support me': '支持作者',
86
+  'Buy me a coffee ~': '请作者喝一杯咖啡~',
87
+  'Alipay': '支付宝',
88
+  'Alipay donation': '支付宝捐赠',
89
+  'Wechat': '微信',
90
+  'Wechat donation': '微信捐赠',
91
+  'Cryptocurrency': '加密货币',
92
+  'Cryptocurrency donation': '加密货币捐赠',
93
+  'Bitcoin donation': '比特币捐赠',
94
+  'Ethereum series, ETH, BNB, MATIC, USDT and other tokens': '以太坊系列,ETH、BNB、MATIC、USDT及其他代币',
95
+  'Tron chain, TRX, USDT, USDC and other tokens': '波场链,TRX、USDT、USDC及其他代币',
96
+  'Dogecoin donation': '狗狗币捐赠',
97
+  '%s copied!': '%s 已复制!',
98
+  'Trigger immediately': '立即触发',
99
+  'TabBarView example': 'NestedScrollView + TabBarView示例',
100
+  'Paging': '分页',
101
+  'Paging example': '分页示例',
102
+  'No Data': '暂无数据',
103
+  'Theme switch': '主题切换',
104
+  'Theme switch example': '主题切换示例',
105
+  'Theme switch describe': '下拉显示切换面板,左右滑动选中主题色,释放进行切换。',
106
+  'Bubbles launch': '泡泡发射',
107
+};
11 108
 
12 109
 void main() async {
13 110
   WidgetsFlutterBinding.ensureInitialized();
@@ -19,6 +116,27 @@ void main() async {
19 116
   BaseDio.get().init();
20 117
   logger = Logger(printer: PrettyPrinter(methodCount: 0));
21 118
   await SPUtils.getInstance().init();
119
+
120
+  EasyRefresh.defaultHeaderBuilder = () => const ClassicHeader(
121
+        dragText: '下拉刷新',
122
+        armedText: '释放开始',
123
+        readyText: '刷新中...',
124
+        processingText: '刷新中...',
125
+        processedText: '成功了',
126
+        noMoreText: '没有更多',
127
+        failedText: '失败了',
128
+        messageText: '最后更新于 %T',
129
+      );
130
+  EasyRefresh.defaultFooterBuilder = () => const ClassicFooter(
131
+        dragText: '上拉加载',
132
+        armedText: '释放开始',
133
+        readyText: '加载中...',
134
+        processingText: '加载中...',
135
+        processedText: '成功了',
136
+        noMoreText: '没有更多',
137
+        failedText: '失败了',
138
+        messageText: '最后更新于 %T',
139
+      );
22 140
   runApp(const MyApp());
23 141
 }
24 142
 

+ 29 - 25
lib/network/base_dio.dart

@@ -1,5 +1,3 @@
1
-import 'dart:io';
2
-
3 1
 import 'package:connectivity_plus/connectivity_plus.dart';
4 2
 import 'package:dio/dio.dart';
5 3
 import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -78,7 +76,7 @@ class MyInterceptor extends Interceptor {
78 76
     if (options.queryParameters.isNotEmpty) map['queryParameters'] = options.queryParameters;
79 77
     if (options.extra.isNotEmpty) map['extra'] = options.extra;
80 78
     logger.i('ApiRequest: $map');
81
-    super.onRequest(options, handler);
79
+    handler.next(options);
82 80
   }
83 81
 
84 82
   @override
@@ -88,10 +86,27 @@ class MyInterceptor extends Interceptor {
88 86
     map['url'] = response.requestOptions.uri;
89 87
     map['statusCode'] = response.statusCode;
90 88
     map['statusMessage'] = response.statusMessage;
91
-    map['data'] = response.data;
89
+    if (response.data != null) {
90
+      var code = response.data!['code'];
91
+      var msg = response.data!['msg'];
92
+      map['code'] = code;
93
+      map['msg'] = msg;
94
+      map['data'] = response.data!['data'];
95
+      if (code != 0) {
96
+        return handler.reject(
97
+          DioException(
98
+            requestOptions: response.requestOptions,
99
+            response: response,
100
+            type: DioExceptionType.badResponse,
101
+            message: msg,
102
+          ),
103
+          true,
104
+        );
105
+      }
106
+    }
92 107
     if (response.extra.isNotEmpty) map['extra'] = response.extra;
93 108
     logger.i('ApiResponse: $map');
94
-    super.onResponse(response, handler);
109
+    handler.next(response);
95 110
   }
96 111
 
97 112
   // 是否有网
@@ -102,26 +117,14 @@ class MyInterceptor extends Interceptor {
102 117
 
103 118
   @override
104 119
   void onError(DioException err, ErrorInterceptorHandler handler) async {
105
-    if (err.error != null && err.error is SocketException) {
106
-      var error = err.error as SocketException;
107
-      String message = error.message;
108
-      // 增加网络检测
109
-      if (err.type == DioExceptionType.unknown) {
110
-        bool isConnectNetWork = await isConnected();
111
-        if (!isConnectNetWork) {
112
-          message = '当前网络不可用,请检查您的网络';
113
-        }
120
+    // 增加网络检测
121
+    if (err.type == DioExceptionType.unknown) {
122
+      bool isConnectNetWork = await isConnected();
123
+      if (!isConnectNetWork) {
124
+        err.copyWith(message: '当前网络不可用,请检查您的网络');
114 125
       }
115
-      err.copyWith(
116
-        message: message,
117
-        error: SocketException(
118
-          message,
119
-          osError: error.osError,
120
-          address: error.address,
121
-          port: error.port,
122
-        ),
123
-      );
124 126
     }
127
+    logger.e('ApiError: message:${err.message}');
125 128
     // error统一处理
126 129
     AppException appException = AppException.create(err);
127 130
     err.copyWith(error: appException);
@@ -129,12 +132,13 @@ class MyInterceptor extends Interceptor {
129 132
     Map<String, Object?> map = {};
130 133
     map['url'] = err.requestOptions.uri;
131 134
     map['statusCode'] = err.response?.statusCode;
135
+    map['message'] = err.message;
132 136
     map['exceptionCode'] = appException.code;
133
-    map['message'] = appException.message;
137
+    map['msg'] = appException.message;
134 138
     if (err.response?.extra.isNotEmpty ?? false) map['extra'] = err.response?.extra;
135 139
     logger.e('ApiError: $map');
136 140
     SmartDialog.showToast(appException.message ?? '');
137
-    super.onError(err, handler);
141
+    handler.next(err);
138 142
   }
139 143
 }
140 144
 

+ 3 - 0
lib/page/main_tab_page.dart

@@ -3,6 +3,7 @@ import 'dart:async';
3 3
 import 'package:flutter/material.dart';
4 4
 import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
5 5
 import 'package:lszlgl/base/base_state.dart';
6
+import 'package:lszlgl/network/api.dart';
6 7
 import 'package:lszlgl/page/home/home_page.dart';
7 8
 import 'package:lszlgl/page/user_center/user_center_page.dart';
8 9
 
@@ -46,6 +47,8 @@ class _MainTabPageState extends State<MainTabPage> {
46 47
     tabIconSelectList = [imgNavHomeSelect, imgNavUserCenterSelect];
47 48
     pageList = [const HomePage(), const UserCenterPage()];
48 49
     ctrl = PageController(initialPage: selectedIndex);
50
+    // 获取用户数据
51
+    Api().userProfile();
49 52
   }
50 53
 
51 54
   @override

+ 0 - 1
lib/page/user_center/account_manage_page.dart

@@ -24,7 +24,6 @@ class _AccountManagePageState extends BaseLifecycleState<AccountManagePage> {
24 24
 
25 25
   void onLogout() {
26 26
     UserService.get().logout();
27
-    MyRouter.startLogin(popAll: true);
28 27
   }
29 28
 
30 29
   @override

+ 68 - 46
lib/page/user_center/user_center_page.dart

@@ -1,7 +1,9 @@
1 1
 import 'package:cached_network_image/cached_network_image.dart';
2
+import 'package:easy_refresh/easy_refresh.dart';
2 3
 import 'package:flutter/material.dart';
3 4
 import 'package:lszlgl/base/base_lifecycle_state.dart';
4 5
 import 'package:lszlgl/base/base_state.dart';
6
+import 'package:lszlgl/network/api.dart';
5 7
 import 'package:lszlgl/network/rsp/user_rsp.dart';
6 8
 import 'package:lszlgl/service/user_service.dart';
7 9
 import 'package:lszlgl/widget/card_item.dart';
@@ -15,7 +17,7 @@ class UserCenterPage extends StatefulWidget {
15 17
 }
16 18
 
17 19
 class _UserCenterPageState extends BaseLifecycleState<UserCenterPage> with AutomaticKeepAliveClientMixin {
18
-  UserRsp? user;
20
+  final userNotify = null.notifier<UserRsp?>();
19 21
 
20 22
   void startAccountManage() {
21 23
     MyRouter.startAccountManage();
@@ -25,9 +27,16 @@ class _UserCenterPageState extends BaseLifecycleState<UserCenterPage> with Autom
25 27
     MyRouter.startSetting();
26 28
   }
27 29
 
30
+  Future<void> onRefresh() async {
31
+    try {
32
+      var value = await Api().userProfile();
33
+      if (value.data != null) userNotify.value = value.data;
34
+    } catch (err) {}
35
+  }
36
+
28 37
   @override
29 38
   void onInit() {
30
-    user = UserService.get().getUser();
39
+    userNotify.value = UserService.get().getUser();
31 40
   }
32 41
 
33 42
   @override
@@ -40,11 +49,22 @@ class _UserCenterPageState extends BaseLifecycleState<UserCenterPage> with Autom
40 49
     return Column(
41 50
       children: [
42 51
         myAppBar(title: '用户中心', autoLeading: false),
43
-        buildUserInfo(),
44
-        const SizedBox(height: 30),
45 52
         Expanded(
46
-          child: SingleChildScrollView(
47
-            child: buildList(),
53
+          child: EasyRefresh(
54
+            onRefresh: onRefresh,
55
+            child: CustomScrollView(
56
+              slivers: [
57
+                SliverToBoxAdapter(
58
+                  child: Column(
59
+                    children: [
60
+                      buildUserInfo(),
61
+                      const SizedBox(height: 30),
62
+                      buildList(),
63
+                    ],
64
+                  ),
65
+                )
66
+              ],
67
+            ),
48 68
           ),
49 69
         ),
50 70
       ],
@@ -52,48 +72,50 @@ class _UserCenterPageState extends BaseLifecycleState<UserCenterPage> with Autom
52 72
   }
53 73
 
54 74
   Widget buildUserInfo() {
55
-    return GestureDetector(
56
-      onTap: startAccountManage,
57
-      child: Container(
58
-        margin: const EdgeInsets.symmetric(horizontal: 12),
59
-        padding: const EdgeInsets.only(left: 8, right: 14, top: 14, bottom: 26),
60
-        clipBehavior: Clip.hardEdge,
61
-        decoration: BoxDecoration(
62
-          borderRadius: const BorderRadius.all(Radius.circular(12)),
63
-          boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), offset: const Offset(0, 5), blurRadius: 4)],
64
-          image: const DecorationImage(image: AssetImage(imgHomeListBg), fit: BoxFit.fill),
65
-        ),
66
-        child: Column(
67
-          children: [
68
-            buildUserText(user?.dept?.name ?? ''),
69
-            Row(
70
-              children: [
71
-                buildUserAvatar(),
72
-                const SizedBox(width: 12),
73
-                Column(
74
-                  crossAxisAlignment: CrossAxisAlignment.start,
75
-                  mainAxisSize: MainAxisSize.min,
76
-                  children: [
77
-                    Text(
78
-                      user?.nickname ?? '',
79
-                      style: const TextStyle(fontSize: 17, color: Color(0xFF333333), fontWeight: FontWeight.w500),
80
-                    ),
81
-                    const SizedBox(height: 14),
82
-                    Text(
83
-                      user?.username ?? '',
84
-                      style: const TextStyle(fontSize: 17, color: Color(0xFF333333), fontWeight: FontWeight.w500),
85
-                    ),
86
-                  ],
87
-                ),
88
-              ],
89
-            ),
90
-          ],
75
+    return userNotify.builder((user) {
76
+      return GestureDetector(
77
+        onTap: startAccountManage,
78
+        child: Container(
79
+          margin: const EdgeInsets.symmetric(horizontal: 12),
80
+          padding: const EdgeInsets.only(left: 8, right: 14, top: 14, bottom: 26),
81
+          clipBehavior: Clip.hardEdge,
82
+          decoration: BoxDecoration(
83
+            borderRadius: const BorderRadius.all(Radius.circular(12)),
84
+            boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), offset: const Offset(0, 5), blurRadius: 4)],
85
+            image: const DecorationImage(image: AssetImage(imgHomeListBg), fit: BoxFit.fill),
86
+          ),
87
+          child: Column(
88
+            children: [
89
+              buildUserText(user?.dept?.name ?? ''),
90
+              Row(
91
+                children: [
92
+                  buildUserAvatar(user?.avatar ?? ''),
93
+                  const SizedBox(width: 12),
94
+                  Column(
95
+                    crossAxisAlignment: CrossAxisAlignment.start,
96
+                    mainAxisSize: MainAxisSize.min,
97
+                    children: [
98
+                      Text(
99
+                        user?.nickname ?? '',
100
+                        style: const TextStyle(fontSize: 17, color: Color(0xFF333333), fontWeight: FontWeight.w500),
101
+                      ),
102
+                      const SizedBox(height: 14),
103
+                      Text(
104
+                        user?.username ?? '',
105
+                        style: const TextStyle(fontSize: 17, color: Color(0xFF333333), fontWeight: FontWeight.w500),
106
+                      ),
107
+                    ],
108
+                  ),
109
+                ],
110
+              ),
111
+            ],
112
+          ),
91 113
         ),
92
-      ),
93
-    );
114
+      );
115
+    });
94 116
   }
95 117
 
96
-  Widget buildUserAvatar() {
118
+  Widget buildUserAvatar(String imageUrl) {
97 119
     return Container(
98 120
       width: 75,
99 121
       height: 75,
@@ -105,7 +127,7 @@ class _UserCenterPageState extends BaseLifecycleState<UserCenterPage> with Autom
105 127
       ),
106 128
       child: CachedNetworkImage(
107 129
         fit: BoxFit.cover,
108
-        imageUrl: user?.avatar ?? '',
130
+        imageUrl: imageUrl,
109 131
         placeholder: (_, __) => const Center(child: CircularProgressIndicator()),
110 132
         errorWidget: (context, url, error) => const Center(child: Icon(Icons.error)),
111 133
       ),

+ 4 - 1
lib/service/user_service.dart

@@ -2,6 +2,7 @@ import 'dart:convert';
2 2
 
3 3
 import 'package:lszlgl/network/rsp/login_rsp.dart';
4 4
 import 'package:lszlgl/network/rsp/user_rsp.dart';
5
+import 'package:lszlgl/router/my_router.dart';
5 6
 import 'package:lszlgl/utils/sp_utils.dart';
6 7
 
7 8
 class UserService {
@@ -30,11 +31,13 @@ class UserService {
30 31
   }
31 32
 
32 33
   /// 退出登录
33
-  Future<void> logout() async {
34
+  Future<void> logout({bool goLogin = true}) async {
34 35
     _login = null;
35 36
     await SPUtils.getInstance().remove('login');
36 37
     _user = null;
37 38
     await SPUtils.getInstance().remove('user');
39
+    // 跳转登录页
40
+    if (goLogin) MyRouter.startLogin(popAll: true);
38 41
   }
39 42
 
40 43
   /// 存储用户信息

+ 114 - 0
lib/widget/page_widget.dart

@@ -0,0 +1,114 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:lszlgl/config/colors.dart';
3
+import 'package:lszlgl/widget/button.dart';
4
+
5
+enum PageState {
6
+  loading,
7
+  success,
8
+  error,
9
+}
10
+
11
+class PageLoadingWidget extends StatelessWidget {
12
+  final AlignmentGeometry? alignment;
13
+  final bool isLoading;
14
+  final String? title;
15
+  final String? message;
16
+  final String? btnText;
17
+  final VoidCallback? onTap;
18
+  final EdgeInsetsGeometry? padding;
19
+
20
+  const PageLoadingWidget({
21
+    super.key,
22
+    this.alignment,
23
+    this.isLoading = false,
24
+    this.title,
25
+    this.message,
26
+    this.btnText,
27
+    this.onTap,
28
+    this.padding = const EdgeInsets.only(top: 150),
29
+  });
30
+
31
+  const PageLoadingWidget.loading({
32
+    super.key,
33
+    this.alignment,
34
+    this.message,
35
+    this.padding = const EdgeInsets.only(top: 150),
36
+  })  : isLoading = true,
37
+        title = null,
38
+        onTap = null,
39
+        btnText = null;
40
+
41
+  const PageLoadingWidget.empty({
42
+    super.key,
43
+    this.alignment,
44
+    this.message,
45
+    this.padding = const EdgeInsets.only(top: 150),
46
+  })  : isLoading = false,
47
+        title = null,
48
+        onTap = null,
49
+        btnText = null;
50
+
51
+  const PageLoadingWidget.error({
52
+    super.key,
53
+    this.alignment,
54
+    this.title,
55
+    this.onTap,
56
+    this.btnText = '点击重试',
57
+    this.padding = const EdgeInsets.only(top: 150),
58
+  })  : isLoading = false,
59
+        message = null;
60
+
61
+  @override
62
+  Widget build(BuildContext context) {
63
+    return Container(
64
+      alignment: alignment ?? Alignment.center,
65
+      padding: padding,
66
+      child: Column(
67
+        children: [
68
+          buildLoading(),
69
+          buildTitle(),
70
+          buildMessage(),
71
+          buildButton(),
72
+        ],
73
+      ),
74
+    );
75
+  }
76
+
77
+  Widget buildLoading() {
78
+    if (!isLoading) return const SizedBox.shrink();
79
+    return const Padding(
80
+      padding: EdgeInsets.only(bottom: 12),
81
+      child: CircularProgressIndicator(strokeWidth: 3),
82
+    );
83
+  }
84
+
85
+  Widget buildTitle() {
86
+    if (title == null) return const SizedBox.shrink();
87
+    return Padding(
88
+      padding: const EdgeInsets.only(bottom: 12),
89
+      child: Text(
90
+        title!,
91
+        style: const TextStyle(fontSize: 16, color: MyColor.c_333333),
92
+      ),
93
+    );
94
+  }
95
+
96
+  Widget buildMessage() {
97
+    if (message == null) return const SizedBox.shrink();
98
+    return Padding(
99
+      padding: const EdgeInsets.only(bottom: 12),
100
+      child: Text(
101
+        message!,
102
+        style: const TextStyle(fontSize: 12, color: MyColor.c_666666),
103
+      ),
104
+    );
105
+  }
106
+
107
+  Widget buildButton() {
108
+    if (btnText == null) return const SizedBox.shrink();
109
+    return MyButton(
110
+      btnText!,
111
+      onTap: onTap,
112
+    );
113
+  }
114
+}

+ 24 - 0
pubspec.lock

@@ -265,6 +265,14 @@ packages:
265 265
       url: "https://pub.dev"
266 266
     source: hosted
267 267
     version: "5.4.1"
268
+  easy_refresh:
269
+    dependency: "direct main"
270
+    description:
271
+      name: easy_refresh
272
+      sha256: f36324c7f50291f85085bd64b39abe6b63909481bfa527f9c654d17ef172de30
273
+      url: "https://pub.dev"
274
+    source: hosted
275
+    version: "3.3.4"
268 276
   fake_async:
269 277
     dependency: transitive
270 278
     description:
@@ -581,6 +589,22 @@ packages:
581 589
       url: "https://pub.dev"
582 590
     source: hosted
583 591
     version: "1.8.3"
592
+  path_drawing:
593
+    dependency: transitive
594
+    description:
595
+      name: path_drawing
596
+      sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977
597
+      url: "https://pub.dev"
598
+    source: hosted
599
+    version: "1.0.1"
600
+  path_parsing:
601
+    dependency: transitive
602
+    description:
603
+      name: path_parsing
604
+      sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
605
+      url: "https://pub.dev"
606
+    source: hosted
607
+    version: "1.0.1"
584 608
   path_provider:
585 609
     dependency: transitive
586 610
     description:

+ 2 - 0
pubspec.yaml

@@ -66,6 +66,8 @@ dependencies:
66 66
   json_annotation: ^4.8.1
67 67
   # 共享参数
68 68
   shared_preferences: ^2.2.2
69
+  # 下拉刷新上拉加载
70
+  easy_refresh: ^3.3.4
69 71
 
70 72
 dev_dependencies:
71 73
   flutter_test: