图片加载中占位符更改以及app启动初始帧提前

This commit is contained in:
NIGGER SLAYER 2026-04-15 15:38:03 +08:00
parent 456642ff04
commit 553c37c743
11 changed files with 1206 additions and 1029 deletions

View File

@ -43,13 +43,12 @@ import 'services/theme/theme_manager.dart';
import 'services/auth/user_profile_manager.dart'; import 'services/auth/user_profile_manager.dart';
import 'ui_kit/theme/socialchat_theme.dart'; import 'ui_kit/theme/socialchat_theme.dart';
bool _isCrashlyticsReady = false;
void main() async { void main() async {
// //
AppConfig.initialize(); AppConfig.initialize();
// 2. 使 runZonedGuarded // 2. 使 runZonedGuarded
runZonedGuarded( runZonedGuarded(
() async { () async {
@ -57,24 +56,24 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// //
GestureBinding.instance.resamplingEnabled = true; GestureBinding.instance.resamplingEnabled = true;
// 1UI //
await _setupA(); await _prepareAppShell();
// 使 _installErrorHandlers();
await _initStore();
// //
runApp(const RootAppWithProviders()); runApp(const RootAppWithProviders());
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_warmUpDeferredServices());
});
}, },
// 9. runZonedGuarded // 9. runZonedGuarded
(error, stackTrace) { (error, stackTrace) {
// runZonedGuarded unawaited(_recordFatalError(error, stackTrace));
FirebaseCrashlytics.instance.recordError(error, stackTrace, fatal: true);
debugPrint('Zoned Error: $error\nStack: $stackTrace');
}, },
); );
} }
/// _initializeEssentialServices ///
Future<void> _setupA() async { Future<void> _prepareAppShell() async {
// //
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
@ -90,34 +89,70 @@ Future<void> _setupA() async {
// //
SCKeybordUtil.init(); SCKeybordUtil.init();
// 使
await _initStore();
}
// Firebase核心初始化 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 { try {
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(); await Firebase.initializeApp();
}
_isCrashlyticsReady = true;
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
if (kDebugMode) { if (kDebugMode) {
debugPrint('Firebase初始化完成'); debugPrint('Firebase/Crashlytics 后台初始化完成');
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint('Firebase初始化异常: $e\n$stackTrace'); debugPrint('Firebase 后台初始化异常: $e\n$stackTrace');
// }
} }
// Flutter错误拦截器配置 Future<void> _recordFlutterError(FlutterErrorDetails details) async {
FlutterError.onError = (details) { if (!_isCrashlyticsReady) {
FirebaseCrashlytics.instance.recordFlutterFatalError(details); debugPrint(
FlutterError.presentError(details); 'Flutter Error before Crashlytics ready: ${details.exceptionAsString()}',
}; );
return;
//
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true; //
};
//
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
} }
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 /// _initializeStorageWithRetry
Future<void> _initStore() async { Future<void> _initStore() async {
@ -161,13 +196,13 @@ class RootAppWithProviders extends StatelessWidget {
/// Provider列表 - /// Provider列表 -
List<SingleChildWidget> _buildP() { List<SingleChildWidget> _buildP() {
return [ return [
// Provider - // Provider -
ChangeNotifierProvider<SocialChatAuthenticationManager>( ChangeNotifierProvider<SocialChatAuthenticationManager>(
lazy: false, lazy: true,
create: (context) => SocialChatAuthenticationManager(), create: (context) => SocialChatAuthenticationManager(),
), ),
ChangeNotifierProvider<SocialChatUserProfileManager>( ChangeNotifierProvider<SocialChatUserProfileManager>(
lazy: false, lazy: true,
create: (context) => SocialChatUserProfileManager(), create: (context) => SocialChatUserProfileManager(),
), ),
@ -221,7 +256,6 @@ class RootAppWithProviders extends StatelessWidget {
create: (context) => ShopManager(), create: (context) => ShopManager(),
), ),
// Provider - // Provider -
ChangeNotifierProvider<SCAppGeneralManager>( ChangeNotifierProvider<SCAppGeneralManager>(
lazy: true, lazy: true,
@ -248,12 +282,11 @@ class _YumiApplicationState extends State<YumiApplication> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// //
_initLink();
//
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_initRouter(); _initRouter();
SCDeviceIdUtils.initDeviceinfo(); SCDeviceIdUtils.initDeviceinfo();
unawaited(_initLink());
}); });
} }
@ -275,9 +308,7 @@ class _YumiApplicationState extends State<YumiApplication> {
void _handleLink(Uri uri) { void _handleLink(Uri uri) {
// //
print('App 根层收到链接: $uri'); debugPrint('App 根层收到链接: $uri');
String path = uri.path;
String id = uri.queryParameters['id'] ?? '';
} }
void _initRouter() { void _initRouter() {
@ -297,9 +328,7 @@ class _YumiApplicationState extends State<YumiApplication> {
builder: (BuildContext context, LoadStatus? mode) { builder: (BuildContext context, LoadStatus? mode) {
Widget body; Widget body;
if (mode == LoadStatus.idle) { if (mode == LoadStatus.idle) {
body = Text( body = Text(SCAppLocalizations.of(context)!.pullToLoadMore);
SCAppLocalizations.of(context)!.pullToLoadMore,
);
} else if (mode == LoadStatus.loading) { } else if (mode == LoadStatus.loading) {
body = CupertinoActivityIndicator(color: Colors.white24); body = CupertinoActivityIndicator(color: Colors.white24);
} else if (mode == LoadStatus.failed) { } else if (mode == LoadStatus.failed) {
@ -307,16 +336,11 @@ class _YumiApplicationState extends State<YumiApplication> {
SCAppLocalizations.of(context)!.loadingFailedClickToRetry, SCAppLocalizations.of(context)!.loadingFailedClickToRetry,
); );
} else if (mode == LoadStatus.canLoading) { } else if (mode == LoadStatus.canLoading) {
body = Text( body = Text(SCAppLocalizations.of(context)!.releaseToLoadMore);
SCAppLocalizations.of(context)!.releaseToLoadMore,
);
} else { } else {
body = Text( body = Text(
"", "",
style: TextStyle( style: TextStyle(fontSize: 12.sp, color: Color(0xff999999)),
fontSize: 12.sp,
color: Color(0xff999999),
),
); );
} }
return Container( return Container(
@ -327,7 +351,6 @@ class _YumiApplicationState extends State<YumiApplication> {
); );
} }
Widget _buildUI() { Widget _buildUI() {
return Consumer<LocalizationManager>( return Consumer<LocalizationManager>(
builder: (context, localeProvider, child) { builder: (context, localeProvider, child) {
@ -343,29 +366,32 @@ class _YumiApplicationState extends State<YumiApplication> {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark, value: SystemUiOverlayStyle.dark,
child: RefreshConfiguration( child: RefreshConfiguration(
headerBuilder: () => MaterialClassicHeader(color: SocialChatTheme.primaryColor), headerBuilder:
() => MaterialClassicHeader(
color: SocialChatTheme.primaryColor,
),
footerBuilder: _buildFooter(), footerBuilder: _buildFooter(),
child: MaterialApp( child: MaterialApp(
title: 'Yumi', title: 'Yumi',
locale: Provider.of<LocalizationManager>(context).locale, locale: localeProvider.locale,
localizationsDelegates: [ localizationsDelegates: const [
SCAppLocalizations.delegate, SCAppLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], ],
supportedLocales: [ supportedLocales: const [
const Locale('en', ''), Locale('en', ''),
const Locale('zh', ''), Locale('zh', ''),
const Locale('ar', ''), Locale('ar', ''),
const Locale('tr', ''), Locale('tr', ''),
const Locale('bn', ''), Locale('bn', ''),
], ],
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
onGenerateRoute: SCLkApplication.router.generator, onGenerateRoute: SCLkApplication.router.generator,
theme: themeManager.currentTheme, theme: themeManager.currentTheme,
home: SplashPage(), home: const SplashPage(),
builder: FlutterSmartDialog.init(), builder: FlutterSmartDialog.init(),
navigatorObservers: [routeObserver], navigatorObservers: [routeObserver],
), ),

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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_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/components/text/sc_text.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import 'package:yumi/shared/business_logic/models/res/gift_res.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); const Duration _kGiftPageSkeletonMaxDuration = Duration(milliseconds: 900);
class GiftTabPage extends StatefulWidget { class GiftTabPage extends StatefulWidget {
String type; final String type;
bool isDark = true; final bool isDark;
Function(int checkedIndex) checkedCall; 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 @override
_GiftTabPageState createState() => _GiftTabPageState(); State<GiftTabPage> createState() => _GiftTabPageState();
} }
class _GiftTabPageState extends State<GiftTabPage> class _GiftTabPageState extends State<GiftTabPage>
@ -202,12 +208,8 @@ class _GiftTabPageState extends State<GiftTabPage>
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Opacity( child: Opacity(
opacity: 0.75, opacity: 0.9,
child: Image.asset( child: SCRotatingDotsLoading(size: 72.w),
"sc_images/general/sc_icon_loading.png",
width: 92.w,
fit: BoxFit.contain,
),
), ),
), ),
), ),
@ -245,12 +247,8 @@ class _GiftTabPageState extends State<GiftTabPage>
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
color: baseColor, 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), SizedBox(height: 7.w),
Container( Container(
@ -276,6 +274,19 @@ class _GiftTabPageState extends State<GiftTabPage>
} }
Widget _buildGiftCoverLoading() { 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( return Container(
width: 48.w, width: 48.w,
height: 48.w, height: 48.w,
@ -424,7 +435,7 @@ class _GiftTabPageState extends State<GiftTabPage>
width: 48.w, width: 48.w,
height: 48.w, height: 48.w,
loadingWidget: _buildGiftCoverLoading(), loadingWidget: _buildGiftCoverLoading(),
errorWidget: _buildGiftCoverLoading(), errorWidget: _buildGiftCoverNoData(),
), ),
SizedBox(height: 5.w), SizedBox(height: 5.w),
Container( Container(

View File

@ -12,8 +12,10 @@ import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
///Home页面 ///Home页面
class SCIndexHomePage extends StatefulWidget { class SCIndexHomePage extends StatefulWidget {
const SCIndexHomePage({super.key});
@override @override
_SCIndexHomePageState createState() => _SCIndexHomePageState(); State<SCIndexHomePage> createState() => _SCIndexHomePageState();
} }
class _SCIndexHomePageState extends State<SCIndexHomePage> class _SCIndexHomePageState extends State<SCIndexHomePage>
@ -21,38 +23,44 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
TabController? _tabController; TabController? _tabController;
final List<Widget> _pages = []; final List<Widget> _pages = [];
final List<Widget> _tabs = []; final List<Widget> _tabs = [];
bool refresh = false; // 👈 false Locale? _lastLocale;
@override @override
void initState() { void didChangeDependencies() {
super.initState(); super.didChangeDependencies();
// final locale = Localizations.localeOf(context);
WidgetsBinding.instance.addPostFrameCallback((_) { if (_lastLocale == locale && _tabController != null && _pages.isNotEmpty) {
_updateTabs(); return;
}); }
_lastLocale = locale;
_rebuildTabs();
} }
void _updateTabs() { void _rebuildTabs() {
_tabController?.dispose();
_pages.clear();
final strategy = SCGlobalConfig.businessLogicStrategy; 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( _tabController = TabController(
length: _pages.length, length: _pages.length,
vsync: this, vsync: this,
initialIndex: strategy.getHomeInitialTabIndex(), initialIndex: initialIndex,
); );
//
_tabController?.addListener(() {});
//
if (mounted) {
setState(() {
refresh = true;
});
}
} }
@override @override
@ -63,8 +71,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// if (_tabController == null || _pages.isEmpty || _tabs.isEmpty) {
if (!refresh || _tabController == null || _pages.isEmpty) {
return Center( return Center(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -78,11 +85,6 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
); );
} }
//
_tabs.clear();
final strategy = SCGlobalConfig.businessLogicStrategy;
_tabs.addAll(strategy.getHomeTabLabels(context));
return Stack( return Stack(
children: [ children: [
Scaffold( Scaffold(
@ -100,7 +102,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
tabAlignment: TabAlignment.start, tabAlignment: TabAlignment.start,
isScrollable: true, isScrollable: true,
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
overlayColor: MaterialStateProperty.all( overlayColor: WidgetStateProperty.all(
Colors.transparent, Colors.transparent,
), ),
indicator: const BoxDecoration(), indicator: const BoxDecoration(),
@ -113,7 +115,7 @@ class _SCIndexHomePageState extends State<SCIndexHomePage>
unselectedLabelStyle: TextStyle( unselectedLabelStyle: TextStyle(
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.white color: Colors.white,
), ),
indicatorColor: Colors.transparent, indicatorColor: Colors.transparent,
dividerColor: Colors.transparent, dividerColor: Colors.transparent,

View File

@ -24,8 +24,10 @@ import '../user/me_page2.dart';
* *
*/ */
class SCIndexPage extends StatefulWidget { class SCIndexPage extends StatefulWidget {
const SCIndexPage({super.key});
@override @override
_SCIndexPageState createState() => _SCIndexPageState(); State<SCIndexPage> createState() => _SCIndexPageState();
} }
class _SCIndexPageState extends State<SCIndexPage> { class _SCIndexPageState extends State<SCIndexPage> {
@ -33,10 +35,12 @@ class _SCIndexPageState extends State<SCIndexPage> {
final List<Widget> _pages = []; final List<Widget> _pages = [];
final List<BottomNavigationBarItem> _bottomItems = []; final List<BottomNavigationBarItem> _bottomItems = [];
SCAppGeneralManager? generalProvider; SCAppGeneralManager? generalProvider;
Locale? _lastLocale;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializePages();
generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false); generalProvider = Provider.of<SCAppGeneralManager>(context, listen: false);
Provider.of<RtcProvider>( Provider.of<RtcProvider>(
context, context,
@ -51,7 +55,9 @@ class _SCIndexPageState extends State<SCIndexPage> {
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
DataPersistence.setLang(SCGlobalConfig.lang); DataPersistence.setLang(SCGlobalConfig.lang);
OverlayManager().activate(); OverlayManager().activate();
WidgetsBinding.instance.addPostFrameCallback((_) {
WakelockPlus.enable(); WakelockPlus.enable();
});
String roomId = DataPersistence.getLastTimeRoomId(); String roomId = DataPersistence.getLastTimeRoomId();
if (roomId.isNotEmpty) { if (roomId.isNotEmpty) {
SCAccountRepository().quitRoom(roomId).catchError((e) { SCAccountRepository().quitRoom(roomId).catchError((e) {
@ -67,9 +73,14 @@ class _SCIndexPageState extends State<SCIndexPage> {
} }
@override @override
Widget build(BuildContext context) { void didChangeDependencies() {
super.didChangeDependencies();
SCFloatIchart().init(context); SCFloatIchart().init(context);
_initWidget(); _rebuildBottomItemsIfNeeded();
}
@override
Widget build(BuildContext context) {
return WillPopScope( return WillPopScope(
onWillPop: _doubleExit, onWillPop: _doubleExit,
child: Stack( child: Stack(
@ -149,14 +160,24 @@ class _SCIndexPageState extends State<SCIndexPage> {
} }
} }
void _initWidget() { void _initializePages() {
_pages.clear(); if (_pages.isNotEmpty) {
_bottomItems.clear(); return;
}
_pages.add(SCIndexHomePage()); _pages.add(SCIndexHomePage());
_pages.add(HomeEventPage()); _pages.add(HomeEventPage());
// _pages.add(IndexDynamicPage());
_pages.add(SCMessagePage()); _pages.add(SCMessagePage());
_pages.add(MePage2()); _pages.add(MePage2());
}
void _rebuildBottomItemsIfNeeded() {
final locale = Localizations.localeOf(context);
if (_lastLocale == locale && _bottomItems.isNotEmpty) {
return;
}
_lastLocale = locale;
_bottomItems.clear();
_bottomItems.add( _bottomItems.add(
BottomNavigationBarItem( BottomNavigationBarItem(

View File

@ -1,6 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.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_routes.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.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/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'; import 'package:yumi/modules/auth/login_route.dart';
class SplashPage extends StatefulWidget { class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override @override
_SplashPageState createState() => _SplashPageState(); State<SplashPage> createState() => _SplashPageState();
} }
class _SplashPageState extends State<SplashPage> { class _SplashPageState extends State<SplashPage> {
Timer? _timer; static const Duration _minimumSplashDuration = Duration(milliseconds: 900);
int countdown = 6;
int status = 0;
SCStartPageRes? currentStartPage;
List<SCStartPageRes> startPages = []; Timer? _timer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
FileCacheManager.getInstance().getFilePath(); WidgetsBinding.instance.addPostFrameCallback((_) {
getStartPage(); unawaited(FileCacheManager.getInstance().getFilePath());
_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(() {});
}
}); });
_startNavigationTimer();
} }
@override @override
@ -60,17 +36,12 @@ class _SplashPageState extends State<SplashPage> {
super.dispose(); super.dispose();
} }
/// ///
void _startTimer(BuildContext context) { void _startNavigationTimer() {
// `Timer``periodic` _timer = Timer(_minimumSplashDuration, () {
_timer = Timer.periodic(Duration(seconds: 1), (timer) { if (mounted) {
if (countdown == 0) { _goMainPage();
_cancelTimer();
_goMainPage(context);
return;
} }
countdown--;
setState(() {});
}); });
} }
@ -96,9 +67,12 @@ class _SplashPageState extends State<SplashPage> {
); );
} }
void _goMainPage(BuildContext context) async { void _goMainPage() async {
try { try {
await SCVersionUtils.checkReview(); await SCVersionUtils.checkReview();
if (!mounted) {
return;
}
var user = AccountStorage().getCurrentUser(); var user = AccountStorage().getCurrentUser();
var token = AccountStorage().getToken(); var token = AccountStorage().getToken();
if (user != null && token.isNotEmpty) { if (user != null && token.isNotEmpty) {
@ -107,6 +81,9 @@ class _SplashPageState extends State<SplashPage> {
SCNavigatorUtils.push(context, LoginRouter.login, replace: true); SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
} }
} catch (e) { } catch (e) {
if (!mounted) {
return;
}
SCNavigatorUtils.push(context, LoginRouter.login, replace: true); SCNavigatorUtils.push(context, LoginRouter.login, replace: true);
} }
} }
@ -117,5 +94,3 @@ class _SplashPageState extends State<SplashPage> {
_timer?.cancel(); _timer?.cancel();
} }
} }

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,44 +12,48 @@ class LocalizationManager with ChangeNotifier {
Locale? get locale => _locale; Locale? get locale => _locale;
LocalizationManager() { LocalizationManager() {
initializeLanguageSettings(); _locale = _resolveInitialLocale();
if (_locale != null) {
SCGlobalConfig.lang = _locale!.languageCode;
}
} }
initializeLanguageSettings() { Locale _resolveInitialLocale() {
String language = DataPersistence.getLang(); String language = DataPersistence.getLang();
if (language.isEmpty) { if (language.isEmpty) {
Locale systemLocale = ui.window.locale; Locale systemLocale = ui.PlatformDispatcher.instance.locale;
if (systemLocale.languageCode.startsWith("ar")) { if (systemLocale.languageCode.startsWith("ar")) {
_locale = Locale('ar', ''); return const Locale('ar', '');
} else if (systemLocale.languageCode == "en") { } else if (systemLocale.languageCode == "en") {
_locale = Locale('en', ''); return const Locale('en', '');
} else if (systemLocale.languageCode == "tr") { } else if (systemLocale.languageCode == "tr") {
_locale = Locale('tr', ''); return const Locale('tr', '');
} else if (systemLocale.languageCode == "bn") { } else if (systemLocale.languageCode == "bn") {
_locale = Locale('bn', ''); return const Locale('bn', '');
} else { } else {
_locale = Locale('en', ''); return const Locale('en', '');
} }
} else { } else {
if (language.startsWith("ar")) { if (language.startsWith("ar")) {
_locale = Locale('ar', ''); return const Locale('ar', '');
} else if (language == "tr") { } else if (language == "tr") {
_locale = Locale('tr', ''); return const Locale('tr', '');
} else if (language == "bn") { } else if (language == "bn") {
_locale = Locale('bn', ''); return const Locale('bn', '');
} else { } else {
_locale = Locale('en', ''); return const Locale('en', '');
} }
} }
if (_locale != null) {
changeAppLanguage(_locale!);
}
} }
void changeAppLanguage(Locale locale) { void changeAppLanguage(Locale locale) {
if (_locale?.languageCode == locale.languageCode &&
_locale?.countryCode == locale.countryCode) {
return;
}
_locale = locale; _locale = locale;
SCGlobalConfig.lang = locale.languageCode; SCGlobalConfig.lang = locale.languageCode;
DataPersistence.setLang(locale.languageCode); unawaited(DataPersistence.setLang(locale.languageCode));
notifyListeners(); notifyListeners();
} }
} }

View File

@ -1,10 +1,11 @@
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/google_sign_in.dart';
/// ///
class SCGoogleAuthService { 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; static User? get currentUser => FirebaseAuth.instance.currentUser;
@ -14,18 +15,23 @@ class SCGoogleAuthService {
// Firebase // Firebase
static Future<void> initialize() async { static Future<void> initialize() async {
if (Firebase.apps.isNotEmpty) {
return;
}
await Firebase.initializeApp(); await Firebase.initializeApp();
} }
// //
static Future<User?> signInWithGoogle() async { static Future<User?> signInWithGoogle() async {
try { try {
await initialize();
// //
final GoogleSignInAccount? googleAccount = await _g.signIn(); final GoogleSignInAccount? googleAccount = await _g.signIn();
if (googleAccount == null) return null; if (googleAccount == null) return null;
// //
final GoogleSignInAuthentication googleAuth = await googleAccount.authentication; final GoogleSignInAuthentication googleAuth =
await googleAccount.authentication;
// Firebase凭证 // Firebase凭证
final OAuthCredential credential = GoogleAuthProvider.credential( final OAuthCredential credential = GoogleAuthProvider.credential(
@ -34,12 +40,12 @@ class SCGoogleAuthService {
); );
// 使Firebase // 使Firebase
final UserCredential userCredential = final UserCredential userCredential = await FirebaseAuth.instance
await FirebaseAuth.instance.signInWithCredential(credential); .signInWithCredential(credential);
return userCredential.user; return userCredential.user;
} catch (e) { } catch (e) {
print('Google sign-in error: $e'); debugPrint('Google sign-in error: $e');
rethrow; // rethrow; //
} }
} }
@ -47,9 +53,9 @@ class SCGoogleAuthService {
// //
static Future<User?> signInSilently() async { static Future<User?> signInSilently() async {
try { try {
await initialize();
// //
final GoogleSignInAccount? googleAccount = final GoogleSignInAccount? googleAccount = await _g.signInSilently();
await _g.signInSilently();
if (googleAccount == null) return null; if (googleAccount == null) return null;
@ -64,12 +70,12 @@ class SCGoogleAuthService {
); );
// 使Firebase // 使Firebase
final UserCredential userCredential = final UserCredential userCredential = await FirebaseAuth.instance
await FirebaseAuth.instance.signInWithCredential(credential); .signInWithCredential(credential);
return userCredential.user; return userCredential.user;
} catch (e) { } catch (e) {
print('Silent sign-in error: $e'); debugPrint('Silent sign-in error: $e');
return null; return null;
} }
} }
@ -77,10 +83,11 @@ class SCGoogleAuthService {
// //
static Future<void> signOut() async { static Future<void> signOut() async {
try { try {
await initialize();
await _g.signOut(); await _g.signOut();
await FirebaseAuth.instance.signOut(); await FirebaseAuth.instance.signOut();
} catch (e) { } catch (e) {
print('Sign out error: $e'); debugPrint('Sign out error: $e');
rethrow; rethrow;
} }
} }

View File

@ -1,6 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
class CustomCachedImage extends StatelessWidget { class CustomCachedImage extends StatelessWidget {
final String imageUrl; final String imageUrl;
@ -31,23 +31,18 @@ class CustomCachedImage extends StatelessWidget {
width: width, width: width,
height: height, height: height,
fit: fit, fit: fit,
placeholder: (context, url) => placeholder: (context, url) => placeholder ?? _defaultPlaceholder(),
placeholder ?? _defaultPlaceholder(), errorWidget:
errorWidget: (context, url, error) => (context, url, error) => errorWidget ?? _defaultErrorWidget(),
errorWidget ?? _defaultErrorWidget(),
), ),
); );
} }
Widget _defaultPlaceholder() { Widget _defaultPlaceholder() {
return Center( return const SCRotatingDotsLoading();
child: CupertinoActivityIndicator(),
);
} }
Widget _defaultErrorWidget() { Widget _defaultErrorWidget() {
return Center( return Center(child: Image.asset("sc_images/general/sc_icon_loading.png"));
child: Image.asset("sc_images/general/sc_icon_loading.png"),
);
} }
} }

View File

@ -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_path_utils.dart';
import 'package:yumi/shared/tools/sc_room_profile_cache.dart'; import 'package:yumi/shared/tools/sc_room_profile_cache.dart';
import 'package:yumi/shared/tools/sc_network_image_utils.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'; import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
@ -215,12 +216,7 @@ Widget netImage({
if (loadingWidget != null) { if (loadingWidget != null) {
return loadingWidget; return loadingWidget;
} }
return noDefaultImg return noDefaultImg ? Container() : const SCRotatingDotsLoading();
? Container()
: Image.asset(
defaultImg ?? "sc_images/general/sc_icon_loading.png",
fit: BoxFit.cover,
);
} }
}, },
); );
@ -354,7 +350,7 @@ searchWidget({
height: width(36), height: width(36),
//width: width(345), //width: width(345),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xff18F2B1).withOpacity(0.1), color: const Color(0xff18F2B1).withValues(alpha: 0.1),
borderRadius: BorderRadius.all(Radius.circular(height(20))), borderRadius: BorderRadius.all(Radius.circular(height(20))),
border: Border.all(color: borderColor ?? Colors.black12, width: 1.w), border: Border.all(color: borderColor ?? Colors.black12, width: 1.w),
), ),

View 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),
);
}
}

View File

@ -3,6 +3,15 @@
## 当前总目标 ## 当前总目标
- 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 - 控制当前 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` 悬浮入口:入口位于聊天区右下侧、距右侧约 `15.w`、位于底部操作栏上方约 `100.w`,当前先使用代码绘制的 `🎮` 占位图标,并已在实现处标注后续替换为正式 UI 图片资源。
- 已新增语言房游戏底部弹窗:点击 `game` 入口后会从底部弹出约三分之一屏高的面板,采用房间页现有半透明磨砂风格,便于后续继续沿用当前视觉体系。 - 已新增语言房游戏底部弹窗:点击 `game` 入口后会从底部弹出约三分之一屏高的面板,采用房间页现有半透明磨砂风格,便于后续继续沿用当前视觉体系。
@ -25,6 +34,7 @@
- 已为首页 Party 页补齐首屏骨架屏:顶部 H5 榜单现按三列奖杯卡位展示头像/标题骨架,下方房间区改为双列房卡骨架,并采用深绿底 + 金色描边的项目内风格;同时拆分榜单与房间列表加载态,仅在“首屏无内容加载”时展示骨架,避免下拉刷新时整页闪烁。 - 已为首页 Party 页补齐首屏骨架屏:顶部 H5 榜单现按三列奖杯卡位展示头像/标题骨架,下方房间区改为双列房卡骨架,并采用深绿底 + 金色描边的项目内风格;同时拆分榜单与房间列表加载态,仅在“首屏无内容加载”时展示骨架,避免下拉刷新时整页闪烁。
- 已为首页 My 板块补齐首屏骨架屏:`My room` 顶部房间卡片改为独立加载态,避免首屏把“正在加载我的房间”误显示成“去创建房间”;`Recent/Followed` 两个子页也已改成同风格双列房卡骨架,进入 tab 时会先展示完整结构占位,再按真实数据切换。 - 已为首页 My 板块补齐首屏骨架屏:`My room` 顶部房间卡片改为独立加载态,避免首屏把“正在加载我的房间”误显示成“去创建房间”;`Recent/Followed` 两个子页也已改成同风格双列房卡骨架,进入 tab 时会先展示完整结构占位,再按真实数据切换。
- 已为 `Me` 入口下的 `Store / Bag` 页面补齐首屏骨架屏:四类 `store` 商品页和四类 `bag` 物品页在“首屏空数据加载”阶段都会先展示同风格的三列卡片骨架,不再只显示小菊花;同时已为商品/物品名称补齐宽度约束与省略号处理,避免超长文案顶出卡片。 - 已为 `Me` 入口下的 `Store / Bag` 页面补齐首屏骨架屏:四类 `store` 商品页和四类 `bag` 物品页在“首屏空数据加载”阶段都会先展示同风格的三列卡片骨架,不再只显示小菊花;同时已为商品/物品名称补齐宽度约束与省略号处理,避免超长文案顶出卡片。
- 已将通用图片加载占位从静态 `loading` 图片升级为代码绘制的环形转点组件:礼物二级页单卡封面、`Store / Bag` 商品图以及复用共享图片组件的网络图,在“加载中”阶段都会显示动态转圈效果;而图片失败或 `no data` 时仍保持原有占位图,不混淆加载态与空态。
- 已继续细化首页房卡体验:`My` 板块房卡骨架数量已从 `4` 个补齐到 `6` 个,与首页 Party 首屏保持一致;同时将房卡右下角原本的静态绿色“直播中”图片替换为代码驱动的三段式动态音频条,首页、我的、搜索结果和房间浮窗现都会展示跳动效果,不再依赖静态资源图;最新已把动效调整为“单周期完整峰谷 + A/B/C 错峰起伏”的连续波形,观感更接近直播音频电平。 - 已继续细化首页房卡体验:`My` 板块房卡骨架数量已从 `4` 个补齐到 `6` 个,与首页 Party 首屏保持一致;同时将房卡右下角原本的静态绿色“直播中”图片替换为代码驱动的三段式动态音频条,首页、我的、搜索结果和房间浮窗现都会展示跳动效果,不再依赖静态资源图;最新已把动效调整为“单周期完整峰谷 + A/B/C 错峰起伏”的连续波形,观感更接近直播音频电平。
- 已继续微调首页房卡信息密度:房卡底部房名与在线人数字号已统一调小,和动态音频条更协调;同时已移除 `My` 板块顶部 `my room` 标题及其占位,让房间卡片整体上移,对齐更紧凑。 - 已继续微调首页房卡信息密度:房卡底部房名与在线人数字号已统一调小,和动态音频条更协调;同时已移除 `My` 板块顶部 `my room` 标题及其占位,让房间卡片整体上移,对齐更紧凑。
- 已继续微调首页视觉细节房卡底栏房名再次缩小并轻微上移和国旗、动态音频条、人数的中线更一致首页顶部排行骨架也已简化为“3 个圆形头像 + 1 根文本条”,更贴近真实卡片结构。 - 已继续微调首页视觉细节房卡底栏房名再次缩小并轻微上移和国旗、动态音频条、人数的中线更一致首页顶部排行骨架也已简化为“3 个圆形头像 + 1 根文本条”,更贴近真实卡片结构。
@ -165,6 +175,7 @@
- 首页 Party 页当前已接入贴合真实版式的骨架屏:进入首页首屏时会先看到三张排行卡位与双列房卡占位,骨架配色已跟随页面深绿+鎏金主视觉,不再只显示居中的白色 `loading`;且排行榜与房间区会按各自请求结果独立切换,先返回的区域会先显示真实内容。 - 首页 Party 页当前已接入贴合真实版式的骨架屏:进入首页首屏时会先看到三张排行卡位与双列房卡占位,骨架配色已跟随页面深绿+鎏金主视觉,不再只显示居中的白色 `loading`;且排行榜与房间区会按各自请求结果独立切换,先返回的区域会先显示真实内容。
- 首页 My 板块当前已接入贴合真实版式的骨架屏:顶部 `My room` 现区分“首次加载中”和“确实未创建房间”,不会再先闪出创建房间卡;`Recent/Followed` 在首屏空数据加载时也会先展示双列房卡骨架,不再只显示小菊花或局部空白。 - 首页 My 板块当前已接入贴合真实版式的骨架屏:顶部 `My room` 现区分“首次加载中”和“确实未创建房间”,不会再先闪出创建房间卡;`Recent/Followed` 在首屏空数据加载时也会先展示双列房卡骨架,不再只显示小菊花或局部空白。
- `Me` 入口下的 `Store / Bag` 页面当前也已接入深绿主题的三列卡片骨架:首次进入并且列表尚未返回时,会先显示与正式卡片比例一致的占位;接口返回后再切到真实商品/物品卡片。`store``bag` 卡片标题现都带宽度约束和单行省略,类似截图里的长名称不会再横向撑破布局。 - `Me` 入口下的 `Store / Bag` 页面当前也已接入深绿主题的三列卡片骨架:首次进入并且列表尚未返回时,会先显示与正式卡片比例一致的占位;接口返回后再切到真实商品/物品卡片。`store``bag` 卡片标题现都带宽度约束和单行省略,类似截图里的长名称不会再横向撑破布局。
- 图片加载态与空态当前已拆分:共享网络图组件和礼物二级页单卡封面加载时会显示代码绘制的环形转点,不再继续复用 `sc_icon_loading.png`;若图片失败或确实无数据,仍旧保留原先那张占位图,避免把 `loading``no data` 看成同一种状态。
- 首页与搜索等房卡右下角的在线状态图标当前已改为代码绘制的动态音频条:三根绿条会按不同相位走完整的波峰波谷周期,错峰连续起伏,视觉上更接近直播/语聊房的实时状态;`My` 板块骨架数量也已与 Party 页对齐为 `6` 个。 - 首页与搜索等房卡右下角的在线状态图标当前已改为代码绘制的动态音频条:三根绿条会按不同相位走完整的波峰波谷周期,错峰连续起伏,视觉上更接近直播/语聊房的实时状态;`My` 板块骨架数量也已与 Party 页对齐为 `6` 个。
- 首页房卡底部信息当前已进一步收紧:房名和人数文字比上一版更小,底栏与动态音频条的视觉重心更一致;`My` 页顶部 `my room` 标题也已移除,下方卡片和 tab 会自然上移填充。 - 首页房卡底部信息当前已进一步收紧:房名和人数文字比上一版更小,底栏与动态音频条的视觉重心更一致;`My` 页顶部 `my room` 标题也已移除,下方卡片和 tab 会自然上移填充。
- 首页顶部排行骨架当前已进一步收窄为单条标题占位,不再保留两条文本骨架;房卡底栏房名也已再次缩一号并做轻微上移,对齐更贴近设计稿观感。 - 首页顶部排行骨架当前已进一步收窄为单条标题占位,不再保留两条文本骨架;房卡底栏房名也已再次缩一号并做轻微上移,对齐更贴近设计稿观感。