图片加载中占位符更改以及app启动初始帧提前
This commit is contained in:
parent
456642ff04
commit
553c37c743
162
lib/main.dart
162
lib/main.dart
@ -43,13 +43,12 @@ 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 {
|
||||
@ -57,24 +56,24 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// 性能优化:启用手势重采样
|
||||
GestureBinding.instance.resamplingEnabled = true;
|
||||
// 阶段1:初始化绝对必要的服务(不阻塞UI)
|
||||
await _setupA();
|
||||
// 使用重试机制初始化存储
|
||||
await _initStore();
|
||||
// 仅保留首帧前真正必要的初始化
|
||||
await _prepareAppShell();
|
||||
_installErrorHandlers();
|
||||
// 立即运行应用,不等待非必要服务初始化
|
||||
runApp(const RootAppWithProviders());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_warmUpDeferredServices());
|
||||
});
|
||||
},
|
||||
// 9. runZonedGuarded 的错误回调(这是捕获异步错误的主要方式)
|
||||
(error, stackTrace) {
|
||||
// 这是捕获 runZonedGuarded 区域内所有未捕获异步错误的地方
|
||||
FirebaseCrashlytics.instance.recordError(error, stackTrace, fatal: true);
|
||||
debugPrint('Zoned Error: $error\nStack: $stackTrace');
|
||||
unawaited(_recordFatalError(error, stackTrace));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 设置核心服务(重命名自_initializeEssentialServices)
|
||||
Future<void> _setupA() async {
|
||||
/// 首帧前的必要初始化
|
||||
Future<void> _prepareAppShell() async {
|
||||
// 配置设备方向:仅竖屏
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
|
||||
@ -90,34 +89,70 @@ Future<void> _setupA() async {
|
||||
|
||||
// 键盘工具初始化
|
||||
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);
|
||||
// 使用重试机制初始化存储
|
||||
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<void> _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<void> _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<void> _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<void> _initStore() async {
|
||||
@ -161,13 +196,13 @@ class RootAppWithProviders extends StatelessWidget {
|
||||
/// 构建应用Provider列表 - 重新排序以优化性能
|
||||
List<SingleChildWidget> _buildP() {
|
||||
return [
|
||||
// 核心用户相关Provider - 必须立即初始化
|
||||
// 核心用户相关Provider - 按需初始化,避免阻塞首屏
|
||||
ChangeNotifierProvider<SocialChatAuthenticationManager>(
|
||||
lazy: false,
|
||||
lazy: true,
|
||||
create: (context) => SocialChatAuthenticationManager(),
|
||||
),
|
||||
ChangeNotifierProvider<SocialChatUserProfileManager>(
|
||||
lazy: false,
|
||||
lazy: true,
|
||||
create: (context) => SocialChatUserProfileManager(),
|
||||
),
|
||||
|
||||
@ -221,7 +256,6 @@ class RootAppWithProviders extends StatelessWidget {
|
||||
create: (context) => ShopManager(),
|
||||
),
|
||||
|
||||
|
||||
// 通用应用管理Provider - 懒加载
|
||||
ChangeNotifierProvider<SCAppGeneralManager>(
|
||||
lazy: true,
|
||||
@ -248,12 +282,11 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始化深层链接处理
|
||||
_initLink();
|
||||
// 延迟初始化路由和设备信息
|
||||
// 将非首帧必需的初始化统一延后到首帧后
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initRouter();
|
||||
SCDeviceIdUtils.initDeviceinfo();
|
||||
unawaited(_initLink());
|
||||
});
|
||||
}
|
||||
|
||||
@ -275,9 +308,7 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
|
||||
void _handleLink(Uri uri) {
|
||||
// 这是处理链接的核心路由逻辑
|
||||
print('App 根层收到链接: $uri');
|
||||
String path = uri.path;
|
||||
String id = uri.queryParameters['id'] ?? '';
|
||||
debugPrint('App 根层收到链接: $uri');
|
||||
}
|
||||
|
||||
void _initRouter() {
|
||||
@ -297,9 +328,7 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
Widget body;
|
||||
if (mode == LoadStatus.idle) {
|
||||
body = Text(
|
||||
SCAppLocalizations.of(context)!.pullToLoadMore,
|
||||
);
|
||||
body = Text(SCAppLocalizations.of(context)!.pullToLoadMore);
|
||||
} else if (mode == LoadStatus.loading) {
|
||||
body = CupertinoActivityIndicator(color: Colors.white24);
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
@ -307,16 +336,11 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
SCAppLocalizations.of(context)!.loadingFailedClickToRetry,
|
||||
);
|
||||
} else if (mode == LoadStatus.canLoading) {
|
||||
body = Text(
|
||||
SCAppLocalizations.of(context)!.releaseToLoadMore,
|
||||
);
|
||||
body = Text(SCAppLocalizations.of(context)!.releaseToLoadMore);
|
||||
} else {
|
||||
body = Text(
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xff999999),
|
||||
),
|
||||
style: TextStyle(fontSize: 12.sp, color: Color(0xff999999)),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
@ -327,7 +351,6 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildUI() {
|
||||
return Consumer<LocalizationManager>(
|
||||
builder: (context, localeProvider, child) {
|
||||
@ -343,29 +366,32 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark,
|
||||
child: RefreshConfiguration(
|
||||
headerBuilder: () => MaterialClassicHeader(color: SocialChatTheme.primaryColor),
|
||||
headerBuilder:
|
||||
() => MaterialClassicHeader(
|
||||
color: SocialChatTheme.primaryColor,
|
||||
),
|
||||
footerBuilder: _buildFooter(),
|
||||
child: MaterialApp(
|
||||
title: 'Yumi',
|
||||
locale: Provider.of<LocalizationManager>(context).locale,
|
||||
localizationsDelegates: [
|
||||
locale: localeProvider.locale,
|
||||
localizationsDelegates: const [
|
||||
SCAppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: [
|
||||
const Locale('en', ''),
|
||||
const Locale('zh', ''),
|
||||
const Locale('ar', ''),
|
||||
const Locale('tr', ''),
|
||||
const Locale('bn', ''),
|
||||
supportedLocales: const [
|
||||
Locale('en', ''),
|
||||
Locale('zh', ''),
|
||||
Locale('ar', ''),
|
||||
Locale('tr', ''),
|
||||
Locale('bn', ''),
|
||||
],
|
||||
navigatorKey: navigatorKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
onGenerateRoute: SCLkApplication.router.generator,
|
||||
theme: themeManager.currentTheme,
|
||||
home: SplashPage(),
|
||||
home: const SplashPage(),
|
||||
builder: FlutterSmartDialog.init(),
|
||||
navigatorObservers: [routeObserver],
|
||||
),
|
||||
|
||||
@ -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<GiftTabPage> createState() => _GiftTabPageState();
|
||||
}
|
||||
|
||||
class _GiftTabPageState extends State<GiftTabPage>
|
||||
@ -202,12 +208,8 @@ class _GiftTabPageState extends State<GiftTabPage>
|
||||
),
|
||||
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<GiftTabPage>
|
||||
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<GiftTabPage>
|
||||
}
|
||||
|
||||
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<GiftTabPage>
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
loadingWidget: _buildGiftCoverLoading(),
|
||||
errorWidget: _buildGiftCoverLoading(),
|
||||
errorWidget: _buildGiftCoverNoData(),
|
||||
),
|
||||
SizedBox(height: 5.w),
|
||||
Container(
|
||||
|
||||
@ -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<SCIndexHomePage> createState() => _SCIndexHomePageState();
|
||||
}
|
||||
|
||||
class _SCIndexHomePageState extends State<SCIndexHomePage>
|
||||
@ -21,38 +23,44 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
||||
TabController? _tabController;
|
||||
final List<Widget> _pages = [];
|
||||
final List<Widget> _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<SCIndexHomePage>
|
||||
|
||||
@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<SCIndexHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
// 正常渲染界面
|
||||
_tabs.clear();
|
||||
final strategy = SCGlobalConfig.businessLogicStrategy;
|
||||
_tabs.addAll(strategy.getHomeTabLabels(context));
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
@ -100,7 +102,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
|
||||
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<SCIndexHomePage>
|
||||
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,
|
||||
|
||||
@ -24,8 +24,10 @@ import '../user/me_page2.dart';
|
||||
* 首页
|
||||
*/
|
||||
class SCIndexPage extends StatefulWidget {
|
||||
const SCIndexPage({super.key});
|
||||
|
||||
@override
|
||||
_SCIndexPageState createState() => _SCIndexPageState();
|
||||
State<SCIndexPage> createState() => _SCIndexPageState();
|
||||
}
|
||||
|
||||
class _SCIndexPageState extends State<SCIndexPage> {
|
||||
@ -33,10 +35,12 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
final List<Widget> _pages = [];
|
||||
final List<BottomNavigationBarItem> _bottomItems = [];
|
||||
SCAppGeneralManager? generalProvider;
|
||||
Locale? _lastLocale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializePages();
|
||||
generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false);
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
@ -51,7 +55,9 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
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) {
|
||||
@ -67,9 +73,14 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
SCFloatIchart().init(context);
|
||||
_initWidget();
|
||||
_rebuildBottomItemsIfNeeded();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: _doubleExit,
|
||||
child: Stack(
|
||||
@ -149,14 +160,24 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _initWidget() {
|
||||
_pages.clear();
|
||||
_bottomItems.clear();
|
||||
void _initializePages() {
|
||||
if (_pages.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pages.add(SCIndexHomePage());
|
||||
_pages.add(HomeEventPage());
|
||||
// _pages.add(IndexDynamicPage());
|
||||
_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(
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
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';
|
||||
@ -9,49 +7,27 @@ 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 {
|
||||
const SplashPage({super.key});
|
||||
|
||||
@override
|
||||
_SplashPageState createState() => _SplashPageState();
|
||||
State<SplashPage> createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends State<SplashPage> {
|
||||
Timer? _timer;
|
||||
int countdown = 6;
|
||||
int status = 0;
|
||||
SCStartPageRes? currentStartPage;
|
||||
static const Duration _minimumSplashDuration = Duration(milliseconds: 900);
|
||||
|
||||
List<SCStartPageRes> startPages = [];
|
||||
Timer? _timer;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
status = 1;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(FileCacheManager.getInstance().getFilePath());
|
||||
});
|
||||
_startNavigationTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -60,17 +36,12 @@ class _SplashPageState extends State<SplashPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 启动倒计时
|
||||
void _startTimer(BuildContext context) {
|
||||
// 计时器(`Timer`)组件的定期(`periodic`)构造函数,创建一个新的重复计时器。
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (countdown == 0) {
|
||||
_cancelTimer();
|
||||
_goMainPage(context);
|
||||
return;
|
||||
/// 启动页最短展示时间
|
||||
void _startNavigationTimer() {
|
||||
_timer = Timer(_minimumSplashDuration, () {
|
||||
if (mounted) {
|
||||
_goMainPage();
|
||||
}
|
||||
countdown--;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,9 +67,12 @@ class _SplashPageState extends State<SplashPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _goMainPage(BuildContext context) async {
|
||||
void _goMainPage() async {
|
||||
try {
|
||||
await SCVersionUtils.checkReview();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
var user = AccountStorage().getCurrentUser();
|
||||
var token = AccountStorage().getToken();
|
||||
if (user != null && token.isNotEmpty) {
|
||||
@ -107,6 +81,9 @@ class _SplashPageState extends State<SplashPage> {
|
||||
SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
|
||||
}
|
||||
}
|
||||
@ -117,5 +94,3 @@ class _SplashPageState extends State<SplashPage> {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -11,44 +12,48 @@ class LocalizationManager with ChangeNotifier {
|
||||
Locale? get locale => _locale;
|
||||
|
||||
LocalizationManager() {
|
||||
initializeLanguageSettings();
|
||||
_locale = _resolveInitialLocale();
|
||||
if (_locale != null) {
|
||||
SCGlobalConfig.lang = _locale!.languageCode;
|
||||
}
|
||||
}
|
||||
|
||||
initializeLanguageSettings() {
|
||||
Locale _resolveInitialLocale() {
|
||||
String language = DataPersistence.getLang();
|
||||
if (language.isEmpty) {
|
||||
Locale systemLocale = ui.window.locale;
|
||||
Locale systemLocale = ui.PlatformDispatcher.instance.locale;
|
||||
if (systemLocale.languageCode.startsWith("ar")) {
|
||||
_locale = Locale('ar', '');
|
||||
return const Locale('ar', '');
|
||||
} else if (systemLocale.languageCode == "en") {
|
||||
_locale = Locale('en', '');
|
||||
return const Locale('en', '');
|
||||
} else if (systemLocale.languageCode == "tr") {
|
||||
_locale = Locale('tr', '');
|
||||
return const Locale('tr', '');
|
||||
} else if (systemLocale.languageCode == "bn") {
|
||||
_locale = Locale('bn', '');
|
||||
return const Locale('bn', '');
|
||||
} else {
|
||||
_locale = Locale('en', '');
|
||||
return const Locale('en', '');
|
||||
}
|
||||
} else {
|
||||
if (language.startsWith("ar")) {
|
||||
_locale = Locale('ar', '');
|
||||
return const Locale('ar', '');
|
||||
} else if (language == "tr") {
|
||||
_locale = Locale('tr', '');
|
||||
return const Locale('tr', '');
|
||||
} else if (language == "bn") {
|
||||
_locale = Locale('bn', '');
|
||||
return const Locale('bn', '');
|
||||
} else {
|
||||
_locale = Locale('en', '');
|
||||
return const Locale('en', '');
|
||||
}
|
||||
}
|
||||
if (_locale != null) {
|
||||
changeAppLanguage(_locale!);
|
||||
}
|
||||
}
|
||||
|
||||
void changeAppLanguage(Locale locale) {
|
||||
if (_locale?.languageCode == locale.languageCode &&
|
||||
_locale?.countryCode == locale.countryCode) {
|
||||
return;
|
||||
}
|
||||
_locale = locale;
|
||||
SCGlobalConfig.lang = locale.languageCode;
|
||||
DataPersistence.setLang(locale.languageCode);
|
||||
unawaited(DataPersistence.setLang(locale.languageCode));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
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 final GoogleSignIn _g = GoogleSignIn(scopes: ['email', 'profile']);
|
||||
|
||||
// 获取当前用户
|
||||
static User? get currentUser => FirebaseAuth.instance.currentUser;
|
||||
@ -14,18 +15,23 @@ class SCGoogleAuthService {
|
||||
|
||||
// 初始化Firebase
|
||||
static Future<void> initialize() async {
|
||||
if (Firebase.apps.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
await Firebase.initializeApp();
|
||||
}
|
||||
|
||||
// 谷歌登录
|
||||
static Future<User?> signInWithGoogle() async {
|
||||
try {
|
||||
await initialize();
|
||||
// 触发谷歌登录
|
||||
final GoogleSignInAccount? googleAccount = await _g.signIn();
|
||||
if (googleAccount == null) return null;
|
||||
|
||||
// 获取认证信息
|
||||
final GoogleSignInAuthentication googleAuth = await googleAccount.authentication;
|
||||
final GoogleSignInAuthentication googleAuth =
|
||||
await googleAccount.authentication;
|
||||
|
||||
// 创建Firebase凭证
|
||||
final OAuthCredential credential = GoogleAuthProvider.credential(
|
||||
@ -34,12 +40,12 @@ class SCGoogleAuthService {
|
||||
);
|
||||
|
||||
// 使用凭证登录Firebase
|
||||
final UserCredential userCredential =
|
||||
await FirebaseAuth.instance.signInWithCredential(credential);
|
||||
final UserCredential userCredential = await FirebaseAuth.instance
|
||||
.signInWithCredential(credential);
|
||||
|
||||
return userCredential.user;
|
||||
} catch (e) {
|
||||
print('Google sign-in error: $e');
|
||||
debugPrint('Google sign-in error: $e');
|
||||
rethrow; // 抛出异常让调用者处理
|
||||
}
|
||||
}
|
||||
@ -47,9 +53,9 @@ class SCGoogleAuthService {
|
||||
// 静默登录(检查现有会话)
|
||||
static Future<User?> signInSilently() async {
|
||||
try {
|
||||
await initialize();
|
||||
// 尝试静默登录
|
||||
final GoogleSignInAccount? googleAccount =
|
||||
await _g.signInSilently();
|
||||
final GoogleSignInAccount? googleAccount = await _g.signInSilently();
|
||||
|
||||
if (googleAccount == null) return null;
|
||||
|
||||
@ -64,12 +70,12 @@ class SCGoogleAuthService {
|
||||
);
|
||||
|
||||
// 使用凭证登录Firebase
|
||||
final UserCredential userCredential =
|
||||
await FirebaseAuth.instance.signInWithCredential(credential);
|
||||
final UserCredential userCredential = await FirebaseAuth.instance
|
||||
.signInWithCredential(credential);
|
||||
|
||||
return userCredential.user;
|
||||
} catch (e) {
|
||||
print('Silent sign-in error: $e');
|
||||
debugPrint('Silent sign-in error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -77,10 +83,11 @@ class SCGoogleAuthService {
|
||||
// 登出
|
||||
static Future<void> signOut() async {
|
||||
try {
|
||||
await initialize();
|
||||
await _g.signOut();
|
||||
await FirebaseAuth.instance.signOut();
|
||||
} catch (e) {
|
||||
print('Sign out error: $e');
|
||||
debugPrint('Sign out error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
|
||||
class CustomCachedImage extends StatelessWidget {
|
||||
final String imageUrl;
|
||||
@ -31,23 +31,18 @@ class CustomCachedImage extends StatelessWidget {
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
placeholder: (context, url) =>
|
||||
placeholder ?? _defaultPlaceholder(),
|
||||
errorWidget: (context, url, error) =>
|
||||
errorWidget ?? _defaultErrorWidget(),
|
||||
placeholder: (context, url) => placeholder ?? _defaultPlaceholder(),
|
||||
errorWidget:
|
||||
(context, url, error) => errorWidget ?? _defaultErrorWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _defaultPlaceholder() {
|
||||
return Center(
|
||||
child: CupertinoActivityIndicator(),
|
||||
);
|
||||
return const SCRotatingDotsLoading();
|
||||
}
|
||||
|
||||
Widget _defaultErrorWidget() {
|
||||
return Center(
|
||||
child: Image.asset("sc_images/general/sc_icon_loading.png"),
|
||||
);
|
||||
return Center(child: Image.asset("sc_images/general/sc_icon_loading.png"));
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
|
||||
128
lib/ui_kit/components/sc_rotating_dots_loading.dart
Normal file
128
lib/ui_kit/components/sc_rotating_dots_loading.dart
Normal file
@ -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<SCRotatingDotsLoading> createState() => _SCRotatingDotsLoadingState();
|
||||
}
|
||||
|
||||
class _SCRotatingDotsLoadingState extends State<SCRotatingDotsLoading>
|
||||
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 =
|
||||
<double>[
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
需求进度.md
11
需求进度.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 会自然上移填充。
|
||||
- 首页顶部排行骨架当前已进一步收窄为单条标题占位,不再保留两条文本骨架;房卡底栏房名也已再次缩一号并做轻微上移,对齐更贴近设计稿观感。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user