From 553c37c743f18b7803b822203935599a224b50ab Mon Sep 17 00:00:00 2001 From: NIGGER SLAYER Date: Wed, 15 Apr 2026 15:38:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=8A=A0=E8=BD=BD=E4=B8=AD?= =?UTF-8?q?=E5=8D=A0=E4=BD=8D=E7=AC=A6=E6=9B=B4=E6=94=B9=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?app=E5=90=AF=E5=8A=A8=E5=88=9D=E5=A7=8B=E5=B8=A7=E6=8F=90?= =?UTF-8?q?=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 788 +++++++++--------- lib/modules/gift/gift_tab_page.dart | 45 +- lib/modules/home/index_home_page.dart | 68 +- lib/modules/index/index_page.dart | 555 ++++++------ lib/modules/splash/splash_page.dart | 207 ++--- .../localization/localization_manager.dart | 113 +-- lib/shared/tools/sc_google_auth_utils.dart | 209 ++--- .../components/custom_cached_image.dart | 101 ++- lib/ui_kit/components/sc_compontent.dart | 10 +- .../components/sc_rotating_dots_loading.dart | 128 +++ 需求进度.md | 11 + 11 files changed, 1206 insertions(+), 1029 deletions(-) create mode 100644 lib/ui_kit/components/sc_rotating_dots_loading.dart diff --git a/lib/main.dart b/lib/main.dart index c392c59..775d467 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,381 +1,407 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui'; - -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:fluro/fluro.dart' as fluro; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/shared/tools/sc_keybord_util.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; -import 'package:provider/provider.dart'; -import 'package:provider/single_child_widget.dart'; -import 'package:pull_to_refresh/pull_to_refresh.dart'; - -import 'app_localizations.dart'; -import 'app/config/app_config.dart'; -import 'app/constants/sc_global_config.dart'; -import 'app/constants/sc_screen.dart'; -import 'app/routes/sc_routes.dart'; -import 'app/routes/sc_lk_application.dart'; -import 'shared/tools/sc_deep_link_handler.dart'; -import 'shared/tools/sc_deviceId_utils.dart'; -import 'modules/splash/splash_page.dart'; -import 'services/general/sc_app_general_manager.dart'; -import 'services/payment/apple_payment_manager.dart'; -import 'services/auth/authentication_manager.dart'; -import 'services/gift/gift_animation_manager.dart'; -import 'services/gift/gift_system_manager.dart'; -import 'services/payment/google_payment_manager.dart'; -import 'services/localization/localization_manager.dart'; -import 'services/room/rc_room_manager.dart'; -import 'services/audio/rtc_manager.dart'; -import 'services/audio/rtm_manager.dart'; -import 'services/shop/shop_manager.dart'; -import 'services/theme/theme_manager.dart'; -import 'services/auth/user_profile_manager.dart'; -import 'ui_kit/theme/socialchat_theme.dart'; - - -void main() async { - - // 初始化应用配置 - AppConfig.initialize(); - - - // 2. 使用 runZonedGuarded 创建一个隔离区域来捕获所有异步错误 - runZonedGuarded( - () async { - // 1. 确保 Widget 绑定初始化 - WidgetsFlutterBinding.ensureInitialized(); - // 性能优化:启用手势重采样 - GestureBinding.instance.resamplingEnabled = true; - // 阶段1:初始化绝对必要的服务(不阻塞UI) - await _setupA(); - // 使用重试机制初始化存储 - await _initStore(); - // 立即运行应用,不等待非必要服务初始化 - runApp(const RootAppWithProviders()); - }, - // 9. runZonedGuarded 的错误回调(这是捕获异步错误的主要方式) - (error, stackTrace) { - // 这是捕获 runZonedGuarded 区域内所有未捕获异步错误的地方 - FirebaseCrashlytics.instance.recordError(error, stackTrace, fatal: true); - debugPrint('Zoned Error: $error\nStack: $stackTrace'); - }, - ); -} - -/// 设置核心服务(重命名自_initializeEssentialServices) -Future _setupA() async { - // 配置设备方向:仅竖屏 - await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - - // Android平台状态栏样式设置 - if (Platform.isAndroid) { - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.dark, - ), - ); - } - - // 键盘工具初始化 - SCKeybordUtil.init(); - - // Firebase核心初始化(必需组件) - try { - await Firebase.initializeApp(); - if (kDebugMode) { - debugPrint('Firebase初始化完成'); - } - } catch (e, stackTrace) { - debugPrint('Firebase初始化异常: $e\n$stackTrace'); - // 非阻塞错误处理:记录但继续运行 - } - - // Flutter错误拦截器配置 - FlutterError.onError = (details) { - FirebaseCrashlytics.instance.recordFlutterFatalError(details); - FlutterError.presentError(details); - }; - - // 平台层错误处理器 - PlatformDispatcher.instance.onError = (error, stack) { - FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); - return true; // 错误已处理 - }; - - // 启用崩溃收集 - await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); -} - - -/// 带重试的数据存储初始化(重命名自_initializeStorageWithRetry) -Future _initStore() async { - const maxAttempts = 3; - int attemptCount = 0; - bool success = false; - - while (attemptCount < maxAttempts && !success) { - attemptCount++; - try { - await DataPersistence.initialize(); - success = true; - if (kDebugMode) { - debugPrint('数据存储初始化成功(尝试次数: $attemptCount)'); - } - } catch (e) { - debugPrint('数据存储初始化尝试 $attemptCount 失败: $e'); - - if (attemptCount >= maxAttempts) { - debugPrint('数据存储初始化失败,已达最大重试次数: $maxAttempts'); - // 不抛出异常,允许应用继续运行 - return; - } - - // 递增延迟:500ms, 1000ms, 1500ms... - int delayMs = 500 * attemptCount; - await Future.delayed(Duration(milliseconds: delayMs)); - } - } -} - -/// 带有所有Provider的根组件 -class RootAppWithProviders extends StatelessWidget { - const RootAppWithProviders({super.key}); - - @override - Widget build(BuildContext context) { - return MultiProvider(providers: _buildP(), child: const YumiApplication()); - } - - /// 构建应用Provider列表 - 重新排序以优化性能 - List _buildP() { - return [ - // 核心用户相关Provider - 必须立即初始化 - ChangeNotifierProvider( - lazy: false, - create: (context) => SocialChatAuthenticationManager(), - ), - ChangeNotifierProvider( - lazy: false, - create: (context) => SocialChatUserProfileManager(), - ), - - // UI与主题相关Provider - 懒加载 - ChangeNotifierProvider( - lazy: true, - create: (context) => ThemeManager(), - ), - ChangeNotifierProvider( - lazy: true, - create: (context) => LocalizationManager(), - ), - - // 实时通信Provider - 懒加载 - ChangeNotifierProvider( - lazy: true, - create: (context) => RealTimeCommunicationManager(), - ), - ChangeNotifierProvider( - lazy: true, - create: (context) => RealTimeMessagingManager(), - ), - - // 房间与社交功能Provider - 懒加载 - ChangeNotifierProvider( - lazy: true, - create: (context) => SocialChatRoomManager(), - ), - - // 礼物与动画系统Provider - 懒加载 - ChangeNotifierProvider( - lazy: true, - create: (context) => SocialChatGiftSystemManager(), - ), - ChangeNotifierProvider( - lazy: true, - create: (context) => GiftAnimationManager(), - ), - - // 支付与商店Provider - 懒加载 - ChangeNotifierProvider( - lazy: true, - create: (context) => AndroidPaymentProcessor(), - ), - ChangeNotifierProvider( - lazy: true, - create: (context) => IOSPaymentProcessor(), - ), - ChangeNotifierProvider( - lazy: true, - create: (context) => ShopManager(), - ), - - - // 通用应用管理Provider - 懒加载 - ChangeNotifierProvider( - lazy: true, - create: (context) => SCAppGeneralManager(), - ), - ]; - } -} - -final GlobalKey navigatorKey = GlobalKey(); -final RouteObserver routeObserver = RouteObserver(); - -class YumiApplication extends StatefulWidget { - const YumiApplication({super.key}); - - @override - State createState() => _YumiApplicationState(); -} - -class _YumiApplicationState extends State { - late fluro.FluroRouter _router; - final SCDeepLinkHandler _deepLinkHandler = SCDeepLinkHandler(); - - @override - void initState() { - super.initState(); - // 初始化深层链接处理 - _initLink(); - // 延迟初始化路由和设备信息 - WidgetsBinding.instance.addPostFrameCallback((_) { - _initRouter(); - SCDeviceIdUtils.initDeviceinfo(); - }); - } - - @override - dispose() { - _deepLinkHandler.dispose(); - super.dispose(); - } - - Future _initLink() async { - // 初始化,并传递一个回调函数用于处理链接 - await _deepLinkHandler.initDeepLinks( - onLinkReceived: (Uri uri) { - // 当收到链接时,无论应用在哪个页面,都可以进行路由 - _handleLink(uri); - }, - ); - } - - void _handleLink(Uri uri) { - // 这是处理链接的核心路由逻辑 - print('App 根层收到链接: $uri'); - String path = uri.path; - String id = uri.queryParameters['id'] ?? ''; - } - - void _initRouter() { - _router = fluro.FluroRouter(); - SCRoutes.configureRoutes(_router); - SCLkApplication.router = _router; - } - - @override - Widget build(BuildContext context) { - return _buildUI(); - } - - // 构建下拉刷新配置的页脚部件 - Widget Function() _buildFooter() { - return () => CustomFooter( - builder: (BuildContext context, LoadStatus? mode) { - Widget body; - if (mode == LoadStatus.idle) { - body = Text( - SCAppLocalizations.of(context)!.pullToLoadMore, - ); - } else if (mode == LoadStatus.loading) { - body = CupertinoActivityIndicator(color: Colors.white24); - } else if (mode == LoadStatus.failed) { - body = Text( - SCAppLocalizations.of(context)!.loadingFailedClickToRetry, - ); - } else if (mode == LoadStatus.canLoading) { - body = Text( - SCAppLocalizations.of(context)!.releaseToLoadMore, - ); - } else { - body = Text( - "", - style: TextStyle( - fontSize: 12.sp, - color: Color(0xff999999), - ), - ); - } - return Container( - height: kTextTabBarHeight + ScreenUtil().statusBarHeight, - child: Center(child: body), - ); - }, - ); - } - - - Widget _buildUI() { - return Consumer( - builder: (context, localeProvider, child) { - // 只有在LocaleProvider可用时才设置语言 - SCGlobalConfig.lang = localeProvider.locale?.languageCode ?? "en"; - return Consumer( - builder: (context, themeManager, child) { - return ScreenUtilInit( - designSize: Size(SCScreen.designWidth, SCScreen.designHeight), - splitScreenMode: false, - minTextAdapt: true, - builder: (context, child) { - return AnnotatedRegion( - value: SystemUiOverlayStyle.dark, - child: RefreshConfiguration( - headerBuilder: () => MaterialClassicHeader(color: SocialChatTheme.primaryColor), - footerBuilder: _buildFooter(), - child: MaterialApp( - title: 'Yumi', - locale: Provider.of(context).locale, - localizationsDelegates: [ - SCAppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', ''), - const Locale('zh', ''), - const Locale('ar', ''), - const Locale('tr', ''), - const Locale('bn', ''), - ], - navigatorKey: navigatorKey, - debugShowCheckedModeBanner: false, - onGenerateRoute: SCLkApplication.router.generator, - theme: themeManager.currentTheme, - home: SplashPage(), - builder: FlutterSmartDialog.init(), - navigatorObservers: [routeObserver], - ), - ), - ); - }, - ); - }, - ); - }, - ); - } -} +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:fluro/fluro.dart' as fluro; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/shared/tools/sc_keybord_util.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import 'app_localizations.dart'; +import 'app/config/app_config.dart'; +import 'app/constants/sc_global_config.dart'; +import 'app/constants/sc_screen.dart'; +import 'app/routes/sc_routes.dart'; +import 'app/routes/sc_lk_application.dart'; +import 'shared/tools/sc_deep_link_handler.dart'; +import 'shared/tools/sc_deviceId_utils.dart'; +import 'modules/splash/splash_page.dart'; +import 'services/general/sc_app_general_manager.dart'; +import 'services/payment/apple_payment_manager.dart'; +import 'services/auth/authentication_manager.dart'; +import 'services/gift/gift_animation_manager.dart'; +import 'services/gift/gift_system_manager.dart'; +import 'services/payment/google_payment_manager.dart'; +import 'services/localization/localization_manager.dart'; +import 'services/room/rc_room_manager.dart'; +import 'services/audio/rtc_manager.dart'; +import 'services/audio/rtm_manager.dart'; +import 'services/shop/shop_manager.dart'; +import 'services/theme/theme_manager.dart'; +import 'services/auth/user_profile_manager.dart'; +import 'ui_kit/theme/socialchat_theme.dart'; + +bool _isCrashlyticsReady = false; + +void main() async { + // 初始化应用配置 + AppConfig.initialize(); + + // 2. 使用 runZonedGuarded 创建一个隔离区域来捕获所有异步错误 + runZonedGuarded( + () async { + // 1. 确保 Widget 绑定初始化 + WidgetsFlutterBinding.ensureInitialized(); + // 性能优化:启用手势重采样 + GestureBinding.instance.resamplingEnabled = true; + // 仅保留首帧前真正必要的初始化 + await _prepareAppShell(); + _installErrorHandlers(); + // 立即运行应用,不等待非必要服务初始化 + runApp(const RootAppWithProviders()); + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(_warmUpDeferredServices()); + }); + }, + // 9. runZonedGuarded 的错误回调(这是捕获异步错误的主要方式) + (error, stackTrace) { + unawaited(_recordFatalError(error, stackTrace)); + }, + ); +} + +/// 首帧前的必要初始化 +Future _prepareAppShell() async { + // 配置设备方向:仅竖屏 + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + + // Android平台状态栏样式设置 + if (Platform.isAndroid) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + ); + } + + // 键盘工具初始化 + SCKeybordUtil.init(); + // 使用重试机制初始化存储 + await _initStore(); +} + +void _installErrorHandlers() { + FlutterError.onError = (details) { + FlutterError.presentError(details); + unawaited(_recordFlutterError(details)); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + unawaited(_recordFatalError(error, stack)); + return true; + }; +} + +/// 首帧后的后台预热初始化 +Future _warmUpDeferredServices() async { + try { + if (Firebase.apps.isEmpty) { + await Firebase.initializeApp(); + } + _isCrashlyticsReady = true; + await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); + if (kDebugMode) { + debugPrint('Firebase/Crashlytics 后台初始化完成'); + } + } catch (e, stackTrace) { + debugPrint('Firebase 后台初始化异常: $e\n$stackTrace'); + } +} + +Future _recordFlutterError(FlutterErrorDetails details) async { + if (!_isCrashlyticsReady) { + debugPrint( + 'Flutter Error before Crashlytics ready: ${details.exceptionAsString()}', + ); + return; + } + + try { + await FirebaseCrashlytics.instance.recordFlutterFatalError(details); + } catch (e, stackTrace) { + debugPrint('Crashlytics 记录 Flutter 错误失败: $e\n$stackTrace'); + } +} + +Future _recordFatalError(Object error, StackTrace stackTrace) async { + if (!_isCrashlyticsReady) { + debugPrint('Unhandled error before Crashlytics ready: $error'); + debugPrintStack(stackTrace: stackTrace); + return; + } + + try { + await FirebaseCrashlytics.instance.recordError( + error, + stackTrace, + fatal: true, + ); + } catch (e, recordStackTrace) { + debugPrint('Crashlytics 记录异常失败: $e\n$recordStackTrace'); + } +} + +/// 带重试的数据存储初始化(重命名自_initializeStorageWithRetry) +Future _initStore() async { + const maxAttempts = 3; + int attemptCount = 0; + bool success = false; + + while (attemptCount < maxAttempts && !success) { + attemptCount++; + try { + await DataPersistence.initialize(); + success = true; + if (kDebugMode) { + debugPrint('数据存储初始化成功(尝试次数: $attemptCount)'); + } + } catch (e) { + debugPrint('数据存储初始化尝试 $attemptCount 失败: $e'); + + if (attemptCount >= maxAttempts) { + debugPrint('数据存储初始化失败,已达最大重试次数: $maxAttempts'); + // 不抛出异常,允许应用继续运行 + return; + } + + // 递增延迟:500ms, 1000ms, 1500ms... + int delayMs = 500 * attemptCount; + await Future.delayed(Duration(milliseconds: delayMs)); + } + } +} + +/// 带有所有Provider的根组件 +class RootAppWithProviders extends StatelessWidget { + const RootAppWithProviders({super.key}); + + @override + Widget build(BuildContext context) { + return MultiProvider(providers: _buildP(), child: const YumiApplication()); + } + + /// 构建应用Provider列表 - 重新排序以优化性能 + List _buildP() { + return [ + // 核心用户相关Provider - 按需初始化,避免阻塞首屏 + ChangeNotifierProvider( + lazy: true, + create: (context) => SocialChatAuthenticationManager(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => SocialChatUserProfileManager(), + ), + + // UI与主题相关Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => ThemeManager(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => LocalizationManager(), + ), + + // 实时通信Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => RealTimeCommunicationManager(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => RealTimeMessagingManager(), + ), + + // 房间与社交功能Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => SocialChatRoomManager(), + ), + + // 礼物与动画系统Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => SocialChatGiftSystemManager(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => GiftAnimationManager(), + ), + + // 支付与商店Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => AndroidPaymentProcessor(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => IOSPaymentProcessor(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => ShopManager(), + ), + + // 通用应用管理Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => SCAppGeneralManager(), + ), + ]; + } +} + +final GlobalKey navigatorKey = GlobalKey(); +final RouteObserver routeObserver = RouteObserver(); + +class YumiApplication extends StatefulWidget { + const YumiApplication({super.key}); + + @override + State createState() => _YumiApplicationState(); +} + +class _YumiApplicationState extends State { + late fluro.FluroRouter _router; + final SCDeepLinkHandler _deepLinkHandler = SCDeepLinkHandler(); + + @override + void initState() { + super.initState(); + // 将非首帧必需的初始化统一延后到首帧后 + WidgetsBinding.instance.addPostFrameCallback((_) { + _initRouter(); + SCDeviceIdUtils.initDeviceinfo(); + unawaited(_initLink()); + }); + } + + @override + dispose() { + _deepLinkHandler.dispose(); + super.dispose(); + } + + Future _initLink() async { + // 初始化,并传递一个回调函数用于处理链接 + await _deepLinkHandler.initDeepLinks( + onLinkReceived: (Uri uri) { + // 当收到链接时,无论应用在哪个页面,都可以进行路由 + _handleLink(uri); + }, + ); + } + + void _handleLink(Uri uri) { + // 这是处理链接的核心路由逻辑 + debugPrint('App 根层收到链接: $uri'); + } + + void _initRouter() { + _router = fluro.FluroRouter(); + SCRoutes.configureRoutes(_router); + SCLkApplication.router = _router; + } + + @override + Widget build(BuildContext context) { + return _buildUI(); + } + + // 构建下拉刷新配置的页脚部件 + Widget Function() _buildFooter() { + return () => CustomFooter( + builder: (BuildContext context, LoadStatus? mode) { + Widget body; + if (mode == LoadStatus.idle) { + body = Text(SCAppLocalizations.of(context)!.pullToLoadMore); + } else if (mode == LoadStatus.loading) { + body = CupertinoActivityIndicator(color: Colors.white24); + } else if (mode == LoadStatus.failed) { + body = Text( + SCAppLocalizations.of(context)!.loadingFailedClickToRetry, + ); + } else if (mode == LoadStatus.canLoading) { + body = Text(SCAppLocalizations.of(context)!.releaseToLoadMore); + } else { + body = Text( + "", + style: TextStyle(fontSize: 12.sp, color: Color(0xff999999)), + ); + } + return Container( + height: kTextTabBarHeight + ScreenUtil().statusBarHeight, + child: Center(child: body), + ); + }, + ); + } + + Widget _buildUI() { + return Consumer( + builder: (context, localeProvider, child) { + // 只有在LocaleProvider可用时才设置语言 + SCGlobalConfig.lang = localeProvider.locale?.languageCode ?? "en"; + return Consumer( + builder: (context, themeManager, child) { + return ScreenUtilInit( + designSize: Size(SCScreen.designWidth, SCScreen.designHeight), + splitScreenMode: false, + minTextAdapt: true, + builder: (context, child) { + return AnnotatedRegion( + value: SystemUiOverlayStyle.dark, + child: RefreshConfiguration( + headerBuilder: + () => MaterialClassicHeader( + color: SocialChatTheme.primaryColor, + ), + footerBuilder: _buildFooter(), + child: MaterialApp( + title: 'Yumi', + locale: localeProvider.locale, + localizationsDelegates: const [ + SCAppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', ''), + Locale('zh', ''), + Locale('ar', ''), + Locale('tr', ''), + Locale('bn', ''), + ], + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + onGenerateRoute: SCLkApplication.router.generator, + theme: themeManager.currentTheme, + home: const SplashPage(), + builder: FlutterSmartDialog.init(), + navigatorObservers: [routeObserver], + ), + ), + ); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/modules/gift/gift_tab_page.dart b/lib/modules/gift/gift_tab_page.dart index 1a36d57..f015cf2 100644 --- a/lib/modules/gift/gift_tab_page.dart +++ b/lib/modules/gift/gift_tab_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; @@ -21,14 +22,19 @@ const Duration _kGiftPageSkeletonMinDuration = Duration(milliseconds: 420); const Duration _kGiftPageSkeletonMaxDuration = Duration(milliseconds: 900); class GiftTabPage extends StatefulWidget { - String type; - bool isDark = true; - Function(int checkedIndex) checkedCall; + final String type; + final bool isDark; + final Function(int checkedIndex) checkedCall; - GiftTabPage(this.type, this.checkedCall, {super.key, this.isDark = true}); + const GiftTabPage( + this.type, + this.checkedCall, { + super.key, + this.isDark = true, + }); @override - _GiftTabPageState createState() => _GiftTabPageState(); + State createState() => _GiftTabPageState(); } class _GiftTabPageState extends State @@ -202,12 +208,8 @@ class _GiftTabPageState extends State ), alignment: Alignment.center, child: Opacity( - opacity: 0.75, - child: Image.asset( - "sc_images/general/sc_icon_loading.png", - width: 92.w, - fit: BoxFit.contain, - ), + opacity: 0.9, + child: SCRotatingDotsLoading(size: 72.w), ), ), ), @@ -245,12 +247,8 @@ class _GiftTabPageState extends State decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.w), color: baseColor, - image: const DecorationImage( - image: AssetImage("sc_images/general/sc_icon_loading.png"), - fit: BoxFit.cover, - opacity: 0.32, - ), ), + child: Center(child: SCRotatingDotsLoading(size: 24.w)), ), SizedBox(height: 7.w), Container( @@ -276,6 +274,19 @@ class _GiftTabPageState extends State } Widget _buildGiftCoverLoading() { + return Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.white.withValues(alpha: 0.08), + ), + alignment: Alignment.center, + child: SCRotatingDotsLoading(size: 24.w), + ); + } + + Widget _buildGiftCoverNoData() { return Container( width: 48.w, height: 48.w, @@ -424,7 +435,7 @@ class _GiftTabPageState extends State width: 48.w, height: 48.w, loadingWidget: _buildGiftCoverLoading(), - errorWidget: _buildGiftCoverLoading(), + errorWidget: _buildGiftCoverNoData(), ), SizedBox(height: 5.w), Container( diff --git a/lib/modules/home/index_home_page.dart b/lib/modules/home/index_home_page.dart index b36a46a..8b711ba 100644 --- a/lib/modules/home/index_home_page.dart +++ b/lib/modules/home/index_home_page.dart @@ -12,8 +12,10 @@ import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; ///Home页面 class SCIndexHomePage extends StatefulWidget { + const SCIndexHomePage({super.key}); + @override - _SCIndexHomePageState createState() => _SCIndexHomePageState(); + State createState() => _SCIndexHomePageState(); } class _SCIndexHomePageState extends State @@ -21,38 +23,44 @@ class _SCIndexHomePageState extends State TabController? _tabController; final List _pages = []; final List _tabs = []; - bool refresh = false; // 👈 改为 false,初始显示加载 + Locale? _lastLocale; @override - void initState() { - super.initState(); - // 在构建完成后加载数据 - WidgetsBinding.instance.addPostFrameCallback((_) { - _updateTabs(); - }); + void didChangeDependencies() { + super.didChangeDependencies(); + final locale = Localizations.localeOf(context); + if (_lastLocale == locale && _tabController != null && _pages.isNotEmpty) { + return; + } + _lastLocale = locale; + _rebuildTabs(); } - void _updateTabs() { - _tabController?.dispose(); - _pages.clear(); - + void _rebuildTabs() { final strategy = SCGlobalConfig.businessLogicStrategy; - _pages.addAll(strategy.getHomeTabPages(context)); + final nextPages = strategy.getHomeTabPages(context); + final nextTabs = strategy.getHomeTabLabels(context); + if (nextPages.isEmpty || nextTabs.length != nextPages.length) { + return; + } + + final desiredIndex = + _tabController?.index ?? strategy.getHomeInitialTabIndex(); + final initialIndex = desiredIndex.clamp(0, nextPages.length - 1); + + _tabController?.dispose(); + _pages + ..clear() + ..addAll(nextPages); + _tabs + ..clear() + ..addAll(nextTabs); _tabController = TabController( length: _pages.length, vsync: this, - initialIndex: strategy.getHomeInitialTabIndex(), + initialIndex: initialIndex, ); - // 可选:监听切换事件 - _tabController?.addListener(() {}); - - // 数据更新完成后,刷新界面 - if (mounted) { - setState(() { - refresh = true; - }); - } } @override @@ -63,8 +71,7 @@ class _SCIndexHomePageState extends State @override Widget build(BuildContext context) { - // 如果数据未准备好,显示加载指示器 - if (!refresh || _tabController == null || _pages.isEmpty) { + if (_tabController == null || _pages.isEmpty || _tabs.isEmpty) { return Center( child: Container( decoration: BoxDecoration( @@ -78,11 +85,6 @@ class _SCIndexHomePageState extends State ); } - // 正常渲染界面 - _tabs.clear(); - final strategy = SCGlobalConfig.businessLogicStrategy; - _tabs.addAll(strategy.getHomeTabLabels(context)); - return Stack( children: [ Scaffold( @@ -100,7 +102,7 @@ class _SCIndexHomePageState extends State tabAlignment: TabAlignment.start, isScrollable: true, splashFactory: NoSplash.splashFactory, - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( Colors.transparent, ), indicator: const BoxDecoration(), @@ -108,12 +110,12 @@ class _SCIndexHomePageState extends State fontWeight: FontWeight.bold, fontStyle: FontStyle.italic, fontSize: 19.sp, - color:SocialChatTheme.primaryLight, + color: SocialChatTheme.primaryLight, ), unselectedLabelStyle: TextStyle( fontWeight: FontWeight.normal, fontSize: 14.sp, - color: Colors.white + color: Colors.white, ), indicatorColor: Colors.transparent, dividerColor: Colors.transparent, diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart index 3d4fc56..28248ab 100644 --- a/lib/modules/index/index_page.dart +++ b/lib/modules/index/index_page.dart @@ -1,267 +1,288 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; -import 'package:yumi/modules/home/index_home_page.dart'; -import 'package:yumi/modules/chat/message/sc_message_page.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/services/audio/rtm_manager.dart'; -import 'package:provider/provider.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/services/general/sc_app_general_manager.dart'; -import 'package:yumi/services/auth/user_profile_manager.dart'; -import '../../shared/tools/sc_heartbeat_utils.dart'; -import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; -import '../../ui_kit/components/sc_float_ichart.dart'; -import '../home/popular/event/home_event_page.dart'; -import '../user/me_page2.dart'; - -/** - * 首页 - */ -class SCIndexPage extends StatefulWidget { - @override - _SCIndexPageState createState() => _SCIndexPageState(); -} - -class _SCIndexPageState extends State { - int _currentIndex = 0; - final List _pages = []; - final List _bottomItems = []; - SCAppGeneralManager? generalProvider; - - @override - void initState() { - super.initState(); - generalProvider = Provider.of(context, listen: false); - Provider.of( - context, - listen: false, - ).initializeRealTimeCommunicationManager(context); - Provider.of(context, listen: false).init(context); - Provider.of( - context, - listen: false, - ).getUserIdentity(); - Provider.of(context, listen: false).balance(); - SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); - DataPersistence.setLang(SCGlobalConfig.lang); - OverlayManager().activate(); - WakelockPlus.enable(); - String roomId = DataPersistence.getLastTimeRoomId(); - if (roomId.isNotEmpty) { - SCAccountRepository().quitRoom(roomId).catchError((e) { - return true; // 错误已处理 - }); - } - } - - @override - void dispose() { - WakelockPlus.disable(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - SCFloatIchart().init(context); - _initWidget(); - return WillPopScope( - onWillPop: _doubleExit, - child: Stack( - children: [ - Image.asset( - "sc_images/index/sc_icon_index_bg.png", - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - fit: BoxFit.fill, - ), - SafeArea( - top: false, - child: Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - body: _pages[_currentIndex], - bottomNavigationBar: Container( - height: 85.w, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/index/sc_index_bottom_navigation_bar_bg.png", - ), - fit: BoxFit.fill, - ), - ), - child: Theme( - data: Theme.of(context).copyWith( - splashFactory: NoSplash.splashFactory, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - ), - child: BottomNavigationBar( - elevation: 0, - enableFeedback: false, - backgroundColor: Colors.transparent, - selectedLabelStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14.sp, - ), - unselectedLabelStyle: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 13.sp, - ), - type: BottomNavigationBarType.fixed, - selectedItemColor: Color(0xffBF854A), - unselectedItemColor: Color(0xffC4C4C4), - showUnselectedLabels: true, - showSelectedLabels: true, - items: _bottomItems, - currentIndex: _currentIndex, - onTap: (index) => setState(() => _currentIndex = index), - ), - ), - ), - ), - ), - ], - ), - ); - } - - ///双击退出 - int _lastClickTime = 0; - - Future _doubleExit() { - int nowTime = DateTime.now().microsecondsSinceEpoch; - if (_lastClickTime != 0 && nowTime - _lastClickTime > 1500) { - return Future.value(true); - } else { - _lastClickTime = DateTime.now().microsecondsSinceEpoch; - Future.delayed(const Duration(milliseconds: 1500), () { - _lastClickTime = 0; - }); - return Future.value(false); - } - } - - void _initWidget() { - _pages.clear(); - _bottomItems.clear(); - _pages.add(SCIndexHomePage()); - _pages.add(HomeEventPage()); - // _pages.add(IndexDynamicPage()); - _pages.add(SCMessagePage()); - _pages.add(MePage2()); - - _bottomItems.add( - BottomNavigationBarItem( - icon: Image.asset( - "sc_images/index/sc_icon_home_no.png", - width: 35.w, - height: 35.w, - ), - activeIcon: Image.asset( - "sc_images/index/sc_icon_home_en.png", - width: 35.w, - height: 35.w, - ), - label: SCAppLocalizations.of(context)!.home, - ), - ); - _bottomItems.add( - BottomNavigationBarItem( - icon: Image.asset( - "sc_images/index/sc_icon_home_no.png", - width: 35.w, - height: 35.w, - ), - activeIcon: Image.asset( - "sc_images/index/sc_icon_home_en.png", - width: 35.w, - height: 35.w, - ), - label: SCAppLocalizations.of(context)!.explore, - ), - ); - - _bottomItems.add( - BottomNavigationBarItem( - icon: Selector( - selector: (c, p) => p.allUnReadCount, - shouldRebuild: (prev, next) => prev != next, - builder: (_, allUnReadCount, __) { - return allUnReadCount > 0 - ? Badge( - backgroundColor: Colors.red, - label: text( - "${allUnReadCount > 99 ? "99+" : allUnReadCount}", - fontSize: 9.sp, - textColor: Colors.white, - fontWeight: FontWeight.w600, - ), - alignment: AlignmentDirectional.topEnd, - child: Image.asset( - "sc_images/index/sc_icon_message_no.png", - width: 35.w, - height: 35.w, - ), - ) - : Image.asset( - "sc_images/index/sc_icon_message_no.png", - width: 35.w, - height: 35.w, - ); - }, - ), - activeIcon: Selector( - selector: (c, p) => p.allUnReadCount, - shouldRebuild: (prev, next) => prev != next, - builder: (_, allUnReadCount, __) { - return allUnReadCount > 0 - ? Badge( - backgroundColor: Colors.red, - label: text( - "${allUnReadCount > 99 ? "99+" : allUnReadCount}", - fontSize: 9.sp, - textColor: Colors.white, - fontWeight: FontWeight.w600, - ), - alignment: AlignmentDirectional.topEnd, - child: Image.asset( - "sc_images/index/sc_icon_message_en.png", - width: 35.w, - height: 35.w, - ), - ) - : Image.asset( - "sc_images/index/sc_icon_message_en.png", - width: 35.w, - height: 35.w, - ); - }, - ), - label: SCAppLocalizations.of(context)!.message, - ), - ); - _bottomItems.add( - BottomNavigationBarItem( - icon: Image.asset( - "sc_images/index/sc_icon_me_no.png", - width: 35.w, - height: 35.w, - ), - activeIcon: Image.asset( - "sc_images/index/sc_icon_me_en.png", - width: 35.w, - height: 35.w, - ), - label: SCAppLocalizations.of(context)!.me, - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; +import 'package:yumi/modules/home/index_home_page.dart'; +import 'package:yumi/modules/chat/message/sc_message_page.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import '../../shared/tools/sc_heartbeat_utils.dart'; +import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; +import '../../ui_kit/components/sc_float_ichart.dart'; +import '../home/popular/event/home_event_page.dart'; +import '../user/me_page2.dart'; + +/** + * 首页 + */ +class SCIndexPage extends StatefulWidget { + const SCIndexPage({super.key}); + + @override + State createState() => _SCIndexPageState(); +} + +class _SCIndexPageState extends State { + int _currentIndex = 0; + final List _pages = []; + final List _bottomItems = []; + SCAppGeneralManager? generalProvider; + Locale? _lastLocale; + + @override + void initState() { + super.initState(); + _initializePages(); + generalProvider = Provider.of(context, listen: false); + Provider.of( + context, + listen: false, + ).initializeRealTimeCommunicationManager(context); + Provider.of(context, listen: false).init(context); + Provider.of( + context, + listen: false, + ).getUserIdentity(); + Provider.of(context, listen: false).balance(); + SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); + DataPersistence.setLang(SCGlobalConfig.lang); + OverlayManager().activate(); + WidgetsBinding.instance.addPostFrameCallback((_) { + WakelockPlus.enable(); + }); + String roomId = DataPersistence.getLastTimeRoomId(); + if (roomId.isNotEmpty) { + SCAccountRepository().quitRoom(roomId).catchError((e) { + return true; // 错误已处理 + }); + } + } + + @override + void dispose() { + WakelockPlus.disable(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + SCFloatIchart().init(context); + _rebuildBottomItemsIfNeeded(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: _doubleExit, + child: Stack( + children: [ + Image.asset( + "sc_images/index/sc_icon_index_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + SafeArea( + top: false, + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: _pages[_currentIndex], + bottomNavigationBar: Container( + height: 85.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_index_bottom_navigation_bar_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Theme( + data: Theme.of(context).copyWith( + splashFactory: NoSplash.splashFactory, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + ), + child: BottomNavigationBar( + elevation: 0, + enableFeedback: false, + backgroundColor: Colors.transparent, + selectedLabelStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14.sp, + ), + unselectedLabelStyle: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 13.sp, + ), + type: BottomNavigationBarType.fixed, + selectedItemColor: Color(0xffBF854A), + unselectedItemColor: Color(0xffC4C4C4), + showUnselectedLabels: true, + showSelectedLabels: true, + items: _bottomItems, + currentIndex: _currentIndex, + onTap: (index) => setState(() => _currentIndex = index), + ), + ), + ), + ), + ), + ], + ), + ); + } + + ///双击退出 + int _lastClickTime = 0; + + Future _doubleExit() { + int nowTime = DateTime.now().microsecondsSinceEpoch; + if (_lastClickTime != 0 && nowTime - _lastClickTime > 1500) { + return Future.value(true); + } else { + _lastClickTime = DateTime.now().microsecondsSinceEpoch; + Future.delayed(const Duration(milliseconds: 1500), () { + _lastClickTime = 0; + }); + return Future.value(false); + } + } + + void _initializePages() { + if (_pages.isNotEmpty) { + return; + } + + _pages.add(SCIndexHomePage()); + _pages.add(HomeEventPage()); + _pages.add(SCMessagePage()); + _pages.add(MePage2()); + } + + void _rebuildBottomItemsIfNeeded() { + final locale = Localizations.localeOf(context); + if (_lastLocale == locale && _bottomItems.isNotEmpty) { + return; + } + _lastLocale = locale; + _bottomItems.clear(); + + _bottomItems.add( + BottomNavigationBarItem( + icon: Image.asset( + "sc_images/index/sc_icon_home_no.png", + width: 35.w, + height: 35.w, + ), + activeIcon: Image.asset( + "sc_images/index/sc_icon_home_en.png", + width: 35.w, + height: 35.w, + ), + label: SCAppLocalizations.of(context)!.home, + ), + ); + _bottomItems.add( + BottomNavigationBarItem( + icon: Image.asset( + "sc_images/index/sc_icon_home_no.png", + width: 35.w, + height: 35.w, + ), + activeIcon: Image.asset( + "sc_images/index/sc_icon_home_en.png", + width: 35.w, + height: 35.w, + ), + label: SCAppLocalizations.of(context)!.explore, + ), + ); + + _bottomItems.add( + BottomNavigationBarItem( + icon: Selector( + selector: (c, p) => p.allUnReadCount, + shouldRebuild: (prev, next) => prev != next, + builder: (_, allUnReadCount, __) { + return allUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${allUnReadCount > 99 ? "99+" : allUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Image.asset( + "sc_images/index/sc_icon_message_no.png", + width: 35.w, + height: 35.w, + ), + ) + : Image.asset( + "sc_images/index/sc_icon_message_no.png", + width: 35.w, + height: 35.w, + ); + }, + ), + activeIcon: Selector( + selector: (c, p) => p.allUnReadCount, + shouldRebuild: (prev, next) => prev != next, + builder: (_, allUnReadCount, __) { + return allUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${allUnReadCount > 99 ? "99+" : allUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Image.asset( + "sc_images/index/sc_icon_message_en.png", + width: 35.w, + height: 35.w, + ), + ) + : Image.asset( + "sc_images/index/sc_icon_message_en.png", + width: 35.w, + height: 35.w, + ); + }, + ), + label: SCAppLocalizations.of(context)!.message, + ), + ); + _bottomItems.add( + BottomNavigationBarItem( + icon: Image.asset( + "sc_images/index/sc_icon_me_no.png", + width: 35.w, + height: 35.w, + ), + activeIcon: Image.asset( + "sc_images/index/sc_icon_me_en.png", + width: 35.w, + height: 35.w, + ), + label: SCAppLocalizations.of(context)!.me, + ), + ); + } +} diff --git a/lib/modules/splash/splash_page.dart b/lib/modules/splash/splash_page.dart index 75c1380..4e94464 100644 --- a/lib/modules/splash/splash_page.dart +++ b/lib/modules/splash/splash_page.dart @@ -1,121 +1,96 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/tools/sc_version_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/app/routes/sc_routes.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart'; -import 'package:yumi/modules/auth/login_route.dart'; - -class SplashPage extends StatefulWidget { - @override - _SplashPageState createState() => _SplashPageState(); -} - -class _SplashPageState extends State { - Timer? _timer; - int countdown = 6; - int status = 0; - SCStartPageRes? currentStartPage; - - List startPages = []; - - @override - void initState() { - super.initState(); - FileCacheManager.getInstance().getFilePath(); - getStartPage(); - _startTimer(context); - } - - ///加载启动页配置 - void getStartPage() { - SCConfigRepositoryImp().getStartPage().then((result) { - startPages = result; - if (startPages.length > 1) { - int index = Random().nextInt(startPages.length); - currentStartPage = result[index]; - } else if (startPages.isNotEmpty) { - currentStartPage = result.first; - } else { - currentStartPage = null; - } +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_version_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/modules/auth/login_route.dart'; - status = 1; +class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends State { + static const Duration _minimumSplashDuration = Duration(milliseconds: 900); + + Timer? _timer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(FileCacheManager.getInstance().getFilePath()); + }); + _startNavigationTimer(); + } + + @override + void dispose() { + _cancelTimer(); + super.dispose(); + } + + /// 启动页最短展示时间 + void _startNavigationTimer() { + _timer = Timer(_minimumSplashDuration, () { if (mounted) { - setState(() {}); + _goMainPage(); } }); } - - @override - void dispose() { - _cancelTimer(); - super.dispose(); - } - - /// 启动倒计时 - void _startTimer(BuildContext context) { - // 计时器(`Timer`)组件的定期(`periodic`)构造函数,创建一个新的重复计时器。 - _timer = Timer.periodic(Duration(seconds: 1), (timer) { - if (countdown == 0) { - _cancelTimer(); - _goMainPage(context); - return; - } - countdown--; - setState(() {}); - }); - } - - @override - Widget build(BuildContext context) { - return Material( - child: Stack( - alignment: Alignment.center, - children: [ - Image.asset( - SCGlobalConfig.businessLogicStrategy.getSplashPageBackgroundImage(), - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - fit: BoxFit.cover, - ), - Image.asset( - SCGlobalConfig.businessLogicStrategy.getSplashPageIcon(), - width: 107.w, - height: 159.w, - ), - ], - ), - ); - } - - void _goMainPage(BuildContext context) async { - try { - await SCVersionUtils.checkReview(); - var user = AccountStorage().getCurrentUser(); - var token = AccountStorage().getToken(); - if (user != null && token.isNotEmpty) { - SCNavigatorUtils.push(context, SCRoutes.home, replace: true); - } else { - SCNavigatorUtils.push(context, LoginRouter.login, replace: true); - } - } catch (e) { - SCNavigatorUtils.push(context, LoginRouter.login, replace: true); - } - } - - /// 取消倒计时的计时器。 - void _cancelTimer() { - // 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。 - _timer?.cancel(); - } -} - - + + @override + Widget build(BuildContext context) { + return Material( + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getSplashPageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy.getSplashPageIcon(), + width: 107.w, + height: 159.w, + ), + ], + ), + ); + } + + void _goMainPage() async { + try { + await SCVersionUtils.checkReview(); + if (!mounted) { + return; + } + var user = AccountStorage().getCurrentUser(); + var token = AccountStorage().getToken(); + if (user != null && token.isNotEmpty) { + SCNavigatorUtils.push(context, SCRoutes.home, replace: true); + } else { + SCNavigatorUtils.push(context, LoginRouter.login, replace: true); + } + } catch (e) { + if (!mounted) { + return; + } + SCNavigatorUtils.push(context, LoginRouter.login, replace: true); + } + } + + /// 取消倒计时的计时器。 + void _cancelTimer() { + // 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。 + _timer?.cancel(); + } +} diff --git a/lib/services/localization/localization_manager.dart b/lib/services/localization/localization_manager.dart index 18e965c..0c4b173 100644 --- a/lib/services/localization/localization_manager.dart +++ b/lib/services/localization/localization_manager.dart @@ -1,54 +1,59 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; - -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; - -class LocalizationManager with ChangeNotifier { - Locale? _locale; - - Locale? get locale => _locale; - - LocalizationManager() { - initializeLanguageSettings(); - } - - initializeLanguageSettings() { - String language = DataPersistence.getLang(); - if (language.isEmpty) { - Locale systemLocale = ui.window.locale; - if (systemLocale.languageCode.startsWith("ar")) { - _locale = Locale('ar', ''); - } else if (systemLocale.languageCode == "en") { - _locale = Locale('en', ''); - } else if (systemLocale.languageCode == "tr") { - _locale = Locale('tr', ''); - } else if (systemLocale.languageCode == "bn") { - _locale = Locale('bn', ''); - } else { - _locale = Locale('en', ''); - } - } else { - if (language.startsWith("ar")) { - _locale = Locale('ar', ''); - } else if (language == "tr") { - _locale = Locale('tr', ''); - } else if (language == "bn") { - _locale = Locale('bn', ''); - } else { - _locale = Locale('en', ''); - } - } - if (_locale != null) { - changeAppLanguage(_locale!); - } - } - - void changeAppLanguage(Locale locale) { - _locale = locale; - SCGlobalConfig.lang = locale.languageCode; - DataPersistence.setLang(locale.languageCode); - notifyListeners(); - } -} +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; + +class LocalizationManager with ChangeNotifier { + Locale? _locale; + + Locale? get locale => _locale; + + LocalizationManager() { + _locale = _resolveInitialLocale(); + if (_locale != null) { + SCGlobalConfig.lang = _locale!.languageCode; + } + } + + Locale _resolveInitialLocale() { + String language = DataPersistence.getLang(); + if (language.isEmpty) { + Locale systemLocale = ui.PlatformDispatcher.instance.locale; + if (systemLocale.languageCode.startsWith("ar")) { + return const Locale('ar', ''); + } else if (systemLocale.languageCode == "en") { + return const Locale('en', ''); + } else if (systemLocale.languageCode == "tr") { + return const Locale('tr', ''); + } else if (systemLocale.languageCode == "bn") { + return const Locale('bn', ''); + } else { + return const Locale('en', ''); + } + } else { + if (language.startsWith("ar")) { + return const Locale('ar', ''); + } else if (language == "tr") { + return const Locale('tr', ''); + } else if (language == "bn") { + return const Locale('bn', ''); + } else { + return const Locale('en', ''); + } + } + } + + void changeAppLanguage(Locale locale) { + if (_locale?.languageCode == locale.languageCode && + _locale?.countryCode == locale.countryCode) { + return; + } + _locale = locale; + SCGlobalConfig.lang = locale.languageCode; + unawaited(DataPersistence.setLang(locale.languageCode)); + notifyListeners(); + } +} diff --git a/lib/shared/tools/sc_google_auth_utils.dart b/lib/shared/tools/sc_google_auth_utils.dart index 5e23b23..b266925 100644 --- a/lib/shared/tools/sc_google_auth_utils.dart +++ b/lib/shared/tools/sc_google_auth_utils.dart @@ -1,101 +1,108 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:google_sign_in/google_sign_in.dart'; - -///谷歌授权登录 -class SCGoogleAuthService { - static final GoogleSignIn _g = GoogleSignIn(scopes: ['email', 'profile'],); - - // 获取当前用户 - static User? get currentUser => FirebaseAuth.instance.currentUser; - - // 检查登录状态 - static bool get isSignedIn => currentUser != null; - - // 初始化Firebase - static Future initialize() async { - await Firebase.initializeApp(); - } - - // 谷歌登录 - static Future signInWithGoogle() async { - try { - // 触发谷歌登录 - final GoogleSignInAccount? googleAccount = await _g.signIn(); - if (googleAccount == null) return null; - - // 获取认证信息 - final GoogleSignInAuthentication googleAuth = await googleAccount.authentication; - - // 创建Firebase凭证 - final OAuthCredential credential = GoogleAuthProvider.credential( - accessToken: googleAuth.accessToken, - idToken: googleAuth.idToken, - ); - - // 使用凭证登录Firebase - final UserCredential userCredential = - await FirebaseAuth.instance.signInWithCredential(credential); - - return userCredential.user; - } catch (e) { - print('Google sign-in error: $e'); - rethrow; // 抛出异常让调用者处理 - } - } - - // 静默登录(检查现有会话) - static Future signInSilently() async { - try { - // 尝试静默登录 - final GoogleSignInAccount? googleAccount = - await _g.signInSilently(); - - if (googleAccount == null) return null; - - // 获取认证信息 - final GoogleSignInAuthentication googleAuth = - await googleAccount.authentication; - - // 创建Firebase凭证 - final OAuthCredential credential = GoogleAuthProvider.credential( - accessToken: googleAuth.accessToken, - idToken: googleAuth.idToken, - ); - - // 使用凭证登录Firebase - final UserCredential userCredential = - await FirebaseAuth.instance.signInWithCredential(credential); - - return userCredential.user; - } catch (e) { - print('Silent sign-in error: $e'); - return null; - } - } - - // 登出 - static Future signOut() async { - try { - await _g.signOut(); - await FirebaseAuth.instance.signOut(); - } catch (e) { - print('Sign out error: $e'); - rethrow; - } - } - - // 获取用户信息 - static Map? getUserInfo() { - final user = currentUser; - if (user == null) return null; - - return { - 'uid': user.uid, - 'name': user.displayName, - 'email': user.email, - 'photoUrl': user.photoURL, - 'isEmailVerified': user.emailVerified, - }; - } -} \ No newline at end of file +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +///谷歌授权登录 +class SCGoogleAuthService { + static final GoogleSignIn _g = GoogleSignIn(scopes: ['email', 'profile']); + + // 获取当前用户 + static User? get currentUser => FirebaseAuth.instance.currentUser; + + // 检查登录状态 + static bool get isSignedIn => currentUser != null; + + // 初始化Firebase + static Future initialize() async { + if (Firebase.apps.isNotEmpty) { + return; + } + await Firebase.initializeApp(); + } + + // 谷歌登录 + static Future signInWithGoogle() async { + try { + await initialize(); + // 触发谷歌登录 + final GoogleSignInAccount? googleAccount = await _g.signIn(); + if (googleAccount == null) return null; + + // 获取认证信息 + final GoogleSignInAuthentication googleAuth = + await googleAccount.authentication; + + // 创建Firebase凭证 + final OAuthCredential credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + // 使用凭证登录Firebase + final UserCredential userCredential = await FirebaseAuth.instance + .signInWithCredential(credential); + + return userCredential.user; + } catch (e) { + debugPrint('Google sign-in error: $e'); + rethrow; // 抛出异常让调用者处理 + } + } + + // 静默登录(检查现有会话) + static Future signInSilently() async { + try { + await initialize(); + // 尝试静默登录 + final GoogleSignInAccount? googleAccount = await _g.signInSilently(); + + if (googleAccount == null) return null; + + // 获取认证信息 + final GoogleSignInAuthentication googleAuth = + await googleAccount.authentication; + + // 创建Firebase凭证 + final OAuthCredential credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + // 使用凭证登录Firebase + final UserCredential userCredential = await FirebaseAuth.instance + .signInWithCredential(credential); + + return userCredential.user; + } catch (e) { + debugPrint('Silent sign-in error: $e'); + return null; + } + } + + // 登出 + static Future signOut() async { + try { + await initialize(); + await _g.signOut(); + await FirebaseAuth.instance.signOut(); + } catch (e) { + debugPrint('Sign out error: $e'); + rethrow; + } + } + + // 获取用户信息 + static Map? getUserInfo() { + final user = currentUser; + if (user == null) return null; + + return { + 'uid': user.uid, + 'name': user.displayName, + 'email': user.email, + 'photoUrl': user.photoURL, + 'isEmailVerified': user.emailVerified, + }; + } +} diff --git a/lib/ui_kit/components/custom_cached_image.dart b/lib/ui_kit/components/custom_cached_image.dart index d68308f..04052c1 100644 --- a/lib/ui_kit/components/custom_cached_image.dart +++ b/lib/ui_kit/components/custom_cached_image.dart @@ -1,53 +1,48 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class CustomCachedImage extends StatelessWidget { - final String imageUrl; - final double? width; - final double? height; - final BoxFit fit; - final double borderRadius; - final Widget? placeholder; - final Widget? errorWidget; - - const CustomCachedImage({ - super.key, - required this.imageUrl, - this.width, - this.height, - this.fit = BoxFit.cover, - this.borderRadius = 0, - this.placeholder, - this.errorWidget, - }); - - @override - Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(borderRadius), - child: CachedNetworkImage( - imageUrl: imageUrl, - width: width, - height: height, - fit: fit, - placeholder: (context, url) => - placeholder ?? _defaultPlaceholder(), - errorWidget: (context, url, error) => - errorWidget ?? _defaultErrorWidget(), - ), - ); - } - - Widget _defaultPlaceholder() { - return Center( - child: CupertinoActivityIndicator(), - ); - } - - Widget _defaultErrorWidget() { - return Center( - child: Image.asset("sc_images/general/sc_icon_loading.png"), - ); - } -} \ No newline at end of file +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart'; + +class CustomCachedImage extends StatelessWidget { + final String imageUrl; + final double? width; + final double? height; + final BoxFit fit; + final double borderRadius; + final Widget? placeholder; + final Widget? errorWidget; + + const CustomCachedImage({ + super.key, + required this.imageUrl, + this.width, + this.height, + this.fit = BoxFit.cover, + this.borderRadius = 0, + this.placeholder, + this.errorWidget, + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: CachedNetworkImage( + imageUrl: imageUrl, + width: width, + height: height, + fit: fit, + placeholder: (context, url) => placeholder ?? _defaultPlaceholder(), + errorWidget: + (context, url, error) => errorWidget ?? _defaultErrorWidget(), + ), + ); + } + + Widget _defaultPlaceholder() { + return const SCRotatingDotsLoading(); + } + + Widget _defaultErrorWidget() { + return Center(child: Image.asset("sc_images/general/sc_icon_loading.png")); + } +} diff --git a/lib/ui_kit/components/sc_compontent.dart b/lib/ui_kit/components/sc_compontent.dart index fee53a7..1450ecf 100644 --- a/lib/ui_kit/components/sc_compontent.dart +++ b/lib/ui_kit/components/sc_compontent.dart @@ -12,6 +12,7 @@ import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_profile_cache.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; +import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart'; import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; @@ -215,12 +216,7 @@ Widget netImage({ if (loadingWidget != null) { return loadingWidget; } - return noDefaultImg - ? Container() - : Image.asset( - defaultImg ?? "sc_images/general/sc_icon_loading.png", - fit: BoxFit.cover, - ); + return noDefaultImg ? Container() : const SCRotatingDotsLoading(); } }, ); @@ -354,7 +350,7 @@ searchWidget({ height: width(36), //width: width(345), decoration: BoxDecoration( - color: Color(0xff18F2B1).withOpacity(0.1), + color: const Color(0xff18F2B1).withValues(alpha: 0.1), borderRadius: BorderRadius.all(Radius.circular(height(20))), border: Border.all(color: borderColor ?? Colors.black12, width: 1.w), ), diff --git a/lib/ui_kit/components/sc_rotating_dots_loading.dart b/lib/ui_kit/components/sc_rotating_dots_loading.dart new file mode 100644 index 0000000..9f1b800 --- /dev/null +++ b/lib/ui_kit/components/sc_rotating_dots_loading.dart @@ -0,0 +1,128 @@ +import 'dart:math' as math; +import 'dart:ui' show lerpDouble; + +import 'package:flutter/material.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +class SCRotatingDotsLoading extends StatefulWidget { + const SCRotatingDotsLoading({ + super.key, + this.size, + this.color = SocialChatTheme.primaryLight, + this.dotCount = 8, + this.duration = const Duration(milliseconds: 900), + this.minAdaptiveSize = 14, + this.maxAdaptiveSize = 40, + }); + + final double? size; + final Color color; + final int dotCount; + final Duration duration; + final double minAdaptiveSize; + final double maxAdaptiveSize; + + @override + State createState() => _SCRotatingDotsLoadingState(); +} + +class _SCRotatingDotsLoadingState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: widget.duration) + ..repeat(); + } + + @override + void didUpdateWidget(covariant SCRotatingDotsLoading oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.duration != widget.duration) { + _controller + ..duration = widget.duration + ..repeat(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final configuredSize = widget.size; + if (configuredSize != null) { + return _buildIndicator(configuredSize); + } + return LayoutBuilder( + builder: (context, constraints) { + final adaptiveSize = _resolveAdaptiveSize(constraints); + return Center(child: _buildIndicator(adaptiveSize)); + }, + ); + } + + Widget _buildIndicator(double size) { + final normalizedSize = size.clamp(8.0, 200.0); + final dotCount = math.max(1, widget.dotCount); + final maxDotSize = normalizedSize * 0.26; + final radius = normalizedSize / 2 - maxDotSize / 2; + + return RepaintBoundary( + child: SizedBox.square( + dimension: normalizedSize, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.rotate( + angle: _controller.value * math.pi * 2, + child: Stack( + children: List.generate(dotCount, (index) { + final progress = 1 - (index / dotCount); + final dotScale = lerpDouble(0.42, 1, progress)!; + final opacity = lerpDouble(0.18, 1, progress)!; + final dotSize = maxDotSize * dotScale; + final angle = (math.pi * 2 * index / dotCount) - math.pi / 2; + final dx = normalizedSize / 2 + math.cos(angle) * radius; + final dy = normalizedSize / 2 + math.sin(angle) * radius; + + return Positioned( + left: dx - dotSize / 2, + top: dy - dotSize / 2, + child: Container( + width: dotSize, + height: dotSize, + decoration: BoxDecoration( + color: widget.color.withValues(alpha: opacity), + shape: BoxShape.circle, + ), + ), + ); + }), + ), + ); + }, + ), + ), + ); + } + + double _resolveAdaptiveSize(BoxConstraints constraints) { + final values = + [ + constraints.maxWidth, + constraints.maxHeight, + ].where((value) => value.isFinite && value > 0).toList(); + final shortest = values.isEmpty ? 28.0 : values.reduce(math.min); + final scaled = shortest * 0.52; + return math.max( + widget.minAdaptiveSize, + math.min(widget.maxAdaptiveSize, scaled), + ); + } +} diff --git a/需求进度.md b/需求进度.md index 9705d9d..da9ae13 100644 --- a/需求进度.md +++ b/需求进度.md @@ -3,6 +3,15 @@ ## 当前总目标 - 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 +## 本轮启动优化(非网络) +- 已移除启动页固定 `6` 秒倒计时,改为最短约 `900ms` 展示后继续路由判断,避免人为等待直接拉长入 app 时间。 +- 已将 Firebase / Crashlytics 从 `runApp` 前阻塞初始化改为首帧后后台预热,并补充“未就绪时仅本地打印”的异常兜底,减少首屏前平台初始化阻塞。 +- 已将 `SocialChatAuthenticationManager`、`SocialChatUserProfileManager` 改为按需创建,避免 splash 阶段就提前触发 Google Sign-In / Firebase 会话检查。 +- 已将 deep link、设备信息、缓存目录创建等非首帧必需动作延后到首帧后执行,降低首屏竞争。 +- 已优化本地语言初始化:启动时不再重复写入 `SharedPreferences` 并触发一次无意义的 `notifyListeners`。 +- 已优化首页容器与 Home 首屏:去掉首次进入时“先 loading 再建 tab”的本地跳变,并将首页 page 列表、底部导航项从 `build` 阶段的重复重建改为复用。 +- 本轮按需求暂未处理网络链路上的启动等待,例如审核态检查或远端启动页配置请求。 + ## 已完成模块 - 已为语言房页面补充右下方 `game` 悬浮入口:入口位于聊天区右下侧、距右侧约 `15.w`、位于底部操作栏上方约 `100.w`,当前先使用代码绘制的 `🎮` 占位图标,并已在实现处标注后续替换为正式 UI 图片资源。 - 已新增语言房游戏底部弹窗:点击 `game` 入口后会从底部弹出约三分之一屏高的面板,采用房间页现有半透明磨砂风格,便于后续继续沿用当前视觉体系。 @@ -25,6 +34,7 @@ - 已为首页 Party 页补齐首屏骨架屏:顶部 H5 榜单现按三列奖杯卡位展示头像/标题骨架,下方房间区改为双列房卡骨架,并采用深绿底 + 金色描边的项目内风格;同时拆分榜单与房间列表加载态,仅在“首屏无内容加载”时展示骨架,避免下拉刷新时整页闪烁。 - 已为首页 My 板块补齐首屏骨架屏:`My room` 顶部房间卡片改为独立加载态,避免首屏把“正在加载我的房间”误显示成“去创建房间”;`Recent/Followed` 两个子页也已改成同风格双列房卡骨架,进入 tab 时会先展示完整结构占位,再按真实数据切换。 - 已为 `Me` 入口下的 `Store / Bag` 页面补齐首屏骨架屏:四类 `store` 商品页和四类 `bag` 物品页在“首屏空数据加载”阶段都会先展示同风格的三列卡片骨架,不再只显示小菊花;同时已为商品/物品名称补齐宽度约束与省略号处理,避免超长文案顶出卡片。 +- 已将通用图片加载占位从静态 `loading` 图片升级为代码绘制的环形转点组件:礼物二级页单卡封面、`Store / Bag` 商品图以及复用共享图片组件的网络图,在“加载中”阶段都会显示动态转圈效果;而图片失败或 `no data` 时仍保持原有占位图,不混淆加载态与空态。 - 已继续细化首页房卡体验:`My` 板块房卡骨架数量已从 `4` 个补齐到 `6` 个,与首页 Party 首屏保持一致;同时将房卡右下角原本的静态绿色“直播中”图片替换为代码驱动的三段式动态音频条,首页、我的、搜索结果和房间浮窗现都会展示跳动效果,不再依赖静态资源图;最新已把动效调整为“单周期完整峰谷 + A/B/C 错峰起伏”的连续波形,观感更接近直播音频电平。 - 已继续微调首页房卡信息密度:房卡底部房名与在线人数字号已统一调小,和动态音频条更协调;同时已移除 `My` 板块顶部 `my room` 标题及其占位,让房间卡片整体上移,对齐更紧凑。 - 已继续微调首页视觉细节:房卡底栏房名再次缩小并轻微上移,和国旗、动态音频条、人数的中线更一致;首页顶部排行骨架也已简化为“3 个圆形头像 + 1 根文本条”,更贴近真实卡片结构。 @@ -165,6 +175,7 @@ - 首页 Party 页当前已接入贴合真实版式的骨架屏:进入首页首屏时会先看到三张排行卡位与双列房卡占位,骨架配色已跟随页面深绿+鎏金主视觉,不再只显示居中的白色 `loading`;且排行榜与房间区会按各自请求结果独立切换,先返回的区域会先显示真实内容。 - 首页 My 板块当前已接入贴合真实版式的骨架屏:顶部 `My room` 现区分“首次加载中”和“确实未创建房间”,不会再先闪出创建房间卡;`Recent/Followed` 在首屏空数据加载时也会先展示双列房卡骨架,不再只显示小菊花或局部空白。 - `Me` 入口下的 `Store / Bag` 页面当前也已接入深绿主题的三列卡片骨架:首次进入并且列表尚未返回时,会先显示与正式卡片比例一致的占位;接口返回后再切到真实商品/物品卡片。`store` 与 `bag` 卡片标题现都带宽度约束和单行省略,类似截图里的长名称不会再横向撑破布局。 +- 图片加载态与空态当前已拆分:共享网络图组件和礼物二级页单卡封面加载时会显示代码绘制的环形转点,不再继续复用 `sc_icon_loading.png`;若图片失败或确实无数据,仍旧保留原先那张占位图,避免把 `loading` 和 `no data` 看成同一种状态。 - 首页与搜索等房卡右下角的在线状态图标当前已改为代码绘制的动态音频条:三根绿条会按不同相位走完整的波峰波谷周期,错峰连续起伏,视觉上更接近直播/语聊房的实时状态;`My` 板块骨架数量也已与 Party 页对齐为 `6` 个。 - 首页房卡底部信息当前已进一步收紧:房名和人数文字比上一版更小,底栏与动态音频条的视觉重心更一致;`My` 页顶部 `my room` 标题也已移除,下方卡片和 tab 会自然上移填充。 - 首页顶部排行骨架当前已进一步收窄为单条标题占位,不再保留两条文本骨架;房卡底栏房名也已再次缩一号并做轻微上移,对齐更贴近设计稿观感。