feat: 添加5页向导页和首次打开检测功能

- 新增向导页组件(guide_page.dart),支持左右滑动浏览
- 实现首次打开检测逻辑,控制向导页显示
- 更新app图标为自定义logo
- 更新app名称为"榴莲皇后"
- 添加响应式尺寸扩展(.w/.h/.sp/.r)
- 优化底部导航栏响应式适配

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hailin 2025-11-27 00:39:29 -08:00
parent 1296bd607c
commit 429173464c
49 changed files with 627 additions and 142 deletions

View File

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="rwa_android_app"
android:label="榴莲皇后"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

View File

@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";

View File

@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -12,15 +12,25 @@ class App extends ConsumerWidget {
final router = ref.watch(appRouterProvider);
return ScreenUtilInit(
designSize: const Size(375, 812),
designSize: const Size(360, 800), // UIPro Figma 稿
minTextAdapt: true,
splitScreenMode: true,
useInheritedMediaQuery: true,
builder: (context, child) {
return MaterialApp.router(
title: 'RWA榴莲',
title: '榴莲皇',
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
routerConfig: router,
builder: (context, widget) {
// UI
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: widget!,
);
},
);
},
);

View File

@ -1,5 +1,22 @@
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
///
/// 使: 16.w (), 16.h (), 16.sp (), 16.r ()
extension ResponsiveNum on num {
///
double get w => ScreenUtil().setWidth(this);
///
double get h => ScreenUtil().setHeight(this);
///
double get sp => ScreenUtil().setSp(this);
/// ()
double get r => ScreenUtil().radius(this);
}
extension NumExtensions on num {
String get formatted {
return NumberFormat('#,##0.##').format(this);

View File

@ -1,7 +1,12 @@
import 'package:flutter_screenutil/flutter_screenutil.dart';
///
///
///
class AppDimensions {
AppDimensions._();
//
// ============ ============
static const double spacingXs = 4.0;
static const double spacingSm = 8.0;
static const double spacingMd = 16.0;
@ -9,7 +14,15 @@ class AppDimensions {
static const double spacingXl = 32.0;
static const double spacingXxl = 48.0;
//
// ============ ============
static double get spacingXsR => 4.w;
static double get spacingSmR => 8.w;
static double get spacingMdR => 16.w;
static double get spacingLgR => 24.w;
static double get spacingXlR => 32.w;
static double get spacingXxlR => 48.w;
// ============ ============
static const double radiusXs = 4.0;
static const double radiusSm = 8.0;
static const double radiusMd = 12.0;
@ -17,34 +30,70 @@ class AppDimensions {
static const double radiusXl = 24.0;
static const double radiusFull = 999.0;
//
// ============ ============
static double get radiusXsR => 4.r;
static double get radiusSmR => 8.r;
static double get radiusMdR => 12.r;
static double get radiusLgR => 16.r;
static double get radiusXlR => 24.r;
// ============ ============
static const double iconXs = 16.0;
static const double iconSm = 20.0;
static const double iconMd = 24.0;
static const double iconLg = 32.0;
static const double iconXl = 48.0;
//
// ============ ============
static double get iconXsR => 16.sp;
static double get iconSmR => 20.sp;
static double get iconMdR => 24.sp;
static double get iconLgR => 32.sp;
static double get iconXlR => 48.sp;
// ============ ============
static const double buttonHeightSm = 36.0;
static const double buttonHeightMd = 44.0;
static const double buttonHeightLg = 52.0;
//
// ============ ============
static double get buttonHeightSmR => 36.h;
static double get buttonHeightMdR => 44.h;
static double get buttonHeightLgR => 52.h;
// ============ ============
static const double inputHeight = 48.0;
//
// ============ ============
static double get inputHeightR => 48.h;
// ============ ============
static const double avatarSm = 32.0;
static const double avatarMd = 48.0;
static const double avatarLg = 64.0;
static const double avatarXl = 96.0;
//
// ============ ============
static double get avatarSmR => 32.w;
static double get avatarMdR => 48.w;
static double get avatarLgR => 64.w;
static double get avatarXlR => 96.w;
// ============ ============
static const double cardPadding = 16.0;
static const double cardRadius = 12.0;
//
static const double bottomNavHeight = 56.0;
static double get cardPaddingR => 16.w;
static double get cardRadiusR => 12.r;
// AppBar
// ============ ============
static const double bottomNavHeight = 65.0;
static const double appBarHeight = 56.0;
static double get bottomNavHeightR => 65.h;
static double get appBarHeightR => 56.h;
// ============ ( 48x48) ============
static const double minTouchTarget = 48.0;
static double get minTouchTargetR => 48.w;
}

View File

@ -0,0 +1,442 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import '../../../../routes/route_paths.dart';
import '../providers/auth_provider.dart';
///
class GuidePageData {
final String? imagePath;
final String title;
final String subtitle;
final Widget? customContent;
const GuidePageData({
this.imagePath,
required this.title,
required this.subtitle,
this.customContent,
});
}
/// -
///
class GuidePage extends ConsumerStatefulWidget {
const GuidePage({super.key});
@override
ConsumerState<GuidePage> createState() => _GuidePageState();
}
class _GuidePageState extends ConsumerState<GuidePage> {
final PageController _pageController = PageController();
int _currentPage = 0;
// 1-4
final List<GuidePageData> _guidePages = const [
GuidePageData(
imagePath: 'assets/images/guide_1.png',
title: '认种一棵榴莲树\n拥有真实RWA资产',
subtitle: '绑定真实果园20年收益让区块链与农业完美结合',
),
GuidePageData(
imagePath: 'assets/images/guide_2.png',
title: '认种即可开启算力\n自动挖矿持续收益',
subtitle: '每一棵树都对应真实资产注入,为算力提供真实价值支撑',
),
GuidePageData(
imagePath: 'assets/images/guide_3.png',
title: '分享链接\n获得团队算力与收益',
subtitle: '真实认种数据透明可信 · 团队越大算力越强',
),
GuidePageData(
imagePath: 'assets/images/guide_4.png',
title: 'MPC多方安全\n所有地址与收益可审计',
subtitle: '你的资产 · 安全透明 · 不可被篡改',
),
];
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _onPageChanged(int page) {
setState(() {
_currentPage = page;
});
}
void _goToNextPage() {
if (_currentPage < 4) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _goToOnboarding() async {
//
await ref.read(authProvider.notifier).markGuideAsSeen();
if (!mounted) return;
context.go(RoutePaths.onboarding);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: PageView.builder(
controller: _pageController,
onPageChanged: _onPageChanged,
itemCount: 5, // 4 + 1
itemBuilder: (context, index) {
if (index < 4) {
return _buildGuidePage(_guidePages[index], index);
} else {
return _buildWelcomePage();
}
},
),
),
);
}
/// (1-4)
Widget _buildGuidePage(GuidePageData data, int index) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
SizedBox(height: 64.h),
//
Expanded(
flex: 5,
child: Container(
width: 312.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
color: const Color(0xFFFFF8E7),
),
child: data.imagePath != null
? ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Image.asset(
data.imagePath!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholderImage(index);
},
),
)
: _buildPlaceholderImage(index),
),
),
SizedBox(height: 48.h),
//
Text(
data.title,
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
height: 1.33,
color: const Color(0xFF292524),
),
textAlign: TextAlign.center,
),
SizedBox(height: 16.h),
//
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Text(
data.subtitle,
style: TextStyle(
fontSize: 14.sp,
height: 1.43,
color: const Color(0xFF57534E),
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 48.h),
//
_buildPageIndicator(),
SizedBox(height: 80.h),
],
),
);
}
///
Widget _buildPlaceholderImage(int index) {
final icons = [
Icons.nature,
Icons.memory,
Icons.people,
Icons.security,
];
final colors = [
const Color(0xFF8BC34A),
const Color(0xFFD4AF37),
const Color(0xFFFF9800),
const Color(0xFF2196F3),
];
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icons[index],
size: 80.sp,
color: colors[index],
),
SizedBox(height: 16.h),
Text(
'向导页 ${index + 1}',
style: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF57534E),
),
),
],
),
);
}
/// (5)
Widget _buildWelcomePage() {
return _WelcomePageContent(
onNext: _goToOnboarding,
);
}
///
Widget _buildPageIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
final isActive = index == _currentPage;
return Container(
width: 8.w,
height: 8.w,
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive
? const Color(0xFF8E794A)
: const Color(0xFFEAE0CD),
),
);
}),
);
}
}
/// (5)
class _WelcomePageContent extends StatefulWidget {
final VoidCallback onNext;
const _WelcomePageContent({required this.onNext});
@override
State<_WelcomePageContent> createState() => _WelcomePageContentState();
}
class _WelcomePageContentState extends State<_WelcomePageContent> {
bool _hasReferrer = true;
final TextEditingController _referralCodeController = TextEditingController();
@override
void dispose() {
_referralCodeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
// 退
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.only(top: 32.h),
child: GestureDetector(
onTap: () {
// 退
Navigator.of(context).maybePop();
},
child: Text(
'退出 Exit',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFFA99F93),
),
),
),
),
),
SizedBox(height: 100.h),
//
Text(
'欢迎加入',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
height: 1.33,
color: const Color(0xFF6F6354),
),
),
SizedBox(height: 12.h),
//
Text(
'创建账号前的最后一步 · 请选择是否有推荐人',
style: TextStyle(
fontSize: 14.sp,
height: 1.43,
color: const Color(0xFFA99F93),
),
),
SizedBox(height: 62.h),
//
_buildReferrerOptions(),
const Spacer(),
//
GestureDetector(
onTap: widget.onNext,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Text(
'下一步 (创建账号)',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
height: 1.5,
color: const Color(0xFFD9C8A9),
),
textAlign: TextAlign.right,
),
),
),
SizedBox(height: 80.h),
],
),
);
}
///
Widget _buildReferrerOptions() {
return Column(
children: [
//
GestureDetector(
onTap: () {
setState(() {
_hasReferrer = true;
});
},
child: Row(
children: [
_buildRadio(_hasReferrer),
SizedBox(width: 12.w),
Text(
'我有推荐人',
style: TextStyle(
fontSize: 16.sp,
height: 1.5,
color: const Color(0xFF6F6354),
),
),
const Spacer(),
//
Icon(
Icons.qr_code_scanner,
size: 24.sp,
color: const Color(0xFFA99F93),
),
],
),
),
SizedBox(height: 14.h),
//
if (_hasReferrer)
Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Color(0xFFEAE1D2),
),
),
),
child: TextField(
controller: _referralCodeController,
decoration: InputDecoration(
hintText: '请输入推荐码 / 序列号',
hintStyle: TextStyle(
fontSize: 16.sp,
color: const Color(0xFFA99F93),
),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
style: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF6F6354),
),
),
),
SizedBox(height: 24.h),
//
GestureDetector(
onTap: () {
setState(() {
_hasReferrer = false;
});
},
child: Row(
children: [
_buildRadio(!_hasReferrer),
SizedBox(width: 12.w),
Text(
'我没有推荐人',
style: TextStyle(
fontSize: 16.sp,
height: 1.5,
color: const Color(0xFF6F6354),
),
),
],
),
),
],
);
}
///
Widget _buildRadio(bool isSelected) {
return Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: isSelected ? 6.w : 2.w,
color: isSelected
? const Color(0xFF2563EB)
: const Color(0xFFA99F93),
),
),
);
}
}

View File

@ -36,8 +36,11 @@ class _SplashPageState extends ConsumerState<SplashPage> {
if (authState.isWalletCreated) {
//
context.go(RoutePaths.ranking);
} else if (authState.isFirstLaunch || !authState.hasSeenGuide) {
//
context.go(RoutePaths.guide);
} else {
//
//
context.go(RoutePaths.onboarding);
}
}

View File

@ -14,12 +14,16 @@ class AuthState {
final AuthStatus status;
final String? walletAddress;
final bool isWalletCreated;
final bool isFirstLaunch;
final bool hasSeenGuide;
final String? errorMessage;
const AuthState({
this.status = AuthStatus.initial,
this.walletAddress,
this.isWalletCreated = false,
this.isFirstLaunch = true,
this.hasSeenGuide = false,
this.errorMessage,
});
@ -27,12 +31,16 @@ class AuthState {
AuthStatus? status,
String? walletAddress,
bool? isWalletCreated,
bool? isFirstLaunch,
bool? hasSeenGuide,
String? errorMessage,
}) {
return AuthState(
status: status ?? this.status,
walletAddress: walletAddress ?? this.walletAddress,
isWalletCreated: isWalletCreated ?? this.isWalletCreated,
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
hasSeenGuide: hasSeenGuide ?? this.hasSeenGuide,
errorMessage: errorMessage,
);
}
@ -47,6 +55,11 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith(status: AuthStatus.checking);
try {
//
final isFirstLaunchStr = await _secureStorage.read(key: StorageKeys.isFirstLaunch);
final isFirstLaunch = isFirstLaunchStr == null || isFirstLaunchStr != 'false';
//
final walletAddress = await _secureStorage.read(key: StorageKeys.walletAddress);
final isWalletCreated = walletAddress != null && walletAddress.isNotEmpty;
@ -55,11 +68,15 @@ class AuthNotifier extends StateNotifier<AuthState> {
status: AuthStatus.authenticated,
walletAddress: walletAddress,
isWalletCreated: true,
isFirstLaunch: false,
hasSeenGuide: true,
);
} else {
state = state.copyWith(
status: AuthStatus.unauthenticated,
isWalletCreated: false,
isFirstLaunch: isFirstLaunch,
hasSeenGuide: !isFirstLaunch,
);
}
} catch (e) {
@ -70,6 +87,15 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
}
///
Future<void> markGuideAsSeen() async {
await _secureStorage.write(key: StorageKeys.isFirstLaunch, value: 'false');
state = state.copyWith(
isFirstLaunch: false,
hasSeenGuide: true,
);
}
Future<void> saveWallet(String walletAddress, String privateKey) async {
await _secureStorage.write(key: StorageKeys.walletAddress, value: walletAddress);
await _secureStorage.write(key: StorageKeys.privateKey, value: privateKey);

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
///
/// Tab
/// 使
class BottomNavBar extends StatelessWidget {
final int currentIndex;
final Function(int) onTap;
@ -15,7 +17,7 @@ class BottomNavBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 65,
height: 65.h,
decoration: const BoxDecoration(
color: Color(0xFFFFF5E6),
border: Border(
@ -77,14 +79,14 @@ class BottomNavBar extends StatelessWidget {
children: [
Icon(
isSelected ? activeIcon : icon,
size: 24,
size: 24.sp,
color: isSelected ? const Color(0xFFD4AF37) : const Color(0xFF8B5A2B),
),
const SizedBox(height: 2),
SizedBox(height: 2.h),
Text(
label,
style: TextStyle(
fontSize: 12,
fontSize: 12.sp,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.33,

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../features/auth/presentation/pages/splash_page.dart';
import '../features/auth/presentation/pages/guide_page.dart';
import '../features/auth/presentation/pages/onboarding_page.dart';
import '../features/auth/presentation/pages/backup_mnemonic_page.dart';
import '../features/auth/presentation/pages/verify_mnemonic_page.dart';
@ -67,6 +68,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const SplashPage(),
),
// Guide Pages ()
GoRoute(
path: RoutePaths.guide,
name: RouteNames.guide,
builder: (context, state) => const GuidePage(),
),
// Onboarding / Create Account
GoRoute(
path: RoutePaths.onboarding,

View File

@ -3,6 +3,7 @@ class RouteNames {
// Auth
static const splash = 'splash';
static const guide = 'guide';
static const onboarding = 'onboarding';
static const createWallet = 'create-wallet';
static const backupMnemonic = 'backup-mnemonic';

View File

@ -3,6 +3,7 @@ class RoutePaths {
// Auth
static const splash = '/';
static const guide = '/guide';
static const onboarding = '/onboarding';
static const createWallet = '/auth/create';
static const backupMnemonic = '/auth/backup-mnemonic';

View File

@ -169,6 +169,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@ -398,6 +406,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@ -605,6 +621,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_picker:
dependency: "direct main"
description:

View File

@ -75,6 +75,16 @@ dev_dependencies:
# 测试
mocktail: ^1.0.3
# 应用图标生成
flutter_launcher_icons: ^0.14.3
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/images/logo/app_icon.png"
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/images/logo/app_icon.png"
flutter:
uses-material-design: true