This commit is contained in:
2026-08-04 17:04:18 +08:00
parent c7ebb5a205
commit 5746d773de
15 changed files with 187 additions and 56 deletions
+8
View File
@@ -11,4 +11,12 @@ class AppConfig {
/// 穿山甲 App ID(空 = 广告未开通,广告位不渲染)
static const String pangleAppId = '';
/// 测试账号:联调/测试用真实用户数据,默认 wenwu901/123456
/// 可用 --dart-define=TEST_ACCOUNT=xxx --dart-define=TEST_PASSWORD=xxx 覆盖(build_web.sh 透传环境变量);
/// 置空则登录页不显示测试登录入口
static const String testAccount =
String.fromEnvironment('TEST_ACCOUNT', defaultValue: 'wenwu901');
static const String testPassword =
String.fromEnvironment('TEST_PASSWORD', defaultValue: '123456');
}
+20 -10
View File
@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import '../../core/auth/auth_provider.dart';
import '../../core/config/app_config.dart';
import '../../shared/app_toast.dart';
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key});
@@ -23,18 +24,17 @@ class _LoginPageState extends ConsumerState<LoginPage> {
super.dispose();
}
Future<void> _submit() async {
final account = _accountCtrl.text.trim();
final password = _passwordCtrl.text;
Future<void> _submit([String? acc, String? pwd]) async {
final account = (acc ?? _accountCtrl.text).trim();
final password = pwd ?? _passwordCtrl.text;
if (account.isEmpty || password.isEmpty) {
Fluttertoast.showToast(msg: '请输入账号和密码');
showToast('请输入账号和密码');
return;
}
await ref.read(authProvider.notifier).login(account, password);
final state = ref.read(authProvider);
if (state.hasError) {
Fluttertoast.showToast(
msg: state.error.toString().replaceFirst('Exception: ', ''));
showToast(state.error.toString().replaceFirst('Exception: ', ''));
return;
}
if (state.value?.authenticated == true && mounted) {
@@ -83,13 +83,12 @@ class _LoginPageState extends ConsumerState<LoginPage> {
'password': password.text,
'name': name.text.trim(),
});
Fluttertoast.showToast(msg: '注册成功,正在登录...');
showToast('注册成功,正在登录...');
_accountCtrl.text = account.text.trim();
_passwordCtrl.text = password.text;
await _submit();
} catch (e) {
Fluttertoast.showToast(
msg: '注册失败:${e.toString().replaceFirst('Exception: ', '')}');
showToast('注册失败:${e.toString().replaceFirst('Exception: ', '')}');
}
}
@@ -146,6 +145,17 @@ class _LoginPageState extends ConsumerState<LoginPage> {
child: CircularProgressIndicator(strokeWidth: 2))
: const Text('登录'),
),
if (AppConfig.testAccount.isNotEmpty) ...[
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: loading
? null
: () => _submit(
AppConfig.testAccount, AppConfig.testPassword),
icon: const Icon(Icons.science_outlined, size: 18),
label: Text('测试账号一键登录(${AppConfig.testAccount}'),
),
],
const SizedBox(height: 8),
TextButton(
onPressed: loading ? null : _showRegisterDialog,
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/config/app_config.dart';
import '../../features/member/member_provider.dart';
import '../../shared/app_toast.dart';
import '../../shared/widgets/empty_view.dart';
import '../../shared/widgets/error_view.dart';
import '../../shared/widgets/loading_view.dart';
@@ -50,7 +50,7 @@ class _CpsProductListPageState extends ConsumerState<CpsProductListPage> {
final link = await ref.read(cpsLinkProvider).link(ref,
productId: product.id, scene: scene, planId: planId);
if (link.isEmpty) {
Fluttertoast.showToast(msg: '暂无可跳转链接');
showToast('暂无可跳转链接');
return;
}
final uri = Uri.parse(link);
@@ -63,15 +63,14 @@ class _CpsProductListPageState extends ConsumerState<CpsProductListPage> {
}
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: '跳转失败:${e.toString().replaceFirst('Exception: ', '')}');
showToast('跳转失败:${e.toString().replaceFirst('Exception: ', '')}');
}
}
Future<void> _copy(String text) async {
final data = ClipboardData(text: text);
await Clipboard.setData(data);
if (mounted) Fluttertoast.showToast(msg: '已复制,打开淘宝即可购买');
if (mounted) showToast('已复制,打开淘宝即可购买');
}
@override
+2 -2
View File
@@ -2,10 +2,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../shared/app_toast.dart';
import 'member_provider.dart';
class PayArgs {
@@ -68,7 +68,7 @@ class _PayPageState extends ConsumerState<PayPage> {
_timer?.cancel();
if (!mounted) return;
setState(() => _phase = PayPhase.paid);
Fluttertoast.showToast(msg: '会员开通成功');
showToast('会员开通成功');
return;
}
if (_elapsed >= 60) {
+6 -8
View File
@@ -1,13 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import '../../core/ads/ads_service.dart';
import '../../core/auth/auth_provider.dart';
import '../../core/config/app_config.dart';
import '../../features/cps/cps_provider.dart';
import '../../shared/app_toast.dart';
import '../../shared/widgets/loading_view.dart';
import '../member/member_provider.dart';
import '../member/pay_page.dart';
@@ -41,8 +41,7 @@ class _MinePageState extends ConsumerState<MinePage> {
ref.read(memberProvider.notifier).refresh();
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: e.toString().replaceFirst('Exception: ', ''));
showToast(e.toString().replaceFirst('Exception: ', ''));
}
}
@@ -50,24 +49,23 @@ class _MinePageState extends ConsumerState<MinePage> {
if (_rewarding) return;
final ads = ref.read(adsServiceProvider);
if (!ads.enabled) {
Fluttertoast.showToast(msg: '广告功能暂未开通');
showToast('广告功能暂未开通');
return;
}
setState(() => _rewarding = true);
try {
final watched = await ads.showRewarded();
if (!watched) {
Fluttertoast.showToast(msg: '未完整观看,无法领取');
showToast('未完整观看,无法领取');
return;
}
final remaining =
await ref.read(memberProvider.notifier).claimReward(adType);
if (!mounted) return;
Fluttertoast.showToast(msg: '$successMsg(今日剩余 $remaining 次)');
showToast('$successMsg(今日剩余 $remaining 次)');
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: e.toString().replaceFirst('Exception: ', ''));
showToast(e.toString().replaceFirst('Exception: ', ''));
} finally {
if (mounted) setState(() => _rewarding = false);
}
+6 -6
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import '../../shared/app_toast.dart';
import '../../shared/widgets/empty_view.dart';
import '../../shared/widgets/error_view.dart';
import '../../shared/widgets/loading_view.dart';
@@ -68,19 +68,19 @@ class _OutfitPageState extends ConsumerState<OutfitPage> {
final end = _endDate;
final location = _locationCtrl.text.trim();
if (start == null) {
Fluttertoast.showToast(msg: '请选择开始日期');
showToast('请选择开始日期');
return;
}
if (end == null) {
Fluttertoast.showToast(msg: '请选择结束日期');
showToast('请选择结束日期');
return;
}
if (location.isEmpty) {
Fluttertoast.showToast(msg: '请输入地点');
showToast('请输入地点');
return;
}
if (end.difference(start).inDays < 1) {
Fluttertoast.showToast(msg: '日期范围至少 1 天');
showToast('日期范围至少 1 天');
return;
}
setState(() => _submitting = true);
@@ -93,7 +93,7 @@ class _OutfitPageState extends ConsumerState<OutfitPage> {
if (!mounted) return;
setState(() => _submitting = false);
if (ok) {
Fluttertoast.showToast(msg: '生成完成');
showToast('生成完成');
ref.read(generateProvider.notifier).reset();
context.push('/plan-viewer');
}
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import '../../shared/app_toast.dart';
import '../../shared/widgets/loading_view.dart';
import 'avatar_provider.dart';
import 'body_provider.dart';
@@ -88,11 +88,10 @@ class _AvatarBuildFlowPageState extends ConsumerState<AvatarBuildFlowPage> {
try {
await ref.read(photoProvider.notifier).upload(type, file.path);
if (!mounted) return;
Fluttertoast.showToast(msg: '${PhotoType.labels[type]}上传成功');
showToast('${PhotoType.labels[type]}上传成功');
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: '上传失败:${e.toString().replaceFirst('Exception: ', '')}');
showToast('上传失败:${e.toString().replaceFirst('Exception: ', '')}');
} finally {
if (mounted) setState(() => _uploadingType = null);
}
@@ -111,12 +110,12 @@ class _AvatarBuildFlowPageState extends ConsumerState<AvatarBuildFlowPage> {
if (!mounted) return;
final a = ref.read(avatarProvider).value;
if (a?.built == true) {
Fluttertoast.showToast(msg: '3D 化身已生成');
showToast('3D 化身已生成');
context.go('/avatar-viewer');
} else if (a?.buildStatus == 'failed') {
setState(() => _buildError = a!.error);
} else {
Fluttertoast.showToast(msg: '构建超时,请稍后重试');
showToast('构建超时,请稍后重试');
}
} catch (e) {
if (mounted) {
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import '../../shared/app_toast.dart';
import '../../shared/widgets/avatar_viewer.dart';
import '../../shared/widgets/loading_view.dart';
import 'avatar_provider.dart';
@@ -54,7 +54,7 @@ class AvatarViewerPage extends ConsumerWidget {
.read(avatarProvider.notifier)
.buildAvatar();
if (context.mounted) {
Fluttertoast.showToast(msg: '化身构建完成');
showToast('化身构建完成');
}
},
icon: const Icon(Icons.refresh),
+2 -2
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import '../../core/config/app_config.dart';
import '../../shared/app_toast.dart';
import '../../features/cps/cps_provider.dart';
import '../../shared/widgets/empty_view.dart';
import '../../shared/widgets/error_view.dart';
@@ -55,7 +55,7 @@ class _WardrobePageState extends ConsumerState<WardrobePage> {
final rec = await ref.read(cpsUpgradeProvider(item.id).future);
if (!mounted) return;
if (rec.isEmpty) {
Fluttertoast.showToast(msg: '暂无升级款');
showToast('暂无升级款');
return;
}
final first = rec.first;
@@ -2,10 +2,10 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import '../../shared/app_toast.dart';
import 'wardrobe_provider.dart';
/// 服装上传:选图 + 分类/季节/风格/颜色信息
@@ -42,11 +42,11 @@ class _WardrobeUploadPageState extends ConsumerState<WardrobeUploadPage> {
Future<void> _submit() async {
if (_imagePath == null) {
Fluttertoast.showToast(msg: '请先选择服装照片');
showToast('请先选择服装照片');
return;
}
if (_category == null) {
Fluttertoast.showToast(msg: '请选择分类');
showToast('请选择分类');
return;
}
setState(() => _uploading = true);
@@ -59,12 +59,11 @@ class _WardrobeUploadPageState extends ConsumerState<WardrobeUploadPage> {
colorInfo: _colorCtrl.text.trim(),
);
if (!mounted) return;
Fluttertoast.showToast(msg: '上传成功');
showToast('上传成功');
context.go('/wardrobe');
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: '上传失败:${e.toString().replaceFirst('Exception: ', '')}');
showToast('上传失败:${e.toString().replaceFirst('Exception: ', '')}');
} finally {
if (mounted) setState(() => _uploading = false);
}
+2
View File
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'core/auth/auth_provider.dart';
import 'shared/app_toast.dart';
import 'features/auth/login_page.dart';
import 'features/cps/cps_product_list_page.dart';
import 'features/cps/cps_provider.dart';
@@ -58,6 +59,7 @@ class SloganApp extends ConsumerWidget {
/// 路由表:启动无 token 进登录页;有 token 进主框架
final routerProvider = Provider<GoRouter>((ref) {
return GoRouter(
navigatorKey: appNavigatorKey,
initialLocation: '/login',
routes: [
GoRoute(
+60
View File
@@ -0,0 +1,60 @@
import 'dart:async';
import 'package:flutter/material.dart';
/// 全局 Navigator key,供不依赖页面 context 的 toast 等场景使用
final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey<NavigatorState>();
OverlayEntry? _entry;
Timer? _timer;
/// 屏幕正中央的 toast,展示时长按文字长度分档:≤8 字 2 秒,≤20 字 3 秒,更长 4 秒
void showToast(String msg) {
_timer?.cancel();
_entry?.remove();
_entry = null;
final overlay = appNavigatorKey.currentState?.overlay;
if (overlay == null) return;
final entry = OverlayEntry(
builder: (_) => Positioned.fill(
child: IgnorePointer(
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 280),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.78),
borderRadius: BorderRadius.circular(8),
),
child: Text(
msg,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
height: 1.4,
),
),
),
),
),
),
);
overlay.insert(entry);
_entry = entry;
_timer = Timer(_durationFor(msg), () {
if (_entry == entry) {
entry.remove();
_entry = null;
}
});
}
Duration _durationFor(String msg) {
final len = msg.length;
if (len <= 8) return const Duration(seconds: 2);
if (len <= 20) return const Duration(seconds: 3);
return const Duration(seconds: 4);
}
-8
View File
@@ -232,14 +232,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
sha256: "7903c9d5339173497bfecbc23bc4212f5a87e0edfac2e1693fb74465ea67da7e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.1.0"
frontend_server_client:
dependency: transitive
description:
-1
View File
@@ -39,7 +39,6 @@ dependencies:
go_router: ^17.3.0
image_picker: ^1.2.3
shared_preferences: ^2.5.5
fluttertoast: ^9.1.0
url_launcher: ^6.3.0
# v2 3D 渲染接入时引入:three_dart ^0.0.16 + three_dart_jsm ^0.0.10 + flutter_gl ^0.0.20
# flutter_gl 0.0.21 在 Flutter 3.44 下无法编译:platformViewRegistry 已移除)
+65
View File
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:slogan_app/shared/app_toast.dart';
void main() {
testWidgets('showToast 显示在屏幕正中心', (tester) async {
await tester.pumpWidget(MaterialApp(
navigatorKey: appNavigatorKey,
home: const Scaffold(body: Center(child: Text('页面'))),
));
showToast('上传成功');
await tester.pump();
final text = find.text('上传成功');
expect(text, findsOneWidget);
final center = tester.getCenter(text);
final screen = tester.getSize(find.byType(MaterialApp));
expect(center.dx, closeTo(screen.width / 2, 1));
expect(center.dy, closeTo(screen.height / 2, 1));
// 等 toast 消失,避免测试结束时残留 Timer
await tester.pump(const Duration(seconds: 3));
});
testWidgets('短文本 2 秒后消失', (tester) async {
await tester.pumpWidget(MaterialApp(
navigatorKey: appNavigatorKey,
home: const Scaffold(body: Center(child: Text('页面'))),
));
showToast('上传成功');
await tester.pump();
expect(find.text('上传成功'), findsOneWidget);
// 2 秒整之前仍在
await tester.pump(const Duration(milliseconds: 1900));
expect(find.text('上传成功'), findsOneWidget);
// 超过 2 秒后消失
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('上传成功'), findsNothing);
});
testWidgets('长文本 4 秒后消失', (tester) async {
await tester.pumpWidget(MaterialApp(
navigatorKey: appNavigatorKey,
home: const Scaffold(body: Center(child: Text('页面'))),
));
showToast('这是一个超过二十个字符的非常长的提示消息内容展示测试');
await tester.pump();
expect(find.text('这是一个超过二十个字符的非常长的提示消息内容展示测试'),
findsOneWidget);
await tester.pump(const Duration(seconds: 3));
expect(find.text('这是一个超过二十个字符的非常长的提示消息内容展示测试'),
findsOneWidget);
await tester.pump(const Duration(seconds: 1, milliseconds: 500));
expect(find.text('这是一个超过二十个字符的非常长的提示消息内容展示测试'),
findsNothing);
});
}