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 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 &#39;../generated/json/app_config_entity.g.dart&#39;;
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 &#39;../../../models/course_entity.dart&#39;;
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 &#39;package:wow_english/models/user_entity.dart&#39;;
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: &#39;none&#39; # 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'
... ...