Commit 84656a6ee30c59e452acbd11d6e90b6a19295f77
Merge remote-tracking branch 'origin/fix-build-wqf'
Showing
13 changed files
with
234 additions
and
84 deletions
README.md
| 1 | # Wow English | 1 | # Wow English |
| 2 | 2 | ||
| 3 | * flutter版本号:3.19.2 | 3 | * flutter版本号:3.19.2 |
| 4 | + | ||
| 5 | +## Android 使用 Whistle 抓包 | ||
| 6 | + | ||
| 7 | +为了让 Dio 的 HTTPS 请求能够经过 Whistle 解密,Android Debug 包默认允许任意服务器证书,不需要在项目中内置或指定 Whistle Root CA。普通 Release 包仍使用系统默认的严格证书校验。 | ||
| 8 | + | ||
| 9 | +### 构建方式 | ||
| 10 | + | ||
| 11 | +Debug 包默认允许 Whistle 抓包: | ||
| 12 | + | ||
| 13 | +```bash | ||
| 14 | +flutter build apk --debug | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +构建允许 Whistle 抓包的 Release 包: | ||
| 18 | + | ||
| 19 | +```bash | ||
| 20 | +flutter build apk --release --dart-define=ENABLE_WHISTLE_CA=true | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +构建正常生产 Release 包(不允许 Whistle 抓包): | ||
| 24 | + | ||
| 25 | +```bash | ||
| 26 | +flutter build apk --release | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +手机通过 Clash Meta 将流量转发到运行 Whistle 的电脑后,即可抓取 Dio 发出的 HTTPS 请求。因为抓包构建不绑定具体 CA,所以更换 Whistle 实例或重新生成 Root CA 后无需修改代码和重新导入证书。 | ||
| 30 | + | ||
| 31 | +> Debug 包以及带 `ENABLE_WHISTLE_CA=true` 的 Release 包会接受任意 HTTPS 证书,无法抵御中间人攻击。此类 APK 只应在内部测试设备和测试人员之间分发,不能作为正式生产包发布。 |
android/app/build.gradle
| @@ -109,5 +109,5 @@ dependencies { | @@ -109,5 +109,5 @@ dependencies { | ||
| 109 | // kotlin扩展(可选) | 109 | // kotlin扩展(可选) |
| 110 | implementation 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2' | 110 | implementation 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2' |
| 111 | // coco2d游戏 | 111 | // coco2d游戏 |
| 112 | - implementation 'io.keyss.android.library:steve_game:1.0.1' | 112 | + implementation 'com.ishow.android:steve_game:1.0.1' |
| 113 | } | 113 | } |
android/build.gradle
| 1 | +apply from: "/Users/stay/StudioProjects/ishowMavenRepositories.gradle" | ||
| 2 | + | ||
| 1 | buildscript { | 3 | buildscript { |
| 2 | ext.kotlin_version = '1.8.21' | 4 | ext.kotlin_version = '1.8.21' |
| 3 | repositories { | 5 | repositories { |
| @@ -29,6 +31,8 @@ allprojects { | @@ -29,6 +31,8 @@ allprojects { | ||
| 29 | } | 31 | } |
| 30 | } | 32 | } |
| 31 | 33 | ||
| 34 | +project.ext.setIshowMavenRepositories(project) | ||
| 35 | + | ||
| 32 | rootProject.buildDir = '../build' | 36 | rootProject.buildDir = '../build' |
| 33 | subprojects { | 37 | subprojects { |
| 34 | project.buildDir = "${rootProject.buildDir}/${project.name}" | 38 | project.buildDir = "${rootProject.buildDir}/${project.name}" |
assets/images/ic_customer_service.png
0 → 100644
102 KB
lib/common/request/request_client.dart
| 1 | import 'dart:convert'; | 1 | import 'dart:convert'; |
| 2 | +import 'dart:io'; | ||
| 2 | 3 | ||
| 3 | import 'package:dio/dio.dart'; | 4 | import 'package:dio/dio.dart'; |
| 5 | +import 'package:dio/io.dart'; | ||
| 4 | import 'package:flutter/foundation.dart'; | 6 | import 'package:flutter/foundation.dart'; |
| 5 | import 'package:pretty_dio_logger/pretty_dio_logger.dart'; | 7 | import 'package:pretty_dio_logger/pretty_dio_logger.dart'; |
| 6 | import 'package:wow_english/utils/toast_util.dart'; | 8 | import 'package:wow_english/utils/toast_util.dart'; |
| @@ -17,14 +19,32 @@ part 'apis.dart'; | @@ -17,14 +19,32 @@ part 'apis.dart'; | ||
| 17 | RequestClient requestClient = RequestClient(); | 19 | RequestClient requestClient = RequestClient(); |
| 18 | 20 | ||
| 19 | class RequestClient { | 21 | class RequestClient { |
| 22 | + static const bool _enableWhistleCaptureInRelease = bool.fromEnvironment( | ||
| 23 | + 'ENABLE_WHISTLE_CA', | ||
| 24 | + defaultValue: false, | ||
| 25 | + ); | ||
| 26 | + | ||
| 20 | late Dio _dio; | 27 | late Dio _dio; |
| 21 | 28 | ||
| 22 | RequestClient() { | 29 | RequestClient() { |
| 23 | - _dio = Dio(BaseOptions(baseUrl: RequestConfig.baseUrl, connectTimeout: RequestConfig.connectTimeout)); | 30 | + _dio = Dio(BaseOptions( |
| 31 | + baseUrl: RequestConfig.baseUrl, | ||
| 32 | + connectTimeout: RequestConfig.connectTimeout)); | ||
| 24 | _dio.interceptors.add(TokenInterceptor()); | 33 | _dio.interceptors.add(TokenInterceptor()); |
| 25 | if (kDebugMode) { | 34 | if (kDebugMode) { |
| 26 | - _dio.interceptors | ||
| 27 | - .add(PrettyDioLogger(requestHeader: true, requestBody: true, responseHeader: true, maxWidth: 120)); | 35 | + _dio.interceptors.add(PrettyDioLogger( |
| 36 | + requestHeader: true, | ||
| 37 | + requestBody: true, | ||
| 38 | + responseHeader: true, | ||
| 39 | + maxWidth: 120)); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + // 仅供内部抓包构建使用;启用后会接受任意 HTTPS 证书。 | ||
| 43 | + if (Platform.isAndroid && (kDebugMode || _enableWhistleCaptureInRelease)) { | ||
| 44 | + _dio.httpClientAdapter = IOHttpClientAdapter( | ||
| 45 | + createHttpClient: () => | ||
| 46 | + HttpClient()..badCertificateCallback = (_, __, ___) => true, | ||
| 47 | + ); | ||
| 28 | } | 48 | } |
| 29 | } | 49 | } |
| 30 | 50 | ||
| @@ -79,7 +99,11 @@ class RequestClient { | @@ -79,7 +99,11 @@ class RequestClient { | ||
| 79 | bool Function(ApiException)? onError, | 99 | bool Function(ApiException)? onError, |
| 80 | }) { | 100 | }) { |
| 81 | return request(url, | 101 | return request(url, |
| 82 | - method: 'GET', queryParameters: queryParameters, headers: headers, onResponse: onResponse, onError: onError); | 102 | + method: 'GET', |
| 103 | + queryParameters: queryParameters, | ||
| 104 | + headers: headers, | ||
| 105 | + onResponse: onResponse, | ||
| 106 | + onError: onError); | ||
| 83 | } | 107 | } |
| 84 | 108 | ||
| 85 | /// post | 109 | /// post |
lib/generated/json/app_config_entity.g.dart
| @@ -7,20 +7,37 @@ AppConfigEntity $AppConfigEntityFromJson(Map<String, dynamic> json) { | @@ -7,20 +7,37 @@ AppConfigEntity $AppConfigEntityFromJson(Map<String, dynamic> json) { | ||
| 7 | if (safe != null) { | 7 | if (safe != null) { |
| 8 | appConfigEntity.safe = safe; | 8 | appConfigEntity.safe = safe; |
| 9 | } | 9 | } |
| 10 | + final List<String>? customerMobileList = | ||
| 11 | + (json['customerMobileList'] as List<dynamic>?) | ||
| 12 | + ?.map((e) => jsonConvert.convert<String>(e) as String) | ||
| 13 | + .toList(); | ||
| 14 | + if (customerMobileList != null) { | ||
| 15 | + appConfigEntity.customerMobileList = customerMobileList; | ||
| 16 | + } | ||
| 17 | + final String? customerQr = jsonConvert.convert<String>(json['customerQr']); | ||
| 18 | + if (customerQr != null) { | ||
| 19 | + appConfigEntity.customerQr = customerQr; | ||
| 20 | + } | ||
| 10 | return appConfigEntity; | 21 | return appConfigEntity; |
| 11 | } | 22 | } |
| 12 | 23 | ||
| 13 | Map<String, dynamic> $AppConfigEntityToJson(AppConfigEntity entity) { | 24 | Map<String, dynamic> $AppConfigEntityToJson(AppConfigEntity entity) { |
| 14 | final Map<String, dynamic> data = <String, dynamic>{}; | 25 | final Map<String, dynamic> data = <String, dynamic>{}; |
| 15 | data['safe'] = entity.safe; | 26 | data['safe'] = entity.safe; |
| 27 | + data['customerMobileList'] = entity.customerMobileList; | ||
| 28 | + data['customerQr'] = entity.customerQr; | ||
| 16 | return data; | 29 | return data; |
| 17 | } | 30 | } |
| 18 | 31 | ||
| 19 | extension AppConfigEntityExtension on AppConfigEntity { | 32 | extension AppConfigEntityExtension on AppConfigEntity { |
| 20 | AppConfigEntity copyWith({ | 33 | AppConfigEntity copyWith({ |
| 21 | String? safe, | 34 | String? safe, |
| 35 | + List<String>? customerMobileList, | ||
| 36 | + String? customerQr, | ||
| 22 | }) { | 37 | }) { |
| 23 | return AppConfigEntity() | 38 | return AppConfigEntity() |
| 24 | - ..safe = safe ?? this.safe; | 39 | + ..safe = safe ?? this.safe |
| 40 | + ..customerMobileList = customerMobileList ?? this.customerMobileList | ||
| 41 | + ..customerQr = customerQr ?? this.customerQr; | ||
| 25 | } | 42 | } |
| 26 | } | 43 | } |
| 27 | \ No newline at end of file | 44 | \ No newline at end of file |
lib/models/app_config_entity.dart
| @@ -6,24 +6,29 @@ import '../generated/json/app_config_entity.g.dart'; | @@ -6,24 +6,29 @@ import '../generated/json/app_config_entity.g.dart'; | ||
| 6 | 6 | ||
| 7 | @JsonSerializable() | 7 | @JsonSerializable() |
| 8 | class AppConfigEntity { | 8 | class AppConfigEntity { |
| 9 | + // 当前是否安全,safe-安全 otherwise-隐藏pay | ||
| 10 | + String? safe; | ||
| 9 | 11 | ||
| 10 | - // 当前是否安全,safe-安全 otherwise-隐藏pay | ||
| 11 | - String? safe; | 12 | + // 客服手机号列表 |
| 13 | + List<String> customerMobileList = []; | ||
| 12 | 14 | ||
| 15 | + // 客服二维码图片地址 | ||
| 16 | + String? customerQr; | ||
| 13 | 17 | ||
| 14 | - AppConfigEntity(); | 18 | + AppConfigEntity(); |
| 15 | 19 | ||
| 16 | - factory AppConfigEntity.fromJson(Map<String, dynamic> json) => $AppConfigEntityFromJson(json); | 20 | + factory AppConfigEntity.fromJson(Map<String, dynamic> json) => |
| 21 | + $AppConfigEntityFromJson(json); | ||
| 17 | 22 | ||
| 18 | - Map<String, dynamic> toJson() => $AppConfigEntityToJson(this); | 23 | + Map<String, dynamic> toJson() => $AppConfigEntityToJson(this); |
| 19 | 24 | ||
| 20 | - @override | ||
| 21 | - String toString() { | ||
| 22 | - return jsonEncode(this); | ||
| 23 | - } | 25 | + @override |
| 26 | + String toString() { | ||
| 27 | + return jsonEncode(this); | ||
| 28 | + } | ||
| 24 | 29 | ||
| 25 | - // 是否审核中(null或者非"safe"即不安全) | ||
| 26 | - bool isAppReviewing() { | ||
| 27 | - return safe != "safe"; | ||
| 28 | - } | 30 | + // 是否审核中(null或者非"safe"即不安全) |
| 31 | + bool isAppReviewing() { | ||
| 32 | + return safe != "safe"; | ||
| 33 | + } | ||
| 29 | } | 34 | } |
| 30 | \ No newline at end of file | 35 | \ No newline at end of file |
lib/pages/home/widgets/BaseHomeHeaderWidget.dart
| @@ -10,6 +10,7 @@ import '../../../models/course_entity.dart'; | @@ -10,6 +10,7 @@ import '../../../models/course_entity.dart'; | ||
| 10 | import '../../../route/route.dart'; | 10 | import '../../../route/route.dart'; |
| 11 | import '../../../utils/image_util.dart'; | 11 | import '../../../utils/image_util.dart'; |
| 12 | import '../../user/bloc/user_bloc.dart'; | 12 | import '../../user/bloc/user_bloc.dart'; |
| 13 | +import 'customer_service_qr_dialog.dart'; | ||
| 13 | 14 | ||
| 14 | typedef HeaderCallback = void Function(dynamic); | 15 | typedef HeaderCallback = void Function(dynamic); |
| 15 | 16 | ||
| @@ -80,6 +81,28 @@ class BaseHomeHeaderWidget extends StatelessWidget { | @@ -80,6 +81,28 @@ class BaseHomeHeaderWidget extends StatelessWidget { | ||
| 80 | textAlign: TextAlign.left, | 81 | textAlign: TextAlign.left, |
| 81 | style: TextStyle(color: Colors.white, fontSize: 30.0), | 82 | style: TextStyle(color: Colors.white, fontSize: 30.0), |
| 82 | )), | 83 | )), |
| 84 | + FutureBuilder( | ||
| 85 | + future: AppConfigHelper.getAppConfig(), | ||
| 86 | + builder: (context, snapshot) { | ||
| 87 | + final customerQr = snapshot.data?.customerQr?.trim() ?? ''; | ||
| 88 | + if (customerQr.isEmpty) { | ||
| 89 | + return const SizedBox.shrink(); | ||
| 90 | + } | ||
| 91 | + return GestureDetector( | ||
| 92 | + onTap: () => | ||
| 93 | + showCustomerServiceQrDialog(context, customerQr), | ||
| 94 | + child: Padding( | ||
| 95 | + padding: EdgeInsets.only(right: 12.w), | ||
| 96 | + child: Image.asset( | ||
| 97 | + 'ic_customer_service'.assetPng, | ||
| 98 | + width: 90.w, | ||
| 99 | + height: 28.h, | ||
| 100 | + fit: BoxFit.contain, | ||
| 101 | + ), | ||
| 102 | + ), | ||
| 103 | + ); | ||
| 104 | + }, | ||
| 105 | + ), | ||
| 83 | Offstage( | 106 | Offstage( |
| 84 | offstage: AppConfigHelper.shouldHidePay() || | 107 | offstage: AppConfigHelper.shouldHidePay() || |
| 85 | !UserUtil.isLogined(), | 108 | !UserUtil.isLogined(), |
lib/pages/home/widgets/customer_service_qr_dialog.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:flutter_screenutil/flutter_screenutil.dart'; | ||
| 3 | + | ||
| 4 | +Future<void> showCustomerServiceQrDialog( | ||
| 5 | + BuildContext context, | ||
| 6 | + String imageUrl, | ||
| 7 | +) { | ||
| 8 | + return showDialog<void>( | ||
| 9 | + context: context, | ||
| 10 | + barrierColor: Colors.black54, | ||
| 11 | + barrierDismissible: true, | ||
| 12 | + builder: (context) => Dialog( | ||
| 13 | + backgroundColor: Colors.transparent, | ||
| 14 | + insetPadding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 10.h), | ||
| 15 | + child: ClipRRect( | ||
| 16 | + borderRadius: BorderRadius.circular(12.r), | ||
| 17 | + child: SizedBox( | ||
| 18 | + height: 340.h, | ||
| 19 | + child: AspectRatio( | ||
| 20 | + aspectRatio: 870 / 1110, | ||
| 21 | + child: Image.network( | ||
| 22 | + imageUrl, | ||
| 23 | + width: double.infinity, | ||
| 24 | + height: double.infinity, | ||
| 25 | + fit: BoxFit.cover, | ||
| 26 | + loadingBuilder: (context, child, progress) { | ||
| 27 | + if (progress == null) { | ||
| 28 | + return child; | ||
| 29 | + } | ||
| 30 | + return Container( | ||
| 31 | + color: Colors.white, | ||
| 32 | + alignment: Alignment.center, | ||
| 33 | + child: const CircularProgressIndicator(), | ||
| 34 | + ); | ||
| 35 | + }, | ||
| 36 | + errorBuilder: (context, error, stackTrace) => Container( | ||
| 37 | + color: Colors.white, | ||
| 38 | + alignment: Alignment.center, | ||
| 39 | + child: Text( | ||
| 40 | + '图片加载失败', | ||
| 41 | + style: TextStyle( | ||
| 42 | + color: Colors.grey[600], | ||
| 43 | + fontSize: 14.sp, | ||
| 44 | + ), | ||
| 45 | + ), | ||
| 46 | + ), | ||
| 47 | + ), | ||
| 48 | + ), | ||
| 49 | + ), | ||
| 50 | + ), | ||
| 51 | + ), | ||
| 52 | + ); | ||
| 53 | +} |
lib/pages/shop/home/shop_home_page.dart
| @@ -5,7 +5,6 @@ import 'package:wow_english/common/extension/string_extension.dart'; | @@ -5,7 +5,6 @@ import 'package:wow_english/common/extension/string_extension.dart'; | ||
| 5 | import 'package:wow_english/common/widgets/we_app_bar.dart'; | 5 | import 'package:wow_english/common/widgets/we_app_bar.dart'; |
| 6 | import 'package:wow_english/pages/shop/home/widgets/product_item.dart'; | 6 | import 'package:wow_english/pages/shop/home/widgets/product_item.dart'; |
| 7 | import 'package:wow_english/route/route.dart'; | 7 | import 'package:wow_english/route/route.dart'; |
| 8 | -import 'package:wow_english/utils/toast_util.dart'; | ||
| 9 | 8 | ||
| 10 | import 'bloc/shop_home_bloc.dart'; | 9 | import 'bloc/shop_home_bloc.dart'; |
| 11 | 10 |
lib/pages/shop/home/widgets/product_item.dart
| @@ -67,10 +67,13 @@ class ProductItem extends StatelessWidget { | @@ -67,10 +67,13 @@ class ProductItem extends StatelessWidget { | ||
| 67 | ), | 67 | ), |
| 68 | ), | 68 | ), |
| 69 | GestureDetector( | 69 | GestureDetector( |
| 70 | + behavior: HitTestBehavior.opaque, | ||
| 70 | onTap: () { | 71 | onTap: () { |
| 71 | onTap(); | 72 | onTap(); |
| 72 | }, | 73 | }, |
| 73 | child: Container( | 74 | child: Container( |
| 75 | + width: double.infinity, | ||
| 76 | + constraints: BoxConstraints(minHeight: 28.h), | ||
| 74 | decoration: BoxDecoration( | 77 | decoration: BoxDecoration( |
| 75 | color: const Color(0xFFF5C51F), | 78 | color: const Color(0xFFF5C51F), |
| 76 | borderRadius: BorderRadius.circular(5.r), | 79 | borderRadius: BorderRadius.circular(5.r), |
| @@ -78,17 +81,14 @@ class ProductItem extends StatelessWidget { | @@ -78,17 +81,14 @@ class ProductItem extends StatelessWidget { | ||
| 78 | color: const Color(0xFF333333), | 81 | color: const Color(0xFF333333), |
| 79 | width: 1.0, | 82 | width: 1.0, |
| 80 | )), | 83 | )), |
| 81 | - padding: EdgeInsets.symmetric( | ||
| 82 | - vertical: 1.h | ||
| 83 | - ), | 84 | + padding: EdgeInsets.symmetric(vertical: 4.h), |
| 84 | child: Center( | 85 | child: Center( |
| 85 | child: Text( | 86 | child: Text( |
| 86 | '立即购买', | 87 | '立即购买', |
| 87 | style: TextStyle( | 88 | style: TextStyle( |
| 88 | fontSize: 10.sp, color: const Color(0xFF333333)), | 89 | fontSize: 10.sp, color: const Color(0xFF333333)), |
| 89 | ), | 90 | ), |
| 90 | - ) | ||
| 91 | - ), | 91 | + )), |
| 92 | ) | 92 | ) |
| 93 | ], | 93 | ], |
| 94 | ), | 94 | ), |
lib/pages/user/user_page.dart
| @@ -13,6 +13,7 @@ import 'package:wow_english/models/user_entity.dart'; | @@ -13,6 +13,7 @@ import 'package:wow_english/models/user_entity.dart'; | ||
| 13 | import 'package:wow_english/pages/user/bloc/user_bloc.dart'; | 13 | import 'package:wow_english/pages/user/bloc/user_bloc.dart'; |
| 14 | import 'package:wow_english/route/route.dart'; | 14 | import 'package:wow_english/route/route.dart'; |
| 15 | import 'package:wow_english/utils/image_util.dart'; | 15 | import 'package:wow_english/utils/image_util.dart'; |
| 16 | +import 'package:wow_english/utils/toast_util.dart'; | ||
| 16 | 17 | ||
| 17 | class UserPage extends StatelessWidget { | 18 | class UserPage extends StatelessWidget { |
| 18 | const UserPage({super.key}); | 19 | const UserPage({super.key}); |
| @@ -196,6 +197,16 @@ class _UserView extends StatelessWidget { | @@ -196,6 +197,16 @@ class _UserView extends StatelessWidget { | ||
| 196 | child: 12.verticalSpace, | 197 | child: 12.verticalSpace, |
| 197 | ), | 198 | ), |
| 198 | OutlinedButton( | 199 | OutlinedButton( |
| 200 | + onPressed: () { | ||
| 201 | + _showCustomerServiceDialog(context); | ||
| 202 | + }, | ||
| 203 | + style: normalButtonStyle, | ||
| 204 | + child: Text( | ||
| 205 | + "联系客服", | ||
| 206 | + style: textStyle21sp, | ||
| 207 | + )), | ||
| 208 | + 12.verticalSpace, | ||
| 209 | + OutlinedButton( | ||
| 199 | onPressed: () => pushNamed(AppRouteName.fogPwd), | 210 | onPressed: () => pushNamed(AppRouteName.fogPwd), |
| 200 | style: normalButtonStyle, | 211 | style: normalButtonStyle, |
| 201 | child: Text( | 212 | child: Text( |
| @@ -232,16 +243,6 @@ class _UserView extends StatelessWidget { | @@ -232,16 +243,6 @@ class _UserView extends StatelessWidget { | ||
| 232 | 12.verticalSpace, | 243 | 12.verticalSpace, |
| 233 | OutlinedButton( | 244 | OutlinedButton( |
| 234 | onPressed: () { | 245 | onPressed: () { |
| 235 | - _showTeacherSelectionDialog(context); | ||
| 236 | - }, | ||
| 237 | - style: normalButtonStyle, | ||
| 238 | - child: Text( | ||
| 239 | - "联系客服", | ||
| 240 | - style: textStyle21sp, | ||
| 241 | - )), | ||
| 242 | - 12.verticalSpace, | ||
| 243 | - OutlinedButton( | ||
| 244 | - onPressed: () { | ||
| 245 | pushNamed(AppRouteName.setting); | 246 | pushNamed(AppRouteName.setting); |
| 246 | }, | 247 | }, |
| 247 | style: normalButtonStyle, | 248 | style: normalButtonStyle, |
| @@ -308,7 +309,33 @@ class _UserView extends StatelessWidget { | @@ -308,7 +309,33 @@ class _UserView extends StatelessWidget { | ||
| 308 | }, | 309 | }, |
| 309 | ); | 310 | ); |
| 310 | 311 | ||
| 311 | - void _showTeacherSelectionDialog(BuildContext context) { | 312 | + Future<void> _showCustomerServiceDialog(BuildContext context) async { |
| 313 | + try { | ||
| 314 | + final config = await AppConfigHelper.getAppConfig(); | ||
| 315 | + if (!context.mounted) { | ||
| 316 | + return; | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + final mobileList = config?.customerMobileList | ||
| 320 | + .map((mobile) => mobile.trim()) | ||
| 321 | + .where((mobile) => mobile.isNotEmpty) | ||
| 322 | + .toSet() | ||
| 323 | + .toList() ?? | ||
| 324 | + []; | ||
| 325 | + if (mobileList.isEmpty) { | ||
| 326 | + showToast('暂无客服联系方式'); | ||
| 327 | + return; | ||
| 328 | + } | ||
| 329 | + _showMobileSelectionDialog(context, mobileList); | ||
| 330 | + } catch (_) { | ||
| 331 | + if (context.mounted) { | ||
| 332 | + showToast('客服联系方式获取失败,请稍后重试'); | ||
| 333 | + } | ||
| 334 | + } | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + void _showMobileSelectionDialog( | ||
| 338 | + BuildContext context, List<String> mobileList) { | ||
| 312 | showModalBottomSheet( | 339 | showModalBottomSheet( |
| 313 | context: context, | 340 | context: context, |
| 314 | backgroundColor: Colors.white, | 341 | backgroundColor: Colors.white, |
| @@ -333,30 +360,28 @@ class _UserView extends StatelessWidget { | @@ -333,30 +360,28 @@ class _UserView extends StatelessWidget { | ||
| 333 | borderRadius: BorderRadius.circular(2), | 360 | borderRadius: BorderRadius.circular(2), |
| 334 | ), | 361 | ), |
| 335 | ), | 362 | ), |
| 336 | - // 老师选项列表 | 363 | + // 客服手机号列表 |
| 337 | Flexible( | 364 | Flexible( |
| 338 | child: SingleChildScrollView( | 365 | child: SingleChildScrollView( |
| 339 | child: Column( | 366 | child: Column( |
| 340 | mainAxisSize: MainAxisSize.min, | 367 | mainAxisSize: MainAxisSize.min, |
| 341 | - children: [ | ||
| 342 | - // 恐龙老师选项 | ||
| 343 | - ListTile( | ||
| 344 | - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | 368 | + children: |
| 369 | + List.generate(mobileList.length * 2 - 1, (index) { | ||
| 370 | + if (index.isOdd) { | ||
| 371 | + return const Divider(height: 1); | ||
| 372 | + } | ||
| 373 | + final mobile = mobileList[index ~/ 2]; | ||
| 374 | + return ListTile( | ||
| 375 | + contentPadding: const EdgeInsets.symmetric( | ||
| 376 | + horizontal: 8, vertical: 4), | ||
| 345 | title: Text( | 377 | title: Text( |
| 346 | - '恐龙老师', | 378 | + mobile, |
| 347 | style: TextStyle( | 379 | style: TextStyle( |
| 348 | fontSize: 14.sp, | 380 | fontSize: 14.sp, |
| 349 | fontWeight: FontWeight.w600, | 381 | fontWeight: FontWeight.w600, |
| 350 | color: Colors.black87, | 382 | color: Colors.black87, |
| 351 | ), | 383 | ), |
| 352 | ), | 384 | ), |
| 353 | - subtitle: Text( | ||
| 354 | - '19357119913', | ||
| 355 | - style: TextStyle( | ||
| 356 | - fontSize: 12.sp, | ||
| 357 | - color: Colors.grey[600], | ||
| 358 | - ), | ||
| 359 | - ), | ||
| 360 | trailing: Icon( | 385 | trailing: Icon( |
| 361 | Icons.phone, | 386 | Icons.phone, |
| 362 | color: Colors.green[600], | 387 | color: Colors.green[600], |
| @@ -364,39 +389,10 @@ class _UserView extends StatelessWidget { | @@ -364,39 +389,10 @@ class _UserView extends StatelessWidget { | ||
| 364 | ), | 389 | ), |
| 365 | onTap: () { | 390 | onTap: () { |
| 366 | Navigator.pop(context); | 391 | Navigator.pop(context); |
| 367 | - _launchPhone('tel:+8619357119913'); | 392 | + _launchPhone(mobile); |
| 368 | }, | 393 | }, |
| 369 | - ), | ||
| 370 | - const Divider(height: 1), | ||
| 371 | - // Rose老师选项 | ||
| 372 | - ListTile( | ||
| 373 | - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||
| 374 | - title: Text( | ||
| 375 | - 'Rose老师', | ||
| 376 | - style: TextStyle( | ||
| 377 | - fontSize: 14.sp, | ||
| 378 | - fontWeight: FontWeight.w600, | ||
| 379 | - color: Colors.black87, | ||
| 380 | - ), | ||
| 381 | - ), | ||
| 382 | - subtitle: Text( | ||
| 383 | - '19033986279', | ||
| 384 | - style: TextStyle( | ||
| 385 | - fontSize: 12.sp, | ||
| 386 | - color: Colors.grey[600], | ||
| 387 | - ), | ||
| 388 | - ), | ||
| 389 | - trailing: Icon( | ||
| 390 | - Icons.phone, | ||
| 391 | - color: Colors.green[600], | ||
| 392 | - size: 18, | ||
| 393 | - ), | ||
| 394 | - onTap: () { | ||
| 395 | - Navigator.pop(context); | ||
| 396 | - _launchPhone('tel:+8619033986279'); | ||
| 397 | - }, | ||
| 398 | - ), | ||
| 399 | - ], | 394 | + ); |
| 395 | + }), | ||
| 400 | ), | 396 | ), |
| 401 | ), | 397 | ), |
| 402 | ), | 398 | ), |
| @@ -433,8 +429,9 @@ class _UserView extends StatelessWidget { | @@ -433,8 +429,9 @@ class _UserView extends StatelessWidget { | ||
| 433 | } | 429 | } |
| 434 | 430 | ||
| 435 | void _launchPhone(String phone) async { | 431 | void _launchPhone(String phone) async { |
| 436 | - if (await canLaunchUrl(Uri.parse(phone))) { | ||
| 437 | - await launchUrl(Uri.parse(phone)); | 432 | + final phoneUri = Uri(scheme: 'tel', path: phone); |
| 433 | + if (await canLaunchUrl(phoneUri)) { | ||
| 434 | + await launchUrl(phoneUri); | ||
| 438 | } else { | 435 | } else { |
| 439 | throw 'Could not phone $phone'; | 436 | throw 'Could not phone $phone'; |
| 440 | } | 437 | } |
pubspec.yaml
| @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||
| 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html |
| 17 | # In Windows, build-name is used as the major, minor, and patch parts | 17 | # In Windows, build-name is used as the major, minor, and patch parts |
| 18 | # of the product and file versions while build-number is used as the build suffix. | 18 | # of the product and file versions while build-number is used as the build suffix. |
| 19 | -version: 1.0.11+11 | 19 | +version: 1.0.13+13 |
| 20 | 20 | ||
| 21 | environment: | 21 | environment: |
| 22 | sdk: '>=3.2.0 <4.0.0' | 22 | sdk: '>=3.2.0 <4.0.0' |