Commit 6770c637cdf78f76fdf2abc5eead22588897ddd6

Authored by 吴启风
1 parent 47569bdf

feat: 动态配置客服联系方式

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,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 &#39;../generated/json/app_config_entity.g.dart&#39;; @@ -6,24 +6,29 @@ import &#39;../generated/json/app_config_entity.g.dart&#39;;
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 &#39;../../../models/course_entity.dart&#39;; @@ -10,6 +10,7 @@ import &#39;../../../models/course_entity.dart&#39;;
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/user/user_page.dart
@@ -13,6 +13,7 @@ import &#39;package:wow_english/models/user_entity.dart&#39;; @@ -13,6 +13,7 @@ import &#39;package:wow_english/models/user_entity.dart&#39;;
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: &#39;none&#39; # Remove this line if you wish to publish to pub.dev @@ -16,7 +16,7 @@ publish_to: &#39;none&#39; # 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.12+12 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'