flu-cli-core 1.0.5 → 1.1.1
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 +9 -0
- 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 +17766 -3244
- package/dist/index.d.cts +783 -99
- package/dist/index.d.ts +783 -99
- package/dist/index.js +17323 -2942
- package/dist/upgrade_snippets-BJ6CQY5Q.js +9 -0
- package/package.json +3 -3
- 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/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/.vscode/settings.json.template +14 -0
- package/templates/starter_project/DEVELOPER_GUIDE.md.template +169 -0
- package/templates/starter_project/README.md.template +117 -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
|
@@ -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
|
);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import 'package:flutter/gestures.dart';
|
|
2
|
+
import 'package:flutter/material.dart';
|
|
3
|
+
|
|
4
|
+
import '../index.dart';
|
|
5
|
+
|
|
6
|
+
/// 隐私协议确认弹窗
|
|
7
|
+
class PrivacyDialog extends StatelessWidget {
|
|
8
|
+
const PrivacyDialog({super.key});
|
|
9
|
+
|
|
10
|
+
/// 显示弹窗
|
|
11
|
+
static Future<bool?> show(BuildContext context) {
|
|
12
|
+
return showDialog<bool>(
|
|
13
|
+
context: context,
|
|
14
|
+
barrierDismissible: false,
|
|
15
|
+
barrierColor: Colors.black.withOpacity(0.32),
|
|
16
|
+
builder: (context) => const PrivacyDialog(),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
Widget build(BuildContext context) {
|
|
22
|
+
final titleStyle = AppTextStyleConfig.I.titleSmall();
|
|
23
|
+
final contentStyle = AppTextStyleConfig.I.body();
|
|
24
|
+
final linkStyle = AppTextStyleConfig.I.body().copyWith(
|
|
25
|
+
color: AppColorConfig.I.primary,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return PopScope(
|
|
29
|
+
canPop: false,
|
|
30
|
+
child: Center(
|
|
31
|
+
child: Container(
|
|
32
|
+
margin: const EdgeInsets.symmetric(horizontal: 32),
|
|
33
|
+
padding: const EdgeInsets.all(24),
|
|
34
|
+
decoration: BoxDecoration(
|
|
35
|
+
color: Colors.white,
|
|
36
|
+
borderRadius: BorderRadius.circular(16),
|
|
37
|
+
boxShadow: const [
|
|
38
|
+
BoxShadow(
|
|
39
|
+
color: Color(0x0D000000),
|
|
40
|
+
blurRadius: 24,
|
|
41
|
+
offset: Offset(0, 8),
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
),
|
|
45
|
+
child: Column(
|
|
46
|
+
mainAxisSize: MainAxisSize.min,
|
|
47
|
+
children: [
|
|
48
|
+
Text('用户协议与隐私政策', style: titleStyle),
|
|
49
|
+
const SizedBox(height: 20),
|
|
50
|
+
ConstrainedBox(
|
|
51
|
+
constraints: const BoxConstraints(maxHeight: 320),
|
|
52
|
+
child: SingleChildScrollView(
|
|
53
|
+
child: RichText(
|
|
54
|
+
text: TextSpan(
|
|
55
|
+
style: contentStyle.copyWith(height: 1.6),
|
|
56
|
+
children: [
|
|
57
|
+
const TextSpan(
|
|
58
|
+
text: '欢迎使用 ${EnvConfig.appName}!在您使用前,请您务必仔细阅读',
|
|
59
|
+
),
|
|
60
|
+
TextSpan(
|
|
61
|
+
text: '《用户协议》',
|
|
62
|
+
style: linkStyle,
|
|
63
|
+
recognizer: TapGestureRecognizer()
|
|
64
|
+
..onTap = () {
|
|
65
|
+
_openAgreementDocument(
|
|
66
|
+
context,
|
|
67
|
+
AgreementDocumentType.userAgreement,
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
const TextSpan(text: '与'),
|
|
72
|
+
TextSpan(
|
|
73
|
+
text: '《隐私政策》',
|
|
74
|
+
style: linkStyle,
|
|
75
|
+
recognizer: TapGestureRecognizer()
|
|
76
|
+
..onTap = () {
|
|
77
|
+
_openAgreementDocument(
|
|
78
|
+
context,
|
|
79
|
+
AgreementDocumentType.privacyPolicy,
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
),
|
|
83
|
+
const TextSpan(
|
|
84
|
+
text: '。我们将严格按照政策规定保护您的个人信息。\n\n'
|
|
85
|
+
'1. 我们会收集您的设备信息用于基础统计。\n'
|
|
86
|
+
'2. 为了提供更好的服务,我们需要部分系统权限。\n\n'
|
|
87
|
+
'如果您同意上述协议,请点击“同意”开始使用。',
|
|
88
|
+
),
|
|
89
|
+
],
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
const SizedBox(height: 24),
|
|
95
|
+
Row(
|
|
96
|
+
children: [
|
|
97
|
+
Expanded(
|
|
98
|
+
child: OutlinedButton(
|
|
99
|
+
onPressed: () {
|
|
100
|
+
Navigator.pop(context, false);
|
|
101
|
+
},
|
|
102
|
+
style: OutlinedButton.styleFrom(
|
|
103
|
+
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
104
|
+
side: BorderSide(color: AppColorConfig.I.divider),
|
|
105
|
+
shape: RoundedRectangleBorder(
|
|
106
|
+
borderRadius: BorderRadius.circular(12),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
child: Text(
|
|
110
|
+
'不同意并退出',
|
|
111
|
+
style: AppTextStyleConfig.I.body().copyWith(
|
|
112
|
+
color: AppColorConfig.I.titleSecondary,
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
const SizedBox(width: 16),
|
|
118
|
+
Expanded(
|
|
119
|
+
child: ElevatedButton(
|
|
120
|
+
onPressed: () {
|
|
121
|
+
Navigator.pop(context, true);
|
|
122
|
+
},
|
|
123
|
+
style: ElevatedButton.styleFrom(
|
|
124
|
+
backgroundColor: AppColorConfig.I.primary,
|
|
125
|
+
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
126
|
+
shape: RoundedRectangleBorder(
|
|
127
|
+
borderRadius: BorderRadius.circular(12),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
child: Text(
|
|
131
|
+
'同意',
|
|
132
|
+
style: AppTextStyleConfig.I.body().copyWith(
|
|
133
|
+
color: Colors.white,
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Future<void> _openAgreementDocument(
|
|
148
|
+
BuildContext context,
|
|
149
|
+
AgreementDocumentType type,
|
|
150
|
+
) async {
|
|
151
|
+
Navigator.push(
|
|
152
|
+
context,
|
|
153
|
+
MaterialPageRoute(
|
|
154
|
+
builder: (context) => AgreementDocumentPage(type: type),
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mixin RequestGuardMixin {
|
|
2
|
+
bool _requestInFlight = false;
|
|
3
|
+
|
|
4
|
+
bool get hasRequestInFlight => _requestInFlight;
|
|
5
|
+
|
|
6
|
+
Future<T> guardRequest<T>(Future<T> Function() action) async {
|
|
7
|
+
if (_requestInFlight) {
|
|
8
|
+
throw StateError('A guarded request is already running.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_requestInFlight = true;
|
|
12
|
+
try {
|
|
13
|
+
return await action();
|
|
14
|
+
} finally {
|
|
15
|
+
_requestInFlight = false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
|
|
3
|
+
mixin DebounceMixin {
|
|
4
|
+
Timer? _debounceTimer;
|
|
5
|
+
|
|
6
|
+
void debounce({
|
|
7
|
+
required Duration duration,
|
|
8
|
+
required void Function() action,
|
|
9
|
+
}) {
|
|
10
|
+
_debounceTimer?.cancel();
|
|
11
|
+
_debounceTimer = Timer(duration, action);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
void disposeDebounceMixin() {
|
|
15
|
+
_debounceTimer?.cancel();
|
|
16
|
+
_debounceTimer = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -40,6 +40,8 @@ class AppHttp {
|
|
|
40
40
|
static void init({
|
|
41
41
|
AppResponseAdapter? adapter,
|
|
42
42
|
List<Interceptor>? interceptors,
|
|
43
|
+
List<Interceptor> Function(Dio dio)? interceptorBuilder,
|
|
44
|
+
Map<String, dynamic> Function()? globalParams,
|
|
43
45
|
bool autoShowError = true,
|
|
44
46
|
}) {
|
|
45
47
|
final instance = AppHttp._instance;
|
|
@@ -58,7 +60,14 @@ class AppHttp {
|
|
|
58
60
|
),
|
|
59
61
|
);
|
|
60
62
|
|
|
61
|
-
// 1.
|
|
63
|
+
// 1. 添加全局请求参数拦截器,保证日志记录的是最终请求。
|
|
64
|
+
if (globalParams != null) {
|
|
65
|
+
instance._dio.interceptors.add(
|
|
66
|
+
GlobalParamsInterceptor(getParams: globalParams),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. 添加日志拦截器(完整输出,不截断)
|
|
62
71
|
if (config.enableLog) {
|
|
63
72
|
instance._dio.interceptors.add(
|
|
64
73
|
FullLogInterceptor(
|
|
@@ -68,7 +77,7 @@ class AppHttp {
|
|
|
68
77
|
);
|
|
69
78
|
}
|
|
70
79
|
|
|
71
|
-
//
|
|
80
|
+
// 3. 添加响应处理拦截器 (核心)
|
|
72
81
|
instance._dio.interceptors.add(
|
|
73
82
|
AppResponseInterceptor(
|
|
74
83
|
adapter: adapter ?? DefaultResponseAdapter(),
|
|
@@ -76,12 +85,18 @@ class AppHttp {
|
|
|
76
85
|
),
|
|
77
86
|
);
|
|
78
87
|
|
|
79
|
-
//
|
|
88
|
+
// 4. 添加网络错误拦截器
|
|
80
89
|
instance._dio.interceptors.add(
|
|
81
90
|
NetworkErrorInterceptor(autoShowError: autoShowError),
|
|
82
91
|
);
|
|
83
92
|
|
|
84
|
-
//
|
|
93
|
+
// 5. 添加需要复用当前 Dio 实例的自定义拦截器。
|
|
94
|
+
final builtInterceptors = interceptorBuilder?.call(instance._dio);
|
|
95
|
+
if (builtInterceptors != null) {
|
|
96
|
+
instance._dio.interceptors.addAll(builtInterceptors);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. 添加普通自定义拦截器。
|
|
85
100
|
if (interceptors != null) {
|
|
86
101
|
instance._dio.interceptors.addAll(interceptors);
|
|
87
102
|
}
|