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>
|
|
@ -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
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 27 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 424 KiB |
|
|
@ -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++";
|
||||
|
|
|
|||
|
|
@ -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"}}
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 440 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 748 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 22 KiB |
|
|
@ -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!,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||