flu-cli-core 1.0.4 → 1.1.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 +22 -17
- package/dist/chunk-BGYZU6TU.js +466 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/factory-LM2CTHPW.js +7 -0
- package/dist/factory-P6ABQFH3.js +7 -0
- package/dist/index.cjs +18901 -3313
- package/dist/index.d.cts +833 -72
- package/dist/index.d.ts +833 -72
- package/dist/index.js +18569 -3132
- package/dist/upgrade_snippets-BJ6CQY5Q.js +9 -0
- package/package.json +9 -4
- package/templates/README.md +12 -0
- package/templates/core_files/auth/auth_middleware.dart.template +33 -0
- package/templates/core_files/auth/auth_service.dart.template +22 -0
- package/templates/core_files/auth/auth_viewmodel_mixin.dart.template +9 -0
- package/templates/core_files/auth/index.dart.template +4 -0
- package/templates/core_files/base/base_service.dart.template +12 -0
- package/templates/core_files/base/index.dart.template +3 -0
- package/templates/core_files/config/agreement_document_page.dart.template +220 -0
- package/templates/core_files/config/app_agreement.dart.template +297 -0
- package/templates/core_files/config/app_config.dart.template +81 -22
- package/templates/core_files/config/app_env.dart.template +107 -0
- package/templates/core_files/config/app_initializer.dart.template +16 -23
- package/templates/core_files/config/index.dart.template +4 -1
- package/templates/core_files/config/privacy_dialog.dart.template +158 -0
- package/templates/core_files/index.dart.template +3 -0
- package/templates/core_files/mixins/page/keep_alive_mixin.dart.template +6 -0
- package/templates/core_files/mixins/page/scroll_controller_mixin.dart.template +7 -0
- package/templates/core_files/mixins/service/request_guard_mixin.dart.template +18 -0
- package/templates/core_files/mixins/viewmodel/debounce_mixin.dart.template +18 -0
- package/templates/core_files/network/app_http.dart.template +19 -4
- package/templates/core_files/network/index.dart.template +4 -0
- package/templates/core_files/network/interceptors/global_params_interceptor.dart.template +77 -0
- package/templates/core_files/network/interceptors/index.dart.template +3 -0
- package/templates/core_files/network/network_monitor.dart.template +18 -0
- package/templates/core_files/network/response_adapter.dart.template +8 -19
- package/templates/core_files/router/app_routes.dart.template +3 -6
- package/templates/core_files/storage/storage_keys.dart.template +6 -0
- package/templates/core_files/theme/app_color_config.dart.template +32 -0
- package/templates/core_files/theme/app_text_size_config.dart.template +22 -0
- package/templates/core_files/theme/app_text_style_config.dart.template +139 -0
- package/templates/core_files/theme/app_theme.dart.template +72 -12
- package/templates/core_files/theme/index.dart.template +3 -0
- package/templates/core_files/utils/loading_util.dart.template +1 -1
- package/templates/examples/eg_list_page.dart.template +1 -2
- package/templates/examples/eg_service.dart.template +27 -4
- package/templates/examples/home_feed_service.dart.template +37 -0
- package/templates/helper_examples/image_picker_example_page.dart.template +289 -0
- package/templates/helper_examples/index.dart.template +4 -0
- package/templates/helper_examples/payment_shell_example_page.dart.template +67 -0
- package/templates/helper_examples/permission_example_page.dart.template +365 -0
- package/templates/helper_examples/webview_example_page.dart.template +44 -0
- package/templates/helpers/image_picker/README.md.template +30 -0
- package/templates/helpers/image_picker/index.dart.template +73 -0
- package/templates/helpers/payment/README.md.template +29 -0
- package/templates/helpers/payment/index.dart.template +66 -0
- package/templates/helpers/permission/README.md.template +30 -0
- package/templates/helpers/permission/index.dart.template +67 -0
- package/templates/helpers/webview/README.md.template +29 -0
- package/templates/helpers/webview/index.dart.template +88 -0
- package/templates/snippets/flu-cli.code-snippets +35 -26
- package/templates/starter_project/.env.dev.template +14 -0
- package/templates/starter_project/.env.prod.example.template +14 -0
- package/templates/starter_project/.env.staging.template +14 -0
- package/templates/starter_project/.vscode/launch.json.template +54 -0
- package/templates/starter_project/DEVELOPER_GUIDE.md.template +150 -0
- package/templates/starter_project/README.md.template +99 -0
- package/templates/starter_project/analysis_options.yaml.template +28 -0
- package/templates/starter_project/lib/app.dart.template +22 -0
- package/templates/starter_project/lib/main.dart.template +34 -0
- package/templates/starter_project/lib/pages/splash_page.dart.template +154 -0
- package/templates/template_clean/lib/features/home/data/datasources/index.dart +1 -0
- package/templates/template_clean/lib/features/home/data/models/index.dart +1 -0
- package/templates/template_clean/lib/features/home/domain/index.dart +1 -0
- package/templates/template_clean/lib/features/home/presentation/pages/home_page.dart +290 -0
- package/templates/template_clean/lib/features/home/presentation/pages/index.dart +2 -0
- package/templates/template_clean/lib/features/home/presentation/pages/splash_page.dart +154 -0
- package/templates/template_clean/lib/features/home/presentation/viewmodels/home_viewmodel.dart +17 -0
- package/templates/template_clean/lib/features/index.dart +2 -0
- package/templates/template_clean/lib/features/user/data/datasources/home_feed_service.dart +37 -0
- package/templates/template_clean/lib/features/user/data/datasources/index.dart +4 -0
- package/templates/template_clean/lib/features/user/data/models/index.dart +3 -0
- package/templates/template_clean/lib/features/user/data/models/user.dart +15 -0
- package/templates/template_clean/lib/features/user/domain/index.dart +1 -0
- package/templates/template_clean/lib/features/user/presentation/pages/index.dart +1 -0
- package/templates/template_clean/lib/features/user/presentation/pages/user_list_page.dart +27 -0
- package/templates/template_clean/lib/features/user/presentation/viewmodels/user_list_viewmodel.dart +88 -0
- package/templates/template_clean/lib/features/user/presentation/widgets/user_item_card.dart +24 -0
- package/templates/template_clean/lib/shared/extensions/index.dart +1 -0
- package/templates/template_clean/lib/shared/widgets/index.dart +1 -0
- package/templates/template_lite/lib/models/index.dart +1 -0
- package/templates/template_lite/lib/pages/home_page.dart +290 -0
- package/templates/template_lite/lib/pages/index.dart +3 -0
- package/templates/template_lite/lib/pages/splash_page.dart +154 -0
- package/templates/template_lite/lib/pages/user_list_page.dart +29 -0
- package/templates/template_lite/lib/services/home_feed_service.dart +37 -0
- package/templates/template_lite/lib/services/index.dart +5 -0
- package/templates/template_lite/lib/utils/index.dart +1 -0
- package/templates/template_lite/lib/viewmodels/home_viewmodel.dart +34 -0
- package/templates/template_lite/lib/viewmodels/index.dart +2 -0
- package/templates/template_lite/lib/viewmodels/user_list_viewmodel.dart +103 -0
- package/templates/template_lite/lib/widgets/index.dart +1 -0
- package/templates/template_lite/lib/widgets/user_item_widget.dart +57 -0
- package/templates/template_modular/lib/features/home/index.dart +2 -0
- package/templates/template_modular/lib/features/home/models/index.dart +1 -0
- package/templates/template_modular/lib/features/home/pages/home_page.dart +290 -0
- package/templates/template_modular/lib/features/home/pages/index.dart +2 -0
- package/templates/template_modular/lib/features/home/pages/splash_page.dart +154 -0
- package/templates/template_modular/lib/features/home/services/index.dart +1 -0
- package/templates/template_modular/lib/features/home/viewmodels/home_viewmodel.dart +17 -0
- package/templates/template_modular/lib/features/index.dart +2 -0
- package/templates/template_modular/lib/features/user/index.dart +6 -0
- package/templates/template_modular/lib/features/user/pages/user_list_page.dart +26 -0
- package/templates/template_modular/lib/features/user/services/home_feed_service.dart +37 -0
- package/templates/template_modular/lib/features/user/viewmodels/user_list_viewmodel.dart +103 -0
- package/templates/template_modular/lib/features/user/widgets/user_item_widget.dart +24 -0
- package/templates/template_modular/lib/shared/utils/index.dart +1 -0
- package/templates/template_modular/lib/shared/widgets/index.dart +1 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart';
|
|
2
|
+
import 'package:flutter/material.dart';
|
|
3
|
+
import 'package:flutter/services.dart';
|
|
4
|
+
|
|
5
|
+
import '../router/navigator_util.dart';
|
|
6
|
+
import '../storage/storage_keys.dart';
|
|
7
|
+
import '../storage/storage_util.dart';
|
|
8
|
+
import 'agreement_document_page.dart';
|
|
9
|
+
import 'app_env.dart';
|
|
10
|
+
|
|
11
|
+
/// 负责在应用首次启动或协议版本变更时展示协议确认弹框。
|
|
12
|
+
class AppAgreementGate extends StatefulWidget {
|
|
13
|
+
const AppAgreementGate({
|
|
14
|
+
super.key,
|
|
15
|
+
required this.child,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
final Widget child;
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
State<AppAgreementGate> createState() => _AppAgreementGateState();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class _AppAgreementGateState extends State<AppAgreementGate> {
|
|
25
|
+
bool _checked = false;
|
|
26
|
+
bool _consentChecked = false;
|
|
27
|
+
bool _hasAcceptedCurrentVersion = false;
|
|
28
|
+
bool _resolved = false;
|
|
29
|
+
bool _dialogVisible = false;
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
void didChangeDependencies() {
|
|
33
|
+
super.didChangeDependencies();
|
|
34
|
+
if (_checked) return;
|
|
35
|
+
_checked = true;
|
|
36
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _showIfNeeded());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Future<void> _showIfNeeded() async {
|
|
40
|
+
if (!mounted || !EnvConfig.enableAgreementDialog) return;
|
|
41
|
+
|
|
42
|
+
final acceptedVersion =
|
|
43
|
+
StorageUtil.getString(StorageKeys.agreementAcceptedVersion) ?? '';
|
|
44
|
+
if (acceptedVersion == EnvConfig.agreementVersion) {
|
|
45
|
+
if (!mounted) return;
|
|
46
|
+
setState(() {
|
|
47
|
+
_hasAcceptedCurrentVersion = true;
|
|
48
|
+
_resolved = true;
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!mounted) return;
|
|
54
|
+
setState(() {
|
|
55
|
+
_resolved = true;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _presentAgreementDialog());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Future<void> _presentAgreementDialog() async {
|
|
62
|
+
if (!mounted || _dialogVisible || _hasAcceptedCurrentVersion) return;
|
|
63
|
+
|
|
64
|
+
final dialogHostContext = NavigatorUtil.context;
|
|
65
|
+
if (dialogHostContext == null) {
|
|
66
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _presentAgreementDialog());
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_dialogVisible = true;
|
|
71
|
+
await showDialog<void>(
|
|
72
|
+
context: dialogHostContext,
|
|
73
|
+
useRootNavigator: true,
|
|
74
|
+
barrierDismissible: false,
|
|
75
|
+
builder: (dialogContext) {
|
|
76
|
+
final theme = Theme.of(dialogContext);
|
|
77
|
+
return PopScope(
|
|
78
|
+
canPop: false,
|
|
79
|
+
child: Dialog(
|
|
80
|
+
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
|
81
|
+
shape: RoundedRectangleBorder(
|
|
82
|
+
borderRadius: BorderRadius.circular(24),
|
|
83
|
+
),
|
|
84
|
+
child: Padding(
|
|
85
|
+
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
|
86
|
+
child: Column(
|
|
87
|
+
mainAxisSize: MainAxisSize.min,
|
|
88
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
89
|
+
children: [
|
|
90
|
+
Row(
|
|
91
|
+
children: [
|
|
92
|
+
Container(
|
|
93
|
+
width: 42,
|
|
94
|
+
height: 42,
|
|
95
|
+
decoration: BoxDecoration(
|
|
96
|
+
color: theme.colorScheme.primary.withAlpha(18),
|
|
97
|
+
borderRadius: BorderRadius.circular(14),
|
|
98
|
+
),
|
|
99
|
+
child: Icon(
|
|
100
|
+
Icons.verified_user_outlined,
|
|
101
|
+
color: theme.colorScheme.primary,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
const SizedBox(width: 12),
|
|
105
|
+
Expanded(
|
|
106
|
+
child: Text(
|
|
107
|
+
'服务协议与隐私保护说明',
|
|
108
|
+
style: theme.textTheme.titleMedium?.copyWith(
|
|
109
|
+
fontWeight: FontWeight.w700,
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
],
|
|
114
|
+
),
|
|
115
|
+
const SizedBox(height: 16),
|
|
116
|
+
Text(
|
|
117
|
+
'欢迎使用 ${EnvConfig.appName}。为了保障你的合法权益并符合应用商店隐私合规要求,请先阅读《用户协议》和《隐私政策》。在你明确同意前,本应用不应开始处理与你授权无关的个人信息。',
|
|
118
|
+
style: theme.textTheme.bodyMedium?.copyWith(height: 1.6),
|
|
119
|
+
),
|
|
120
|
+
const SizedBox(height: 14),
|
|
121
|
+
Container(
|
|
122
|
+
padding: const EdgeInsets.all(14),
|
|
123
|
+
decoration: BoxDecoration(
|
|
124
|
+
color: theme.colorScheme.surface,
|
|
125
|
+
borderRadius: BorderRadius.circular(16),
|
|
126
|
+
border: Border.all(color: theme.dividerColor),
|
|
127
|
+
),
|
|
128
|
+
child: Column(
|
|
129
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
130
|
+
children: [
|
|
131
|
+
Text(
|
|
132
|
+
'你可以查看完整文本,了解以下内容:',
|
|
133
|
+
style: theme.textTheme.bodySmall?.copyWith(
|
|
134
|
+
fontWeight: FontWeight.w600,
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
const SizedBox(height: 8),
|
|
138
|
+
Text(
|
|
139
|
+
'1. 我们处理哪些信息及其用途\n'
|
|
140
|
+
'2. 权限申请时机与拒绝影响\n'
|
|
141
|
+
'3. 信息保存期限、共享规则与你的权利\n'
|
|
142
|
+
'4. 运营主体、联系方式与协议生效日期',
|
|
143
|
+
style: theme.textTheme.bodySmall?.copyWith(height: 1.6),
|
|
144
|
+
),
|
|
145
|
+
],
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
const SizedBox(height: 12),
|
|
149
|
+
Wrap(
|
|
150
|
+
spacing: 8,
|
|
151
|
+
runSpacing: 8,
|
|
152
|
+
children: [
|
|
153
|
+
OutlinedButton(
|
|
154
|
+
onPressed: () => _openAgreementDocument(
|
|
155
|
+
AgreementDocumentType.userAgreement,
|
|
156
|
+
),
|
|
157
|
+
child: const Text('查看《用户协议》'),
|
|
158
|
+
),
|
|
159
|
+
OutlinedButton(
|
|
160
|
+
onPressed: () => _openAgreementDocument(
|
|
161
|
+
AgreementDocumentType.privacyPolicy,
|
|
162
|
+
),
|
|
163
|
+
child: const Text('查看《隐私政策》'),
|
|
164
|
+
),
|
|
165
|
+
],
|
|
166
|
+
),
|
|
167
|
+
const SizedBox(height: 12),
|
|
168
|
+
CheckboxListTile(
|
|
169
|
+
value: _consentChecked,
|
|
170
|
+
dense: true,
|
|
171
|
+
contentPadding: EdgeInsets.zero,
|
|
172
|
+
controlAffinity: ListTileControlAffinity.leading,
|
|
173
|
+
title: Text.rich(
|
|
174
|
+
TextSpan(
|
|
175
|
+
style: theme.textTheme.bodySmall?.copyWith(height: 1.5),
|
|
176
|
+
children: [
|
|
177
|
+
const TextSpan(text: '我已阅读并同意《用户协议》与《隐私政策》,并同意 '),
|
|
178
|
+
TextSpan(
|
|
179
|
+
text: EnvConfig.appOperatorName,
|
|
180
|
+
style: TextStyle(
|
|
181
|
+
color: theme.colorScheme.primary,
|
|
182
|
+
fontWeight: FontWeight.w600,
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
const TextSpan(text: ' 按上述规则处理我的个人信息。'),
|
|
186
|
+
],
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
onChanged: (value) {
|
|
190
|
+
if (!mounted) return;
|
|
191
|
+
setState(() {
|
|
192
|
+
_consentChecked = value ?? false;
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
),
|
|
196
|
+
const SizedBox(height: 8),
|
|
197
|
+
SizedBox(
|
|
198
|
+
width: double.infinity,
|
|
199
|
+
child: FilledButton(
|
|
200
|
+
onPressed: _consentChecked
|
|
201
|
+
? () => _handleAccept(dialogContext)
|
|
202
|
+
: null,
|
|
203
|
+
child: const Text('同意并继续'),
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
const SizedBox(height: 8),
|
|
207
|
+
SizedBox(
|
|
208
|
+
width: double.infinity,
|
|
209
|
+
child: TextButton(
|
|
210
|
+
onPressed: _handleReject,
|
|
211
|
+
child: const Text('暂不使用'),
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
],
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
_dialogVisible = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
Future<void> _openAgreementDocument(AgreementDocumentType type) {
|
|
225
|
+
return NavigatorUtil.push(
|
|
226
|
+
AgreementDocumentPage(type: type),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Future<void> _handleAccept(BuildContext dialogContext) async {
|
|
231
|
+
await StorageUtil.setString(
|
|
232
|
+
StorageKeys.agreementAcceptedVersion,
|
|
233
|
+
EnvConfig.agreementVersion,
|
|
234
|
+
);
|
|
235
|
+
if (!mounted) return;
|
|
236
|
+
setState(() {
|
|
237
|
+
_hasAcceptedCurrentVersion = true;
|
|
238
|
+
_consentChecked = false;
|
|
239
|
+
_resolved = true;
|
|
240
|
+
});
|
|
241
|
+
if (dialogContext.mounted) {
|
|
242
|
+
Navigator.of(dialogContext).pop();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
void _handleReject() {
|
|
247
|
+
if (kIsWeb) {
|
|
248
|
+
if (!mounted) return;
|
|
249
|
+
setState(() {
|
|
250
|
+
_hasAcceptedCurrentVersion = false;
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
SystemNavigator.pop();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
Widget _buildBlockedShell(BuildContext context) {
|
|
258
|
+
final theme = Theme.of(context);
|
|
259
|
+
return ColoredBox(
|
|
260
|
+
color: theme.scaffoldBackgroundColor,
|
|
261
|
+
child: Center(
|
|
262
|
+
child: Padding(
|
|
263
|
+
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
264
|
+
child: Column(
|
|
265
|
+
mainAxisSize: MainAxisSize.min,
|
|
266
|
+
children: [
|
|
267
|
+
const SizedBox(
|
|
268
|
+
width: 32,
|
|
269
|
+
height: 32,
|
|
270
|
+
child: CircularProgressIndicator(strokeWidth: 3),
|
|
271
|
+
),
|
|
272
|
+
const SizedBox(height: 16),
|
|
273
|
+
Text(
|
|
274
|
+
_resolved ? '等待用户阅读并确认协议...' : '正在准备协议说明...',
|
|
275
|
+
textAlign: TextAlign.center,
|
|
276
|
+
style: theme.textTheme.bodyMedium,
|
|
277
|
+
),
|
|
278
|
+
],
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
Widget build(BuildContext context) {
|
|
287
|
+
if (!EnvConfig.enableAgreementDialog) return widget.child;
|
|
288
|
+
if (_hasAcceptedCurrentVersion) return widget.child;
|
|
289
|
+
return Stack(
|
|
290
|
+
fit: StackFit.expand,
|
|
291
|
+
children: [
|
|
292
|
+
widget.child,
|
|
293
|
+
_buildBlockedShell(context),
|
|
294
|
+
],
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -2,20 +2,20 @@ import 'package:flutter/foundation.dart';
|
|
|
2
2
|
|
|
3
3
|
import '../storage/storage_keys.dart';
|
|
4
4
|
import '../storage/storage_util.dart';
|
|
5
|
+
import 'app_env.dart';
|
|
5
6
|
|
|
6
|
-
///
|
|
7
|
+
/// 应用常量(不随环境变化的固定值)
|
|
7
8
|
class AppConstants {
|
|
8
9
|
AppConstants._();
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
static
|
|
11
|
+
/// 应用名称 (便捷访问)
|
|
12
|
+
static String get appName => EnvConfig.appName;
|
|
13
|
+
static String get appVersion => EnvConfig.appVersion;
|
|
12
14
|
|
|
13
|
-
///
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/// 国内可直接访问的 API 地址 (示例使用 图虫API)
|
|
18
|
-
static const String apiBaseUrl = 'https://api.tuchong.com';
|
|
15
|
+
/// API 基础地址。
|
|
16
|
+
///
|
|
17
|
+
/// 默认值仅作为占位,真实业务项目应通过 env 文件或 dart-define 覆盖。
|
|
18
|
+
static String get apiBaseUrl => EnvConfig.apiBaseUrl;
|
|
19
19
|
|
|
20
20
|
static const int apiTimeout = 30000;
|
|
21
21
|
static const int pageSize = 20;
|
|
@@ -23,7 +23,7 @@ class AppConstants {
|
|
|
23
23
|
|
|
24
24
|
/// 应用配置
|
|
25
25
|
///
|
|
26
|
-
///
|
|
26
|
+
/// 合并编译期环境变量 + 运行时状态,统一对外提供
|
|
27
27
|
///
|
|
28
28
|
/// 使用示例:
|
|
29
29
|
/// ```dart
|
|
@@ -37,23 +37,38 @@ class AppConfig {
|
|
|
37
37
|
static AppConfig? _instance;
|
|
38
38
|
static AppConfig get I => _instance!;
|
|
39
39
|
|
|
40
|
-
// ====================
|
|
40
|
+
// ==================== 环境配置(来自 .env) ====================
|
|
41
|
+
|
|
42
|
+
/// 当前运行环境
|
|
43
|
+
final AppEnv env;
|
|
41
44
|
|
|
42
45
|
/// API 基础地址
|
|
43
46
|
final String apiBaseUrl;
|
|
44
47
|
|
|
48
|
+
/// 是否启用日志
|
|
49
|
+
final bool enableLog;
|
|
50
|
+
|
|
51
|
+
/// 应用名称 (便捷访问)
|
|
52
|
+
String get appName => EnvConfig.appName;
|
|
53
|
+
|
|
54
|
+
// ==================== 运行时配置 ====================
|
|
55
|
+
|
|
45
56
|
/// 连接超时时间
|
|
46
57
|
final Duration connectTimeout;
|
|
47
58
|
|
|
48
59
|
/// 接收超时时间
|
|
49
60
|
final Duration receiveTimeout;
|
|
50
61
|
|
|
51
|
-
///
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/// 是否使用 Mock 数据 (用于网络示例演示)
|
|
62
|
+
/// 是否使用 Mock 数据。
|
|
63
|
+
///
|
|
64
|
+
/// 只有选择示例时,示例服务才会读取该开关。
|
|
55
65
|
final bool useMockData;
|
|
56
66
|
|
|
67
|
+
/// 已接受的协议版本。
|
|
68
|
+
///
|
|
69
|
+
/// 空字符串表示尚未接受当前协议。
|
|
70
|
+
String agreementAcceptedVersion;
|
|
71
|
+
|
|
57
72
|
// ==================== 生命周期状态 ====================
|
|
58
73
|
|
|
59
74
|
/// 是否首次启动
|
|
@@ -68,7 +83,12 @@ class AppConfig {
|
|
|
68
83
|
/// 上一个版本号
|
|
69
84
|
final String previousVersion;
|
|
70
85
|
|
|
86
|
+
// ==================== 扩展配置 ====================
|
|
87
|
+
|
|
88
|
+
final dynamic extra;
|
|
89
|
+
|
|
71
90
|
AppConfig._({
|
|
91
|
+
required this.env,
|
|
72
92
|
required this.apiBaseUrl,
|
|
73
93
|
required this.connectTimeout,
|
|
74
94
|
required this.receiveTimeout,
|
|
@@ -78,25 +98,26 @@ class AppConfig {
|
|
|
78
98
|
required this.isFirstLaunchAfterUpdate,
|
|
79
99
|
required this.currentVersion,
|
|
80
100
|
required this.previousVersion,
|
|
101
|
+
required this.agreementAcceptedVersion,
|
|
102
|
+
this.extra,
|
|
81
103
|
});
|
|
82
104
|
|
|
83
105
|
/// 初始化应用配置
|
|
84
106
|
///
|
|
85
107
|
/// 必须在应用启动时调用(runApp 之前)
|
|
86
108
|
static Future<void> init({
|
|
87
|
-
String? apiBaseUrl,
|
|
88
109
|
Duration connectTimeout =
|
|
89
110
|
const Duration(milliseconds: AppConstants.apiTimeout),
|
|
90
111
|
Duration receiveTimeout =
|
|
91
112
|
const Duration(milliseconds: AppConstants.apiTimeout),
|
|
92
|
-
bool? enableLog,
|
|
93
113
|
bool useMockData = false,
|
|
114
|
+
dynamic extra,
|
|
94
115
|
}) async {
|
|
95
116
|
// 1. 初始化存储
|
|
96
117
|
await StorageUtil.init();
|
|
97
118
|
|
|
98
119
|
// 2. 获取版本信息
|
|
99
|
-
|
|
120
|
+
final currentVersion = AppConstants.appVersion;
|
|
100
121
|
final previousVersion = StorageUtil.getString(StorageKeys.appVersion) ?? '';
|
|
101
122
|
|
|
102
123
|
// 3. 判断启动状态
|
|
@@ -104,23 +125,33 @@ class AppConfig {
|
|
|
104
125
|
final isFirstLaunchAfterUpdate =
|
|
105
126
|
previousVersion.isNotEmpty && previousVersion != currentVersion;
|
|
106
127
|
|
|
107
|
-
// 4.
|
|
128
|
+
// 4. 获取协议状态。兼容早期只存 bool 的 demo 版本。
|
|
129
|
+
final legacyPrivacyAgreed =
|
|
130
|
+
StorageUtil.getBool(StorageKeys.privacyAgreed) ?? false;
|
|
131
|
+
final agreementAcceptedVersion =
|
|
132
|
+
StorageUtil.getString(StorageKeys.agreementAcceptedVersion) ??
|
|
133
|
+
(legacyPrivacyAgreed ? EnvConfig.agreementVersion : '');
|
|
134
|
+
|
|
135
|
+
// 5. 更新存储
|
|
108
136
|
if (isFirstLaunch) {
|
|
109
137
|
await StorageUtil.setBool(StorageKeys.isFirstLaunch, false);
|
|
110
138
|
}
|
|
111
139
|
await StorageUtil.setString(StorageKeys.appVersion, currentVersion);
|
|
112
140
|
|
|
113
|
-
//
|
|
141
|
+
// 6. 创建实例
|
|
114
142
|
_instance = AppConfig._(
|
|
115
|
-
|
|
143
|
+
env: AppEnv.fromString(EnvConfig.env),
|
|
144
|
+
apiBaseUrl: EnvConfig.apiBaseUrl,
|
|
116
145
|
connectTimeout: connectTimeout,
|
|
117
146
|
receiveTimeout: receiveTimeout,
|
|
118
|
-
enableLog: enableLog
|
|
147
|
+
enableLog: EnvConfig.enableLog,
|
|
119
148
|
useMockData: useMockData,
|
|
120
149
|
isFirstLaunch: isFirstLaunch,
|
|
121
150
|
isFirstLaunchAfterUpdate: isFirstLaunchAfterUpdate,
|
|
122
151
|
currentVersion: currentVersion,
|
|
123
152
|
previousVersion: previousVersion,
|
|
153
|
+
agreementAcceptedVersion: agreementAcceptedVersion,
|
|
154
|
+
extra: extra,
|
|
124
155
|
);
|
|
125
156
|
|
|
126
157
|
_printInitLog();
|
|
@@ -130,13 +161,41 @@ class AppConfig {
|
|
|
130
161
|
if (!_instance!.enableLog) return;
|
|
131
162
|
debugPrint('┌─────────────────────────────────────');
|
|
132
163
|
debugPrint('│ AppConfig 初始化完成');
|
|
164
|
+
debugPrint('│ 环境: ${_instance!.env.name}');
|
|
133
165
|
debugPrint('│ API: ${_instance!.apiBaseUrl}');
|
|
166
|
+
debugPrint('│ 应用: ${_instance!.appName}');
|
|
134
167
|
debugPrint('│ 版本: ${_instance!.currentVersion}');
|
|
135
168
|
debugPrint('│ 首次启动: ${_instance!.isFirstLaunch}');
|
|
136
169
|
debugPrint('│ 更新后首次: ${_instance!.isFirstLaunchAfterUpdate}');
|
|
170
|
+
debugPrint('│ 协议版本: ${EnvConfig.agreementVersion}');
|
|
171
|
+
debugPrint('│ 协议已同意: ${_instance!.hasAcceptedAgreement}');
|
|
172
|
+
if (_instance!.extra != null) {
|
|
173
|
+
debugPrint('│ 扩展配置: ${_instance!.extra}');
|
|
174
|
+
}
|
|
137
175
|
debugPrint('└─────────────────────────────────────');
|
|
138
176
|
}
|
|
139
177
|
|
|
178
|
+
/// 便捷判断
|
|
179
|
+
bool get isDev => env == AppEnv.dev;
|
|
180
|
+
bool get isStaging => env == AppEnv.staging;
|
|
181
|
+
bool get isProduction => env == AppEnv.production;
|
|
182
|
+
|
|
183
|
+
bool get hasAcceptedAgreement =>
|
|
184
|
+
agreementAcceptedVersion == EnvConfig.agreementVersion;
|
|
185
|
+
|
|
186
|
+
bool get shouldShowSplash =>
|
|
187
|
+
EnvConfig.enableAgreementDialog && !hasAcceptedAgreement;
|
|
188
|
+
|
|
189
|
+
/// 接受当前协议版本。
|
|
190
|
+
static Future<void> acceptCurrentAgreement() async {
|
|
191
|
+
_instance?.agreementAcceptedVersion = EnvConfig.agreementVersion;
|
|
192
|
+
await StorageUtil.setString(
|
|
193
|
+
StorageKeys.agreementAcceptedVersion,
|
|
194
|
+
EnvConfig.agreementVersion,
|
|
195
|
+
);
|
|
196
|
+
await StorageUtil.setBool(StorageKeys.privacyAgreed, true);
|
|
197
|
+
}
|
|
198
|
+
|
|
140
199
|
/// 是否已初始化
|
|
141
200
|
static bool get isInitialized => _instance != null;
|
|
142
201
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// 编译期环境变量读取。
|
|
2
|
+
// 所有值都可通过 `--dart-define-from-file` 或 `--dart-define` 覆盖。
|
|
3
|
+
|
|
4
|
+
/// 运行环境
|
|
5
|
+
enum AppEnv {
|
|
6
|
+
dev,
|
|
7
|
+
staging,
|
|
8
|
+
production;
|
|
9
|
+
|
|
10
|
+
/// 从 .env 文件的 APP_ENV 字符串解析
|
|
11
|
+
static AppEnv fromString(String value) {
|
|
12
|
+
return AppEnv.values.firstWhere(
|
|
13
|
+
(e) => e.name == value,
|
|
14
|
+
orElse: () => AppEnv.dev,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class EnvConfig {
|
|
20
|
+
EnvConfig._();
|
|
21
|
+
|
|
22
|
+
/// 当前运行环境
|
|
23
|
+
static const String env = String.fromEnvironment(
|
|
24
|
+
'APP_ENV',
|
|
25
|
+
defaultValue: 'dev',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/// 应用名称
|
|
29
|
+
static const String appName = String.fromEnvironment(
|
|
30
|
+
'APP_NAME',
|
|
31
|
+
defaultValue: '{{projectDisplayName}}',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/// 应用版本
|
|
35
|
+
static const String appVersion = String.fromEnvironment(
|
|
36
|
+
'APP_VERSION',
|
|
37
|
+
defaultValue: '1.0.0',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/// 应用运营商名称
|
|
41
|
+
static const String appOperatorName = String.fromEnvironment(
|
|
42
|
+
'APP_OPERATOR_NAME',
|
|
43
|
+
defaultValue: '{{projectDisplayName}} Team',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/// 隐私联系邮箱
|
|
47
|
+
static const String privacyContactEmail = String.fromEnvironment(
|
|
48
|
+
'PRIVACY_CONTACT_EMAIL',
|
|
49
|
+
defaultValue: 'privacy@example.com',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/// 隐私协议生效日期
|
|
53
|
+
static const String agreementEffectiveDate = String.fromEnvironment(
|
|
54
|
+
'AGREEMENT_EFFECTIVE_DATE',
|
|
55
|
+
defaultValue: '2026-01-01',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/// API 基础地址
|
|
59
|
+
static const String apiBaseUrl = String.fromEnvironment(
|
|
60
|
+
'API_BASE_URL',
|
|
61
|
+
defaultValue: 'https://example.com',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/// 是否启用日志
|
|
65
|
+
static const bool enableLog = bool.fromEnvironment(
|
|
66
|
+
'ENABLE_LOG',
|
|
67
|
+
defaultValue: true,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/// 是否使用 Mock 数据
|
|
71
|
+
static const bool useMockData = bool.fromEnvironment(
|
|
72
|
+
'USE_MOCK_DATA',
|
|
73
|
+
defaultValue: true,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/// 首页样板列表模式。
|
|
77
|
+
///
|
|
78
|
+
/// 仅用于 starter 验收样板;真实网络示例接入时应替换同一首页列表槽位。
|
|
79
|
+
static const String demoListMode = String.fromEnvironment(
|
|
80
|
+
'DEMO_LIST_MODE',
|
|
81
|
+
defaultValue: 'finite',
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/// 是否启用隐私协议对话框
|
|
85
|
+
static const bool enableAgreementDialog = bool.fromEnvironment(
|
|
86
|
+
'ENABLE_AGREEMENT_DIALOG',
|
|
87
|
+
defaultValue: true,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/// 隐私协议版本
|
|
91
|
+
static const String agreementVersion = String.fromEnvironment(
|
|
92
|
+
'AGREEMENT_VERSION',
|
|
93
|
+
defaultValue: '1.0.0',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
/// 用户协议 URL
|
|
97
|
+
static const String userAgreementUrl = String.fromEnvironment(
|
|
98
|
+
'USER_AGREEMENT_URL',
|
|
99
|
+
defaultValue: 'https://example.com/agreement',
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/// 隐私政策 URL
|
|
103
|
+
static const String privacyPolicyUrl = String.fromEnvironment(
|
|
104
|
+
'PRIVACY_POLICY_URL',
|
|
105
|
+
defaultValue: 'https://example.com/privacy',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
{{#if_network}}
|
|
4
2
|
import '../network/app_http.dart';
|
|
5
3
|
import '../network/interceptors/index.dart';
|
|
6
4
|
import '../network/response_adapter.dart';
|
|
7
5
|
{{/if_network}}
|
|
6
|
+
|
|
8
7
|
import 'app_config.dart';
|
|
8
|
+
import 'app_env.dart';
|
|
9
9
|
/// 应用初始化器
|
|
10
10
|
///
|
|
11
11
|
/// 统一管理应用启动前的所有初始化逻辑
|
|
@@ -23,15 +23,13 @@ class AppInitializer {
|
|
|
23
23
|
|
|
24
24
|
/// 初始化应用
|
|
25
25
|
static Future<void> init({
|
|
26
|
-
|
|
27
|
-
bool?
|
|
28
|
-
bool useMockData = false,
|
|
26
|
+
dynamic extra,
|
|
27
|
+
bool? useMockData,
|
|
29
28
|
}) async {
|
|
30
29
|
// 1. 初始化应用配置
|
|
31
30
|
await AppConfig.init(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
useMockData: useMockData,
|
|
31
|
+
useMockData: useMockData ?? EnvConfig.useMockData,
|
|
32
|
+
extra: extra,
|
|
35
33
|
);
|
|
36
34
|
|
|
37
35
|
{{#if_network}}
|
|
@@ -46,26 +44,21 @@ class AppInitializer {
|
|
|
46
44
|
/// 初始化网络层配置
|
|
47
45
|
static void _initNetwork() {
|
|
48
46
|
AppHttp.init(
|
|
49
|
-
//
|
|
50
|
-
adapter:
|
|
47
|
+
// 默认使用通用 {code, msg/message, data} 响应适配器。
|
|
48
|
+
adapter: DefaultResponseAdapter(),
|
|
51
49
|
|
|
52
50
|
// 自动显示错误弹窗 (默认 true)
|
|
53
51
|
autoShowError: true,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// return StorageUtil.getString('token') ?? '';
|
|
61
|
-
return '';
|
|
62
|
-
},
|
|
63
|
-
),
|
|
52
|
+
globalParams: () => {
|
|
53
|
+
'appVersion': AppConfig.I.currentVersion,
|
|
54
|
+
'appEnv': AppConfig.I.env.name,
|
|
55
|
+
},
|
|
56
|
+
interceptorBuilder: (dio) => [
|
|
57
|
+
{{AUTH_INTERCEPTOR_BLOCK}}
|
|
64
58
|
// 请求重试
|
|
65
59
|
RetryInterceptor(
|
|
66
|
-
dio:
|
|
67
|
-
maxRetries:
|
|
68
|
-
retryDelay: const Duration(seconds: 1),
|
|
60
|
+
dio: dio,
|
|
61
|
+
maxRetries: 2,
|
|
69
62
|
),
|
|
70
63
|
],
|
|
71
64
|
);
|