flu-cli-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/chunk-FOMWV2YP.js +378 -0
- package/dist/chunk-SW6YDKXI.js +112 -0
- package/dist/factory-6DDXZYQP.js +6 -0
- package/dist/index.cjs +4668 -0
- package/dist/index.d.cts +644 -0
- package/dist/index.d.ts +644 -0
- package/dist/index.js +4037 -0
- package/dist/upgrade_snippets-XFR7Q444.js +8 -0
- package/locales/en-US.json +59 -0
- package/locales/zh-CN.json +59 -0
- package/package.json +52 -0
- package/templates/README.md +129 -0
- package/templates/core_files/base/base_list_page.dart.template +225 -0
- package/templates/core_files/base/base_list_viewmodel.dart.template +164 -0
- package/templates/core_files/base/base_page.dart.template +252 -0
- package/templates/core_files/base/base_viewmodel.dart.template +68 -0
- package/templates/core_files/base/index.dart.template +5 -0
- package/templates/core_files/config/app_config.dart.template +142 -0
- package/templates/core_files/config/app_initializer.dart.template +74 -0
- package/templates/core_files/config/index.dart.template +3 -0
- package/templates/core_files/index.dart.template +8 -0
- package/templates/core_files/network/README.md +378 -0
- package/templates/core_files/network/app_error_code.dart.template +49 -0
- package/templates/core_files/network/app_http.dart.template +306 -0
- package/templates/core_files/network/app_response.dart.template +81 -0
- package/templates/core_files/network/index.dart.template +12 -0
- package/templates/core_files/network/interceptors/app_response_interceptor.dart.template +44 -0
- package/templates/core_files/network/interceptors/auth_interceptor.dart.template +30 -0
- package/templates/core_files/network/interceptors/error_interceptor.dart.template +48 -0
- package/templates/core_files/network/interceptors/index.dart.template +6 -0
- package/templates/core_files/network/interceptors/log_interceptor.dart.template +97 -0
- package/templates/core_files/network/interceptors/network_error_interceptor.dart.template +58 -0
- package/templates/core_files/network/interceptors/retry_interceptor.dart.template +69 -0
- package/templates/core_files/network/response_adapter.dart.template +69 -0
- package/templates/core_files/router/app_routes.dart.template +32 -0
- package/templates/core_files/router/index.dart.template +3 -0
- package/templates/core_files/router/navigator_util_getx.dart.template +131 -0
- package/templates/core_files/router/navigator_util_material.dart.template +191 -0
- package/templates/core_files/storage/index.dart.template +3 -0
- package/templates/core_files/storage/storage_keys.dart.template +34 -0
- package/templates/core_files/storage/storage_util.dart.template +102 -0
- package/templates/core_files/theme/app_theme.dart.template +37 -0
- package/templates/core_files/theme/index.dart.template +3 -0
- package/templates/core_files/theme/status_views_theme.dart.template +40 -0
- package/templates/core_files/utils/index.dart.template +2 -0
- package/templates/core_files/utils/loading_util.dart.template +55 -0
- package/templates/core_files/utils/toast_util.dart.template +128 -0
- package/templates/examples/eg_list_page.dart.template +340 -0
- package/templates/examples/eg_list_viewmodel.dart.template +31 -0
- package/templates/examples/eg_service.dart.template +78 -0
- package/templates/examples/mock_data.dart.template +50388 -0
- package/templates/examples/tu_chong_model.dart.template +633 -0
- package/templates/request_helper.dart.template +59 -0
- package/templates/snippets/flu-cli.code-snippets +268 -0
- package/templates/snippets/flu-cli.code-snippets.backup +268 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
class StatusViewsTheme extends ThemeExtension<StatusViewsTheme> {
|
|
3
|
+
final Widget Function(BuildContext ctx)? loadingBuilder;
|
|
4
|
+
final Widget Function(BuildContext ctx, String? msg)? errorBuilder;
|
|
5
|
+
final Widget Function(BuildContext ctx)? emptyBuilder;
|
|
6
|
+
final Color? refreshIndicatorColor;
|
|
7
|
+
final Widget Function(BuildContext ctx, bool hasMore)? loadMoreBuilder;
|
|
8
|
+
const StatusViewsTheme({
|
|
9
|
+
this.loadingBuilder,
|
|
10
|
+
this.errorBuilder,
|
|
11
|
+
this.emptyBuilder,
|
|
12
|
+
this.refreshIndicatorColor,
|
|
13
|
+
this.loadMoreBuilder,
|
|
14
|
+
});
|
|
15
|
+
@override
|
|
16
|
+
StatusViewsTheme copyWith({
|
|
17
|
+
Widget Function(BuildContext)? loadingBuilder,
|
|
18
|
+
Widget Function(BuildContext, String?)? errorBuilder,
|
|
19
|
+
Widget Function(BuildContext)? emptyBuilder,
|
|
20
|
+
Color? refreshIndicatorColor,
|
|
21
|
+
Widget Function(BuildContext, bool)? loadMoreBuilder,
|
|
22
|
+
}) {
|
|
23
|
+
return StatusViewsTheme(
|
|
24
|
+
loadingBuilder: loadingBuilder ?? this.loadingBuilder,
|
|
25
|
+
errorBuilder: errorBuilder ?? this.errorBuilder,
|
|
26
|
+
emptyBuilder: emptyBuilder ?? this.emptyBuilder,
|
|
27
|
+
refreshIndicatorColor:
|
|
28
|
+
refreshIndicatorColor ?? this.refreshIndicatorColor,
|
|
29
|
+
loadMoreBuilder: loadMoreBuilder ?? this.loadMoreBuilder,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
@override
|
|
33
|
+
ThemeExtension<StatusViewsTheme> lerp(
|
|
34
|
+
ThemeExtension<StatusViewsTheme>? other,
|
|
35
|
+
double t,
|
|
36
|
+
) {
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
|
|
3
|
+
/// Loading 工具类 - 基于 Overlay 的原生实现
|
|
4
|
+
///
|
|
5
|
+
/// 优点:
|
|
6
|
+
/// 1. 零第三方依赖:保持项目纯洁,不增加 pubspec.yaml 负担。
|
|
7
|
+
/// 2. 灵活性高:完全基于 Flutter 原生机制,易于自定义 UI 样式。
|
|
8
|
+
/// 3. 轻量级:不占用额外打包体积。
|
|
9
|
+
class LoadingUtil {
|
|
10
|
+
static OverlayEntry? _overlayEntry;
|
|
11
|
+
|
|
12
|
+
/// 显示 Loading
|
|
13
|
+
///
|
|
14
|
+
/// [context] 上下文
|
|
15
|
+
/// [message] 加载提示文字
|
|
16
|
+
static void show(BuildContext context, {String? message}) {
|
|
17
|
+
if (_overlayEntry != null) return;
|
|
18
|
+
|
|
19
|
+
_overlayEntry = OverlayEntry(
|
|
20
|
+
builder: (context) => Material(
|
|
21
|
+
color: Colors.black54,
|
|
22
|
+
child: Center(
|
|
23
|
+
child: Container(
|
|
24
|
+
padding: const EdgeInsets.all(20),
|
|
25
|
+
decoration: BoxDecoration(
|
|
26
|
+
color: Colors.white,
|
|
27
|
+
borderRadius: BorderRadius.circular(8),
|
|
28
|
+
),
|
|
29
|
+
child: Column(
|
|
30
|
+
mainAxisSize: MainAxisSize.min,
|
|
31
|
+
children: [
|
|
32
|
+
const CircularProgressIndicator(),
|
|
33
|
+
if (message != null) ...[
|
|
34
|
+
const SizedBox(height: 16),
|
|
35
|
+
Text(
|
|
36
|
+
message,
|
|
37
|
+
style: const TextStyle(fontSize: 14, color: Colors.black),
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
],
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
Overlay.of(context).insert(_overlayEntry!);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// 隐藏 Loading
|
|
51
|
+
static void dismiss() {
|
|
52
|
+
_overlayEntry?.remove();
|
|
53
|
+
_overlayEntry = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
{{#if_getx}}
|
|
3
|
+
import 'package:get/get.dart';
|
|
4
|
+
{{else}}
|
|
5
|
+
import '../router/navigator_util.dart';
|
|
6
|
+
{{/if_getx}}
|
|
7
|
+
|
|
8
|
+
/// Toast 工具类 - 自动适配状态管理器
|
|
9
|
+
///
|
|
10
|
+
/// Material 模式: 基于 SnackBar
|
|
11
|
+
/// GetX 模式: 基于 Get.snackbar
|
|
12
|
+
///
|
|
13
|
+
/// 优点:
|
|
14
|
+
/// 1. 零第三方依赖(GetX 除外)
|
|
15
|
+
/// 2. 无需 BuildContext,全局可调用
|
|
16
|
+
/// 3. 自动清除旧消息,防止堆叠
|
|
17
|
+
class ToastUtil {
|
|
18
|
+
/// 显示 Toast 消息
|
|
19
|
+
///
|
|
20
|
+
/// [message] 提示消息
|
|
21
|
+
/// [duration] 显示时长,默认 2 秒
|
|
22
|
+
/// [showCloseIcon] 是否显示关闭图标,默认 true
|
|
23
|
+
static void show(
|
|
24
|
+
String message, {
|
|
25
|
+
Duration duration = const Duration(seconds: 2),
|
|
26
|
+
bool showCloseIcon = true,
|
|
27
|
+
}) {
|
|
28
|
+
if (message.isEmpty) return;
|
|
29
|
+
|
|
30
|
+
{{#if_getx}}
|
|
31
|
+
// GetX 模式: 使用 Get.snackbar
|
|
32
|
+
Get.snackbar(
|
|
33
|
+
'',
|
|
34
|
+
message,
|
|
35
|
+
snackPosition: SnackPosition.BOTTOM,
|
|
36
|
+
duration: duration,
|
|
37
|
+
margin: const EdgeInsets.all(16),
|
|
38
|
+
borderRadius: 8,
|
|
39
|
+
isDismissible: true,
|
|
40
|
+
dismissDirection: DismissDirection.horizontal,
|
|
41
|
+
showProgressIndicator: false,
|
|
42
|
+
titleText: const SizedBox.shrink(), // 不显示标题
|
|
43
|
+
messageText: Text(
|
|
44
|
+
message,
|
|
45
|
+
style: const TextStyle(color: Colors.white),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
{{else}}
|
|
49
|
+
// Material 模式: 使用 ScaffoldMessenger
|
|
50
|
+
final state = NavigatorUtil.scaffoldMessengerKey.currentState;
|
|
51
|
+
if (state == null) {
|
|
52
|
+
debugPrint('⚠️ 无法显示Toast, ScaffoldMessengerState为空: $message');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 清除之前的 SnackBar,防止堆叠
|
|
57
|
+
state.clearSnackBars();
|
|
58
|
+
|
|
59
|
+
// 显示新的 SnackBar
|
|
60
|
+
state.showSnackBar(
|
|
61
|
+
SnackBar(
|
|
62
|
+
content: Text(message),
|
|
63
|
+
behavior: SnackBarBehavior.floating, // 浮动样式
|
|
64
|
+
duration: duration,
|
|
65
|
+
showCloseIcon: showCloseIcon,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
{{/if_getx}}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// 显示成功消息
|
|
72
|
+
static void success(String message) {
|
|
73
|
+
{{#if_getx}}
|
|
74
|
+
Get.snackbar(
|
|
75
|
+
'成功',
|
|
76
|
+
message,
|
|
77
|
+
snackPosition: SnackPosition.BOTTOM,
|
|
78
|
+
backgroundColor: Colors.green,
|
|
79
|
+
colorText: Colors.white,
|
|
80
|
+
icon: const Icon(Icons.check_circle, color: Colors.white),
|
|
81
|
+
duration: const Duration(seconds: 2),
|
|
82
|
+
margin: const EdgeInsets.all(16),
|
|
83
|
+
borderRadius: 8,
|
|
84
|
+
);
|
|
85
|
+
{{else}}
|
|
86
|
+
show(message);
|
|
87
|
+
{{/if_getx}}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// 显示错误消息
|
|
91
|
+
static void error(String message) {
|
|
92
|
+
{{#if_getx}}
|
|
93
|
+
Get.snackbar(
|
|
94
|
+
'错误',
|
|
95
|
+
message,
|
|
96
|
+
snackPosition: SnackPosition.BOTTOM,
|
|
97
|
+
backgroundColor: Colors.red,
|
|
98
|
+
colorText: Colors.white,
|
|
99
|
+
icon: const Icon(Icons.error, color: Colors.white),
|
|
100
|
+
duration: const Duration(seconds: 3),
|
|
101
|
+
margin: const EdgeInsets.all(16),
|
|
102
|
+
borderRadius: 8,
|
|
103
|
+
);
|
|
104
|
+
{{else}}
|
|
105
|
+
show(message);
|
|
106
|
+
{{/if_getx}}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// 显示警告消息
|
|
110
|
+
static void warning(String message) {
|
|
111
|
+
{{#if_getx}}
|
|
112
|
+
Get.snackbar(
|
|
113
|
+
'警告',
|
|
114
|
+
message,
|
|
115
|
+
snackPosition: SnackPosition.BOTTOM,
|
|
116
|
+
backgroundColor: Colors.orange,
|
|
117
|
+
colorText: Colors.white,
|
|
118
|
+
icon: const Icon(Icons.warning, color: Colors.white),
|
|
119
|
+
duration: const Duration(seconds: 2),
|
|
120
|
+
margin: const EdgeInsets.all(16),
|
|
121
|
+
borderRadius: 8,
|
|
122
|
+
);
|
|
123
|
+
{{else}}
|
|
124
|
+
show(message);
|
|
125
|
+
{{/if_getx}}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '{{CORE_IMPORT}}';
|
|
3
|
+
|
|
4
|
+
{{#if_clean}}
|
|
5
|
+
import '{{DATA_IMPORT}}';
|
|
6
|
+
{{else}}
|
|
7
|
+
import '{{MODELS_IMPORT}}';
|
|
8
|
+
{{/if_clean}}
|
|
9
|
+
import '{{VIEWMODELS_IMPORT}}';
|
|
10
|
+
|
|
11
|
+
/// 示例:图片列表页
|
|
12
|
+
class EgListPage extends BaseListPage<TuChongItem, EgListViewModel> {
|
|
13
|
+
const EgListPage({super.key});
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
State<EgListPage> createState() => _EgListPageState();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class _EgListPageState
|
|
20
|
+
extends BaseListPageState<TuChongItem, EgListViewModel, EgListPage> {
|
|
21
|
+
@override
|
|
22
|
+
String get title => '图片列表';
|
|
23
|
+
@override
|
|
24
|
+
bool get enableRefresh => true;
|
|
25
|
+
@override
|
|
26
|
+
EgListViewModel createViewModel() => EgListViewModel();
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
Widget buildItem(BuildContext context, TuChongItem item, int index) {
|
|
30
|
+
Widget body = Container();
|
|
31
|
+
|
|
32
|
+
body = Column(
|
|
33
|
+
mainAxisAlignment: MainAxisAlignment.start,
|
|
34
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
35
|
+
children: [
|
|
36
|
+
_buildUserInfo(item),
|
|
37
|
+
const SizedBox(
|
|
38
|
+
height: 10,
|
|
39
|
+
),
|
|
40
|
+
_buildTagsWidget(item),
|
|
41
|
+
ExampleTuItmeImagelessV(tuChongItem: item),
|
|
42
|
+
],
|
|
43
|
+
);
|
|
44
|
+
body = Container(
|
|
45
|
+
margin: const EdgeInsets.only(
|
|
46
|
+
left: 15,
|
|
47
|
+
right: 15,
|
|
48
|
+
top: 15,
|
|
49
|
+
),
|
|
50
|
+
padding: const EdgeInsets.all(15),
|
|
51
|
+
decoration: BoxDecoration(
|
|
52
|
+
color: Colors.white,
|
|
53
|
+
borderRadius: BorderRadius.circular(10),
|
|
54
|
+
boxShadow: const [
|
|
55
|
+
BoxShadow(
|
|
56
|
+
color: Colors.black12,
|
|
57
|
+
offset: Offset(0, 0),
|
|
58
|
+
blurRadius: 3,
|
|
59
|
+
),
|
|
60
|
+
],
|
|
61
|
+
),
|
|
62
|
+
child: body,
|
|
63
|
+
);
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 构建用户信息
|
|
68
|
+
_buildUserInfo(TuChongItem item) {
|
|
69
|
+
final Widget body = Row(
|
|
70
|
+
children: [
|
|
71
|
+
_buildImageWidget(
|
|
72
|
+
url: item.imageUrl,
|
|
73
|
+
),
|
|
74
|
+
const SizedBox(
|
|
75
|
+
width: 10,
|
|
76
|
+
),
|
|
77
|
+
Expanded(
|
|
78
|
+
child: Column(
|
|
79
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
80
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
81
|
+
children: [
|
|
82
|
+
Text(
|
|
83
|
+
item.site?.name ?? AppConstants.appName,
|
|
84
|
+
style: const TextStyle(
|
|
85
|
+
fontSize: 16,
|
|
86
|
+
fontWeight: FontWeight.bold,
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
if (item.site?.description?.isNotEmpty ?? false)
|
|
90
|
+
Text(
|
|
91
|
+
item.site?.description ?? '',
|
|
92
|
+
style: const TextStyle(
|
|
93
|
+
fontSize: 14,
|
|
94
|
+
color: Colors.grey,
|
|
95
|
+
),
|
|
96
|
+
maxLines: 1,
|
|
97
|
+
overflow: TextOverflow.ellipsis,
|
|
98
|
+
),
|
|
99
|
+
],
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
],
|
|
103
|
+
);
|
|
104
|
+
return body;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 构建标签
|
|
108
|
+
_buildTagsWidget(TuChongItem tuChongItem) {
|
|
109
|
+
Widget body = Wrap(
|
|
110
|
+
spacing: 6,
|
|
111
|
+
runSpacing: 6,
|
|
112
|
+
children: [
|
|
113
|
+
...List.generate(
|
|
114
|
+
tuChongItem.tagColors?.length ?? 0,
|
|
115
|
+
(index) {
|
|
116
|
+
final Widget body = Container(
|
|
117
|
+
decoration: BoxDecoration(
|
|
118
|
+
color: tuChongItem.tagColors?[index],
|
|
119
|
+
borderRadius: BorderRadius.circular(
|
|
120
|
+
4,
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
padding: const EdgeInsets.symmetric(
|
|
124
|
+
horizontal: 6,
|
|
125
|
+
),
|
|
126
|
+
child: Text(
|
|
127
|
+
tuChongItem.tags?[index] ?? '',
|
|
128
|
+
style: const TextStyle(
|
|
129
|
+
fontSize: 12,
|
|
130
|
+
color: Colors.white,
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
return body;
|
|
135
|
+
},
|
|
136
|
+
),
|
|
137
|
+
],
|
|
138
|
+
);
|
|
139
|
+
body = Padding(
|
|
140
|
+
padding: const EdgeInsets.only(bottom: 10),
|
|
141
|
+
child: body,
|
|
142
|
+
);
|
|
143
|
+
return body;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class ExampleTuItmeImagelessV extends StatelessWidget {
|
|
148
|
+
const ExampleTuItmeImagelessV({
|
|
149
|
+
super.key,
|
|
150
|
+
required this.tuChongItem,
|
|
151
|
+
this.heroTag = 'tuChongItem',
|
|
152
|
+
this.type = 0,
|
|
153
|
+
});
|
|
154
|
+
final TuChongItem tuChongItem;
|
|
155
|
+
|
|
156
|
+
final String heroTag;
|
|
157
|
+
final int type;
|
|
158
|
+
@override
|
|
159
|
+
Widget build(BuildContext context) {
|
|
160
|
+
Widget body = configImageListWidget();
|
|
161
|
+
|
|
162
|
+
body = ClipRRect(
|
|
163
|
+
borderRadius: BorderRadius.circular(10),
|
|
164
|
+
child: body,
|
|
165
|
+
);
|
|
166
|
+
body = Container(
|
|
167
|
+
padding: const EdgeInsets.all(5),
|
|
168
|
+
decoration: BoxDecoration(
|
|
169
|
+
color: Colors.lightBlue.withOpacity(0.1),
|
|
170
|
+
borderRadius: BorderRadius.circular(
|
|
171
|
+
10,
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
child: body,
|
|
175
|
+
);
|
|
176
|
+
return body;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// 配置图片数组视图
|
|
180
|
+
configImageListWidget() {
|
|
181
|
+
if (tuChongItem.realImages?.length == 1) {
|
|
182
|
+
return configOnlyOneImageWidget();
|
|
183
|
+
} else {
|
|
184
|
+
return GridView.count(
|
|
185
|
+
crossAxisCount: tuChongItem.crossAxisCount ?? 1,
|
|
186
|
+
childAspectRatio: tuChongItem.crossAxisCount == 2 ? 1 : 1,
|
|
187
|
+
crossAxisSpacing: 5.0,
|
|
188
|
+
mainAxisSpacing: 5.0,
|
|
189
|
+
shrinkWrap: true,
|
|
190
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
191
|
+
children: [
|
|
192
|
+
...List.generate(
|
|
193
|
+
tuChongItem.realImages!.length > 9
|
|
194
|
+
? 9
|
|
195
|
+
: tuChongItem.realImages!.length, (index) {
|
|
196
|
+
final Widget body = configImageItem(
|
|
197
|
+
imageUrl: tuChongItem.realImages![index],
|
|
198
|
+
index: index,
|
|
199
|
+
allNum: tuChongItem.images?.length ?? 0,
|
|
200
|
+
heroTag: heroTag,
|
|
201
|
+
);
|
|
202
|
+
return body;
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// 配置单图视图
|
|
210
|
+
configOnlyOneImageWidget() {
|
|
211
|
+
final Widget body = LayoutBuilder(
|
|
212
|
+
builder: (p0, p1) {
|
|
213
|
+
final ImageItem imageItem = tuChongItem.images![0];
|
|
214
|
+
final double ratio = (imageItem.width ?? 0) / (imageItem.height ?? 1);
|
|
215
|
+
final double imageH = p1.maxWidth / ratio;
|
|
216
|
+
final Widget body = configTuchongImageWidget(
|
|
217
|
+
url: imageItem.imageUrl,
|
|
218
|
+
width: p1.maxWidth,
|
|
219
|
+
height: imageH,
|
|
220
|
+
index: 0,
|
|
221
|
+
heroTag: heroTag,
|
|
222
|
+
);
|
|
223
|
+
return body;
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
return body;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// 配置图片item
|
|
230
|
+
configImageItem({
|
|
231
|
+
required String imageUrl,
|
|
232
|
+
required int index,
|
|
233
|
+
required int allNum,
|
|
234
|
+
String? heroTag,
|
|
235
|
+
}) {
|
|
236
|
+
Widget body = LayoutBuilder(
|
|
237
|
+
builder: (p0, p1) {
|
|
238
|
+
/// 加载图片
|
|
239
|
+
final Widget body = configTuchongImageWidget(
|
|
240
|
+
url: imageUrl,
|
|
241
|
+
heroTag: heroTag,
|
|
242
|
+
width: p1.maxWidth,
|
|
243
|
+
height: p1.maxHeight,
|
|
244
|
+
index: index,
|
|
245
|
+
isTap: false,
|
|
246
|
+
);
|
|
247
|
+
return body;
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
if (index == 8 && allNum > (index + 1)) {
|
|
251
|
+
body = Stack(
|
|
252
|
+
children: [
|
|
253
|
+
body,
|
|
254
|
+
Positioned.fill(
|
|
255
|
+
child: configResidueNumWidget(
|
|
256
|
+
num: allNum - 9,
|
|
257
|
+
),
|
|
258
|
+
),
|
|
259
|
+
],
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return body;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// 配置图片显示视图
|
|
267
|
+
configTuchongImageWidget({
|
|
268
|
+
String? url,
|
|
269
|
+
double? width,
|
|
270
|
+
double? height,
|
|
271
|
+
String? heroTag,
|
|
272
|
+
bool isTap = true,
|
|
273
|
+
required int index,
|
|
274
|
+
}) {
|
|
275
|
+
final Widget body = url != null
|
|
276
|
+
? _buildImageWidget(
|
|
277
|
+
url: url,
|
|
278
|
+
width: width,
|
|
279
|
+
height: height,
|
|
280
|
+
borderRadius: 6,
|
|
281
|
+
)
|
|
282
|
+
: Container();
|
|
283
|
+
|
|
284
|
+
return body;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// 创建图片剩余个数
|
|
288
|
+
configResidueNumWidget({required int num}) {
|
|
289
|
+
Widget body = Text(
|
|
290
|
+
'+$num',
|
|
291
|
+
style: const TextStyle(
|
|
292
|
+
fontSize: 16,
|
|
293
|
+
color: Colors.white,
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
body = Container(
|
|
297
|
+
decoration: const BoxDecoration(
|
|
298
|
+
color: Colors.black12,
|
|
299
|
+
),
|
|
300
|
+
alignment: Alignment.center,
|
|
301
|
+
child: body,
|
|
302
|
+
);
|
|
303
|
+
return body;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// 配置用户头像
|
|
308
|
+
_buildImageWidget({
|
|
309
|
+
required String url,
|
|
310
|
+
double? width = 50,
|
|
311
|
+
double? height = 50,
|
|
312
|
+
double borderRadius = 50,
|
|
313
|
+
}) {
|
|
314
|
+
if (url.isEmpty) {
|
|
315
|
+
return SizedBox(
|
|
316
|
+
width: width,
|
|
317
|
+
height: height,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
Widget body = Image.network(
|
|
321
|
+
url,
|
|
322
|
+
width: width,
|
|
323
|
+
height: height,
|
|
324
|
+
fit: BoxFit.cover,
|
|
325
|
+
errorBuilder: (context, error, stackTrace) {
|
|
326
|
+
return Container(
|
|
327
|
+
width: width,
|
|
328
|
+
height: height,
|
|
329
|
+
color: Colors.grey[200],
|
|
330
|
+
child: const Icon(Icons.broken_image, color: Colors.grey),
|
|
331
|
+
);
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
body = ClipRRect(
|
|
335
|
+
borderRadius: BorderRadius.circular(borderRadius),
|
|
336
|
+
child: body,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
return body;
|
|
340
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import '{{CORE_IMPORT}}';
|
|
2
|
+
{{#if_clean}}
|
|
3
|
+
import '{{DATA_IMPORT}}';
|
|
4
|
+
{{else}}
|
|
5
|
+
import '{{MODELS_IMPORT}}';
|
|
6
|
+
import '{{SERVICES_IMPORT}}';
|
|
7
|
+
{{/if_clean}}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/// 图片列表 ViewModel
|
|
12
|
+
class EgListViewModel extends BaseListViewModel<TuChongItem> {
|
|
13
|
+
final EgService _photoService = EgService();
|
|
14
|
+
@override
|
|
15
|
+
int get pageSize => 10;
|
|
16
|
+
@override
|
|
17
|
+
Future<List<TuChongItem>> fetchPage({
|
|
18
|
+
required int page,
|
|
19
|
+
required int pageSize,
|
|
20
|
+
}) async {
|
|
21
|
+
int? lastPostId;
|
|
22
|
+
if (items.length > 1) {
|
|
23
|
+
lastPostId = items.last.postId;
|
|
24
|
+
}
|
|
25
|
+
return await _photoService.getTuChongList(
|
|
26
|
+
page: page,
|
|
27
|
+
pageSize: pageSize,
|
|
28
|
+
lastPostId: lastPostId,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart';
|
|
2
|
+
import '{{CORE_IMPORT}}';
|
|
3
|
+
{{#if_clean}}
|
|
4
|
+
import '{{DATA_IMPORT}}';
|
|
5
|
+
{{else}}
|
|
6
|
+
import '{{MODELS_IMPORT}}';
|
|
7
|
+
{{/if_clean}}
|
|
8
|
+
|
|
9
|
+
class EgService {
|
|
10
|
+
final AppHttp _http;
|
|
11
|
+
final bool useMock;
|
|
12
|
+
|
|
13
|
+
EgService({
|
|
14
|
+
AppHttp? http,
|
|
15
|
+
bool? useMock,
|
|
16
|
+
}) : _http = http ?? AppHttp(),
|
|
17
|
+
useMock = useMock ?? AppConfig.I.useMockData;
|
|
18
|
+
|
|
19
|
+
Future<List<TuChongItem>> getTuChongList({
|
|
20
|
+
required int page,
|
|
21
|
+
int pageSize = 10,
|
|
22
|
+
int? lastPostId,
|
|
23
|
+
}) async {
|
|
24
|
+
// 如果开启了 Mock 模式,从 mockData 加载数据
|
|
25
|
+
if (useMock) {
|
|
26
|
+
return _loadMockData(page, pageSize);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Map<String, dynamic> queryParameters = {};
|
|
30
|
+
if (lastPostId != null) {
|
|
31
|
+
queryParameters = {
|
|
32
|
+
'post_id': lastPostId,
|
|
33
|
+
'page': page,
|
|
34
|
+
'type': 'loadmore',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
final response = await _http.get(
|
|
39
|
+
'/feed-app',
|
|
40
|
+
queryParameters: queryParameters,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (response.isSuccess && response.data is List) {
|
|
44
|
+
return (response.data as List)
|
|
45
|
+
.map((e) => TuChongItem.fromJson(e as Map<String, dynamic>))
|
|
46
|
+
.toList();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 降级处理:请求失败时加载模拟数据
|
|
50
|
+
return _loadMockData(1, pageSize);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// 加载本地 Mock 数据
|
|
54
|
+
List<TuChongItem> _loadMockData(int page, int pageSize) {
|
|
55
|
+
try {
|
|
56
|
+
// 从 mock_data.dart 获取数据
|
|
57
|
+
// 结构为 { 'feedList': [...] }
|
|
58
|
+
final List? feedList = mock['feedList'];
|
|
59
|
+
if (feedList != null) {
|
|
60
|
+
// 模拟分页
|
|
61
|
+
final int start = (page - 1) * pageSize;
|
|
62
|
+
if (start >= feedList.length) return [];
|
|
63
|
+
|
|
64
|
+
final int end = start + pageSize > feedList.length
|
|
65
|
+
? feedList.length
|
|
66
|
+
: start + pageSize;
|
|
67
|
+
final subList = feedList.sublist(start, end);
|
|
68
|
+
|
|
69
|
+
return subList
|
|
70
|
+
.map((e) => TuChongItem.fromJson(e as Map<String, dynamic>))
|
|
71
|
+
.toList();
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
debugPrint('Mock 数据解析错误: $e');
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|