Commit 6770c637cdf78f76fdf2abc5eead22588897ddd6
1 parent
47569bdf
feat: 动态配置客服联系方式
Showing
7 changed files
with
168 additions
and
73 deletions
assets/images/ic_customer_service.png
0 → 100644
102 KB
lib/generated/json/app_config_entity.g.dart
| ... | ... | @@ -7,20 +7,37 @@ AppConfigEntity $AppConfigEntityFromJson(Map<String, dynamic> json) { |
| 7 | 7 | if (safe != null) { |
| 8 | 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 | 21 | return appConfigEntity; |
| 11 | 22 | } |
| 12 | 23 | |
| 13 | 24 | Map<String, dynamic> $AppConfigEntityToJson(AppConfigEntity entity) { |
| 14 | 25 | final Map<String, dynamic> data = <String, dynamic>{}; |
| 15 | 26 | data['safe'] = entity.safe; |
| 27 | + data['customerMobileList'] = entity.customerMobileList; | |
| 28 | + data['customerQr'] = entity.customerQr; | |
| 16 | 29 | return data; |
| 17 | 30 | } |
| 18 | 31 | |
| 19 | 32 | extension AppConfigEntityExtension on AppConfigEntity { |
| 20 | 33 | AppConfigEntity copyWith({ |
| 21 | 34 | String? safe, |
| 35 | + List<String>? customerMobileList, | |
| 36 | + String? customerQr, | |
| 22 | 37 | }) { |
| 23 | 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 | 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 | 6 | |
| 7 | 7 | @JsonSerializable() |
| 8 | 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 | 35 | \ No newline at end of file | ... | ... |
lib/pages/home/widgets/BaseHomeHeaderWidget.dart
| ... | ... | @@ -10,6 +10,7 @@ import '../../../models/course_entity.dart'; |
| 10 | 10 | import '../../../route/route.dart'; |
| 11 | 11 | import '../../../utils/image_util.dart'; |
| 12 | 12 | import '../../user/bloc/user_bloc.dart'; |
| 13 | +import 'customer_service_qr_dialog.dart'; | |
| 13 | 14 | |
| 14 | 15 | typedef HeaderCallback = void Function(dynamic); |
| 15 | 16 | |
| ... | ... | @@ -80,6 +81,28 @@ class BaseHomeHeaderWidget extends StatelessWidget { |
| 80 | 81 | textAlign: TextAlign.left, |
| 81 | 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 | 106 | Offstage( |
| 84 | 107 | offstage: AppConfigHelper.shouldHidePay() || |
| 85 | 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/user/user_page.dart
| ... | ... | @@ -13,6 +13,7 @@ import 'package:wow_english/models/user_entity.dart'; |
| 13 | 13 | import 'package:wow_english/pages/user/bloc/user_bloc.dart'; |
| 14 | 14 | import 'package:wow_english/route/route.dart'; |
| 15 | 15 | import 'package:wow_english/utils/image_util.dart'; |
| 16 | +import 'package:wow_english/utils/toast_util.dart'; | |
| 16 | 17 | |
| 17 | 18 | class UserPage extends StatelessWidget { |
| 18 | 19 | const UserPage({super.key}); |
| ... | ... | @@ -196,6 +197,16 @@ class _UserView extends StatelessWidget { |
| 196 | 197 | child: 12.verticalSpace, |
| 197 | 198 | ), |
| 198 | 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 | 210 | onPressed: () => pushNamed(AppRouteName.fogPwd), |
| 200 | 211 | style: normalButtonStyle, |
| 201 | 212 | child: Text( |
| ... | ... | @@ -232,16 +243,6 @@ class _UserView extends StatelessWidget { |
| 232 | 243 | 12.verticalSpace, |
| 233 | 244 | OutlinedButton( |
| 234 | 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 | 246 | pushNamed(AppRouteName.setting); |
| 246 | 247 | }, |
| 247 | 248 | style: normalButtonStyle, |
| ... | ... | @@ -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 | 339 | showModalBottomSheet( |
| 313 | 340 | context: context, |
| 314 | 341 | backgroundColor: Colors.white, |
| ... | ... | @@ -333,30 +360,28 @@ class _UserView extends StatelessWidget { |
| 333 | 360 | borderRadius: BorderRadius.circular(2), |
| 334 | 361 | ), |
| 335 | 362 | ), |
| 336 | - // 老师选项列表 | |
| 363 | + // 客服手机号列表 | |
| 337 | 364 | Flexible( |
| 338 | 365 | child: SingleChildScrollView( |
| 339 | 366 | child: Column( |
| 340 | 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 | 377 | title: Text( |
| 346 | - '恐龙老师', | |
| 378 | + mobile, | |
| 347 | 379 | style: TextStyle( |
| 348 | 380 | fontSize: 14.sp, |
| 349 | 381 | fontWeight: FontWeight.w600, |
| 350 | 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 | 385 | trailing: Icon( |
| 361 | 386 | Icons.phone, |
| 362 | 387 | color: Colors.green[600], |
| ... | ... | @@ -364,39 +389,10 @@ class _UserView extends StatelessWidget { |
| 364 | 389 | ), |
| 365 | 390 | onTap: () { |
| 366 | 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 | 429 | } |
| 434 | 430 | |
| 435 | 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 | 435 | } else { |
| 439 | 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 | 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html |
| 17 | 17 | # In Windows, build-name is used as the major, minor, and patch parts |
| 18 | 18 | # of the product and file versions while build-number is used as the build suffix. |
| 19 | -version: 1.0.12+12 | |
| 19 | +version: 1.0.13+13 | |
| 20 | 20 | |
| 21 | 21 | environment: |
| 22 | 22 | sdk: '>=3.2.0 <4.0.0' | ... | ... |