From b63414542b73b9491ff1936ed39dad31583e3a8f Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 11 Feb 2026 17:57:16 -0800 Subject: [PATCH] feat: Create genex-mobile app with coupon lifecycle management redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基于 frontend/mobile 创建全新的 genex-mobile 应用,重新设计为券的生命周期管理平台。 ## 底部导航重构 (5 tabs → 4 tabs) - 首页 / 交易 / 消息 / 我的 - 移除独立的"我的券"Tab,功能合并到首页券钱包中 - "市场"重命名为"交易",图标改为行情图标 ## 首页改造 ### 券钱包(替代原Banner轮播区域) - 紫色渐变卡片展示"我的钱包",含券数量统计(可使用/待核销/已过期) - 水平滚动的券迷你卡片列表,支持Tab筛选(全部/可使用/待核销/已过期) - 快捷操作栏:接收 / 转赠 / 出售 / 核销 - 接收功能:点击弹出底部Sheet,展示接收ID和接收二维码 - 对方可通过扫码或输入ID转赠券到钱包 - 接收ID支持一键复制 ### 分类网格重新设计(从商品品类改为省钱机制) - 原8个商品分类(餐饮/购物/娱乐/出行/生活/品牌/折扣/全部) - 改为6个省钱导向分类:限时抢购 / 新券首发 / 折扣排行 / 即将到期 / 比价 / 全部分类 - 3列×2行布局,每个入口强调"怎么省"而非"卖什么" ### 其他区域保持不变 - AI智能推荐卡片 - 精选好券列表 - AI FAB浮动按钮 ## 交易页(币安交易所风格) ### 一级市场(打新申购 / Launchpad风格) - 券发行卡片:品牌信息 / 发行价 / 面值 / 折扣 / 发行量 - 销售进度条和百分比 - 状态标签:即将开始(含倒计时)/ 申购中 / 已结束 ### 二级市场(交易所行情列表) - 交易对分类Tab:券/法币 | 券/数字货币 | 券/稳定币 | 收藏 - 行情列表:交易对名称 / 最新价格 / 24h涨跌幅(红绿色块) - 成交量和USD等价显示 - 支持的交易对示例:SBUX/USD, NIKE/BTC, AMZN/USDT 等 ### 交易对详情页(K线 + 盘口 + 下单) - 价格头部:当前价 / 24h涨跌 / OHLC数据(高/低/开盘/成交量) - K线图(含模拟蜡烛图渲染 + 成交量柱状图) - 时间周期选择器:1m / 5m / 15m / 1h / 4h / 1D / 1W - 交易深度:买卖盘口(Bid/Ask)带深度条可视化 - 下单表单:买入/卖出切换 / 限价单/市价单 / 价格数量输入 / 比例快选(25%/50%/75%/100%) - 当前委托和历史委托列表 - 底部买入/卖出快捷按钮 ## 券详情页增强 - 新增"附近可用门店"区域(LBS定位功能入口) - 展示附近门店列表:门店名 / 距离 / 营业状态 ## 技术细节 - 保持原有设计系统:紫色主色调 #6C5CE7 / Material 3 / 亮色模式 - Flutter analyze 零错误通过 - 所有新增页面使用 mock 数据,便于后续接入真实API Co-Authored-By: Claude Opus 4.6 --- frontend/genex-mobile/.gitignore | 56 ++ frontend/genex-mobile/.metadata | 33 + frontend/genex-mobile/README.md | 16 + frontend/genex-mobile/analysis_options.yaml | 1 + frontend/genex-mobile/android/.gitignore | 14 + .../genex-mobile/android/app/build.gradle.kts | 72 ++ .../android/app/proguard-rules.pro | 8 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 46 + .../cn/gogenex/genex_consumer/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../genex-mobile/android/build.gradle.kts | 24 + .../genex-mobile/android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../genex-mobile/android/settings.gradle.kts | 26 + frontend/genex-mobile/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../genex-mobile/ios/Flutter/Debug.xcconfig | 1 + .../genex-mobile/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 616 ++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../genex-mobile/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + frontend/genex-mobile/ios/Runner/Info.plist | 49 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../ios/RunnerTests/RunnerTests.swift | 12 + .../lib/app/i18n/app_localizations.dart | 302 ++++++ frontend/genex-mobile/lib/app/main_shell.dart | 89 ++ .../lib/app/theme/app_colors.dart | 123 +++ .../lib/app/theme/app_spacing.dart | 116 +++ .../genex-mobile/lib/app/theme/app_theme.dart | 230 +++++ .../lib/app/theme/app_typography.dart | 159 +++ .../presentation/pages/agent_chat_page.dart | 171 ++++ .../ai_agent/presentation/widgets/ai_fab.dart | 276 ++++++ .../pages/forgot_password_page.dart | 213 ++++ .../auth/presentation/pages/login_page.dart | 213 ++++ .../presentation/pages/register_page.dart | 286 ++++++ .../auth/presentation/pages/welcome_page.dart | 184 ++++ .../pages/coupon_detail_page.dart | 503 ++++++++++ .../coupons/presentation/pages/home_page.dart | 651 ++++++++++++ .../presentation/pages/market_page.dart | 645 ++++++++++++ .../pages/my_coupon_detail_page.dart | 275 ++++++ .../presentation/pages/my_coupons_page.dart | 249 +++++ .../pages/order_confirm_page.dart | 376 +++++++ .../presentation/pages/payment_page.dart | 144 +++ .../pages/payment_success_page.dart | 126 +++ .../presentation/pages/redeem_qr_page.dart | 146 +++ .../presentation/pages/search_page.dart | 150 +++ .../widgets/receive_coupon_sheet.dart | 222 +++++ .../presentation/pages/issuer_main_page.dart | 732 ++++++++++++++ .../pages/merchant_ai_assistant_page.dart | 930 ++++++++++++++++++ .../pages/merchant_home_page.dart | 669 +++++++++++++ .../pages/message_detail_page.dart | 146 +++ .../presentation/pages/message_page.dart | 215 ++++ .../profile/presentation/pages/kyc_page.dart | 167 ++++ .../pages/payment_management_page.dart | 136 +++ .../presentation/pages/pro_mode_page.dart | 456 +++++++++ .../presentation/pages/profile_page.dart | 231 +++++ .../presentation/pages/settings_page.dart | 111 +++ .../presentation/pages/sell_order_page.dart | 189 ++++ .../pages/trading_detail_page.dart | 796 +++++++++++++++ .../presentation/pages/trading_page.dart | 203 ++++ .../presentation/pages/transfer_page.dart | 211 ++++ .../presentation/pages/deposit_page.dart | 141 +++ .../pages/transaction_records_page.dart | 132 +++ .../presentation/pages/wallet_page.dart | 193 ++++ .../presentation/pages/withdraw_page.dart | 144 +++ frontend/genex-mobile/lib/main.dart | 120 +++ .../lib/shared/widgets/ai_confirm_dialog.dart | 317 ++++++ .../lib/shared/widgets/confirm_sheet.dart | 187 ++++ .../lib/shared/widgets/coupon_card.dart | 344 +++++++ .../lib/shared/widgets/credit_badge.dart | 81 ++ .../lib/shared/widgets/empty_state.dart | 104 ++ .../lib/shared/widgets/genex_button.dart | 161 +++ .../lib/shared/widgets/kyc_badge.dart | 60 ++ .../lib/shared/widgets/price_tag.dart | 101 ++ .../lib/shared/widgets/skeleton_loader.dart | 121 +++ .../lib/shared/widgets/status_tag.dart | 85 ++ frontend/genex-mobile/pubspec.yaml | 19 + 116 files changed, 14846 insertions(+) create mode 100644 frontend/genex-mobile/.gitignore create mode 100644 frontend/genex-mobile/.metadata create mode 100644 frontend/genex-mobile/README.md create mode 100644 frontend/genex-mobile/analysis_options.yaml create mode 100644 frontend/genex-mobile/android/.gitignore create mode 100644 frontend/genex-mobile/android/app/build.gradle.kts create mode 100644 frontend/genex-mobile/android/app/proguard-rules.pro create mode 100644 frontend/genex-mobile/android/app/src/debug/AndroidManifest.xml create mode 100644 frontend/genex-mobile/android/app/src/main/AndroidManifest.xml create mode 100644 frontend/genex-mobile/android/app/src/main/kotlin/cn/gogenex/genex_consumer/MainActivity.kt create mode 100644 frontend/genex-mobile/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 frontend/genex-mobile/android/app/src/main/res/drawable/launch_background.xml create mode 100644 frontend/genex-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 frontend/genex-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 frontend/genex-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 frontend/genex-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 frontend/genex-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 frontend/genex-mobile/android/app/src/main/res/values-night/styles.xml create mode 100644 frontend/genex-mobile/android/app/src/main/res/values/styles.xml create mode 100644 frontend/genex-mobile/android/app/src/profile/AndroidManifest.xml create mode 100644 frontend/genex-mobile/android/build.gradle.kts create mode 100644 frontend/genex-mobile/android/gradle.properties create mode 100644 frontend/genex-mobile/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 frontend/genex-mobile/android/settings.gradle.kts create mode 100644 frontend/genex-mobile/ios/.gitignore create mode 100644 frontend/genex-mobile/ios/Flutter/AppFrameworkInfo.plist create mode 100644 frontend/genex-mobile/ios/Flutter/Debug.xcconfig create mode 100644 frontend/genex-mobile/ios/Flutter/Release.xcconfig create mode 100644 frontend/genex-mobile/ios/Runner.xcodeproj/project.pbxproj create mode 100644 frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 frontend/genex-mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 frontend/genex-mobile/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 frontend/genex-mobile/ios/Runner/AppDelegate.swift create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 frontend/genex-mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 frontend/genex-mobile/ios/Runner/Base.lproj/Main.storyboard create mode 100644 frontend/genex-mobile/ios/Runner/Info.plist create mode 100644 frontend/genex-mobile/ios/Runner/Runner-Bridging-Header.h create mode 100644 frontend/genex-mobile/ios/RunnerTests/RunnerTests.swift create mode 100644 frontend/genex-mobile/lib/app/i18n/app_localizations.dart create mode 100644 frontend/genex-mobile/lib/app/main_shell.dart create mode 100644 frontend/genex-mobile/lib/app/theme/app_colors.dart create mode 100644 frontend/genex-mobile/lib/app/theme/app_spacing.dart create mode 100644 frontend/genex-mobile/lib/app/theme/app_theme.dart create mode 100644 frontend/genex-mobile/lib/app/theme/app_typography.dart create mode 100644 frontend/genex-mobile/lib/features/ai_agent/presentation/pages/agent_chat_page.dart create mode 100644 frontend/genex-mobile/lib/features/ai_agent/presentation/widgets/ai_fab.dart create mode 100644 frontend/genex-mobile/lib/features/auth/presentation/pages/forgot_password_page.dart create mode 100644 frontend/genex-mobile/lib/features/auth/presentation/pages/login_page.dart create mode 100644 frontend/genex-mobile/lib/features/auth/presentation/pages/register_page.dart create mode 100644 frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/coupon_detail_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/home_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/market_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupons_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/order_confirm_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_success_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/redeem_qr_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/pages/search_page.dart create mode 100644 frontend/genex-mobile/lib/features/coupons/presentation/widgets/receive_coupon_sheet.dart create mode 100644 frontend/genex-mobile/lib/features/issuer/presentation/pages/issuer_main_page.dart create mode 100644 frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_ai_assistant_page.dart create mode 100644 frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_home_page.dart create mode 100644 frontend/genex-mobile/lib/features/message/presentation/pages/message_detail_page.dart create mode 100644 frontend/genex-mobile/lib/features/message/presentation/pages/message_page.dart create mode 100644 frontend/genex-mobile/lib/features/profile/presentation/pages/kyc_page.dart create mode 100644 frontend/genex-mobile/lib/features/profile/presentation/pages/payment_management_page.dart create mode 100644 frontend/genex-mobile/lib/features/profile/presentation/pages/pro_mode_page.dart create mode 100644 frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart create mode 100644 frontend/genex-mobile/lib/features/profile/presentation/pages/settings_page.dart create mode 100644 frontend/genex-mobile/lib/features/trading/presentation/pages/sell_order_page.dart create mode 100644 frontend/genex-mobile/lib/features/trading/presentation/pages/trading_detail_page.dart create mode 100644 frontend/genex-mobile/lib/features/trading/presentation/pages/trading_page.dart create mode 100644 frontend/genex-mobile/lib/features/trading/presentation/pages/transfer_page.dart create mode 100644 frontend/genex-mobile/lib/features/wallet/presentation/pages/deposit_page.dart create mode 100644 frontend/genex-mobile/lib/features/wallet/presentation/pages/transaction_records_page.dart create mode 100644 frontend/genex-mobile/lib/features/wallet/presentation/pages/wallet_page.dart create mode 100644 frontend/genex-mobile/lib/features/wallet/presentation/pages/withdraw_page.dart create mode 100644 frontend/genex-mobile/lib/main.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/ai_confirm_dialog.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/confirm_sheet.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/coupon_card.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/credit_badge.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/empty_state.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/genex_button.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/kyc_badge.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/price_tag.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/skeleton_loader.dart create mode 100644 frontend/genex-mobile/lib/shared/widgets/status_tag.dart create mode 100644 frontend/genex-mobile/pubspec.yaml diff --git a/frontend/genex-mobile/.gitignore b/frontend/genex-mobile/.gitignore new file mode 100644 index 0000000..96352b9 --- /dev/null +++ b/frontend/genex-mobile/.gitignore @@ -0,0 +1,56 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Signing keys (do NOT commit to public repos) +android/key.properties +android/app/keystore/ +*.jks +*.keystore + +# Flutter generated +.flutter-plugins +.dart_tool/ +pubspec.lock diff --git a/frontend/genex-mobile/.metadata b/frontend/genex-mobile/.metadata new file mode 100644 index 0000000..54737a7 --- /dev/null +++ b/frontend/genex-mobile/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "19074d12f7eaf6a8180cd4036a430c1d76de904e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: android + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: ios + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/genex-mobile/README.md b/frontend/genex-mobile/README.md new file mode 100644 index 0000000..4841360 --- /dev/null +++ b/frontend/genex-mobile/README.md @@ -0,0 +1,16 @@ +# genex_consumer + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/frontend/genex-mobile/analysis_options.yaml b/frontend/genex-mobile/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/frontend/genex-mobile/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/frontend/genex-mobile/android/.gitignore b/frontend/genex-mobile/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/frontend/genex-mobile/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/frontend/genex-mobile/android/app/build.gradle.kts b/frontend/genex-mobile/android/app/build.gradle.kts new file mode 100644 index 0000000..8a5a83f --- /dev/null +++ b/frontend/genex-mobile/android/app/build.gradle.kts @@ -0,0 +1,72 @@ +import java.util.Properties +import java.io.FileInputStream + +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "cn.gogenex.genex_consumer" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + applicationId = "cn.gogenex.consumer" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + create("release") { + keyAlias = keystoreProperties["keyAlias"] as String? + keyPassword = keystoreProperties["keyPassword"] as String? + storeFile = keystoreProperties["storeFile"]?.let { file(it as String) } + storePassword = keystoreProperties["storePassword"] as String? + } + } + + buildTypes { + getByName("debug") { + isDebuggable = true + applicationIdSuffix = ".debug" + versionNameSuffix = "-debug" + } + getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + signingConfig = if (keystorePropertiesFile.exists()) { + signingConfigs.getByName("release") + } else { + signingConfigs.getByName("debug") + } + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } +} + +flutter { + source = "../.." +} diff --git a/frontend/genex-mobile/android/app/proguard-rules.pro b/frontend/genex-mobile/android/app/proguard-rules.pro new file mode 100644 index 0000000..d6d76a5 --- /dev/null +++ b/frontend/genex-mobile/android/app/proguard-rules.pro @@ -0,0 +1,8 @@ +# Flutter +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } +-dontwarn io.flutter.embedding.** diff --git a/frontend/genex-mobile/android/app/src/debug/AndroidManifest.xml b/frontend/genex-mobile/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/frontend/genex-mobile/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/genex-mobile/android/app/src/main/AndroidManifest.xml b/frontend/genex-mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..304fac0 --- /dev/null +++ b/frontend/genex-mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/genex-mobile/android/app/src/main/kotlin/cn/gogenex/genex_consumer/MainActivity.kt b/frontend/genex-mobile/android/app/src/main/kotlin/cn/gogenex/genex_consumer/MainActivity.kt new file mode 100644 index 0000000..8e305e3 --- /dev/null +++ b/frontend/genex-mobile/android/app/src/main/kotlin/cn/gogenex/genex_consumer/MainActivity.kt @@ -0,0 +1,5 @@ +package cn.gogenex.genex_consumer + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/frontend/genex-mobile/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/genex-mobile/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/frontend/genex-mobile/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/genex-mobile/android/app/src/main/res/drawable/launch_background.xml b/frontend/genex-mobile/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/frontend/genex-mobile/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/genex-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/genex-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/genex-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/genex-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/genex-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/android/app/src/main/res/values-night/styles.xml b/frontend/genex-mobile/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/frontend/genex-mobile/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/genex-mobile/android/app/src/main/res/values/styles.xml b/frontend/genex-mobile/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/frontend/genex-mobile/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/genex-mobile/android/app/src/profile/AndroidManifest.xml b/frontend/genex-mobile/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/frontend/genex-mobile/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/genex-mobile/android/build.gradle.kts b/frontend/genex-mobile/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/frontend/genex-mobile/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/frontend/genex-mobile/android/gradle.properties b/frontend/genex-mobile/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/frontend/genex-mobile/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/frontend/genex-mobile/android/gradle/wrapper/gradle-wrapper.properties b/frontend/genex-mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/frontend/genex-mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/frontend/genex-mobile/android/settings.gradle.kts b/frontend/genex-mobile/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/frontend/genex-mobile/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/frontend/genex-mobile/ios/.gitignore b/frontend/genex-mobile/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/frontend/genex-mobile/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/frontend/genex-mobile/ios/Flutter/AppFrameworkInfo.plist b/frontend/genex-mobile/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/frontend/genex-mobile/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/frontend/genex-mobile/ios/Flutter/Debug.xcconfig b/frontend/genex-mobile/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/frontend/genex-mobile/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/frontend/genex-mobile/ios/Flutter/Release.xcconfig b/frontend/genex-mobile/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/frontend/genex-mobile/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/frontend/genex-mobile/ios/Runner.xcodeproj/project.pbxproj b/frontend/genex-mobile/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..efab12a --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = cn.gogenex.genexConsumer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cn.gogenex.genexConsumer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cn.gogenex.genexConsumer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cn.gogenex.genexConsumer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = cn.gogenex.genexConsumer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = cn.gogenex.genexConsumer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/genex-mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/genex-mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/genex-mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/genex-mobile/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/frontend/genex-mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/genex-mobile/ios/Runner/AppDelegate.swift b/frontend/genex-mobile/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "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" + } +} diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_xN#0001NP)t-s|Ns9~ z#rXRE|M&d=0au&!`~QyF`q}dRnBDt}*!qXo`c{v z{Djr|@Adh0(D_%#_&mM$D6{kE_x{oE{l@J5@%H*?%=t~i_`ufYOPkAEn!pfkr2$fs z652Tz0001XNklqeeKN4RM4i{jKqmiC$?+xN>3Apn^ z0QfuZLym_5b<*QdmkHjHlj811{If)dl(Z2K0A+ekGtrFJb?g|wt#k#pV-#A~bK=OT ts8>{%cPtyC${m|1#B1A6#u!Q;umknL1chzTM$P~L002ovPDHLkV1lTfnu!1a literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 zcmV;526g#~P){YQnis^a@{&-nmRmq)<&%Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b zE>q9${Lb$D&h7k=+4=!ek^n+`0zq>LL1O?lVyea53S5x`Nqqo2YyeuIrQrJj9XjOp z{;T5qbj3}&1vg1VK~#9!?b~^C5-}JC@Pyrv-6dSEqJqT}#j9#dJ@GzT@B8}x zU&J@bBI>f6w6en+CeI)3^kC*U?}X%OD8$Fd$H&LV$H&LV$H&LV#|K5~mLYf|VqzOc zkc7qL~0sOYuM{tG`rYEDV{DWY`Z8&)kW*hc2VkBuY+^Yx&92j&StN}Wp=LD zxoGxXw6f&8sB^u})h@b@z0RBeD`K7RMR9deyL(ZJu#39Z>rT)^>v}Khq8U-IbIvT> z?4pV9qGj=2)TNH3d)=De<+^w;>S7m_eFKTvzeaBeir45xY!^m!FmxnljbSS_3o=g( z->^wC9%qkR{kbGnW8MfFew_o9h3(r55Is`L$8KI@d+*%{=Nx+FXJ98L0PjFIu;rGnnfY zn1R5Qnp<{Jq0M1vX=X&F8gtLmcWv$1*M@4ZfF^9``()#hGTeKeP`1!iED ztNE(TN}M5}3Bbc*d=FIv`DNv&@|C6yYj{sSqUj5oo$#*0$7pu|Dd2TLI>t5%I zIa4Dvr(iayb+5x=j*Vum9&irk)xV1`t509lnPO0%skL8_1c#Xbamh(2@f?4yUI zhhuT5<#8RJhGz4%b$`PJwKPAudsm|at?u;*hGgnA zU1;9gnxVBC)wA(BsB`AW54N{|qmikJR*%x0c`{LGsSfa|NK61pYH(r-UQ4_JXd!Rsz)=k zL{GMc5{h138)fF5CzHEDM>+FqY)$pdN3}Ml+riTgJOLN0F*Vh?{9ESR{SVVg>*>=# zix;VJHPtvFFCRY$Ks*F;VX~%*r9F)W`PmPE9F!(&s#x07n2<}?S{(ygpXgX-&B&OM zONY&BRQ(#%0%jeQs?oJ4P!p*R98>qCy5p8w>_gpuh39NcOlp)(wOoz0sY-Qz55eB~ z7OC-fKBaD1sE3$l-6QgBJO!n?QOTza`!S_YK z_v-lm^7{VO^8Q@M_^8F)09Ki6%=s?2_5eupee(w1FB%aqSweusQ-T+CH0Xt{` zFjMvW{@C&TB)k25()nh~_yJ9coBRL(0oO@HK~z}7?bm5j;y@69;bvlHb2tf!$ReA~x{22wTq550 z?f?Hnw(;m3ip30;QzdV~7pi!wyMYhDtXW#cO7T>|f=bdFhu+F!zMZ2UFj;GUKX7tI z;hv3{q~!*pMj75WP_c}>6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FaF{8 z;u`Mw0ly(uE>*CgQYv{be6ab2LWhlaH1^iLIM{olnag$78^Fd}%dR7;JECQ+hmk|o z!u2&!3MqPfP5ChDSkFSH8F2WVOEf0(E_M(JL17G}Y+fg0_IuW%WQ zG(mG&u?|->YSdk0;8rc{yw2@2Z&GA}z{Wb91Ooz9VhA{b2DYE7RmG zjL}?eq#iX%3#k;JWMx_{^2nNax`xPhByFiDX+a7uTGU|otOvIAUy|dEKkXOm-`aWS z27pUzD{a)Ct<6p{{3)+lq@i`t@%>-wT4r?*S}k)58e09WZYP0{{R3FC5Sl00039P)t-s|Ns9~ z#rP?<_5oL$Q^olD{r_0T`27C={r>*`|Nj71npVa5OTzc(_WfbW_({R{p56NV{r*M2 z_xt?)2V0#0NsfV0u>{42ctGP(8vQj-Btk1n|O0ZD=YLwd&R{Ko41Gr9H= zY@z@@bOAMB5Ltl$E>bJJ{>JP30ZxkmI%?eW{k`b?Wy<&gOo;dS`~CR$Vwb@XWtR|N zi~t=w02?-0&j0TD{>bb6sNwsK*!p?V`RMQUl(*DVjk-9Cx+-z1KXab|Ka2oXhX5f% z`$|e!000AhNklrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`? zTG`AHia671e^vgmp!llKp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?hz91 z7p83F3%LVu9;S$tSI$C^%^yud1dfTM_6p2|+5Ejp$bd`GDvbR|xit>i!ZD&F>@CJrPmu*UjD&?DfZs=$@e3FQA(vNiU+$A*%a} z?`XcG2jDxJ_ZQ#Md`H{4Lpf6QBDp81_KWZ6Tk#yCy1)32zO#3<7>b`eT7UyYH1eGz z;O(rH$=QR*L%%ZcBpc=eGua?N55nD^K(8<#gl2+pN_j~b2MHs4#mcLmv%DkspS-3< zpI1F=^9siI0s-;IN_IrA;5xm~3?3!StX}pUv0vkxMaqm+zxrg7X7(I&*N~&dEd0kD z-FRV|g=|QuUsuh>-xCI}vD2imzYIOIdcCVV=$Bz@*u0+Bs<|L^)32nN*=wu3n%Ynw z@1|eLG>!8ruU1pFXUfb`j>(=Gy~?Rn4QJ-c3%3T|(Frd!bI`9u&zAnyFYTqlG#&J7 zAkD(jpw|oZLNiA>;>hgp1KX7-wxC~31II47gc zHcehD6Uxlf%+M^^uN5Wc*G%^;>D5qT{>=uxUhX%WJu^Z*(_Wq9y}npFO{Hhb>s6<9 zNi0pHXWFaVZnb)1+RS&F)xOv6&aeILcI)`k#0YE+?e)5&#r7J#c`3Z7x!LpTc01dx zrdC3{Z;joZ^KN&))zB_i)I9fWedoN>Zl-6_Iz+^G&*ak2jpF07*qoM6N<$f;w%0(f|Me literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/frontend/genex-mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/genex-mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/genex-mobile/ios/Runner/Base.lproj/Main.storyboard b/frontend/genex-mobile/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/genex-mobile/ios/Runner/Info.plist b/frontend/genex-mobile/ios/Runner/Info.plist new file mode 100644 index 0000000..741e60e --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Genex Consumer + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + genex_consumer + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/frontend/genex-mobile/ios/Runner/Runner-Bridging-Header.h b/frontend/genex-mobile/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/frontend/genex-mobile/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/frontend/genex-mobile/ios/RunnerTests/RunnerTests.swift b/frontend/genex-mobile/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/frontend/genex-mobile/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/frontend/genex-mobile/lib/app/i18n/app_localizations.dart b/frontend/genex-mobile/lib/app/i18n/app_localizations.dart new file mode 100644 index 0000000..20f07c6 --- /dev/null +++ b/frontend/genex-mobile/lib/app/i18n/app_localizations.dart @@ -0,0 +1,302 @@ +/// Genex Mobile App - i18n 多语言支持 +/// +/// 支持语言: zh-CN (默认), en-US, ja-JP +/// 使用方式: AppLocalizations.of(context).translate('key') + +class AppLocalizations { + final String locale; + + AppLocalizations(this.locale); + + static AppLocalizations of(dynamic context) { + // In production, obtain from InheritedWidget / Provider + return AppLocalizations('zh-CN'); + } + + String translate(String key) { + return _localizedValues[locale]?[key] ?? + _localizedValues['zh-CN']?[key] ?? + key; + } + + // Shorthand + String t(String key) => translate(key); + + static const supportedLocales = ['zh-CN', 'en-US', 'ja-JP']; + + static const Map> _localizedValues = { + 'zh-CN': _zhCN, + 'en-US': _enUS, + 'ja-JP': _jaJP, + }; + + static const Map _zhCN = { + // Common + 'app_name': 'Genex', + 'confirm': '确认', + 'cancel': '取消', + 'save': '保存', + 'delete': '删除', + 'edit': '编辑', + 'search': '搜索', + 'loading': '加载中...', + 'retry': '重试', + 'done': '完成', + 'next': '下一步', + 'back': '返回', + 'close': '关闭', + 'more': '更多', + 'all': '全部', + + // Tabs + 'tab_home': '首页', + 'tab_market': '市场', + 'tab_wallet': '钱包', + 'tab_profile': '我的', + + // Home + 'home_greeting': '你好', + 'home_search_hint': '搜索券、品牌...', + 'home_recommended': 'AI推荐', + 'home_hot': '热门券', + 'home_new': '新上架', + 'home_categories': '分类浏览', + + // Coupon + 'coupon_buy': '购买', + 'coupon_sell': '出售', + 'coupon_transfer': '转赠', + 'coupon_use': '使用', + 'coupon_detail': '券详情', + 'coupon_face_value': '面值', + 'coupon_price': '价格', + 'coupon_discount': '折扣', + 'coupon_valid_until': '有效期至', + 'coupon_brand': '品牌', + 'coupon_category': '类别', + 'coupon_my_coupons': '我的券', + 'coupon_available': '可用', + 'coupon_used': '已使用', + 'coupon_expired': '已过期', + + // Trading + 'trade_buy_order': '买单', + 'trade_sell_order': '卖单', + 'trade_price_input': '输入价格', + 'trade_quantity': '数量', + 'trade_total': '合计', + 'trade_history': '交易记录', + 'trade_pending': '待成交', + 'trade_completed': '已完成', + + // Wallet + 'wallet_balance': '余额', + 'wallet_deposit': '充值', + 'wallet_withdraw': '提现', + 'wallet_transactions': '交易记录', + + // Profile + 'profile_settings': '设置', + 'profile_kyc': '身份认证', + 'profile_kyc_l0': '未认证', + 'profile_kyc_l1': 'L1 基础认证', + 'profile_kyc_l2': 'L2 身份认证', + 'profile_kyc_l3': 'L3 高级认证', + 'profile_language': '语言', + 'profile_currency': '货币', + 'profile_help': '帮助中心', + 'profile_about': '关于', + 'profile_logout': '退出登录', + 'profile_pro_mode': '高级模式', + + // Payment + 'payment_method': '支付方式', + 'payment_confirm': '确认支付', + 'payment_success': '支付成功', + + // AI + 'ai_assistant': 'AI助手', + 'ai_ask': '问我任何问题...', + 'ai_suggestion': 'AI建议', + }; + + static const Map _enUS = { + // Common + 'app_name': 'Genex', + 'confirm': 'Confirm', + 'cancel': 'Cancel', + 'save': 'Save', + 'delete': 'Delete', + 'edit': 'Edit', + 'search': 'Search', + 'loading': 'Loading...', + 'retry': 'Retry', + 'done': 'Done', + 'next': 'Next', + 'back': 'Back', + 'close': 'Close', + 'more': 'More', + 'all': 'All', + + // Tabs + 'tab_home': 'Home', + 'tab_market': 'Market', + 'tab_wallet': 'Wallet', + 'tab_profile': 'Profile', + + // Home + 'home_greeting': 'Hello', + 'home_search_hint': 'Search coupons, brands...', + 'home_recommended': 'AI Picks', + 'home_hot': 'Trending', + 'home_new': 'New Arrivals', + 'home_categories': 'Categories', + + // Coupon + 'coupon_buy': 'Buy', + 'coupon_sell': 'Sell', + 'coupon_transfer': 'Gift', + 'coupon_use': 'Redeem', + 'coupon_detail': 'Coupon Details', + 'coupon_face_value': 'Face Value', + 'coupon_price': 'Price', + 'coupon_discount': 'Discount', + 'coupon_valid_until': 'Valid Until', + 'coupon_brand': 'Brand', + 'coupon_category': 'Category', + 'coupon_my_coupons': 'My Coupons', + 'coupon_available': 'Available', + 'coupon_used': 'Used', + 'coupon_expired': 'Expired', + + // Trading + 'trade_buy_order': 'Buy Order', + 'trade_sell_order': 'Sell Order', + 'trade_price_input': 'Enter Price', + 'trade_quantity': 'Quantity', + 'trade_total': 'Total', + 'trade_history': 'Trade History', + 'trade_pending': 'Pending', + 'trade_completed': 'Completed', + + // Wallet + 'wallet_balance': 'Balance', + 'wallet_deposit': 'Deposit', + 'wallet_withdraw': 'Withdraw', + 'wallet_transactions': 'Transactions', + + // Profile + 'profile_settings': 'Settings', + 'profile_kyc': 'Verification', + 'profile_kyc_l0': 'Unverified', + 'profile_kyc_l1': 'L1 Basic', + 'profile_kyc_l2': 'L2 Identity', + 'profile_kyc_l3': 'L3 Advanced', + 'profile_language': 'Language', + 'profile_currency': 'Currency', + 'profile_help': 'Help Center', + 'profile_about': 'About', + 'profile_logout': 'Log Out', + 'profile_pro_mode': 'Pro Mode', + + // Payment + 'payment_method': 'Payment Method', + 'payment_confirm': 'Confirm Payment', + 'payment_success': 'Payment Successful', + + // AI + 'ai_assistant': 'AI Assistant', + 'ai_ask': 'Ask me anything...', + 'ai_suggestion': 'AI Suggestion', + }; + + static const Map _jaJP = { + // Common + 'app_name': 'Genex', + 'confirm': '確認', + 'cancel': 'キャンセル', + 'save': '保存', + 'delete': '削除', + 'edit': '編集', + 'search': '検索', + 'loading': '読み込み中...', + 'retry': 'リトライ', + 'done': '完了', + 'next': '次へ', + 'back': '戻る', + 'close': '閉じる', + 'more': 'もっと見る', + 'all': 'すべて', + + // Tabs + 'tab_home': 'ホーム', + 'tab_market': 'マーケット', + 'tab_wallet': 'ウォレット', + 'tab_profile': 'マイページ', + + // Home + 'home_greeting': 'こんにちは', + 'home_search_hint': 'クーポン、ブランドを検索...', + 'home_recommended': 'AIおすすめ', + 'home_hot': '人気', + 'home_new': '新着', + 'home_categories': 'カテゴリー', + + // Coupon + 'coupon_buy': '購入', + 'coupon_sell': '売却', + 'coupon_transfer': '贈与', + 'coupon_use': '使用', + 'coupon_detail': 'クーポン詳細', + 'coupon_face_value': '額面', + 'coupon_price': '価格', + 'coupon_discount': '割引', + 'coupon_valid_until': '有効期限', + 'coupon_brand': 'ブランド', + 'coupon_category': 'カテゴリー', + 'coupon_my_coupons': 'マイクーポン', + 'coupon_available': '利用可能', + 'coupon_used': '使用済み', + 'coupon_expired': '期限切れ', + + // Trading + 'trade_buy_order': '買い注文', + 'trade_sell_order': '売り注文', + 'trade_price_input': '価格を入力', + 'trade_quantity': '数量', + 'trade_total': '合計', + 'trade_history': '取引履歴', + 'trade_pending': '未約定', + 'trade_completed': '約定済み', + + // Wallet + 'wallet_balance': '残高', + 'wallet_deposit': '入金', + 'wallet_withdraw': '出金', + 'wallet_transactions': '取引履歴', + + // Profile + 'profile_settings': '設定', + 'profile_kyc': '本人確認', + 'profile_kyc_l0': '未確認', + 'profile_kyc_l1': 'L1 基本認証', + 'profile_kyc_l2': 'L2 身分認証', + 'profile_kyc_l3': 'L3 高度認証', + 'profile_language': '言語', + 'profile_currency': '通貨', + 'profile_help': 'ヘルプ', + 'profile_about': 'アプリについて', + 'profile_logout': 'ログアウト', + 'profile_pro_mode': 'プロモード', + + // Payment + 'payment_method': '支払い方法', + 'payment_confirm': '支払いを確認', + 'payment_success': '支払い完了', + + // AI + 'ai_assistant': 'AIアシスタント', + 'ai_ask': '何でも聞いてください...', + 'ai_suggestion': 'AIの提案', + }; +} diff --git a/frontend/genex-mobile/lib/app/main_shell.dart b/frontend/genex-mobile/lib/app/main_shell.dart new file mode 100644 index 0000000..f2ff41d --- /dev/null +++ b/frontend/genex-mobile/lib/app/main_shell.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import '../app/theme/app_colors.dart'; +import '../features/coupons/presentation/pages/home_page.dart'; +import '../features/coupons/presentation/pages/market_page.dart'; +import '../features/message/presentation/pages/message_page.dart'; +import '../features/profile/presentation/pages/profile_page.dart'; + +/// GenexMobile主Shell - Bottom Navigation +/// +/// Tab: 首页 / 交易 / 消息 / 我的 +class MainShell extends StatefulWidget { + const MainShell({super.key}); + + @override + State createState() => _MainShellState(); +} + +class _MainShellState extends State { + int _currentIndex = 0; + + final _pages = const [ + HomePage(), + MarketPage(), + MessagePage(), + ProfilePage(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack( + index: _currentIndex, + children: _pages, + ), + bottomNavigationBar: Container( + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.borderLight, width: 0.5)), + ), + child: NavigationBar( + selectedIndex: _currentIndex, + onDestinationSelected: (index) => setState(() => _currentIndex = index), + destinations: [ + _buildDestination(Icons.home_rounded, Icons.home_outlined, '首页'), + _buildDestination(Icons.show_chart_rounded, Icons.show_chart_outlined, '交易'), + _buildBadgeDestination( + Icons.notifications_rounded, + Icons.notifications_outlined, + '消息', + 2, + ), + _buildDestination(Icons.person_rounded, Icons.person_outlined, '我的'), + ], + ), + ), + ); + } + + NavigationDestination _buildDestination( + IconData selected, + IconData unselected, + String label, + ) { + return NavigationDestination( + icon: Icon(unselected), + selectedIcon: Icon(selected), + label: label, + ); + } + + NavigationDestination _buildBadgeDestination( + IconData selected, + IconData unselected, + String label, + int count, + ) { + return NavigationDestination( + icon: Badge( + label: Text('$count', style: const TextStyle(fontSize: 10)), + child: Icon(unselected), + ), + selectedIcon: Badge( + label: Text('$count', style: const TextStyle(fontSize: 10)), + child: Icon(selected), + ), + label: label, + ); + } +} diff --git a/frontend/genex-mobile/lib/app/theme/app_colors.dart b/frontend/genex-mobile/lib/app/theme/app_colors.dart new file mode 100644 index 0000000..50a833f --- /dev/null +++ b/frontend/genex-mobile/lib/app/theme/app_colors.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; + +/// Genex Design System - Color Tokens +/// +/// 紫色系科技风,干净清爽型(参考 Stripe/Alipay/Venmo) +/// Primary: #6C5CE7 (创新/科技紫) +class AppColors { + AppColors._(); + + // ============================================================ + // Primary Purple Palette + // ============================================================ + static const Color primary = Color(0xFF6C5CE7); + static const Color primaryLight = Color(0xFF9B8FFF); + static const Color primaryDark = Color(0xFF4834D4); + static const Color primarySurface = Color(0xFFF3F1FF); + static const Color primaryContainer = Color(0xFFE8E5FF); + + // ============================================================ + // Neutral Palette (Cool Gray) + // ============================================================ + static const Color gray50 = Color(0xFFF8F9FC); + static const Color gray100 = Color(0xFFF1F3F8); + static const Color gray200 = Color(0xFFE4E7F0); + static const Color gray300 = Color(0xFFCDD2DE); + static const Color gray400 = Color(0xFFA0A8BE); + static const Color gray500 = Color(0xFF7A839E); + static const Color gray600 = Color(0xFF5C6478); + static const Color gray700 = Color(0xFF3D4459); + static const Color gray800 = Color(0xFF262B3A); + static const Color gray900 = Color(0xFF141723); + + // ============================================================ + // Semantic Colors + // ============================================================ + static const Color success = Color(0xFF00C48C); + static const Color successLight = Color(0xFFE6FAF3); + static const Color warning = Color(0xFFFFAB2E); + static const Color warningLight = Color(0xFFFFF7E6); + static const Color error = Color(0xFFFF4757); + static const Color errorLight = Color(0xFFFFF0F0); + static const Color info = Color(0xFF3B82F6); + static const Color infoLight = Color(0xFFEFF6FF); + + // ============================================================ + // Background & Surface + // ============================================================ + static const Color background = Color(0xFFF8F9FC); + static const Color surface = Color(0xFFFFFFFF); + static const Color surfaceVariant = Color(0xFFF1F3F8); + static const Color surfaceElevated = Color(0xFFFFFFFF); + static const Color scrim = Color(0x52000000); + + // ============================================================ + // Text Colors + // ============================================================ + static const Color textPrimary = Color(0xFF141723); + static const Color textSecondary = Color(0xFF5C6478); + static const Color textTertiary = Color(0xFFA0A8BE); + static const Color textDisabled = Color(0xFFCDD2DE); + static const Color textOnPrimary = Color(0xFFFFFFFF); + static const Color textLink = Color(0xFF6C5CE7); + + // ============================================================ + // Border Colors + // ============================================================ + static const Color border = Color(0xFFE4E7F0); + static const Color borderLight = Color(0xFFF1F3F8); + static const Color borderFocus = Color(0xFF6C5CE7); + + // ============================================================ + // Coupon-specific Colors (券专属) + // ============================================================ + static const Color couponDining = Color(0xFFFF6B6B); + static const Color couponShopping = Color(0xFF6C5CE7); + static const Color couponEntertainment = Color(0xFFFFAB2E); + static const Color couponTravel = Color(0xFF00C48C); + static const Color couponOther = Color(0xFF3B82F6); + + // ============================================================ + // Credit Rating Colors (信用等级) + // ============================================================ + static const Color creditAAA = Color(0xFF00C48C); + static const Color creditAA = Color(0xFF3B82F6); + static const Color creditA = Color(0xFF6C5CE7); + static const Color creditBBB = Color(0xFFFFAB2E); + static const Color creditBB = Color(0xFFFF6B6B); + + // ============================================================ + // Coupon Status Colors (券状态) + // ============================================================ + static const Color statusActive = Color(0xFF00C48C); + static const Color statusPending = Color(0xFFFFAB2E); + static const Color statusExpired = Color(0xFFA0A8BE); + static const Color statusUsed = Color(0xFFCDD2DE); + + // ============================================================ + // Track Colors (Utility / Securities) + // ============================================================ + static const Color utilityTrack = Color(0xFF00C48C); + static const Color securitiesTrack = Color(0xFFFFAB2E); + + // ============================================================ + // Gradient Definitions + // ============================================================ + static const LinearGradient primaryGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF6C5CE7), Color(0xFF9B8FFF)], + ); + + static const LinearGradient cardGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF6C5CE7), Color(0xFF4834D4)], + ); + + static const LinearGradient successGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF00C48C), Color(0xFF00E6A0)], + ); +} diff --git a/frontend/genex-mobile/lib/app/theme/app_spacing.dart b/frontend/genex-mobile/lib/app/theme/app_spacing.dart new file mode 100644 index 0000000..d9d8970 --- /dev/null +++ b/frontend/genex-mobile/lib/app/theme/app_spacing.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; + +/// Genex Design System - Spacing & Layout Tokens +/// +/// 基于 4px 网格系统,保持一致的留白节奏 +class AppSpacing { + AppSpacing._(); + + // ============================================================ + // Base Grid (4px) + // ============================================================ + static const double xs = 4; + static const double sm = 8; + static const double md = 12; + static const double lg = 16; + static const double xl = 20; + static const double xxl = 24; + static const double xxxl = 32; + static const double huge = 40; + static const double massive = 48; + static const double gigantic = 64; + + // ============================================================ + // Page Padding + // ============================================================ + static const EdgeInsets pagePadding = EdgeInsets.symmetric(horizontal: 20); + static const EdgeInsets pageWithTop = EdgeInsets.fromLTRB(20, 16, 20, 0); + + // ============================================================ + // Card Padding + // ============================================================ + static const EdgeInsets cardPadding = EdgeInsets.all(16); + static const EdgeInsets cardPaddingCompact = EdgeInsets.all(12); + + // ============================================================ + // Section Spacing + // ============================================================ + static const double sectionGap = 24; + static const double itemGap = 12; + static const double inlineGap = 8; + + // ============================================================ + // Border Radius + // ============================================================ + static const double radiusSm = 8; + static const double radiusMd = 12; + static const double radiusLg = 16; + static const double radiusXl = 20; + static const double radiusFull = 999; + + static final BorderRadius borderRadiusSm = BorderRadius.circular(radiusSm); + static final BorderRadius borderRadiusMd = BorderRadius.circular(radiusMd); + static final BorderRadius borderRadiusLg = BorderRadius.circular(radiusLg); + static final BorderRadius borderRadiusXl = BorderRadius.circular(radiusXl); + static final BorderRadius borderRadiusFull = BorderRadius.circular(radiusFull); + + // ============================================================ + // Elevation / Shadow + // ============================================================ + static const List shadowSm = [ + BoxShadow( + color: Color(0x0A000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ]; + + static const List shadowMd = [ + BoxShadow( + color: Color(0x0F000000), + blurRadius: 16, + offset: Offset(0, 4), + ), + ]; + + static const List shadowLg = [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 24, + offset: Offset(0, 8), + ), + ]; + + static const List shadowPrimary = [ + BoxShadow( + color: Color(0x336C5CE7), + blurRadius: 16, + offset: Offset(0, 4), + ), + ]; + + // ============================================================ + // Animation Durations + // ============================================================ + static const Duration animFast = Duration(milliseconds: 150); + static const Duration animNormal = Duration(milliseconds: 250); + static const Duration animSlow = Duration(milliseconds: 350); + + // ============================================================ + // Component Sizes + // ============================================================ + static const double buttonHeight = 52; + static const double buttonHeightSm = 40; + static const double inputHeight = 52; + static const double appBarHeight = 56; + static const double bottomNavHeight = 80; + static const double tabBarHeight = 44; + static const double avatarSm = 32; + static const double avatarMd = 40; + static const double avatarLg = 56; + static const double iconSm = 20; + static const double iconMd = 24; + static const double iconLg = 28; + static const double couponCardHeight = 120; + static const double couponCardHeightLg = 160; +} diff --git a/frontend/genex-mobile/lib/app/theme/app_theme.dart b/frontend/genex-mobile/lib/app/theme/app_theme.dart new file mode 100644 index 0000000..7b7bdc1 --- /dev/null +++ b/frontend/genex-mobile/lib/app/theme/app_theme.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'app_colors.dart'; +import 'app_typography.dart'; +import 'app_spacing.dart'; + +/// Genex Material 3 Theme Configuration +/// +/// 干净清爽型紫色主题,零区块链感知的金融App体验 +class AppTheme { + AppTheme._(); + + static ThemeData get light => ThemeData( + useMaterial3: true, + brightness: Brightness.light, + + // Color Scheme + colorScheme: const ColorScheme.light( + primary: AppColors.primary, + primaryContainer: AppColors.primaryContainer, + secondary: AppColors.success, + secondaryContainer: AppColors.successLight, + tertiary: AppColors.info, + error: AppColors.error, + errorContainer: AppColors.errorLight, + surface: AppColors.surface, + surfaceContainerHighest: AppColors.surfaceVariant, + onPrimary: Colors.white, + onSecondary: Colors.white, + onSurface: AppColors.textPrimary, + onSurfaceVariant: AppColors.textSecondary, + outline: AppColors.border, + outlineVariant: AppColors.borderLight, + scrim: AppColors.scrim, + ), + + scaffoldBackgroundColor: AppColors.background, + + // AppBar + appBarTheme: const AppBarTheme( + elevation: 0, + scrolledUnderElevation: 0.5, + centerTitle: true, + backgroundColor: AppColors.surface, + foregroundColor: AppColors.textPrimary, + surfaceTintColor: Colors.transparent, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + statusBarBrightness: Brightness.light, + ), + titleTextStyle: AppTypography.h3, + iconTheme: IconThemeData(color: AppColors.textPrimary, size: 24), + ), + + // Bottom Navigation + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + backgroundColor: AppColors.surface, + selectedItemColor: AppColors.primary, + unselectedItemColor: AppColors.textTertiary, + elevation: 0, + selectedLabelStyle: TextStyle(fontSize: 11, fontWeight: FontWeight.w600), + unselectedLabelStyle: TextStyle(fontSize: 11, fontWeight: FontWeight.w400), + ), + + // Navigation Bar (M3) + navigationBarTheme: NavigationBarThemeData( + backgroundColor: AppColors.surface, + indicatorColor: AppColors.primaryContainer, + surfaceTintColor: Colors.transparent, + elevation: 0, + height: AppSpacing.bottomNavHeight, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + iconTheme: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const IconThemeData(color: AppColors.primary, size: 24); + } + return const IconThemeData(color: AppColors.textTertiary, size: 24); + }), + labelTextStyle: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return AppTypography.caption.copyWith( + color: AppColors.primary, + fontWeight: FontWeight.w600, + ); + } + return AppTypography.caption; + }), + ), + + // Card + cardTheme: CardThemeData( + elevation: 0, + color: AppColors.surface, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + side: const BorderSide(color: AppColors.borderLight, width: 1), + ), + margin: EdgeInsets.zero, + ), + + // Elevated Button + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + elevation: 0, + minimumSize: const Size(0, AppSpacing.buttonHeight), + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + ), + textStyle: AppTypography.labelLarge, + ), + ), + + // Outlined Button + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primary, + side: const BorderSide(color: AppColors.primary, width: 1.5), + minimumSize: const Size(0, AppSpacing.buttonHeight), + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + ), + textStyle: AppTypography.labelLarge, + ), + ), + + // Text Button + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: AppColors.primary, + textStyle: AppTypography.labelMedium, + ), + ), + + // Input Decoration + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.gray50, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( + borderRadius: AppSpacing.borderRadiusMd, + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: AppSpacing.borderRadiusMd, + borderSide: const BorderSide(color: AppColors.borderLight, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: AppSpacing.borderRadiusMd, + borderSide: const BorderSide(color: AppColors.primary, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: AppSpacing.borderRadiusMd, + borderSide: const BorderSide(color: AppColors.error, width: 1), + ), + hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary), + labelStyle: AppTypography.bodyMedium, + errorStyle: AppTypography.caption.copyWith(color: AppColors.error), + ), + + // Chip + chipTheme: ChipThemeData( + backgroundColor: AppColors.gray50, + selectedColor: AppColors.primaryContainer, + labelStyle: AppTypography.labelSmall, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusFull, + ), + side: const BorderSide(color: AppColors.borderLight), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + ), + + // TabBar + tabBarTheme: TabBarThemeData( + labelColor: AppColors.primary, + unselectedLabelColor: AppColors.textTertiary, + labelStyle: AppTypography.labelMedium, + unselectedLabelStyle: AppTypography.labelMedium, + indicatorSize: TabBarIndicatorSize.label, + indicator: const UnderlineTabIndicator( + borderSide: BorderSide(color: AppColors.primary, width: 2.5), + ), + dividerColor: Colors.transparent, + ), + + // Dialog + dialogTheme: DialogThemeData( + backgroundColor: AppColors.surface, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusLg, + ), + titleTextStyle: AppTypography.h2, + contentTextStyle: AppTypography.bodyMedium, + ), + + // BottomSheet + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: AppColors.surface, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + showDragHandle: true, + dragHandleColor: AppColors.gray300, + dragHandleSize: Size(36, 4), + ), + + // Divider + dividerTheme: const DividerThemeData( + color: AppColors.borderLight, + thickness: 1, + space: 0, + ), + + // Snackbar + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.gray800, + contentTextStyle: AppTypography.bodyMedium.copyWith(color: Colors.white), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + ), + ), + ); +} diff --git a/frontend/genex-mobile/lib/app/theme/app_typography.dart b/frontend/genex-mobile/lib/app/theme/app_typography.dart new file mode 100644 index 0000000..8b76cee --- /dev/null +++ b/frontend/genex-mobile/lib/app/theme/app_typography.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; + +/// Genex Design System - Typography Tokens +/// +/// 字体层级:清晰、易读、干净 +/// 基于 SF Pro (iOS) / Roboto (Android) 系统字体 +class AppTypography { + AppTypography._(); + + static const String _fontFamily = 'SF Pro Display'; + static const String _fontFamilyFallback = 'Roboto'; + + // ============================================================ + // Display - 超大标题(启动页/空状态) + // ============================================================ + static const TextStyle displayLarge = TextStyle( + fontSize: 34, + fontWeight: FontWeight.w700, + height: 1.2, + letterSpacing: -0.5, + color: AppColors.textPrimary, + ); + + static const TextStyle displayMedium = TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + height: 1.25, + letterSpacing: -0.3, + color: AppColors.textPrimary, + ); + + // ============================================================ + // Heading - 页面标题 / 模块标题 + // ============================================================ + static const TextStyle h1 = TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + height: 1.3, + color: AppColors.textPrimary, + ); + + static const TextStyle h2 = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + height: 1.35, + color: AppColors.textPrimary, + ); + + static const TextStyle h3 = TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + height: 1.4, + color: AppColors.textPrimary, + ); + + // ============================================================ + // Body - 正文 + // ============================================================ + static const TextStyle bodyLarge = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + height: 1.5, + color: AppColors.textPrimary, + ); + + static const TextStyle bodyMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.5, + color: AppColors.textPrimary, + ); + + static const TextStyle bodySmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + height: 1.5, + color: AppColors.textSecondary, + ); + + // ============================================================ + // Label - 按钮文字/标签/Tab + // ============================================================ + static const TextStyle labelLarge = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + height: 1.4, + letterSpacing: 0.2, + color: AppColors.textPrimary, + ); + + static const TextStyle labelMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.4, + letterSpacing: 0.1, + color: AppColors.textPrimary, + ); + + static const TextStyle labelSmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.4, + letterSpacing: 0.2, + color: AppColors.textSecondary, + ); + + // ============================================================ + // Caption - 辅助文字 + // ============================================================ + static const TextStyle caption = TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + height: 1.4, + color: AppColors.textTertiary, + ); + + // ============================================================ + // Price - 价格专用 + // ============================================================ + static const TextStyle priceLarge = TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + height: 1.2, + color: AppColors.primary, + ); + + static const TextStyle priceMedium = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + height: 1.2, + color: AppColors.primary, + ); + + static const TextStyle priceSmall = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + height: 1.2, + color: AppColors.primary, + ); + + static const TextStyle priceOriginal = TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + height: 1.2, + color: AppColors.textTertiary, + decoration: TextDecoration.lineThrough, + ); + + // ============================================================ + // Discount Badge - 折扣标签 + // ============================================================ + static const TextStyle discountBadge = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + height: 1.0, + color: Colors.white, + ); +} diff --git a/frontend/genex-mobile/lib/features/ai_agent/presentation/pages/agent_chat_page.dart b/frontend/genex-mobile/lib/features/ai_agent/presentation/pages/agent_chat_page.dart new file mode 100644 index 0000000..c179647 --- /dev/null +++ b/frontend/genex-mobile/lib/features/ai_agent/presentation/pages/agent_chat_page.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// AI Agent 全屏对话页面(消费者端) +/// +/// 场景:智能推券、比价分析、组合建议、投资教育 +class AgentChatPage extends StatefulWidget { + const AgentChatPage({super.key}); + + @override + State createState() => _AgentChatPageState(); +} + +class _AgentChatPageState extends State { + final _controller = TextEditingController(); + final _scrollController = ScrollController(); + final List<_Msg> _messages = [ + _Msg(true, '你好!我是 Genex AI 助手,可以帮你发现高性价比好券、比价分析、组合推荐。试试问我:'), + ]; + final _suggestions = ['推荐适合我的券', '星巴克券值不值得买?', '帮我做比价分析', '我的券快到期了怎么办?']; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 20), + SizedBox(width: 8), + Text('AI 助手'), + ], + ), + actions: [ + IconButton(icon: const Icon(Icons.more_horiz_rounded), onPressed: () {}), + ], + ), + body: Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: _messages.length, + itemBuilder: (context, i) => _buildBubble(_messages[i]), + ), + ), + + // Suggestion Chips + if (_messages.length <= 2) + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + children: _suggestions.map((s) => Padding( + padding: const EdgeInsets.only(right: 8), + child: ActionChip( + label: Text(s, style: const TextStyle(fontSize: 12)), + onPressed: () => _send(s), + backgroundColor: AppColors.primarySurface, + side: BorderSide.none, + ), + )).toList(), + ), + ), + + // Input + Container( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.borderLight)), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: InputDecoration( + hintText: '问我任何关于券的问题...', + border: OutlineInputBorder( + borderRadius: AppSpacing.borderRadiusFull, + borderSide: const BorderSide(color: AppColors.borderLight), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + onSubmitted: _send, + ), + ), + const SizedBox(width: 8), + Container( + decoration: const BoxDecoration(color: AppColors.primary, shape: BoxShape.circle), + child: IconButton( + icon: const Icon(Icons.send_rounded, color: Colors.white, size: 20), + onPressed: () => _send(_controller.text), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildBubble(_Msg msg) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: msg.isAi ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + if (msg.isAi) ...[ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 16), + ), + const SizedBox(width: 8), + ], + Flexible( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: msg.isAi ? AppColors.gray50 : AppColors.primary, + borderRadius: BorderRadius.circular(16).copyWith( + topLeft: msg.isAi ? const Radius.circular(4) : null, + topRight: !msg.isAi ? const Radius.circular(4) : null, + ), + ), + child: Text( + msg.text, + style: AppTypography.bodyMedium.copyWith( + color: msg.isAi ? AppColors.textPrimary : Colors.white, + height: 1.5, + ), + ), + ), + ), + ], + ), + ); + } + + void _send(String text) { + if (text.trim().isEmpty) return; + setState(() { + _messages.add(_Msg(false, text)); + _controller.clear(); + }); + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) { + setState(() { + _messages.add(_Msg(true, '根据您的偏好和消费习惯,推荐以下高性价比券:\n\n1. 星巴克 \$25 礼品卡 - 当前售价 \$21.25(8.5折),信用AAA\n2. Amazon \$100 购物券 - 当前售价 \$85(8.5折),信用AA\n\n这两张券的折扣率在同类中最优,且发行方信用等级高。')); + }); + } + }); + } +} + +class _Msg { + final bool isAi; + final String text; + _Msg(this.isAi, this.text); +} diff --git a/frontend/genex-mobile/lib/features/ai_agent/presentation/widgets/ai_fab.dart b/frontend/genex-mobile/lib/features/ai_agent/presentation/widgets/ai_fab.dart new file mode 100644 index 0000000..6119fb2 --- /dev/null +++ b/frontend/genex-mobile/lib/features/ai_agent/presentation/widgets/ai_fab.dart @@ -0,0 +1,276 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// AI Agent 悬浮入口按钮 +/// +/// 右下角悬浮,显示未读建议数量红点 +/// 点击展开对话面板,长按显示快捷操作 +class AiFab extends StatelessWidget { + final int unreadCount; + final VoidCallback onTap; + final VoidCallback? onLongPress; + + const AiFab({ + super.key, + this.unreadCount = 0, + required this.onTap, + this.onLongPress, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + onLongPress: onLongPress, + child: Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + shape: BoxShape.circle, + boxShadow: AppSpacing.shadowPrimary, + ), + child: Stack( + children: [ + const Center( + child: Icon( + Icons.auto_awesome_rounded, + color: Colors.white, + size: 26, + ), + ), + if (unreadCount > 0) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: AppColors.error, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints(minWidth: 16, minHeight: 16), + child: Text( + unreadCount > 99 ? '99+' : '$unreadCount', + style: const TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + ); + } +} + +/// AI Agent 对话面板 +/// +/// 底部Sheet展开,支持文字/语音输入,流式输出 +class AiChatPanel extends StatelessWidget { + const AiChatPanel({super.key}); + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.65, + minChildSize: 0.3, + maxChildSize: 0.9, + builder: (context, scrollController) { + return Container( + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Drag Handle + Container( + margin: const EdgeInsets.only(top: 8), + width: 36, + height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + + // Header + Padding( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 8), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon( + Icons.auto_awesome_rounded, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 10), + Text('AI 助手', style: AppTypography.h3), + const Spacer(), + IconButton( + icon: const Icon(Icons.close_rounded, size: 22), + onPressed: () => Navigator.of(context).pop(), + color: AppColors.textSecondary, + ), + ], + ), + ), + + const Divider(), + + // Chat Content + Expanded( + child: ListView( + controller: scrollController, + padding: const EdgeInsets.all(20), + children: [ + _buildAiMessage( + '你好!我是 Genex AI 助手,可以帮你管理券资产、查找优惠、分析价格。有什么需要帮助的吗?', + ), + const SizedBox(height: 12), + _buildSuggestionChips(), + ], + ), + ), + + // Input Bar + Container( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.borderLight)), + ), + child: Row( + children: [ + Expanded( + child: Container( + height: 44, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + const SizedBox(width: 16), + Expanded( + child: TextField( + decoration: InputDecoration( + hintText: '输入消息...', + hintStyle: AppTypography.bodyMedium + .copyWith(color: AppColors.textTertiary), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + ), + ), + IconButton( + icon: const Icon(Icons.mic_rounded, size: 20), + onPressed: () {}, + color: AppColors.textTertiary, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 8), + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_upward_rounded, + color: Colors.white, + size: 20, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildAiMessage(String text) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 14), + ), + const SizedBox(width: 10), + Flexible( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Text(text, style: AppTypography.bodyMedium), + ), + ), + ], + ); + } + + Widget _buildSuggestionChips() { + final suggestions = [ + '帮我找高折扣券', + '我的券快到期了吗?', + '推荐今日好券', + '分析我的券资产', + ]; + + return Wrap( + spacing: 8, + runSpacing: 8, + children: suggestions.map((s) { + return ActionChip( + label: Text(s, style: AppTypography.labelSmall.copyWith(color: AppColors.primary)), + onPressed: () {}, + backgroundColor: AppColors.primarySurface, + side: BorderSide(color: AppColors.primary.withValues(alpha: 0.2)), + shape: RoundedRectangleBorder(borderRadius: AppSpacing.borderRadiusFull), + ); + }).toList(), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/auth/presentation/pages/forgot_password_page.dart b/frontend/genex-mobile/lib/features/auth/presentation/pages/forgot_password_page.dart new file mode 100644 index 0000000..8c9b32b --- /dev/null +++ b/frontend/genex-mobile/lib/features/auth/presentation/pages/forgot_password_page.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A1. 忘记密码 - 手机号/邮箱验证 → 输入验证码 → 设置新密码 → 成功 +/// +/// 分步骤流程:Step1 输入账号 → Step2 验证码 → Step3 新密码 → Step4 成功 +class ForgotPasswordPage extends StatefulWidget { + const ForgotPasswordPage({super.key}); + + @override + State createState() => _ForgotPasswordPageState(); +} + +class _ForgotPasswordPageState extends State { + int _step = 0; // 0: 输入账号, 1: 验证码, 2: 新密码, 3: 成功 + final _phoneController = TextEditingController(); + final _codeController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmController = TextEditingController(); + bool _obscurePassword = true; + bool _obscureConfirm = true; + + @override + void dispose() { + _phoneController.dispose(); + _codeController.dispose(); + _passwordController.dispose(); + _confirmController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_step == 3 ? '' : '找回密码'), + leading: _step == 3 + ? const SizedBox.shrink() + : IconButton( + icon: const Icon(Icons.arrow_back_ios_rounded, size: 20), + onPressed: () { + if (_step > 0 && _step < 3) { + setState(() => _step--); + } else { + Navigator.pop(context); + } + }, + ), + ), + body: SafeArea( + child: Padding( + padding: AppSpacing.pagePadding, + child: _buildStep(), + ), + ), + ); + } + + Widget _buildStep() { + switch (_step) { + case 0: + return _buildStepAccount(); + case 1: + return _buildStepCode(); + case 2: + return _buildStepPassword(); + case 3: + return _buildStepSuccess(); + default: + return const SizedBox.shrink(); + } + } + + Widget _buildStepAccount() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text('输入手机号或邮箱', style: AppTypography.h1), + const SizedBox(height: 8), + Text('我们将向您发送验证码', style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + const SizedBox(height: 32), + TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + hintText: '手机号 / 邮箱地址', + prefixIcon: Icon(Icons.person_outline_rounded), + ), + ), + const SizedBox(height: 24), + GenexButton( + label: '获取验证码', + onPressed: () => setState(() => _step = 1), + ), + ], + ); + } + + Widget _buildStepCode() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text('输入验证码', style: AppTypography.h1), + const SizedBox(height: 8), + Text( + '验证码已发送至 ${_phoneController.text.isNotEmpty ? _phoneController.text : '***'}', + style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary), + ), + const SizedBox(height: 32), + TextField( + controller: _codeController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: '6位验证码', + prefixIcon: Icon(Icons.lock_outline_rounded), + ), + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + child: const Text('重新发送'), + ), + ), + const SizedBox(height: 8), + GenexButton( + label: '下一步', + onPressed: () => setState(() => _step = 2), + ), + ], + ); + } + + Widget _buildStepPassword() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text('设置新密码', style: AppTypography.h1), + const SizedBox(height: 8), + Text('请输入新密码(8位以上)', style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + const SizedBox(height: 32), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + hintText: '新密码', + prefixIcon: const Icon(Icons.lock_outline_rounded), + suffixIcon: IconButton( + icon: Icon(_obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined), + onPressed: () => setState(() => _obscurePassword = !_obscurePassword), + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _confirmController, + obscureText: _obscureConfirm, + decoration: InputDecoration( + hintText: '确认新密码', + prefixIcon: const Icon(Icons.lock_outline_rounded), + suffixIcon: IconButton( + icon: Icon(_obscureConfirm ? Icons.visibility_off_outlined : Icons.visibility_outlined), + onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), + ), + ), + ), + const SizedBox(height: 24), + GenexButton( + label: '确认修改', + onPressed: () => setState(() => _step = 3), + ), + ], + ); + } + + Widget _buildStepSuccess() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: AppColors.successLight, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check_rounded, color: AppColors.success, size: 40), + ), + const SizedBox(height: 24), + Text('密码修改成功', style: AppTypography.h1), + const SizedBox(height: 8), + Text('请使用新密码登录', style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: GenexButton( + label: '返回登录', + onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil('/login', (_) => false), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/auth/presentation/pages/login_page.dart b/frontend/genex-mobile/lib/features/auth/presentation/pages/login_page.dart new file mode 100644 index 0000000..90b0b41 --- /dev/null +++ b/frontend/genex-mobile/lib/features/auth/presentation/pages/login_page.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A1. 登录页 - 手机号/邮箱+密码 / 验证码快捷登录 +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + final _phoneController = TextEditingController(); + final _passwordController = TextEditingController(); + final _codeController = TextEditingController(); + bool _obscurePassword = true; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + _phoneController.dispose(); + _passwordController.dispose(); + _codeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: SafeArea( + child: Padding( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text('欢迎回来', style: AppTypography.displayMedium), + const SizedBox(height: 8), + Text( + '登录 Genex 管理你的券资产', + style: AppTypography.bodyLarge.copyWith(color: AppColors.textSecondary), + ), + const SizedBox(height: 32), + + // Tab: Password / SMS Code + Container( + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: TabBar( + controller: _tabController, + indicator: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusFull, + boxShadow: AppSpacing.shadowSm, + ), + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + labelColor: AppColors.textPrimary, + unselectedLabelColor: AppColors.textTertiary, + labelStyle: AppTypography.labelMedium, + tabs: const [ + Tab(text: '密码登录'), + Tab(text: '验证码登录'), + ], + ), + ), + const SizedBox(height: 24), + + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPasswordLogin(), + _buildCodeLogin(), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPasswordLogin() { + return Column( + children: [ + // Phone/Email Input + TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + hintText: '手机号或邮箱', + prefixIcon: Icon(Icons.person_outline_rounded, color: AppColors.textTertiary), + ), + ), + const SizedBox(height: 16), + + // Password Input + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + hintText: '密码', + prefixIcon: const Icon(Icons.lock_outline_rounded, color: AppColors.textTertiary), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, + color: AppColors.textTertiary, + size: 20, + ), + onPressed: () => setState(() => _obscurePassword = !_obscurePassword), + ), + ), + ), + const SizedBox(height: 12), + + // Forgot Password + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/forgot-password'); + }, + child: Text('忘记密码?', style: AppTypography.labelSmall.copyWith( + color: AppColors.primary, + )), + ), + ), + const SizedBox(height: 24), + + // Login Button + GenexButton( + label: '登录', + onPressed: () { + Navigator.pushReplacementNamed(context, '/main'); + }, + ), + ], + ); + } + + Widget _buildCodeLogin() { + return Column( + children: [ + // Phone Input + TextField( + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + hintText: '手机号', + prefixIcon: Icon(Icons.phone_android_rounded, color: AppColors.textTertiary), + ), + ), + const SizedBox(height: 16), + + // Code Input + Send Button + Row( + children: [ + Expanded( + child: TextField( + controller: _codeController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: '验证码', + prefixIcon: Icon(Icons.shield_outlined, color: AppColors.textTertiary), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: AppSpacing.inputHeight, + child: GenexButton( + label: '获取验证码', + variant: GenexButtonVariant.secondary, + size: GenexButtonSize.medium, + fullWidth: false, + onPressed: () { + // SMS: send verification code + }, + ), + ), + ], + ), + const SizedBox(height: 24), + + GenexButton( + label: '登录', + onPressed: () { + Navigator.pushReplacementNamed(context, '/main'); + }, + ), + ], + ); + } +} diff --git a/frontend/genex-mobile/lib/features/auth/presentation/pages/register_page.dart b/frontend/genex-mobile/lib/features/auth/presentation/pages/register_page.dart new file mode 100644 index 0000000..697fd2c --- /dev/null +++ b/frontend/genex-mobile/lib/features/auth/presentation/pages/register_page.dart @@ -0,0 +1,286 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A1. 手机号注册页 +/// +/// 手机号输入、获取验证码、设置密码、用户协议勾选 +/// 注册成功后后台静默创建MPC钱包 +class RegisterPage extends StatefulWidget { + final bool isEmail; + + const RegisterPage({super.key, this.isEmail = false}); + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final _accountController = TextEditingController(); + final _codeController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _obscurePassword = true; + bool _agreeTerms = false; + + @override + void dispose() { + _accountController.dispose(); + _codeController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text('创建账号', style: AppTypography.displayMedium), + const SizedBox(height: 8), + Text( + widget.isEmail ? '使用邮箱注册 Genex 账号' : '使用手机号注册 Genex 账号', + style: AppTypography.bodyLarge.copyWith(color: AppColors.textSecondary), + ), + const SizedBox(height: 40), + + // Step indicator + _buildStepIndicator(), + const SizedBox(height: 32), + + // Account Input (Phone/Email) + Text( + widget.isEmail ? '邮箱地址' : '手机号', + style: AppTypography.labelMedium, + ), + const SizedBox(height: 8), + TextField( + controller: _accountController, + keyboardType: + widget.isEmail ? TextInputType.emailAddress : TextInputType.phone, + decoration: InputDecoration( + hintText: widget.isEmail ? '请输入邮箱地址' : '请输入手机号', + prefixIcon: Icon( + widget.isEmail ? Icons.email_outlined : Icons.phone_android_rounded, + color: AppColors.textTertiary, + ), + ), + ), + const SizedBox(height: 20), + + // Verification Code + Text('验证码', style: AppTypography.labelMedium), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: _codeController, + keyboardType: TextInputType.number, + maxLength: 6, + decoration: const InputDecoration( + hintText: '请输入6位验证码', + counterText: '', + prefixIcon: Icon(Icons.shield_outlined, color: AppColors.textTertiary), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: AppSpacing.inputHeight, + child: GenexButton( + label: '获取验证码', + variant: GenexButtonVariant.secondary, + size: GenexButtonSize.medium, + fullWidth: false, + onPressed: () {}, + ), + ), + ], + ), + const SizedBox(height: 20), + + // Password + Text('设置密码', style: AppTypography.labelMedium), + const SizedBox(height: 8), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + hintText: '8-20位,含字母和数字', + prefixIcon: const Icon(Icons.lock_outline_rounded, color: AppColors.textTertiary), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: AppColors.textTertiary, + size: 20, + ), + onPressed: () => setState(() => _obscurePassword = !_obscurePassword), + ), + ), + ), + const SizedBox(height: 8), + _buildPasswordStrength(), + const SizedBox(height: 32), + + // Terms Agreement + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Checkbox( + value: _agreeTerms, + onChanged: (v) => setState(() => _agreeTerms = v ?? false), + activeColor: AppColors.primary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + ), + const SizedBox(width: 8), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _agreeTerms = !_agreeTerms), + child: RichText( + text: TextSpan( + style: AppTypography.bodySmall, + children: [ + const TextSpan(text: '我已阅读并同意 '), + TextSpan( + text: '《用户协议》', + style: AppTypography.bodySmall.copyWith(color: AppColors.primary), + ), + const TextSpan(text: ' 和 '), + TextSpan( + text: '《隐私政策》', + style: AppTypography.bodySmall.copyWith(color: AppColors.primary), + ), + ], + ), + ), + ), + ), + ], + ), + const SizedBox(height: 32), + + // Register Button + GenexButton( + label: '注册', + onPressed: _agreeTerms ? () { + Navigator.pushReplacementNamed(context, '/main'); + } : null, + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ); + } + + Widget _buildStepIndicator() { + return Row( + children: [ + _buildStep(1, '验证', true), + _buildStepLine(true), + _buildStep(2, '设密码', true), + _buildStepLine(false), + _buildStep(3, '完成', false), + ], + ); + } + + Widget _buildStep(int number, String label, bool active) { + return Column( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: active ? AppColors.primary : AppColors.gray200, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '$number', + style: TextStyle( + color: active ? Colors.white : AppColors.textTertiary, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(height: 4), + Text( + label, + style: AppTypography.caption.copyWith( + color: active ? AppColors.primary : AppColors.textTertiary, + ), + ), + ], + ); + } + + Widget _buildStepLine(bool active) { + return Expanded( + child: Container( + height: 2, + margin: const EdgeInsets.only(bottom: 18), + color: active ? AppColors.primary : AppColors.gray200, + ), + ); + } + + Widget _buildPasswordStrength() { + final password = _passwordController.text; + final hasLength = password.length >= 8; + final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password); + final hasDigit = RegExp(r'\d').hasMatch(password); + + return Row( + children: [ + _buildCheck('8位以上', hasLength), + const SizedBox(width: 16), + _buildCheck('含字母', hasLetter), + const SizedBox(width: 16), + _buildCheck('含数字', hasDigit), + ], + ); + } + + Widget _buildCheck(String label, bool passed) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + passed ? Icons.check_circle_rounded : Icons.circle_outlined, + size: 14, + color: passed ? AppColors.success : AppColors.textTertiary, + ), + const SizedBox(width: 4), + Text( + label, + style: AppTypography.caption.copyWith( + color: passed ? AppColors.success : AppColors.textTertiary, + ), + ), + ], + ); + } +} diff --git a/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart b/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart new file mode 100644 index 0000000..c0800c5 --- /dev/null +++ b/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A1. 欢迎页 - 品牌展示 + 注册/登录入口 +/// +/// 品牌Logo、Slogan、手机号注册、邮箱注册、社交登录入口(Google/Apple) +class WelcomePage extends StatelessWidget { + const WelcomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const Spacer(flex: 2), + + // Brand Logo + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusXl, + boxShadow: AppSpacing.shadowPrimary, + ), + child: const Icon( + Icons.diamond_rounded, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 24), + + // Brand Name + Text( + 'Genex', + style: AppTypography.displayLarge.copyWith( + color: AppColors.primary, + letterSpacing: 2, + ), + ), + const SizedBox(height: 8), + + // Slogan + Text( + '让每一张券都有价值', + style: AppTypography.bodyLarge.copyWith( + color: AppColors.textSecondary, + ), + ), + + const Spacer(flex: 3), + + // Phone Register + GenexButton( + label: '手机号注册', + icon: Icons.phone_android_rounded, + onPressed: () { + Navigator.pushNamed(context, '/register'); + }, + ), + const SizedBox(height: 12), + + // Email Register + GenexButton( + label: '邮箱注册', + icon: Icons.email_outlined, + variant: GenexButtonVariant.outline, + onPressed: () { + Navigator.pushNamed(context, '/register'); + }, + ), + const SizedBox(height: 24), + + // Social Login Divider + Row( + children: [ + const Expanded(child: Divider(color: AppColors.border)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text('其他方式登录', style: AppTypography.caption), + ), + const Expanded(child: Divider(color: AppColors.border)), + ], + ), + const SizedBox(height: 16), + + // Social Login Buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _SocialLoginButton( + icon: Icons.g_mobiledata_rounded, + label: 'Google', + onTap: () { + Navigator.pushReplacementNamed(context, '/main'); + }, + ), + const SizedBox(width: 24), + _SocialLoginButton( + icon: Icons.apple_rounded, + label: 'Apple', + onTap: () { + Navigator.pushReplacementNamed(context, '/main'); + }, + ), + ], + ), + const SizedBox(height: 32), + + // Already have account + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('已有账号?', style: AppTypography.bodyMedium.copyWith( + color: AppColors.textSecondary, + )), + GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/login'); + }, + child: Text('登录', style: AppTypography.labelMedium.copyWith( + color: AppColors.primary, + )), + ), + ], + ), + const SizedBox(height: 16), + + // Terms + Text( + '注册即表示同意《用户协议》和《隐私政策》', + style: AppTypography.caption.copyWith(fontSize: 10), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ); + } +} + +class _SocialLoginButton extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + + const _SocialLoginButton({ + required this.icon, + required this.label, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + color: AppColors.gray50, + shape: BoxShape.circle, + border: Border.all(color: AppColors.border), + ), + child: Icon(icon, size: 28, color: AppColors.textPrimary), + ), + const SizedBox(height: 6), + Text(label, style: AppTypography.caption), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/coupon_detail_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/coupon_detail_page.dart new file mode 100644 index 0000000..6bee969 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/coupon_detail_page.dart @@ -0,0 +1,503 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/price_tag.dart'; +import '../../../../shared/widgets/credit_badge.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A2. 券详情页 +/// +/// 券图片、品牌Logo、面值、当前价格、折扣率、有效期、使用条件、 +/// 使用门店、发行方信用等级、立即购买、加入收藏、价格走势图、同类券推荐 +class CouponDetailPage extends StatelessWidget { + const CouponDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + slivers: [ + // Hero Image + AppBar + SliverAppBar( + expandedHeight: 220, + pinned: true, + backgroundColor: AppColors.surface, + leading: _buildBackButton(context), + actions: [ + IconButton( + icon: const Icon(Icons.share_rounded, size: 22), + onPressed: () {}, + style: IconButton.styleFrom( + backgroundColor: Colors.black26, + foregroundColor: Colors.white, + ), + ), + const SizedBox(width: 8), + ], + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: const BoxDecoration(gradient: AppColors.primaryGradient), + child: const Center( + child: Icon( + Icons.confirmation_number_rounded, + size: 64, + color: Colors.white24, + ), + ), + ), + ), + ), + + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Brand + Title + Rating + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // Brand logo placeholder + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.gray100, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.store_rounded, + color: AppColors.textTertiary, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Starbucks', style: AppTypography.bodySmall), + Text('星巴克 \$25 礼品卡', style: AppTypography.h2), + ], + ), + ), + const CreditBadge(rating: 'AAA', size: CreditBadgeSize.large), + ], + ), + ], + ), + ), + + // Price Section + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PriceTag( + currentPrice: 21.25, + faceValue: 25.0, + size: PriceTagSize.large, + ), + const SizedBox(height: 8), + Text( + '比面值节省 \$3.75', + style: AppTypography.bodySmall.copyWith(color: AppColors.success), + ), + ], + ), + ), + ), + + // Info Cards + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: _buildInfoSection(), + ), + + // Usage Rules + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: _buildUsageRules(), + ), + + // Available Stores + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: _buildStores(), + ), + + // Nearby Redemption (附近核销) + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: _buildNearbyRedemption(), + ), + + // Price Trend (Optional) + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: _buildPriceTrend(), + ), + + // Similar Coupons + Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), + child: Text('同类券推荐', style: AppTypography.h3), + ), + SizedBox( + height: 180, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.fromLTRB(20, 12, 20, 12), + itemCount: 5, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) => _buildSimilarCard(index), + ), + ), + + const SizedBox(height: 120), + ], + ), + ), + ], + ), + + // Bottom Buy Bar + bottomNavigationBar: Container( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.borderLight)), + ), + child: Row( + children: [ + // Favorite + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.favorite_border_rounded, color: AppColors.textTertiary, size: 22), + Text('收藏', style: AppTypography.caption), + ], + ), + const SizedBox(width: 24), + + // Buy Button + Expanded( + child: GenexButton( + label: '立即购买 \$21.25', + onPressed: () { + Navigator.pushNamed(context, '/order/confirm'); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildBackButton(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 18), + onPressed: () => Navigator.of(context).pop(), + style: IconButton.styleFrom( + backgroundColor: Colors.black26, + foregroundColor: Colors.white, + ), + ), + ); + } + + Widget _buildInfoSection() { + final items = [ + ('面值', '\$25.00'), + ('有效期', '2026/12/31'), + ('类型', '消费券'), + ('发行方', 'Starbucks Inc.'), + ]; + + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: items.asMap().entries.map((entry) { + final isLast = entry.key == items.length - 1; + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(entry.value.$1, style: AppTypography.bodyMedium.copyWith( + color: AppColors.textSecondary, + )), + Text(entry.value.$2, style: AppTypography.labelMedium), + ], + ), + if (!isLast) const Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Divider(), + ), + ], + ); + }).toList(), + ), + ); + } + + Widget _buildUsageRules() { + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('使用说明', style: AppTypography.labelMedium), + const SizedBox(height: 12), + _buildRuleItem(Icons.check_circle_outline, '全国星巴克门店通用'), + _buildRuleItem(Icons.check_circle_outline, '可转赠给好友'), + _buildRuleItem(Icons.check_circle_outline, '有效期内随时使用'), + _buildRuleItem(Icons.info_outline_rounded, '不可叠加使用'), + _buildRuleItem(Icons.info_outline_rounded, '不可兑换现金'), + ], + ), + ); + } + + Widget _buildRuleItem(IconData icon, String text) { + final isPositive = icon == Icons.check_circle_outline; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Icon(icon, size: 16, color: isPositive ? AppColors.success : AppColors.textTertiary), + const SizedBox(width: 8), + Text(text, style: AppTypography.bodySmall), + ], + ), + ); + } + + Widget _buildStores() { + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('使用门店', style: AppTypography.labelMedium), + Text('全国 12,800+ 门店', style: AppTypography.caption.copyWith( + color: AppColors.primary, + )), + ], + ), + const SizedBox(height: 12), + Text( + '支持全国所有星巴克直营门店使用', + style: AppTypography.bodySmall, + ), + ], + ), + ); + } + + Widget _buildPriceTrend() { + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('价格走势', style: AppTypography.labelMedium), + Text('近30天', style: AppTypography.caption), + ], + ), + const SizedBox(height: 16), + // Placeholder for price chart + Container( + height: 120, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Center( + child: Text( + '价格走势图 (fl_chart)', + style: AppTypography.bodySmall.copyWith(color: AppColors.textTertiary), + ), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildTrendStat('最高', '\$22.50', AppColors.error), + _buildTrendStat('最低', '\$20.00', AppColors.success), + _buildTrendStat('均价', '\$21.10', AppColors.textSecondary), + _buildTrendStat('历史成交', '1,234笔', AppColors.primary), + ], + ), + ], + ), + ); + } + + Widget _buildTrendStat(String label, String value, Color color) { + return Column( + children: [ + Text(value, style: AppTypography.labelSmall.copyWith(color: color)), + const SizedBox(height: 2), + Text(label, style: AppTypography.caption), + ], + ); + } + + Widget _buildNearbyRedemption() { + final stores = [ + ('星巴克 中关村店', '距离 0.3km', '营业中'), + ('星巴克 海淀黄庄店', '距离 0.8km', '营业中'), + ('星巴克 五道口店', '距离 1.2km', '营业中'), + ]; + + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(Icons.location_on_rounded, + size: 18, color: AppColors.primary), + const SizedBox(width: 6), + Text('附近可用门店', style: AppTypography.labelMedium), + ], + ), + Text('查看全部', + style: AppTypography.caption + .copyWith(color: AppColors.primary)), + ], + ), + const SizedBox(height: 12), + ...stores.map((store) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.store_rounded, + size: 16, color: AppColors.primary), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(store.$1, style: AppTypography.labelSmall), + Text(store.$2, style: AppTypography.caption), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text(store.$3, + style: AppTypography.caption.copyWith( + color: AppColors.success, + fontSize: 10, + )), + ), + ], + ), + )), + ], + ), + ); + } + + Widget _buildSimilarCard(int index) { + return Container( + width: 130, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 80, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + ), + child: Center( + child: Icon(Icons.confirmation_number_outlined, + color: AppColors.primary.withValues(alpha: 0.3), size: 28), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('品牌 ${index + 1}', style: AppTypography.caption), + const SizedBox(height: 2), + Text('\$${(index + 1) * 8}.50', + style: AppTypography.priceSmall.copyWith(fontSize: 14)), + Text('\$${(index + 1) * 10}', style: AppTypography.priceOriginal.copyWith(fontSize: 10)), + ], + ), + ), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/home_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/home_page.dart new file mode 100644 index 0000000..c89546d --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/home_page.dart @@ -0,0 +1,651 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/coupon_card.dart'; +import '../../../ai_agent/presentation/widgets/ai_fab.dart'; +import '../widgets/receive_coupon_sheet.dart'; + +/// 首页 - 券钱包 + 分类网格 + AI推荐 + 精选券 +/// +/// Tab导航:首页/交易/消息/我的 +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + int _walletFilter = 0; // 0=全部, 1=可使用, 2=待核销, 3=已过期 + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + CustomScrollView( + slivers: [ + // Floating App Bar + SliverAppBar( + floating: true, + pinned: false, + backgroundColor: AppColors.background, + elevation: 0, + toolbarHeight: 60, + title: _buildSearchBar(context), + actions: [ + IconButton( + icon: const Icon(Icons.qr_code_scanner_rounded, size: 24), + onPressed: () {}, + color: AppColors.textPrimary, + ), + ], + ), + + // Coupon Wallet (replaces Banner) + SliverToBoxAdapter(child: _buildCouponWallet(context)), + + // Category Grid (6 new categories) + SliverToBoxAdapter(child: _buildCategoryGrid()), + + // AI Smart Suggestions + SliverToBoxAdapter(child: _buildAiSuggestions()), + + // Section: Featured Coupons + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('精选好券', style: AppTypography.h2), + GestureDetector( + onTap: () {}, + child: Text('查看全部', style: AppTypography.labelSmall.copyWith( + color: AppColors.primary, + )), + ), + ], + ), + ), + ), + + // Coupon List + SliverPadding( + padding: AppSpacing.pagePadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: CouponCard( + brandName: _mockBrands[index % _mockBrands.length], + couponName: _mockNames[index % _mockNames.length], + faceValue: _mockFaceValues[index % _mockFaceValues.length], + currentPrice: _mockPrices[index % _mockPrices.length], + creditRating: _mockRatings[index % _mockRatings.length], + expiryDate: DateTime.now().add(Duration(days: (index + 1) * 5)), + onTap: () { + Navigator.pushNamed(context, '/coupon/detail'); + }, + ), + ), + childCount: 10, + ), + ), + ), + + const SliverPadding(padding: EdgeInsets.only(bottom: 100)), + ], + ), + + // AI FAB + Positioned( + right: 20, + bottom: 100, + child: AiFab( + unreadCount: 3, + onTap: () { + Navigator.pushNamed(context, '/ai-chat'); + }, + ), + ), + ], + ), + ); + } + + Widget _buildSearchBar(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/search'); + }, + child: Container( + height: 40, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + const SizedBox(width: 14), + const Icon(Icons.search_rounded, size: 20, color: AppColors.textTertiary), + const SizedBox(width: 8), + Text( + '搜索券、品牌、分类...', + style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary), + ), + ], + ), + ), + ); + } + + // ============================================================ + // Coupon Wallet Section (replaces Banner) + // ============================================================ + Widget _buildCouponWallet(BuildContext context) { + return Container( + margin: const EdgeInsets.fromLTRB(20, 8, 20, 0), + decoration: BoxDecoration( + gradient: AppColors.cardGradient, + borderRadius: AppSpacing.borderRadiusLg, + boxShadow: AppSpacing.shadowPrimary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: 我的钱包 + 接收 button + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(Icons.account_balance_wallet_rounded, + size: 20, color: Colors.white), + const SizedBox(width: 8), + Text('我的钱包', + style: AppTypography.h3.copyWith(color: Colors.white)), + ], + ), + GestureDetector( + onTap: () => _showReceiveSheet(context), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 0.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.qr_code_rounded, + size: 14, color: Colors.white), + const SizedBox(width: 4), + Text('接收', + style: AppTypography.labelSmall.copyWith( + color: Colors.white, + fontWeight: FontWeight.w500, + )), + ], + ), + ), + ), + ], + ), + ), + + // Stats row + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 0), + child: Row( + children: [ + _buildWalletStat('可使用', '3', true), + const SizedBox(width: 20), + _buildWalletStat('待核销', '1', false), + const SizedBox(width: 20), + _buildWalletStat('已过期', '0', false), + ], + ), + ), + + // Filter tabs + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Row( + children: [ + _buildWalletTab('全部', 0), + const SizedBox(width: 6), + _buildWalletTab('可使用', 1), + const SizedBox(width: 6), + _buildWalletTab('待核销', 2), + const SizedBox(width: 6), + _buildWalletTab('已过期', 3), + ], + ), + ), + + // Coupon mini-cards (horizontal scroll) + SizedBox( + height: 88, + child: _filteredWalletCoupons.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + '暂无券,去交易市场看看吧', + style: AppTypography.bodySmall.copyWith( + color: Colors.white.withValues(alpha: 0.6), + ), + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.fromLTRB(16, 10, 16, 14), + itemCount: _filteredWalletCoupons.length, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (context, index) { + final coupon = _filteredWalletCoupons[index]; + return _buildWalletCouponCard(context, coupon); + }, + ), + ), + + // Quick actions bar + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.1), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildWalletAction(Icons.qr_code_rounded, '接收', () { + _showReceiveSheet(context); + }), + _buildWalletAction(Icons.card_giftcard_rounded, '转赠', () { + Navigator.pushNamed(context, '/transfer'); + }), + _buildWalletAction(Icons.sell_rounded, '出售', () { + Navigator.pushNamed(context, '/sell'); + }), + _buildWalletAction(Icons.check_circle_outline_rounded, '核销', () { + Navigator.pushNamed(context, '/redeem'); + }), + ], + ), + ), + ], + ), + ); + } + + Widget _buildWalletStat(String label, String count, bool highlight) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + count, + style: AppTypography.h2.copyWith( + color: highlight ? Colors.white : Colors.white.withValues(alpha: 0.7), + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: AppTypography.caption.copyWith( + color: Colors.white.withValues(alpha: 0.6), + ), + ), + ], + ); + } + + Widget _buildWalletTab(String label, int index) { + final isSelected = _walletFilter == index; + return GestureDetector( + onTap: () => setState(() => _walletFilter = index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withValues(alpha: 0.25) + : Colors.transparent, + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all( + color: isSelected + ? Colors.white.withValues(alpha: 0.4) + : Colors.transparent, + width: 0.5, + ), + ), + child: Text( + label, + style: AppTypography.caption.copyWith( + color: isSelected + ? Colors.white + : Colors.white.withValues(alpha: 0.5), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + ); + } + + Widget _buildWalletCouponCard(BuildContext context, _WalletCoupon coupon) { + return GestureDetector( + onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail'), + child: Container( + width: 140, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 0.5, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.confirmation_number_outlined, + size: 14, color: Colors.white.withValues(alpha: 0.7)), + const SizedBox(width: 4), + Expanded( + child: Text( + coupon.brandName, + style: AppTypography.caption.copyWith( + color: Colors.white.withValues(alpha: 0.7), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Text( + coupon.name, + style: AppTypography.labelSmall.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '\$${coupon.faceValue.toStringAsFixed(0)}', + style: AppTypography.priceSmall.copyWith( + color: Colors.white, + fontSize: 13, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: coupon.statusColor.withValues(alpha: 0.3), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + coupon.statusLabel, + style: AppTypography.caption.copyWith( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildWalletAction(IconData icon, String label, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 20, color: Colors.white.withValues(alpha: 0.9)), + const SizedBox(height: 4), + Text( + label, + style: AppTypography.caption.copyWith( + color: Colors.white.withValues(alpha: 0.8), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + void _showReceiveSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => const ReceiveCouponSheet(), + ); + } + + List<_WalletCoupon> get _filteredWalletCoupons { + if (_walletFilter == 0) return _mockWalletCoupons; + if (_walletFilter == 1) { + return _mockWalletCoupons + .where((c) => c.status == CouponStatus.active) + .toList(); + } + if (_walletFilter == 2) { + return _mockWalletCoupons + .where((c) => c.status == CouponStatus.pending) + .toList(); + } + return _mockWalletCoupons + .where((c) => c.status == CouponStatus.expired) + .toList(); + } + + // ============================================================ + // Category Grid (6 new savings-focused categories) + // ============================================================ + Widget _buildCategoryGrid() { + final categories = [ + ('限时抢购', Icons.flash_on_rounded, AppColors.error), + ('新券首发', Icons.fiber_new_rounded, AppColors.primary), + ('折扣排行', Icons.trending_up_rounded, AppColors.couponEntertainment), + ('即将到期', Icons.timer_rounded, AppColors.info), + ('比价', Icons.compare_arrows_rounded, AppColors.couponShopping), + ('全部分类', Icons.grid_view_rounded, AppColors.textSecondary), + ]; + + return Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 1.1, + ), + itemCount: categories.length, + itemBuilder: (context, index) { + final (name, icon, color) = categories[index]; + return GestureDetector( + onTap: () {}, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Icon(icon, color: color, size: 24), + ), + const SizedBox(height: 6), + Text(name, style: AppTypography.caption.copyWith( + color: AppColors.textPrimary, + fontWeight: FontWeight.w500, + )), + ], + ), + ); + }, + ), + ); + } + + Widget _buildAiSuggestions() { + return Container( + margin: const EdgeInsets.fromLTRB(20, 16, 20, 0), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)), + ), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 16), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('AI 推荐', style: AppTypography.labelSmall.copyWith( + color: AppColors.primary, + )), + const SizedBox(height: 2), + Text( + '根据你的偏好,发现了3张高性价比券', + style: AppTypography.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const Icon(Icons.chevron_right_rounded, color: AppColors.primary, size: 20), + ], + ), + ); + } +} + +// ============================================================ +// Wallet Coupon Model +// ============================================================ +class _WalletCoupon { + final String brandName; + final String name; + final double faceValue; + final CouponStatus status; + final DateTime expiryDate; + + const _WalletCoupon({ + required this.brandName, + required this.name, + required this.faceValue, + required this.status, + required this.expiryDate, + }); + + String get statusLabel { + switch (status) { + case CouponStatus.active: + return '可使用'; + case CouponStatus.pending: + return '待核销'; + case CouponStatus.expired: + return '已过期'; + case CouponStatus.used: + return '已使用'; + } + } + + Color get statusColor { + switch (status) { + case CouponStatus.active: + return AppColors.success; + case CouponStatus.pending: + return AppColors.warning; + case CouponStatus.expired: + return AppColors.textTertiary; + case CouponStatus.used: + return AppColors.textDisabled; + } + } +} + +// Mock data +final _mockWalletCoupons = [ + _WalletCoupon( + brandName: 'Starbucks', + name: '星巴克 \$25 礼品卡', + faceValue: 25.0, + status: CouponStatus.active, + expiryDate: DateTime.now().add(const Duration(days: 30)), + ), + _WalletCoupon( + brandName: 'Amazon', + name: 'Amazon \$100 购物券', + faceValue: 100.0, + status: CouponStatus.active, + expiryDate: DateTime.now().add(const Duration(days: 45)), + ), + _WalletCoupon( + brandName: 'Nike', + name: 'Nike \$80 运动券', + faceValue: 80.0, + status: CouponStatus.pending, + expiryDate: DateTime.now().add(const Duration(days: 15)), + ), + _WalletCoupon( + brandName: 'Target', + name: 'Target \$30 折扣券', + faceValue: 30.0, + status: CouponStatus.active, + expiryDate: DateTime.now().add(const Duration(days: 60)), + ), +]; + +const _mockBrands = ['Starbucks', 'Amazon', 'Walmart', 'Target', 'Nike']; +const _mockNames = ['星巴克 \$25 礼品卡', 'Amazon \$100 购物券', 'Walmart \$50 生活券', 'Target \$30 折扣券', 'Nike \$80 运动券']; +const _mockFaceValues = [25.0, 100.0, 50.0, 30.0, 80.0]; +const _mockPrices = [21.25, 85.0, 42.5, 24.0, 68.0]; +const _mockRatings = ['AAA', 'AA', 'AAA', 'A', 'AA']; diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/market_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/market_page.dart new file mode 100644 index 0000000..80dc664 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/market_page.dart @@ -0,0 +1,645 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 交易 - 币安风格交易所 +/// +/// 一级市场(打新申购)/ 二级市场(交易所) +/// 支持交易对:券/法币、券/数字货币、券/稳定币 +class MarketPage extends StatefulWidget { + const MarketPage({super.key}); + + @override + State createState() => _MarketPageState(); +} + +class _MarketPageState extends State + with SingleTickerProviderStateMixin { + late TabController _marketTabController; + int _pairTypeIndex = 0; // 0=券/法币, 1=券/数字货币, 2=券/稳定币, 3=收藏 + + @override + void initState() { + super.initState(); + _marketTabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _marketTabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('交易'), + actions: [ + IconButton( + icon: const Icon(Icons.search_rounded, size: 22), + onPressed: () {}, + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(AppSpacing.tabBarHeight), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: TabBar( + controller: _marketTabController, + indicator: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusFull, + boxShadow: AppSpacing.shadowSm, + ), + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + labelColor: AppColors.textPrimary, + unselectedLabelColor: AppColors.textTertiary, + tabs: const [ + Tab(text: '一级市场'), + Tab(text: '二级市场'), + ], + ), + ), + ), + ), + body: TabBarView( + controller: _marketTabController, + children: [ + _buildPrimaryMarket(), + _buildSecondaryMarket(), + ], + ), + ); + } + + // ============================================================ + // 一级市场 - 打新申购 (Launchpad style) + // ============================================================ + Widget _buildPrimaryMarket() { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 100), + itemCount: _mockLaunches.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final launch = _mockLaunches[index]; + return _buildLaunchCard(context, launch); + }, + ); + } + + Widget _buildLaunchCard(BuildContext context, _LaunchItem launch) { + return GestureDetector( + onTap: () => Navigator.pushNamed(context, '/coupon/detail'), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + boxShadow: AppSpacing.shadowSm, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: brand + status badge + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.confirmation_number_outlined, + color: AppColors.primary, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(launch.brandName, style: AppTypography.labelMedium), + Text(launch.couponName, style: AppTypography.bodySmall), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: launch.statusColor.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + launch.statusText, + style: AppTypography.caption.copyWith( + color: launch.statusColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Price + Supply info + Row( + children: [ + _buildLaunchInfo('发行价', '\$${launch.issuePrice.toStringAsFixed(2)}'), + _buildLaunchInfo('面值', '\$${launch.faceValue.toStringAsFixed(0)}'), + _buildLaunchInfo('折扣', '${(launch.issuePrice / launch.faceValue * 10).toStringAsFixed(1)}折'), + _buildLaunchInfo('发行量', '${launch.totalSupply}'), + ], + ), + + const SizedBox(height: 12), + + // Progress bar + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('销售进度', style: AppTypography.caption), + Text( + '${(launch.soldPercent * 100).toStringAsFixed(1)}%', + style: AppTypography.caption.copyWith( + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 6), + ClipRRect( + borderRadius: AppSpacing.borderRadiusFull, + child: LinearProgressIndicator( + value: launch.soldPercent, + minHeight: 6, + backgroundColor: AppColors.gray100, + valueColor: + const AlwaysStoppedAnimation(AppColors.primary), + ), + ), + ], + ), + + if (launch.status == 0) ...[ + const SizedBox(height: 12), + // Countdown + Row( + children: [ + const Icon(Icons.access_time_rounded, + size: 14, color: AppColors.warning), + const SizedBox(width: 4), + Text( + '距开始: ${launch.countdown}', + style: AppTypography.caption.copyWith( + color: AppColors.warning, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ], + ), + ), + ); + } + + Widget _buildLaunchInfo(String label, String value) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.caption), + const SizedBox(height: 2), + Text(value, + style: AppTypography.labelSmall.copyWith( + color: AppColors.textPrimary, + fontWeight: FontWeight.w600, + )), + ], + ), + ); + } + + // ============================================================ + // 二级市场 - 交易所行情 (Binance style) + // ============================================================ + Widget _buildSecondaryMarket() { + return Column( + children: [ + const SizedBox(height: 12), + // Pair type tabs + _buildPairTypeTabs(), + + const SizedBox(height: 8), + + // Column headers + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Row( + children: [ + Expanded( + flex: 3, + child: + Text('交易对', style: AppTypography.caption)), + Expanded( + flex: 2, + child: Text('最新价', + style: AppTypography.caption, + textAlign: TextAlign.right)), + Expanded( + flex: 2, + child: Text('24h涨跌', + style: AppTypography.caption, + textAlign: TextAlign.right)), + ], + ), + ), + + const Divider(height: 1), + + // Trading pairs list + Expanded( + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 100), + itemCount: _currentPairs.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final pair = _currentPairs[index]; + return _buildTradingPairRow(context, pair); + }, + ), + ), + ], + ); + } + + Widget _buildPairTypeTabs() { + final tabs = ['券/法币', '券/数字货币', '券/稳定币', '收藏']; + return SizedBox( + height: 36, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: tabs.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) { + final isSelected = _pairTypeIndex == index; + return GestureDetector( + onTap: () => setState(() => _pairTypeIndex = index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + border: + isSelected ? null : Border.all(color: AppColors.borderLight), + ), + alignment: Alignment.center, + child: Text( + tabs[index], + style: AppTypography.labelSmall.copyWith( + color: isSelected ? Colors.white : AppColors.textSecondary, + ), + ), + ), + ); + }, + ), + ); + } + + Widget _buildTradingPairRow(BuildContext context, _TradingPair pair) { + final isPositive = pair.change24h >= 0; + final changeColor = isPositive ? AppColors.success : AppColors.error; + final changePrefix = isPositive ? '+' : ''; + + return InkWell( + onTap: () { + Navigator.pushNamed(context, '/trading/detail', arguments: pair); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row( + children: [ + // Trading pair name + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + pair.baseName, + style: AppTypography.labelMedium.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Text( + ' / ${pair.quoteName}', + style: AppTypography.bodySmall.copyWith( + color: AppColors.textTertiary, + ), + ), + ], + ), + const SizedBox(height: 2), + Text( + 'Vol ${pair.volume24h}', + style: AppTypography.caption, + ), + ], + ), + ), + + // Price + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + pair.priceDisplay, + style: AppTypography.labelMedium.copyWith( + color: changeColor, + fontWeight: FontWeight.w600, + ), + ), + Text( + '≈ \$${pair.priceUsd.toStringAsFixed(2)}', + style: AppTypography.caption, + ), + ], + ), + ), + + // 24h Change + Expanded( + flex: 2, + child: Align( + alignment: Alignment.centerRight, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: changeColor, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Text( + '$changePrefix${pair.change24h.toStringAsFixed(2)}%', + style: AppTypography.labelSmall.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + List<_TradingPair> get _currentPairs { + switch (_pairTypeIndex) { + case 0: + return _fiatPairs; + case 1: + return _cryptoPairs; + case 2: + return _stablePairs; + case 3: + return _favoritePairs; + default: + return _fiatPairs; + } + } +} + +// ============================================================ +// Data Models +// ============================================================ +class _LaunchItem { + final String brandName; + final String couponName; + final double issuePrice; + final double faceValue; + final int totalSupply; + final double soldPercent; + final int status; // 0=upcoming, 1=live, 2=ended + final String countdown; + + const _LaunchItem({ + required this.brandName, + required this.couponName, + required this.issuePrice, + required this.faceValue, + required this.totalSupply, + required this.soldPercent, + required this.status, + this.countdown = '', + }); + + String get statusText { + switch (status) { + case 0: + return '即将开始'; + case 1: + return '申购中'; + case 2: + return '已结束'; + default: + return ''; + } + } + + Color get statusColor { + switch (status) { + case 0: + return AppColors.warning; + case 1: + return AppColors.success; + case 2: + return AppColors.textTertiary; + default: + return AppColors.textTertiary; + } + } +} + +class _TradingPair { + final String baseName; + final String quoteName; + final double price; + final double priceUsd; + final double change24h; + final String volume24h; + final double high24h; + final double low24h; + final double open24h; + + const _TradingPair({ + required this.baseName, + required this.quoteName, + required this.price, + required this.priceUsd, + required this.change24h, + required this.volume24h, + required this.high24h, + required this.low24h, + required this.open24h, + }); + + String get priceDisplay => price >= 1 + ? price.toStringAsFixed(2) + : price.toStringAsFixed(6); + + String get pairSymbol => '$baseName/$quoteName'; +} + +// ============================================================ +// Mock Data +// ============================================================ +final _mockLaunches = [ + const _LaunchItem( + brandName: 'Starbucks', + couponName: '星巴克 \$25 礼品卡 2026春季限定', + issuePrice: 21.25, + faceValue: 25.0, + totalSupply: 10000, + soldPercent: 0.73, + status: 1, + ), + const _LaunchItem( + brandName: 'Nike', + couponName: 'Nike \$100 运动券 Air Max 联名', + issuePrice: 82.0, + faceValue: 100.0, + totalSupply: 5000, + soldPercent: 0.0, + status: 0, + countdown: '2天 14:30:00', + ), + const _LaunchItem( + brandName: 'Amazon', + couponName: 'Amazon \$50 购物券 Prime专属', + issuePrice: 42.5, + faceValue: 50.0, + totalSupply: 20000, + soldPercent: 1.0, + status: 2, + ), + const _LaunchItem( + brandName: 'Walmart', + couponName: 'Walmart \$30 生活券', + issuePrice: 24.0, + faceValue: 30.0, + totalSupply: 15000, + soldPercent: 0.45, + status: 1, + ), +]; + +// 券/法币 trading pairs +final _fiatPairs = [ + const _TradingPair( + baseName: 'SBUX', quoteName: 'USD', + price: 21.35, priceUsd: 21.35, change24h: 2.15, + volume24h: '125.3K', high24h: 21.80, low24h: 20.90, open24h: 20.90, + ), + const _TradingPair( + baseName: 'AMZN', quoteName: 'USD', + price: 85.20, priceUsd: 85.20, change24h: -1.23, + volume24h: '89.7K', high24h: 87.50, low24h: 84.10, open24h: 86.26, + ), + const _TradingPair( + baseName: 'NIKE', quoteName: 'USD', + price: 68.50, priceUsd: 68.50, change24h: 5.32, + volume24h: '234.1K', high24h: 69.20, low24h: 65.00, open24h: 65.04, + ), + const _TradingPair( + baseName: 'TGT', quoteName: 'CNY', + price: 168.80, priceUsd: 24.00, change24h: -0.56, + volume24h: '45.2K', high24h: 170.50, low24h: 167.00, open24h: 169.75, + ), + const _TradingPair( + baseName: 'WMT', quoteName: 'USD', + price: 42.30, priceUsd: 42.30, change24h: 1.87, + volume24h: '67.8K', high24h: 43.00, low24h: 41.50, open24h: 41.52, + ), + const _TradingPair( + baseName: 'COST', quoteName: 'USD', + price: 38.75, priceUsd: 38.75, change24h: 3.41, + volume24h: '156.2K', high24h: 39.50, low24h: 37.40, open24h: 37.47, + ), +]; + +// 券/数字货币 trading pairs +final _cryptoPairs = [ + const _TradingPair( + baseName: 'SBUX', quoteName: 'BTC', + price: 0.000215, priceUsd: 21.35, change24h: 1.85, + volume24h: '12.5K', high24h: 0.000220, low24h: 0.000210, open24h: 0.000211, + ), + const _TradingPair( + baseName: 'AMZN', quoteName: 'ETH', + price: 0.0234, priceUsd: 85.20, change24h: -2.10, + volume24h: '8.3K', high24h: 0.0240, low24h: 0.0230, open24h: 0.0239, + ), + const _TradingPair( + baseName: 'NIKE', quoteName: 'BTC', + price: 0.000690, priceUsd: 68.50, change24h: 4.56, + volume24h: '15.7K', high24h: 0.000700, low24h: 0.000660, open24h: 0.000660, + ), +]; + +// 券/稳定币 trading pairs +final _stablePairs = [ + const _TradingPair( + baseName: 'SBUX', quoteName: 'USDT', + price: 21.30, priceUsd: 21.30, change24h: 2.05, + volume24h: '342.5K', high24h: 21.75, low24h: 20.85, open24h: 20.87, + ), + const _TradingPair( + baseName: 'AMZN', quoteName: 'USDT', + price: 85.10, priceUsd: 85.10, change24h: -1.35, + volume24h: '189.2K', high24h: 87.40, low24h: 84.00, open24h: 86.26, + ), + const _TradingPair( + baseName: 'NIKE', quoteName: 'USDC', + price: 68.45, priceUsd: 68.45, change24h: 5.20, + volume24h: '278.9K', high24h: 69.10, low24h: 64.90, open24h: 65.07, + ), + const _TradingPair( + baseName: 'WMT', quoteName: 'USDT', + price: 42.25, priceUsd: 42.25, change24h: 1.92, + volume24h: '98.4K', high24h: 42.90, low24h: 41.45, open24h: 41.45, + ), + const _TradingPair( + baseName: 'TGT', quoteName: 'USDC', + price: 24.10, priceUsd: 24.10, change24h: -0.41, + volume24h: '34.1K', high24h: 24.50, low24h: 23.80, open24h: 24.20, + ), +]; + +// Favorites +final _favoritePairs = [ + _stablePairs[0], // SBUX/USDT + _fiatPairs[2], // NIKE/USD +]; diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart new file mode 100644 index 0000000..0fef4df --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; +import '../../../../shared/widgets/status_tag.dart'; + +/// A4. 券详情(持有券)- QR码/条形码 + 转赠/出售/提取 +/// +/// 券二维码/条形码(核销用)、券信息、使用说明、 +/// 「转赠」「出售」「使用说明」按钮 +class MyCouponDetailPage extends StatelessWidget { + const MyCouponDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text('券详情'), + actions: [ + IconButton( + icon: const Icon(Icons.more_horiz_rounded), + onPressed: () => _showMoreOptions(context), + ), + ], + ), + body: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + children: [ + const SizedBox(height: 16), + + // QR Code Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: AppColors.cardGradient, + borderRadius: AppSpacing.borderRadiusLg, + boxShadow: AppSpacing.shadowPrimary, + ), + child: Column( + children: [ + // Brand + Status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Starbucks', style: AppTypography.bodySmall.copyWith( + color: Colors.white70, + )), + Text('星巴克 \$25 礼品卡', style: AppTypography.h2.copyWith( + color: Colors.white, + )), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text('可使用', style: AppTypography.caption.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + )), + ), + ], + ), + const SizedBox(height: 24), + + // QR Code area + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.qr_code_rounded, size: 140, + color: AppColors.textPrimary), + const SizedBox(height: 8), + Text('GNX-STB-A1B2C3D4', + style: AppTypography.caption.copyWith( + letterSpacing: 1.5, + fontWeight: FontWeight.w600, + )), + ], + ), + ), + const SizedBox(height: 16), + + // Instructions + Text( + '出示此二维码给商户扫描核销', + style: AppTypography.bodySmall.copyWith(color: Colors.white70), + ), + + const SizedBox(height: 8), + + // Barcode toggle + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.view_headline_rounded, size: 18, + color: Colors.white70), + label: Text('切换条形码', style: AppTypography.labelSmall.copyWith( + color: Colors.white70, + )), + ), + ], + ), + ), + const SizedBox(height: 20), + + // Face Value + Expiry + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + _infoRow('面值', '\$25.00'), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow('购买价格', '\$21.25'), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow('有效期', '2026/12/31'), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow('订单号', 'GNX-20260209-001234'), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow('剩余可转售次数', '3次'), + ], + ), + ), + const SizedBox(height: 16), + + // Action Buttons + Row( + children: [ + Expanded( + child: GenexButton( + label: '转赠', + icon: Icons.card_giftcard_rounded, + variant: GenexButtonVariant.secondary, + onPressed: () { + Navigator.pushNamed(context, '/transfer'); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: GenexButton( + label: '出售', + icon: Icons.sell_rounded, + variant: GenexButtonVariant.outline, + onPressed: () { + Navigator.pushNamed(context, '/sell'); + }, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Usage Rules + Container( + width: double.infinity, + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('使用说明', style: AppTypography.labelMedium), + const SizedBox(height: 12), + _ruleItem('全国星巴克门店通用'), + _ruleItem('请在有效期内使用'), + _ruleItem('每次消费仅可使用一张'), + _ruleItem('不可兑换现金'), + ], + ), + ), + + const SizedBox(height: 80), + ], + ), + ), + ); + } + + Widget _infoRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodyMedium.copyWith( + color: AppColors.textSecondary, + )), + Text(value, style: AppTypography.labelMedium), + ], + ); + } + + Widget _ruleItem(String text) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Container( + width: 4, height: 4, + decoration: const BoxDecoration( + color: AppColors.textTertiary, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text(text, style: AppTypography.bodySmall), + ], + ), + ); + } + + void _showMoreOptions(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (ctx) => Container( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + const SizedBox(height: 16), + _optionTile(Icons.wallet_rounded, '提取到外部钱包', '需KYC L2+认证', () {}), + const Divider(), + _optionTile(Icons.receipt_long_rounded, '查看交易记录', '', () {}), + const Divider(), + _optionTile(Icons.help_outline_rounded, '使用帮助', '', () {}), + ], + ), + ), + ); + } + + Widget _optionTile(IconData icon, String title, String subtitle, VoidCallback onTap) { + return ListTile( + leading: Icon(icon, color: AppColors.textPrimary), + title: Text(title, style: AppTypography.labelMedium), + subtitle: subtitle.isNotEmpty + ? Text(subtitle, style: AppTypography.caption) + : null, + trailing: const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary), + onTap: onTap, + contentPadding: EdgeInsets.zero, + ); + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupons_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupons_page.dart new file mode 100644 index 0000000..139db64 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupons_page.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/coupon_card.dart'; +import '../../../../shared/widgets/status_tag.dart'; +import '../../../../shared/widgets/empty_state.dart'; + +/// A4. 我的券列表 +/// +/// 券卡片(图片+品牌+面值+状态+到期倒计时) +/// 筛选(全部/可使用/待核销/已过期)、批量操作 +class MyCouponsPage extends StatefulWidget { + const MyCouponsPage({super.key}); + + @override + State createState() => _MyCouponsPageState(); +} + +class _MyCouponsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 4, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('我的券'), + actions: [ + IconButton( + icon: const Icon(Icons.sort_rounded, size: 22), + onPressed: () {}, + ), + ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: '全部'), + Tab(text: '可使用'), + Tab(text: '待核销'), + Tab(text: '已过期'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildCouponList(null), + _buildCouponList(CouponStatus.active), + _buildCouponList(CouponStatus.pending), + _buildCouponList(CouponStatus.expired), + ], + ), + ); + } + + Widget _buildCouponList(CouponStatus? filter) { + // Example: show empty state for expired tab + if (filter == CouponStatus.expired) { + return EmptyState.noCoupons( + onBrowse: () { + // noop - market tab accessible from bottom nav + }, + ); + } + + return ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 100), + itemCount: 5, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + return _MyCouponCard( + brandName: ['Starbucks', 'Amazon', 'Target', 'Nike', 'Walmart'][index], + couponName: [ + '星巴克 \$25 礼品卡', + 'Amazon \$100 购物券', + 'Target \$30 折扣券', + 'Nike \$80 运动券', + 'Walmart \$50 生活券', + ][index], + faceValue: [25.0, 100.0, 30.0, 80.0, 50.0][index], + status: filter ?? CouponStatus.active, + expiryDate: DateTime.now().add(Duration(days: (index + 1) * 7)), + onTap: () { + Navigator.pushNamed(context, '/coupon/mine/detail'); + }, + ); + }, + ); + } +} + +/// 我的券卡片 - 带状态和操作入口 +class _MyCouponCard extends StatelessWidget { + final String brandName; + final String couponName; + final double faceValue; + final CouponStatus status; + final DateTime expiryDate; + final VoidCallback? onTap; + + const _MyCouponCard({ + required this.brandName, + required this.couponName, + required this.faceValue, + required this.status, + required this.expiryDate, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + boxShadow: AppSpacing.shadowSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + Row( + children: [ + // Coupon image placeholder + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon( + Icons.confirmation_number_outlined, + color: AppColors.primary.withValues(alpha: 0.4), + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(brandName, style: AppTypography.caption), + Text(couponName, style: AppTypography.labelMedium), + const SizedBox(height: 4), + Row( + children: [ + Text('面值 \$${faceValue.toStringAsFixed(0)}', + style: AppTypography.bodySmall), + const SizedBox(width: 8), + _statusWidget, + ], + ), + ], + ), + ), + const Icon(Icons.chevron_right_rounded, + color: AppColors.textTertiary, size: 20), + ], + ), + + // Expiry + Quick Actions + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + Icon(Icons.access_time_rounded, size: 14, color: _expiryColor), + const SizedBox(width: 4), + Text( + _expiryText, + style: AppTypography.caption.copyWith(color: _expiryColor), + ), + const Spacer(), + if (status == CouponStatus.active) ...[ + _quickAction('转赠', Icons.card_giftcard_rounded), + const SizedBox(width: 12), + _quickAction('出售', Icons.sell_rounded), + ], + ], + ), + ), + ], + ), + ), + ); + } + + Widget get _statusWidget { + switch (status) { + case CouponStatus.active: + return StatusTags.active(); + case CouponStatus.pending: + return StatusTags.pending(); + case CouponStatus.expired: + return StatusTags.expired(); + case CouponStatus.used: + return StatusTags.used(); + } + } + + Widget _quickAction(String label, IconData icon) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: AppColors.primary), + const SizedBox(width: 3), + Text(label, style: AppTypography.caption.copyWith( + color: AppColors.primary, + fontWeight: FontWeight.w500, + )), + ], + ); + } + + String get _expiryText { + final days = expiryDate.difference(DateTime.now()).inDays; + if (days < 0) return '已过期'; + if (days == 0) return '今天到期'; + if (days <= 7) return '$days天后到期'; + return '${expiryDate.year}/${expiryDate.month}/${expiryDate.day}到期'; + } + + Color get _expiryColor { + final days = expiryDate.difference(DateTime.now()).inDays; + if (days <= 3) return AppColors.error; + if (days <= 7) return AppColors.warning; + return AppColors.textTertiary; + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/order_confirm_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/order_confirm_page.dart new file mode 100644 index 0000000..65ca215 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/order_confirm_page.dart @@ -0,0 +1,376 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A3. 确认订单 + 支付页面 +/// +/// 券信息摘要、数量选择、价格计算、支付方式选择 +/// 支付成功:成功动画、订单号、「查看我的券」「继续逛」 +class OrderConfirmPage extends StatefulWidget { + const OrderConfirmPage({super.key}); + + @override + State createState() => _OrderConfirmPageState(); +} + +class _OrderConfirmPageState extends State { + int _quantity = 1; + int _selectedPayment = 0; + + double get _unitPrice => 21.25; + double get _totalPrice => _unitPrice * _quantity; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text('确认订单'), + ), + body: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + + // Coupon Info Summary + _buildCouponSummary(), + const SizedBox(height: 16), + + // Quantity Selector + _buildQuantitySelector(), + const SizedBox(height: 16), + + // Payment Method + _buildPaymentMethods(), + const SizedBox(height: 16), + + // Price Breakdown + _buildPriceBreakdown(), + const SizedBox(height: 16), + + // Utility Track Notice + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + Icon(Icons.verified_user_rounded, size: 16, + color: AppColors.utilityTrack), + const SizedBox(width: 8), + Expanded( + child: Text( + '您正在购买消费券用于消费', + style: AppTypography.bodySmall.copyWith( + color: AppColors.gray700, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 100), + ], + ), + ), + + // Bottom Pay Bar + bottomNavigationBar: Container( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.borderLight)), + ), + child: Row( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('合计', style: AppTypography.caption), + Text( + '\$${_totalPrice.toStringAsFixed(2)}', + style: AppTypography.priceMedium, + ), + ], + ), + const SizedBox(width: 20), + Expanded( + child: GenexButton( + label: '确认支付', + onPressed: () => _showPaymentAuth(context), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCouponSummary() { + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon(Icons.confirmation_number_outlined, + color: AppColors.primary.withValues(alpha: 0.4), size: 28), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Starbucks', style: AppTypography.caption), + Text('星巴克 \$25 礼品卡', style: AppTypography.labelMedium), + const SizedBox(height: 4), + Row( + children: [ + Text( + '\$21.25', + style: AppTypography.priceSmall.copyWith(fontSize: 15), + ), + const SizedBox(width: 6), + Text('\$25', style: AppTypography.priceOriginal), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text('8.5折', style: AppTypography.discountBadge), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildQuantitySelector() { + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('购买数量', style: AppTypography.labelMedium), + Row( + children: [ + _buildQtyButton(Icons.remove_rounded, () { + if (_quantity > 1) setState(() => _quantity--); + }), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text('$_quantity', style: AppTypography.h3), + ), + _buildQtyButton(Icons.add_rounded, () { + if (_quantity < 10) setState(() => _quantity++); + }), + ], + ), + ], + ), + ); + } + + Widget _buildQtyButton(IconData icon, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.border), + ), + child: Icon(icon, size: 18, color: AppColors.textPrimary), + ), + ); + } + + Widget _buildPaymentMethods() { + final methods = [ + ('银行卡/信用卡', Icons.credit_card_rounded), + ('Apple Pay', Icons.apple_rounded), + ]; + + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('支付方式', style: AppTypography.labelMedium), + const SizedBox(height: 12), + ...methods.asMap().entries.map((entry) { + final isSelected = _selectedPayment == entry.key; + return GestureDetector( + onTap: () => setState(() => _selectedPayment = entry.key), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: entry.key < methods.length - 1 + ? const Border(bottom: BorderSide(color: AppColors.borderLight)) + : null, + ), + child: Row( + children: [ + Icon(entry.value.$2, size: 24, color: AppColors.textPrimary), + const SizedBox(width: 12), + Text(entry.value.$1, style: AppTypography.bodyMedium), + const Spacer(), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColors.primary : AppColors.border, + width: isSelected ? 6 : 1.5, + ), + ), + ), + ], + ), + ), + ); + }), + ], + ), + ); + } + + Widget _buildPriceBreakdown() { + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + _priceRow('单价', '\$${_unitPrice.toStringAsFixed(2)}'), + const SizedBox(height: 8), + _priceRow('数量', '×$_quantity'), + const Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Divider(), + ), + _priceRow( + '合计', + '\$${_totalPrice.toStringAsFixed(2)}', + valueStyle: AppTypography.priceMedium, + ), + const SizedBox(height: 4), + Align( + alignment: Alignment.centerRight, + child: Text( + '比面值节省 \$${(25.0 * _quantity - _totalPrice).toStringAsFixed(2)}', + style: AppTypography.caption.copyWith(color: AppColors.success), + ), + ), + ], + ), + ); + } + + Widget _priceRow(String label, String value, {TextStyle? valueStyle}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + Text(value, style: valueStyle ?? AppTypography.labelMedium), + ], + ); + } + + void _showPaymentAuth(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (ctx) => Container( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Container( + width: 36, height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + const SizedBox(height: 20), + Text('确认支付', style: AppTypography.h2), + const SizedBox(height: 8), + Text( + '\$${_totalPrice.toStringAsFixed(2)}', + style: AppTypography.priceLarge.copyWith(fontSize: 36), + ), + const SizedBox(height: 4), + Text('星巴克 \$25 礼品卡 × $_quantity', + style: AppTypography.bodySmall), + const SizedBox(height: 32), + // Biometric / Password + Container( + width: 64, height: 64, + decoration: BoxDecoration( + color: AppColors.primarySurface, + shape: BoxShape.circle, + ), + child: const Icon(Icons.fingerprint_rounded, + size: 36, color: AppColors.primary), + ), + const SizedBox(height: 12), + Text('请验证指纹或面容以完成支付', style: AppTypography.bodySmall), + const SizedBox(height: 24), + GenexButton( + label: '使用密码支付', + variant: GenexButtonVariant.text, + onPressed: () { + Navigator.pop(ctx); + Navigator.pushNamed(context, '/payment'); + }, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_page.dart new file mode 100644 index 0000000..a45e207 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_page.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// A6. 支付方式选择页 +/// +/// 选择支付方式:信用卡/借记卡、Apple Pay、Google Pay +/// 后端自动完成:法币→稳定币→链上原子交换(消费者无感知) +class PaymentPage extends StatefulWidget { + const PaymentPage({super.key}); + + @override + State createState() => _PaymentPageState(); +} + +class _PaymentPageState extends State { + int _selectedMethod = 0; + + final _methods = const [ + _PaymentMethod('Visa •••• 4242', Icons.credit_card_rounded, 'visa'), + _PaymentMethod('Apple Pay', Icons.apple_rounded, 'apple_pay'), + _PaymentMethod('Google Pay', Icons.account_balance_wallet_rounded, 'google_pay'), + _PaymentMethod('银行转账', Icons.account_balance_rounded, 'bank'), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('选择支付方式')), + body: Column( + children: [ + // Order Summary + Container( + margin: const EdgeInsets.all(20), + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.confirmation_number_rounded, color: AppColors.primary), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('星巴克 \$25 礼品卡', style: AppTypography.labelMedium), + const SizedBox(height: 2), + Text('面值 \$25.00', style: AppTypography.bodySmall), + ], + ), + ), + Text('\$21.25', style: AppTypography.priceMedium), + ], + ), + ), + + // Payment Methods + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: _methods.length, + itemBuilder: (context, index) { + final method = _methods[index]; + final isSelected = _selectedMethod == index; + return GestureDetector( + onTap: () => setState(() => _selectedMethod = index), + child: Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all( + color: isSelected ? AppColors.primary : AppColors.borderLight, + width: isSelected ? 1.5 : 1, + ), + ), + child: Row( + children: [ + Icon(method.icon, color: isSelected ? AppColors.primary : AppColors.textSecondary, size: 24), + const SizedBox(width: 14), + Expanded( + child: Text(method.name, style: AppTypography.labelMedium), + ), + if (isSelected) + const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 22), + ], + ), + ), + ); + }, + ), + ), + + // Add new method + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add_rounded), + label: const Text('添加新支付方式'), + ), + ), + + // Pay Button + Container( + padding: const EdgeInsets.all(20), + child: SizedBox( + width: double.infinity, + height: AppSpacing.buttonHeight, + child: ElevatedButton( + onPressed: () { + // 后端自动完成法币→稳定币→链上原子交换 + Navigator.pushNamed(context, '/payment/success'); + }, + child: const Text('确认支付 \$21.25'), + ), + ), + ), + ], + ), + ); + } +} + +class _PaymentMethod { + final String name; + final IconData icon; + final String type; + + const _PaymentMethod(this.name, this.icon, this.type); +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_success_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_success_page.dart new file mode 100644 index 0000000..27ae308 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/payment_success_page.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A3. 支付成功页 +/// +/// 成功动画、订单号、「查看我的券」「继续逛」 +class PaymentSuccessPage extends StatelessWidget { + final String orderNumber; + final double amount; + final String couponName; + + const PaymentSuccessPage({ + super.key, + this.orderNumber = 'GNX-20260209-001234', + this.amount = 21.25, + this.couponName = '星巴克 \$25 礼品卡', + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: AppSpacing.pagePadding, + child: Column( + children: [ + const Spacer(flex: 2), + + // Success Icon + Container( + width: 88, + height: 88, + decoration: const BoxDecoration( + gradient: AppColors.successGradient, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check_rounded, + color: Colors.white, + size: 44, + ), + ), + const SizedBox(height: 24), + + Text('支付成功', style: AppTypography.h1), + const SizedBox(height: 8), + Text( + '券已到账,可在「我的券」中查看', + style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary), + ), + const SizedBox(height: 32), + + // Order Info Card + Container( + width: double.infinity, + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + _infoRow('券名称', couponName), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + _infoRow('支付金额', '\$$amount'), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + _infoRow('订单号', orderNumber), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + _infoRow('支付时间', '2026-02-09 14:32:15'), + ], + ), + ), + + const Spacer(flex: 3), + + // Actions + GenexButton( + label: '查看我的券', + onPressed: () { + Navigator.pushNamedAndRemoveUntil(context, '/main', (route) => false); + }, + ), + const SizedBox(height: 12), + GenexButton( + label: '继续逛', + variant: GenexButtonVariant.outline, + onPressed: () { + Navigator.pushNamedAndRemoveUntil(context, '/main', (route) => false); + }, + ), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _infoRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodyMedium.copyWith( + color: AppColors.textSecondary, + )), + Flexible( + child: Text( + value, + style: AppTypography.labelMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/redeem_qr_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/redeem_qr_page.dart new file mode 100644 index 0000000..6ba8a49 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/redeem_qr_page.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// A8. 出示券码页面(消费者端) +/// +/// QR码 + 数字券码 + 倒计时 + 自动调高亮度 +/// 商户扫码核销 +class RedeemQrPage extends StatefulWidget { + const RedeemQrPage({super.key}); + + @override + State createState() => _RedeemQrPageState(); +} + +class _RedeemQrPageState extends State { + int _remainingSeconds = 300; // 5 minutes + + @override + void initState() { + super.initState(); + _startCountdown(); + } + + void _startCountdown() { + Future.doWhile(() async { + await Future.delayed(const Duration(seconds: 1)); + if (!mounted) return false; + setState(() => _remainingSeconds--); + return _remainingSeconds > 0; + }); + } + + String get _formattedTime { + final min = _remainingSeconds ~/ 60; + final sec = _remainingSeconds % 60; + return '${min.toString().padLeft(2, '0')}:${sec.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.gray900, + appBar: AppBar( + backgroundColor: AppColors.gray900, + foregroundColor: Colors.white, + title: const Text('出示券码'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Coupon Info + Text('星巴克 \$25 礼品卡', style: AppTypography.h2.copyWith(color: Colors.white)), + const SizedBox(height: 4), + Text('面值 \$25.00', style: AppTypography.bodyMedium.copyWith(color: Colors.white60)), + const SizedBox(height: 32), + + // QR Code Area + Container( + width: 240, + height: 240, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: AppColors.gray200), + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Center( + child: Icon(Icons.qr_code_2_rounded, size: 160, color: AppColors.gray900), + ), + ), + ), + const SizedBox(height: 20), + + // Numeric Code + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + '8429 3751 0062', + style: AppTypography.h1.copyWith( + color: Colors.white, + letterSpacing: 2, + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(height: 24), + + // Countdown + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.timer_outlined, color: Colors.white54, size: 18), + const SizedBox(width: 6), + Text( + '有效时间 $_formattedTime', + style: AppTypography.bodyMedium.copyWith(color: Colors.white54), + ), + ], + ), + const SizedBox(height: 8), + TextButton( + onPressed: () { + setState(() => _remainingSeconds = 300); + }, + child: Text('刷新券码', style: AppTypography.labelMedium.copyWith(color: AppColors.primaryLight)), + ), + const SizedBox(height: 40), + + // Hint + Container( + margin: const EdgeInsets.symmetric(horizontal: 40), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.08), + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Row( + children: [ + const Icon(Icons.info_outline_rounded, color: Colors.white38, size: 18), + const SizedBox(width: 10), + Expanded( + child: Text( + '请将此码出示给商户扫描,屏幕已自动调至最高亮度', + style: AppTypography.caption.copyWith(color: Colors.white54), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/search_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/search_page.dart new file mode 100644 index 0000000..3938fc7 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/search_page.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/coupon_card.dart'; + +/// A3. 搜索页 - 搜索券、品牌、分类 +/// +/// 热门搜索标签 + 搜索历史 + 实时搜索结果 +class SearchPage extends StatefulWidget { + const SearchPage({super.key}); + + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State { + final _searchController = TextEditingController(); + bool _hasInput = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: _buildSearchInput(), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ], + ), + body: _hasInput ? _buildSearchResults() : _buildSearchSuggestions(), + ); + } + + Widget _buildSearchInput() { + return Container( + height: 40, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all(color: AppColors.borderLight), + ), + child: TextField( + controller: _searchController, + autofocus: true, + decoration: const InputDecoration( + hintText: '搜索券、品牌、分类...', + prefixIcon: Icon(Icons.search_rounded, size: 20), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 10), + ), + onChanged: (v) => setState(() => _hasInput = v.isNotEmpty), + ), + ); + } + + Widget _buildSearchSuggestions() { + return SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + // Hot Tags + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('热门搜索', style: AppTypography.h3), + GestureDetector(onTap: () {}, child: const Icon(Icons.refresh_rounded, size: 18, color: AppColors.textTertiary)), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: ['星巴克', 'Amazon', '餐饮券', '折扣券', '旅游', 'Nike'].map((tag) { + return GestureDetector( + onTap: () { + _searchController.text = tag; + setState(() => _hasInput = true); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all(color: AppColors.borderLight), + ), + child: Text(tag, style: AppTypography.labelSmall), + ), + ); + }).toList(), + ), + const SizedBox(height: 32), + + // History + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('搜索历史', style: AppTypography.h3), + GestureDetector( + onTap: () {}, + child: Text('清空', style: AppTypography.labelSmall.copyWith(color: AppColors.textTertiary)), + ), + ], + ), + const SizedBox(height: 12), + ...['星巴克 礼品卡', 'Nike 运动券', '餐饮 折扣'].map((h) { + return ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.history_rounded, size: 18, color: AppColors.textTertiary), + title: Text(h, style: AppTypography.bodyMedium), + trailing: const Icon(Icons.north_west_rounded, size: 16, color: AppColors.textTertiary), + onTap: () { + _searchController.text = h; + setState(() => _hasInput = true); + }, + ); + }), + ], + ), + ); + } + + Widget _buildSearchResults() { + return ListView.builder( + padding: AppSpacing.pagePadding, + itemCount: 5, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: CouponCard( + brandName: ['Starbucks', 'Amazon', 'Walmart', 'Target', 'Nike'][index], + couponName: ['星巴克 \$25 礼品卡', 'Amazon \$100 购物券', 'Walmart \$50 生活券', 'Target \$30 折扣券', 'Nike \$80 运动券'][index], + faceValue: [25.0, 100.0, 50.0, 30.0, 80.0][index], + currentPrice: [21.25, 85.0, 42.5, 24.0, 68.0][index], + creditRating: 'AAA', + expiryDate: DateTime.now().add(Duration(days: (index + 1) * 10)), + onTap: () { + Navigator.pushNamed(context, '/coupon/detail'); + }, + ), + ); + }, + ); + } +} diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/widgets/receive_coupon_sheet.dart b/frontend/genex-mobile/lib/features/coupons/presentation/widgets/receive_coupon_sheet.dart new file mode 100644 index 0000000..4939109 --- /dev/null +++ b/frontend/genex-mobile/lib/features/coupons/presentation/widgets/receive_coupon_sheet.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 接收券 - 底部弹出Sheet +/// +/// 展示接收ID和接收二维码,对方可以通过扫码转赠券到钱包 +class ReceiveCouponSheet extends StatelessWidget { + const ReceiveCouponSheet({super.key}); + + static const String _mockReceiveId = 'GNX-USR-8A3F-K9D2'; + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag handle + Container( + margin: const EdgeInsets.only(top: 12), + width: 36, + height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + + // Title + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('接收券', style: AppTypography.h2), + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon(Icons.close_rounded, + color: AppColors.textTertiary, size: 24), + ), + ], + ), + ), + + const SizedBox(height: 8), + + // Description + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + '向他人展示下方二维码或接收ID,对方可通过扫码或输入ID将券转赠到你的钱包。', + style: AppTypography.bodySmall, + ), + ), + + const SizedBox(height: 24), + + // QR Code area + Container( + margin: const EdgeInsets.symmetric(horizontal: 40), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight, width: 1.5), + boxShadow: AppSpacing.shadowMd, + ), + child: Column( + children: [ + // QR Code placeholder + Container( + width: 180, + height: 180, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Stack( + alignment: Alignment.center, + children: [ + // QR code pattern placeholder + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 9, + mainAxisSpacing: 2, + crossAxisSpacing: 2, + ), + itemCount: 81, + itemBuilder: (context, index) { + final isCorner = _isQrCorner(index); + final isDark = + isCorner || (index * 7 + 3) % 3 == 0; + return Container( + decoration: BoxDecoration( + color: isDark + ? AppColors.textPrimary + : Colors.transparent, + borderRadius: BorderRadius.circular(1), + ), + ); + }, + ), + // Center logo + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: const Icon( + Icons.account_balance_wallet_rounded, + color: AppColors.primary, + size: 18), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Receive ID + Text('接收ID', style: AppTypography.caption), + const SizedBox(height: 6), + GestureDetector( + onTap: () { + Clipboard.setData( + const ClipboardData(text: _mockReceiveId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('接收ID已复制到剪贴板'), + duration: Duration(seconds: 2), + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all( + color: + AppColors.primary.withValues(alpha: 0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _mockReceiveId, + style: AppTypography.labelMedium.copyWith( + color: AppColors.primary, + fontFamily: 'monospace', + letterSpacing: 1.0, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.copy_rounded, + size: 16, color: AppColors.primary), + ], + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Tips + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.infoLight, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + const Icon(Icons.info_outline_rounded, + size: 16, color: AppColors.info), + const SizedBox(width: 8), + Expanded( + child: Text( + '接收的券将自动存入你的钱包,可在首页钱包中查看和管理。', + style: AppTypography.bodySmall + .copyWith(color: AppColors.info), + ), + ), + ], + ), + ), + ), + + SizedBox( + height: MediaQuery.of(context).padding.bottom + 24), + ], + ), + ); + } + + bool _isQrCorner(int index) { + final row = index ~/ 9; + final col = index % 9; + if (row < 3 && col < 3) return true; + if (row < 3 && col > 5) return true; + if (row > 5 && col < 3) return true; + return false; + } +} diff --git a/frontend/genex-mobile/lib/features/issuer/presentation/pages/issuer_main_page.dart b/frontend/genex-mobile/lib/features/issuer/presentation/pages/issuer_main_page.dart new file mode 100644 index 0000000..24d784d --- /dev/null +++ b/frontend/genex-mobile/lib/features/issuer/presentation/pages/issuer_main_page.dart @@ -0,0 +1,732 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; +import '../../../../shared/widgets/credit_badge.dart'; +import '../../../ai_agent/presentation/widgets/ai_fab.dart'; + +/// C. 发行方管理后台App - 主页 + 底部导航 +/// +/// Tab: 发券中心 / 核销管理 / 财务 / 数据 / 更多 +class IssuerMainPage extends StatefulWidget { + const IssuerMainPage({super.key}); + + @override + State createState() => _IssuerMainPageState(); +} + +class _IssuerMainPageState extends State { + int _currentIndex = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack( + index: _currentIndex, + children: const [ + _IssuerDashboard(), + _CouponCenter(), + _RedeemManagement(), + _FinancePage(), + _IssuerMore(), + ], + ), + bottomNavigationBar: NavigationBar( + selectedIndex: _currentIndex, + onDestinationSelected: (i) => setState(() => _currentIndex = i), + destinations: const [ + NavigationDestination(icon: Icon(Icons.dashboard_outlined), + selectedIcon: Icon(Icons.dashboard_rounded), label: '总览'), + NavigationDestination(icon: Icon(Icons.add_card_outlined), + selectedIcon: Icon(Icons.add_card_rounded), label: '发券'), + NavigationDestination(icon: Icon(Icons.fact_check_outlined), + selectedIcon: Icon(Icons.fact_check_rounded), label: '核销'), + NavigationDestination(icon: Icon(Icons.account_balance_outlined), + selectedIcon: Icon(Icons.account_balance_rounded), label: '财务'), + NavigationDestination(icon: Icon(Icons.more_horiz_rounded), + selectedIcon: Icon(Icons.more_horiz_rounded), label: '更多'), + ], + ), + floatingActionButton: AiFab( + unreadCount: 2, + onTap: () {}, + ), + ); + } +} + +/// C5. 发行方仪表盘 - 数据概览 +class _IssuerDashboard extends StatelessWidget { + const _IssuerDashboard(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('发行方管理'), + actions: [ + IconButton(icon: const Icon(Icons.notifications_outlined), onPressed: () {}), + ], + ), + body: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + + // Company Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.cardGradient, + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Row( + children: [ + Container( + width: 48, height: 48, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.business_rounded, color: Colors.white, size: 26), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Starbucks Inc.', style: AppTypography.h2.copyWith(color: Colors.white)), + const SizedBox(height: 4), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text('AAA', style: AppTypography.caption.copyWith( + color: Colors.white, fontWeight: FontWeight.w700, + )), + ), + const SizedBox(width: 8), + Text('已认证发行方', style: AppTypography.bodySmall.copyWith( + color: Colors.white70, + )), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // AI Suggestion Card + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)), + ), + child: Row( + children: [ + Container( + width: 28, height: 28, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 14), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + 'AI建议:当前市场需求旺盛,建议增发 \$50 面值礼品卡', + style: AppTypography.bodySmall, + maxLines: 2, + ), + ), + const Icon(Icons.chevron_right_rounded, color: AppColors.primary, size: 18), + ], + ), + ), + const SizedBox(height: 20), + + // Stats Grid + _buildStatsGrid(), + const SizedBox(height: 24), + + // Quick Actions + Text('快捷操作', style: AppTypography.h3), + const SizedBox(height: 12), + Row( + children: [ + _quickAction(Icons.add_card_rounded, '创建券', AppColors.primary), + const SizedBox(width: 12), + _quickAction(Icons.people_outline_rounded, '门店管理', AppColors.info), + const SizedBox(width: 12), + _quickAction(Icons.analytics_outlined, '销售分析', AppColors.success), + const SizedBox(width: 12), + _quickAction(Icons.download_rounded, '对账单', AppColors.warning), + ], + ), + const SizedBox(height: 24), + + // Recent Coupons + Text('我的券', style: AppTypography.h3), + const SizedBox(height: 12), + ...List.generate(3, (i) => _couponItem(i)), + + const SizedBox(height: 80), + ], + ), + ), + ); + } + + Widget _buildStatsGrid() { + final stats = [ + ('发行总量', '12,800', AppColors.primary), + ('已售出', '9,650', AppColors.success), + ('已核销', '6,240', AppColors.info), + ('核销率', '64.7%', AppColors.warning), + ]; + + return GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.8, + children: stats.map((s) => Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(s.$1, style: AppTypography.caption), + Text(s.$2, style: AppTypography.h1.copyWith(color: s.$3)), + ], + ), + )).toList(), + ); + } + + Widget _quickAction(IconData icon, String label, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + children: [ + Icon(icon, color: color, size: 24), + const SizedBox(height: 6), + Text(label, style: AppTypography.caption.copyWith( + color: color, fontWeight: FontWeight.w500, + )), + ], + ), + ), + ); + } + + Widget _couponItem(int index) { + final names = ['\$25 礼品卡', '\$50 满减券', '\$10 折扣券']; + final statuses = ['已上架', '审核中', '已售罄']; + final colors = [AppColors.success, AppColors.warning, AppColors.textTertiary]; + + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 40, height: 40, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon(Icons.confirmation_number_outlined, + color: AppColors.primary.withValues(alpha: 0.4), size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(names[index], style: AppTypography.labelMedium), + Text('发行 1,000 / 已售 ${[850, 0, 500][index]}', + style: AppTypography.caption), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: colors[index].withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text(statuses[index], style: AppTypography.caption.copyWith( + color: colors[index], fontWeight: FontWeight.w500, + )), + ), + ], + ), + ); + } +} + +/// C2. 发券中心 +class _CouponCenter extends StatelessWidget { + const _CouponCenter(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('发券中心')), + body: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + + // Template Selection + Text('选择券模板', style: AppTypography.h3), + const SizedBox(height: 12), + GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.2, + children: [ + _templateCard('满减券', Icons.local_offer_rounded, AppColors.couponDining), + _templateCard('折扣券', Icons.percent_rounded, AppColors.couponShopping), + _templateCard('礼品卡', Icons.card_giftcard_rounded, AppColors.couponEntertainment), + _templateCard('储值券', Icons.account_balance_wallet_rounded, AppColors.couponTravel), + ], + ), + const SizedBox(height: 24), + + // My Coupons Management List + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('券管理', style: AppTypography.h3), + TextButton(onPressed: () {}, child: const Text('查看全部')), + ], + ), + ...List.generate(5, (i) { + final statusColors = [AppColors.success, AppColors.warning, AppColors.success, AppColors.textTertiary, AppColors.error]; + final statuses = ['已上架', '审核中', '已上架', '已下架', '已售罄']; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Container( + width: 40, height: 40, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.confirmation_number_outlined, + color: AppColors.primary, size: 20), + ), + title: Text('券活动 ${i + 1}', style: AppTypography.labelMedium), + subtitle: Text('已售 ${(i + 1) * 120} / ${(i + 1) * 200}', + style: AppTypography.caption), + trailing: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: statusColors[i].withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text(statuses[i], style: AppTypography.caption.copyWith( + color: statusColors[i], fontWeight: FontWeight.w500, + )), + ), + ); + }), + + const SizedBox(height: 80), + ], + ), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + // Navigator: → CreateCouponPage + }, + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + icon: const Icon(Icons.add_rounded), + label: const Text('创建新券'), + ), + ); + } + + Widget _templateCard(String name, IconData icon, Color color) { + return Container( + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: color.withValues(alpha: 0.2)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: color, size: 32), + const SizedBox(height: 8), + Text(name, style: AppTypography.labelMedium.copyWith(color: color)), + ], + ), + ); + } +} + +/// C3. 核销管理 +class _RedeemManagement extends StatelessWidget { + const _RedeemManagement(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('核销管理')), + body: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + + // Stats + Row( + children: [ + _stat('今日', '156笔', AppColors.primary), + const SizedBox(width: 12), + _stat('本周', '892笔', AppColors.success), + const SizedBox(width: 12), + _stat('本月', '3,450笔', AppColors.info), + ], + ), + const SizedBox(height: 24), + + // Trend Chart placeholder + Container( + height: 200, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Center( + child: Text('核销趋势图 (fl_chart)', + style: AppTypography.bodySmall.copyWith(color: AppColors.textTertiary)), + ), + ), + const SizedBox(height: 24), + + // Store Management + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('门店管理', style: AppTypography.h3), + TextButton(onPressed: () {}, child: const Text('全部门店')), + ], + ), + const SizedBox(height: 8), + ...List.generate(3, (i) => Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + const Icon(Icons.store_rounded, color: AppColors.primary), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(['总部', '朝阳门店', '国贸门店'][i], + style: AppTypography.labelMedium), + Text('今日 ${[56, 23, 18][i]} 笔', style: AppTypography.caption), + ], + ), + ), + Text('${[3, 2, 1][i]} 名员工', style: AppTypography.caption), + ], + ), + )), + const SizedBox(height: 80), + ], + ), + ), + ); + } + + Widget _stat(String label, String value, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + children: [ + Text(value, style: AppTypography.h3.copyWith(color: color)), + const SizedBox(height: 4), + Text(label, style: AppTypography.caption), + ], + ), + ), + ); + } +} + +/// C4. 财务管理 +class _FinancePage extends StatelessWidget { + const _FinancePage(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('财务管理')), + body: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + + // Revenue Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('总销售额', style: AppTypography.bodySmall.copyWith( + color: Colors.white70, + )), + const SizedBox(height: 4), + Text('\$128,450.00', style: AppTypography.displayLarge.copyWith( + color: Colors.white, fontSize: 32, + )), + const SizedBox(height: 20), + Row( + children: [ + _revenueItem('已到账', '\$98,200'), + const SizedBox(width: 24), + _revenueItem('待结算', '\$24,250'), + const SizedBox(width: 24), + _revenueItem('Breakage', '\$6,000'), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // Quick actions + Row( + children: [ + Expanded( + child: GenexButton( + label: '提现', + icon: Icons.account_balance_rounded, + onPressed: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: GenexButton( + label: '对账报表', + icon: Icons.receipt_long_rounded, + variant: GenexButtonVariant.outline, + onPressed: () {}, + ), + ), + ], + ), + const SizedBox(height: 24), + + // Settlement details + Text('结算明细', style: AppTypography.h3), + const SizedBox(height: 12), + ...List.generate(5, (i) => Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 36, height: 36, + decoration: BoxDecoration( + color: AppColors.successLight, + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_downward_rounded, + color: AppColors.success, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('核销结算 - \$25券 × ${(i + 1) * 5}笔', + style: AppTypography.labelSmall), + Text('02/${10 - i}', style: AppTypography.caption), + ], + ), + ), + Text('+\$${(i + 1) * 125}.00', style: AppTypography.labelMedium.copyWith( + color: AppColors.success, + )), + ], + ), + )), + + const SizedBox(height: 80), + ], + ), + ), + ); + } + + Widget _revenueItem(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.caption.copyWith(color: Colors.white54)), + const SizedBox(height: 2), + Text(value, style: AppTypography.labelMedium.copyWith(color: Colors.white)), + ], + ); + } +} + +/// C6. 更多 (信用等级、额度、设置) +class _IssuerMore extends StatelessWidget { + const _IssuerMore(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('更多')), + body: ListView( + padding: AppSpacing.pagePadding, + children: [ + const SizedBox(height: 16), + + // Credit & Quota Card + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.verified_rounded, color: AppColors.creditAAA), + const SizedBox(width: 8), + Text('信用等级', style: AppTypography.labelMedium), + const Spacer(), + const CreditBadge(rating: 'AAA', size: CreditBadgeSize.large), + ], + ), + const Divider(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('发行额度', style: AppTypography.caption), + Text('\$500,000', style: AppTypography.h2.copyWith(color: AppColors.primary)), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text('已用额度', style: AppTypography.caption), + Text('\$128,450', style: AppTypography.h3), + ], + ), + ], + ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: AppSpacing.borderRadiusFull, + child: LinearProgressIndicator( + value: 128450 / 500000, + backgroundColor: AppColors.gray100, + valueColor: const AlwaysStoppedAnimation(AppColors.primary), + minHeight: 8, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Menu items + _menuItem(Icons.bar_chart_rounded, '数据中心', '发行量/销量/兑付率'), + _menuItem(Icons.people_rounded, '用户画像', '购买用户分布分析'), + _menuItem(Icons.shield_outlined, '信用详情', '评分详情与提升建议'), + _menuItem(Icons.history_rounded, '额度变动', '历史额度调整记录'), + _menuItem(Icons.business_rounded, '企业信息', '营业执照/联系人'), + _menuItem(Icons.settings_outlined, '设置', '通知/安全/语言'), + _menuItem(Icons.help_outline_rounded, '帮助中心', '常见问题与客服'), + ], + ), + ); + } + + Widget _menuItem(IconData icon, String title, String subtitle) { + return Container( + margin: const EdgeInsets.only(bottom: 2), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 4), + leading: Icon(icon, color: AppColors.textPrimary, size: 22), + title: Text(title, style: AppTypography.bodyMedium), + subtitle: Text(subtitle, style: AppTypography.caption), + trailing: const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary, size: 20), + onTap: () {}, + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_ai_assistant_page.dart b/frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_ai_assistant_page.dart new file mode 100644 index 0000000..50fcaab --- /dev/null +++ b/frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_ai_assistant_page.dart @@ -0,0 +1,930 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 商户端 AI 助手页面 +/// +/// 核销辅助:异常券识别、核销建议 +/// 客流预测:基于历史数据的每日/每周客流预测 +/// 营销建议:促销活动建议、热门券品类推荐 +/// 异常预警:可疑券检测、高频核销预警 +class MerchantAiAssistantPage extends StatefulWidget { + const MerchantAiAssistantPage({super.key}); + + @override + State createState() => + _MerchantAiAssistantPageState(); +} + +class _MerchantAiAssistantPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('AI 助手'), + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: '核销辅助'), + Tab(text: '客流预测'), + Tab(text: '异常预警'), + ], + labelColor: AppColors.primary, + unselectedLabelColor: AppColors.textTertiary, + indicatorColor: AppColors.primary, + labelStyle: AppTypography.labelMedium, + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildRedeemAssistTab(), + _buildTrafficPredictionTab(), + _buildAnomalyAlertTab(), + ], + ), + ); + } + + // ======================================== + // Tab 1: 核销辅助 + // ======================================== + Widget _buildRedeemAssistTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // AI Quick Actions + _buildAiQuickActions(), + const SizedBox(height: 20), + + // Redeem Tips + _buildRedeemTips(), + const SizedBox(height: 20), + + // Hot Coupons Today + _buildHotCouponsToday(), + const SizedBox(height: 20), + + // Marketing Suggestions + _buildMarketingSuggestions(), + ], + ), + ); + } + + Widget _buildAiQuickActions() { + final actions = [ + ('验券真伪', Icons.verified_user_rounded, AppColors.success), + ('查券状态', Icons.search_rounded, AppColors.info), + ('批量核销', Icons.playlist_add_check_rounded, AppColors.primary), + ('问题反馈', Icons.feedback_rounded, AppColors.warning), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: AppSpacing.borderRadiusSm, + ), + child: + const Center(child: Text('✨', style: TextStyle(fontSize: 16))), + ), + const SizedBox(width: 10), + const Text( + 'AI 快捷操作', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.white), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: actions.map((a) { + final (label, icon, _) = a; + return Expanded( + child: GestureDetector( + onTap: () {}, + child: Column( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon(icon, color: Colors.white, size: 22), + ), + const SizedBox(height: 6), + Text( + label, + style: TextStyle( + fontSize: 11, + color: Colors.white.withValues(alpha: 0.9), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildRedeemTips() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.lightbulb_outline_rounded, + color: AppColors.warning, size: 20), + const SizedBox(width: 8), + Text('核销提示', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + _buildTipItem( + '星巴克 \$25 礼品卡有批次更新', + '新批次(#B2026-03)已上线,请注意核验二维码格式', + AppColors.info, + ), + _buildTipItem( + '午间高峰期即将到来', + '预计 11:30-13:00 核销量将达峰值 ~15笔/小时', + AppColors.warning, + ), + _buildTipItem( + '本店暂不支持 Nike 体验券', + '该券仅限旗舰店核销,请引导顾客至正确门店', + AppColors.error, + ), + ], + ), + ); + } + + Widget _buildTipItem(String title, String desc, Color color) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 6, + height: 6, + margin: const EdgeInsets.only(top: 6), + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: AppTypography.labelMedium + .copyWith(fontSize: 13)), + const SizedBox(height: 2), + Text(desc, + style: AppTypography.bodySmall + .copyWith(color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + ); + } + + Widget _buildHotCouponsToday() { + final hotCoupons = [ + ('星巴克 \$25 礼品卡', 12, AppColors.couponDining), + ('Amazon \$50 购物券', 8, AppColors.couponShopping), + ('电影票 \$12', 5, AppColors.couponEntertainment), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.local_fire_department_rounded, + color: AppColors.error, size: 20), + const SizedBox(width: 8), + Text('今日热门核销', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + ...hotCoupons.map((c) { + final (name, count, color) = c; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon(Icons.confirmation_number_rounded, + color: color, size: 18), + ), + const SizedBox(width: 10), + Expanded( + child: Text(name, style: AppTypography.bodyMedium)), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + '$count笔', + style: AppTypography.labelSmall + .copyWith(color: AppColors.primary), + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _buildMarketingSuggestions() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.auto_awesome_rounded, + color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('AI 营销建议', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + _buildSuggestionItem( + '推荐搭配销售', + '购买咖啡券的顾客同时对糕点券感兴趣,建议推荐组合', + Icons.restaurant_rounded, + ), + _buildSuggestionItem( + '周末促销建议', + '历史数据显示周六核销量+30%,建议推出周末限时活动', + Icons.campaign_rounded, + ), + ], + ), + ); + } + + Widget _buildSuggestionItem(String title, String desc, IconData icon) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: AppColors.primary, size: 18), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + fontSize: 13, fontWeight: FontWeight.w600)), + const SizedBox(height: 2), + Text(desc, + style: AppTypography.bodySmall + .copyWith(color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + ); + } + + // ======================================== + // Tab 2: 客流预测 + // ======================================== + Widget _buildTrafficPredictionTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Today's Prediction + _buildTodayPrediction(), + const SizedBox(height: 20), + + // Hourly Breakdown + _buildHourlyBreakdown(), + const SizedBox(height: 20), + + // Weekly Forecast + _buildWeeklyForecast(), + const SizedBox(height: 20), + + // Staffing Suggestion + _buildStaffingSuggestion(), + ], + ), + ); + } + + Widget _buildTodayPrediction() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.cardGradient, + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Column( + children: [ + const Row( + children: [ + Icon(Icons.insights_rounded, color: Colors.white, size: 22), + SizedBox(width: 10), + Text( + '今日客流预测', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: Colors.white), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _predictionStat('预计核销', '45笔'), + _predictionStat('高峰时段', '11:30-13:00'), + _predictionStat('预计收入', '\$892'), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + const Text('✨', + style: TextStyle(fontSize: 14)), + const SizedBox(width: 8), + Expanded( + child: Text( + '较上周同期增长12%,建议午间增加1名收银员', + style: TextStyle( + fontSize: 12, + color: Colors.white.withValues(alpha: 0.9), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _predictionStat(String label, String value) { + return Column( + children: [ + Text( + value, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 11, color: Colors.white.withValues(alpha: 0.7)), + ), + ], + ); + } + + Widget _buildHourlyBreakdown() { + final hours = [ + ('9:00', 3), + ('10:00', 5), + ('11:00', 8), + ('12:00', 12), + ('13:00', 9), + ('14:00', 4), + ('15:00', 3), + ('16:00', 2), + ('17:00', 5), + ('18:00', 7), + ]; + final maxCount = 12; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('分时段预测', style: AppTypography.labelLarge), + const SizedBox(height: 16), + ...hours.map((h) { + final (time, count) = h; + final pct = count / maxCount; + final isPeak = count >= 8; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 44, + child: Text(time, + style: AppTypography.caption + .copyWith(fontFamily: 'monospace')), + ), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(3), + child: LinearProgressIndicator( + value: pct, + backgroundColor: AppColors.gray100, + valueColor: AlwaysStoppedAnimation( + isPeak ? AppColors.primary : AppColors.primaryLight, + ), + minHeight: 16, + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 30, + child: Text( + '$count笔', + style: TextStyle( + fontSize: 11, + fontWeight: isPeak ? FontWeight.w600 : FontWeight.w400, + color: isPeak ? AppColors.primary : AppColors.textSecondary, + ), + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _buildWeeklyForecast() { + final days = [ + ('周一', 38, false), + ('周二', 42, false), + ('周三', 45, true), + ('周四', 40, false), + ('周五', 52, false), + ('周六', 68, false), + ('周日', 55, false), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('本周预测', style: AppTypography.labelLarge), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.end, + children: days.map((d) { + final (day, count, isToday) = d; + final heightPct = count / 68; + return Column( + children: [ + Text( + '$count', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: isToday ? AppColors.primary : AppColors.textTertiary, + ), + ), + const SizedBox(height: 4), + Container( + width: 28, + height: 80 * heightPct, + decoration: BoxDecoration( + color: isToday ? AppColors.primary : AppColors.primarySurface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(4)), + ), + ), + const SizedBox(height: 4), + Text( + day, + style: TextStyle( + fontSize: 11, + fontWeight: isToday ? FontWeight.w600 : FontWeight.w400, + color: isToday ? AppColors.primary : AppColors.textSecondary, + ), + ), + ], + ); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildStaffingSuggestion() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.people_alt_rounded, + color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('排班建议', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + _staffRow('上午 (9:00-13:00)', '建议 2 人', '含午间高峰'), + _staffRow('下午 (13:00-17:00)', '建议 1 人', '客流较少'), + _staffRow('傍晚 (17:00-21:00)', '建议 2 人', '下班高峰'), + ], + ), + ); + } + + Widget _staffRow(String period, String suggestion, String reason) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Expanded( + flex: 3, + child: Text(period, style: AppTypography.bodySmall), + ), + Expanded( + flex: 2, + child: Text(suggestion, + style: AppTypography.labelSmall + .copyWith(color: AppColors.primary)), + ), + Expanded( + flex: 2, + child: Text(reason, style: AppTypography.caption), + ), + ], + ), + ); + } + + // ======================================== + // Tab 3: 异常预警 + // ======================================== + Widget _buildAnomalyAlertTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Alert Summary + _buildAlertSummary(), + const SizedBox(height: 20), + + // Active Alerts + _buildActiveAlerts(), + const SizedBox(height: 20), + + // Suspicious Patterns + _buildSuspiciousPatterns(), + const SizedBox(height: 20), + + // Recent Resolved + _buildResolvedAlerts(), + ], + ), + ); + } + + Widget _buildAlertSummary() { + return Row( + children: [ + _alertStatCard('待处理', '2', AppColors.error), + const SizedBox(width: 12), + _alertStatCard('今日已处理', '5', AppColors.success), + const SizedBox(width: 12), + _alertStatCard('风险指数', '低', AppColors.info), + ], + ); + } + + Widget _alertStatCard(String label, String value, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: color.withValues(alpha: 0.2)), + ), + child: Column( + children: [ + Text(value, + style: TextStyle( + fontSize: 22, fontWeight: FontWeight.w700, color: color)), + const SizedBox(height: 2), + Text(label, style: AppTypography.caption.copyWith(color: color)), + ], + ), + ), + ); + } + + Widget _buildActiveAlerts() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.error.withValues(alpha: 0.3)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.warning_amber_rounded, + color: AppColors.error, size: 20), + const SizedBox(width: 8), + Text('活跃预警', + style: + AppTypography.labelLarge.copyWith(color: AppColors.error)), + ], + ), + const SizedBox(height: 12), + _alertItem( + '高频核销检测', + '用户#78901 在 5 分钟内尝试核销 3 张同品牌券', + '2 分钟前', + AppColors.error, + Icons.speed_rounded, + ), + const Divider(height: 20), + _alertItem( + '疑似伪造券码', + '券码 GNX-FAKE-001 格式异常,不在系统记录中', + '15 分钟前', + AppColors.warning, + Icons.gpp_bad_rounded, + ), + ], + ), + ); + } + + Widget _alertItem( + String title, String desc, String time, Color color, IconData icon) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon(icon, color: color, size: 18), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.labelMedium), + const SizedBox(height: 2), + Text(desc, style: AppTypography.bodySmall), + const SizedBox(height: 4), + Text(time, style: AppTypography.caption), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSuspiciousPatterns() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.pattern_rounded, + color: AppColors.warning, size: 20), + const SizedBox(width: 8), + Text('可疑模式检测', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + _patternItem( + '同一用户连续核销', '3次/5分钟 (阈值: 2次/5分钟)', 0.8, AppColors.error), + const SizedBox(height: 10), + _patternItem( + '非营业时间核销尝试', '0次/本周', 0.0, AppColors.success), + const SizedBox(height: 10), + _patternItem( + '过期券核销尝试', '2次/今日', 0.4, AppColors.warning), + ], + ), + ); + } + + Widget _patternItem( + String label, String detail, double severity, Color color) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.labelMedium.copyWith(fontSize: 13)), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + severity > 0.6 + ? '异常' + : severity > 0.2 + ? '注意' + : '正常', + style: TextStyle( + fontSize: 10, fontWeight: FontWeight.w600, color: color), + ), + ), + ], + ), + const SizedBox(height: 4), + Text(detail, style: AppTypography.caption), + const SizedBox(height: 6), + ClipRRect( + borderRadius: BorderRadius.circular(3), + child: LinearProgressIndicator( + value: severity, + backgroundColor: AppColors.gray100, + valueColor: AlwaysStoppedAnimation(color), + minHeight: 4, + ), + ), + ], + ); + } + + Widget _buildResolvedAlerts() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('今日已处理', style: AppTypography.labelLarge), + const SizedBox(height: 12), + _resolvedItem('过期券核销拦截', '系统自动拦截', '10:24'), + _resolvedItem('重复核销拦截', '同一券码二次扫描', '11:05'), + _resolvedItem('非本店券提醒', '引导至正确门店', '12:30'), + _resolvedItem('余额不足核销', '告知顾客充值', '13:15'), + _resolvedItem('系统超时重试', '网络恢复后自动完成', '14:02'), + ], + ), + ); + } + + Widget _resolvedItem(String title, String desc, String time) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + const Icon(Icons.check_circle_rounded, + color: AppColors.success, size: 16), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.bodyMedium), + Text(desc, style: AppTypography.caption), + ], + ), + ), + Text(time, + style: AppTypography.caption + .copyWith(fontFamily: 'monospace')), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_home_page.dart b/frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_home_page.dart new file mode 100644 index 0000000..0f9b40b --- /dev/null +++ b/frontend/genex-mobile/lib/features/merchant/presentation/pages/merchant_home_page.dart @@ -0,0 +1,669 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// B. 商户核销端 - 主界面 +/// +/// B1: 员工登录(手机号+门店选择) +/// B2: 扫码核销(主页面)、券信息确认、核销成功、手动输码、离线提示 +/// B3: 核销记录列表、待同步队列 +/// B4: 门店仪表盘(店长权限) +class MerchantHomePage extends StatelessWidget { + const MerchantHomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + // Header + _buildHeader(), + + // Network Status + _buildNetworkStatus(isOnline: true), + + // Main Scanner Area + Expanded(child: _buildScannerArea(context)), + + // Bottom Actions + _buildBottomActions(context), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 12), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.store_rounded, color: AppColors.primary, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('星巴克 朝阳门店', style: AppTypography.labelMedium), + Text('收银员 - 张三', style: AppTypography.caption), + ], + ), + ), + // Today's stats + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.check_circle_rounded, size: 14, color: AppColors.success), + const SizedBox(width: 4), + Text('今日 23 笔', style: AppTypography.labelSmall.copyWith( + color: AppColors.success, + )), + ], + ), + ), + ], + ), + ); + } + + Widget _buildNetworkStatus({required bool isOnline}) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isOnline ? AppColors.successLight : AppColors.warningLight, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, height: 8, + decoration: BoxDecoration( + color: isOnline ? AppColors.success : AppColors.warning, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + isOnline ? '在线模式' : '离线模式 - 待同步 3 笔', + style: AppTypography.caption.copyWith( + color: isOnline ? AppColors.success : AppColors.warning, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildScannerArea(BuildContext context) { + return Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.gray900, + borderRadius: AppSpacing.borderRadiusXl, + ), + child: Stack( + children: [ + // Camera placeholder + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Scan frame + Container( + width: 240, + height: 240, + decoration: BoxDecoration( + border: Border.all(color: AppColors.primary, width: 2), + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Stack( + children: [ + // Corner markers + ..._buildCornerMarkers(), + // Center icon + Center( + child: Icon( + Icons.qr_code_scanner_rounded, + size: 48, + color: Colors.white.withValues(alpha: 0.5), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Text( + '将券二维码对准扫描框', + style: AppTypography.bodyMedium.copyWith(color: Colors.white70), + ), + ], + ), + ), + + // Flashlight toggle + Positioned( + bottom: 20, + left: 0, + right: 0, + child: Center( + child: GestureDetector( + onTap: () {}, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white12, + shape: BoxShape.circle, + ), + child: const Icon(Icons.flashlight_on_rounded, + color: Colors.white70, size: 22), + ), + const SizedBox(height: 4), + Text('手电筒', style: AppTypography.caption.copyWith(color: Colors.white54)), + ], + ), + ), + ), + ), + ], + ), + ); + } + + List _buildCornerMarkers() { + const size = 24.0; + const thickness = 3.0; + const color = AppColors.primary; + const radius = Radius.circular(4); + + return [ + // Top-left + Positioned( + top: 0, left: 0, + child: Container( + width: size, height: thickness, + decoration: const BoxDecoration(color: color, borderRadius: BorderRadius.only(topLeft: radius)), + ), + ), + Positioned( + top: 0, left: 0, + child: Container(width: thickness, height: size, color: color), + ), + // Top-right + Positioned( + top: 0, right: 0, + child: Container(width: size, height: thickness, color: color), + ), + Positioned( + top: 0, right: 0, + child: Container(width: thickness, height: size, color: color), + ), + // Bottom-left + Positioned( + bottom: 0, left: 0, + child: Container(width: size, height: thickness, color: color), + ), + Positioned( + bottom: 0, left: 0, + child: Container(width: thickness, height: size, color: color), + ), + // Bottom-right + Positioned( + bottom: 0, right: 0, + child: Container(width: size, height: thickness, color: color), + ), + Positioned( + bottom: 0, right: 0, + child: Container(width: thickness, height: size, color: color), + ), + ]; + } + + Widget _buildBottomActions(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), + child: Row( + children: [ + _bottomAction(Icons.keyboard_rounded, '手动输码', () { + _showManualInput(context); + }), + const SizedBox(width: 16), + _bottomAction(Icons.history_rounded, '核销记录', () { + // Navigator: → RedeemHistoryPage + }), + const SizedBox(width: 16), + _bottomAction(Icons.bar_chart_rounded, '门店数据', () { + // Navigator: → StoreDashboardPage + }), + ], + ), + ); + } + + Widget _bottomAction(IconData icon, String label, VoidCallback onTap) { + return Expanded( + child: GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + Icon(icon, color: AppColors.primary, size: 24), + const SizedBox(height: 4), + Text(label, style: AppTypography.caption.copyWith( + color: AppColors.textPrimary, + fontWeight: FontWeight.w500, + )), + ], + ), + ), + ), + ); + } + + void _showManualInput(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (ctx) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(ctx).viewInsets.bottom, + ), + child: Container( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + const SizedBox(height: 20), + Text('手动输入券码', style: AppTypography.h2), + const SizedBox(height: 16), + TextField( + autofocus: true, + decoration: const InputDecoration( + hintText: '请输入券码', + prefixIcon: Icon(Icons.confirmation_number_outlined, + color: AppColors.textTertiary), + ), + textCapitalization: TextCapitalization.characters, + ), + const SizedBox(height: 16), + GenexButton( + label: '查询', + onPressed: () {}, + ), + ], + ), + ), + ), + ); + } +} + +/// B2. 核销确认弹窗 +class RedeemConfirmSheet extends StatelessWidget { + const RedeemConfirmSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + const SizedBox(height: 20), + + // Consumer Avatar + Info + Row( + children: [ + Container( + width: 48, height: 48, + decoration: const BoxDecoration( + color: AppColors.primarySurface, + shape: BoxShape.circle, + ), + child: const Icon(Icons.person_rounded, color: AppColors.primary), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('用户昵称', style: AppTypography.labelMedium), + Text('消费者', style: AppTypography.caption), + ], + ), + ], + ), + const SizedBox(height: 20), + + // Coupon Info + Container( + width: double.infinity, + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + children: [ + _row('券名称', '星巴克 \$25 礼品卡'), + const SizedBox(height: 8), + _row('面值', '\$25.00'), + const SizedBox(height: 8), + _row('有效期', '2026/12/31'), + const SizedBox(height: 8), + _row('使用条件', '无最低消费'), + ], + ), + ), + const SizedBox(height: 24), + + GenexButton( + label: '确认核销', + onPressed: () { + Navigator.of(context).pop(); + // Show success + }, + ), + const SizedBox(height: 8), + GenexButton( + label: '取消', + variant: GenexButtonVariant.text, + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } + + Widget _row(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodySmall.copyWith(color: AppColors.textSecondary)), + Text(value, style: AppTypography.labelSmall), + ], + ); + } +} + +/// B2. 核销成功页 +class RedeemSuccessSheet extends StatelessWidget { + const RedeemSuccessSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + const SizedBox(height: 32), + Container( + width: 72, height: 72, + decoration: const BoxDecoration( + gradient: AppColors.successGradient, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check_rounded, color: Colors.white, size: 36), + ), + const SizedBox(height: 16), + Text('核销成功', style: AppTypography.h1), + const SizedBox(height: 8), + Text('星巴克 \$25 礼品卡', style: AppTypography.bodyMedium.copyWith( + color: AppColors.textSecondary, + )), + const SizedBox(height: 32), + GenexButton( + label: '继续核销', + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } +} + +/// B3. 核销记录页 +class RedeemHistoryPage extends StatelessWidget { + const RedeemHistoryPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text('核销记录'), + actions: [ + TextButton( + onPressed: () {}, + child: Text('今日', style: AppTypography.labelSmall.copyWith( + color: AppColors.primary, + )), + ), + ], + ), + body: ListView.separated( + padding: const EdgeInsets.all(20), + itemCount: 8, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final isSync = index < 6; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 36, height: 36, + decoration: BoxDecoration( + color: isSync ? AppColors.successLight : AppColors.warningLight, + shape: BoxShape.circle, + ), + child: Icon( + isSync ? Icons.check_rounded : Icons.sync_rounded, + size: 18, + color: isSync ? AppColors.success : AppColors.warning, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('品牌 ${index + 1} \$${(index + 1) * 10} 券', + style: AppTypography.labelSmall), + Text('核销员: 张三 · 14:${30 + index}', + style: AppTypography.caption), + ], + ), + ), + Text( + isSync ? '已同步' : '待同步', + style: AppTypography.caption.copyWith( + color: isSync ? AppColors.success : AppColors.warning, + ), + ), + ], + ), + ); + }, + ), + ); + } +} + +/// B4. 门店仪表盘 +class StoreDashboardPage extends StatelessWidget { + const StoreDashboardPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text('门店数据'), + ), + body: SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + + // Today Stats + Row( + children: [ + _statCard('今日核销', '23笔', Icons.check_circle_rounded, AppColors.success), + const SizedBox(width: 12), + _statCard('核销金额', '\$1,456', Icons.attach_money_rounded, AppColors.primary), + ], + ), + const SizedBox(height: 24), + + // Weekly Trend (placeholder) + Text('本周趋势', style: AppTypography.h3), + const SizedBox(height: 12), + Container( + height: 200, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Center( + child: Text('周核销趋势图 (fl_chart)', + style: AppTypography.bodySmall.copyWith(color: AppColors.textTertiary)), + ), + ), + const SizedBox(height: 24), + + // Staff Ranking + Text('核销员排行', style: AppTypography.h3), + const SizedBox(height: 12), + ...List.generate(3, (index) { + final names = ['张三', '李四', '王五']; + final counts = [12, 8, 3]; + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 28, height: 28, + decoration: BoxDecoration( + color: index == 0 + ? AppColors.primary + : AppColors.gray200, + shape: BoxShape.circle, + ), + child: Center( + child: Text('${index + 1}', style: TextStyle( + color: index == 0 ? Colors.white : AppColors.textSecondary, + fontSize: 13, + fontWeight: FontWeight.w600, + )), + ), + ), + const SizedBox(width: 12), + Text(names[index], style: AppTypography.labelMedium), + const Spacer(), + Text('${counts[index]}笔', style: AppTypography.bodyMedium.copyWith( + color: AppColors.primary, + )), + ], + ), + ); + }), + ], + ), + ), + ); + } + + Widget _statCard(String label, String value, IconData icon, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + boxShadow: AppSpacing.shadowSm, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: color, size: 24), + const SizedBox(height: 12), + Text(value, style: AppTypography.h1.copyWith(color: color)), + const SizedBox(height: 4), + Text(label, style: AppTypography.caption), + ], + ), + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/message/presentation/pages/message_detail_page.dart b/frontend/genex-mobile/lib/features/message/presentation/pages/message_detail_page.dart new file mode 100644 index 0000000..1815d49 --- /dev/null +++ b/frontend/genex-mobile/lib/features/message/presentation/pages/message_detail_page.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 消息详情页面 +/// +/// 查看单条通知的详细内容 +/// 类型:交易通知、到期提醒、系统通知、活动推送 +class MessageDetailPage extends StatelessWidget { + final String title; + final String type; + + const MessageDetailPage({ + super.key, + this.title = '交易成功通知', + this.type = 'transaction', + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('消息详情')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon & Type + Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: _typeColor.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Icon(_typeIcon, color: _typeColor, size: 22), + ), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: _typeColor.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text(_typeLabel, style: TextStyle(fontSize: 11, color: _typeColor, fontWeight: FontWeight.w600)), + ), + ], + ), + const SizedBox(height: 16), + + // Title + Text(title, style: AppTypography.h1), + const SizedBox(height: 8), + Text('2026年2月10日 14:32', style: AppTypography.bodySmall), + const SizedBox(height: 24), + + // Content + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '您成功购买了 星巴克 \$25 礼品卡,支付金额 \$21.25。', + style: TextStyle(fontSize: 15, height: 1.6), + ), + SizedBox(height: 16), + _DetailRow('券名称', '星巴克 \$25 礼品卡'), + _DetailRow('面值', '\$25.00'), + _DetailRow('支付金额', '\$21.25'), + _DetailRow('订单号', 'GNX20260210001'), + _DetailRow('支付方式', 'Visa •••• 4242'), + ], + ), + ), + const SizedBox(height: 20), + + // Actions + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () {}, + child: const Text('查看券详情'), + ), + ), + ], + ), + ), + ); + } + + Color get _typeColor { + switch (type) { + case 'transaction': return AppColors.success; + case 'expiry': return AppColors.warning; + case 'system': return AppColors.info; + default: return AppColors.primary; + } + } + + IconData get _typeIcon { + switch (type) { + case 'transaction': return Icons.receipt_long_rounded; + case 'expiry': return Icons.timer_rounded; + case 'system': return Icons.settings_rounded; + default: return Icons.campaign_rounded; + } + } + + String get _typeLabel { + switch (type) { + case 'transaction': return '交易通知'; + case 'expiry': return '到期提醒'; + case 'system': return '系统通知'; + default: return '活动推送'; + } + } +} + +class _DetailRow extends StatelessWidget { + final String label; + final String value; + const _DetailRow(this.label, this.value); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodySmall), + Text(value, style: AppTypography.labelMedium), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/message/presentation/pages/message_page.dart b/frontend/genex-mobile/lib/features/message/presentation/pages/message_page.dart new file mode 100644 index 0000000..e01960f --- /dev/null +++ b/frontend/genex-mobile/lib/features/message/presentation/pages/message_page.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/empty_state.dart'; + +/// A8. 消息模块 +/// +/// 交易通知、系统公告、券到期提醒、价格提醒 +/// 分类Tab + 消息详情 +class MessagePage extends StatefulWidget { + const MessagePage({super.key}); + + @override + State createState() => _MessagePageState(); +} + +class _MessagePageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 4, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('消息'), + actions: [ + TextButton( + onPressed: () {}, + child: Text('全部已读', style: AppTypography.labelSmall.copyWith( + color: AppColors.primary, + )), + ), + ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: '全部'), + Tab(text: '交易'), + Tab(text: '到期'), + Tab(text: '公告'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildMessageList(all: true), + _buildMessageList(type: MessageType.transaction), + _buildMessageList(type: MessageType.expiry), + _buildMessageList(type: MessageType.announcement), + ], + ), + ); + } + + Widget _buildMessageList({bool all = false, MessageType? type}) { + if (type == MessageType.announcement) { + return EmptyState.noMessages(); + } + + final messages = _mockMessages + .where((m) => all || m.type == type) + .toList(); + + return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: messages.length, + separatorBuilder: (_, __) => const Divider(indent: 76), + itemBuilder: (context, index) { + final msg = messages[index]; + return _buildMessageItem(msg); + }, + ); + } + + Widget _buildMessageItem(_MockMessage msg) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), + leading: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: _iconBgColor(msg.type), + shape: BoxShape.circle, + ), + child: Icon(_iconData(msg.type), size: 22, color: _iconColor(msg.type)), + ), + title: Row( + children: [ + Expanded( + child: Text(msg.title, style: AppTypography.labelMedium.copyWith( + fontWeight: msg.isRead ? FontWeight.w400 : FontWeight.w600, + )), + ), + Text(msg.time, style: AppTypography.caption), + ], + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + msg.body, + style: AppTypography.bodySmall, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + trailing: !msg.isRead + ? Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: AppColors.primary, + shape: BoxShape.circle, + ), + ) + : null, + onTap: () { + Navigator.pushNamed(context, '/message/detail'); + }, + ); + } + + IconData _iconData(MessageType type) { + switch (type) { + case MessageType.transaction: + return Icons.swap_horiz_rounded; + case MessageType.expiry: + return Icons.access_time_rounded; + case MessageType.price: + return Icons.trending_up_rounded; + case MessageType.announcement: + return Icons.campaign_rounded; + } + } + + Color _iconColor(MessageType type) { + switch (type) { + case MessageType.transaction: + return AppColors.primary; + case MessageType.expiry: + return AppColors.warning; + case MessageType.price: + return AppColors.success; + case MessageType.announcement: + return AppColors.info; + } + } + + Color _iconBgColor(MessageType type) { + return _iconColor(type).withValues(alpha: 0.1); + } +} + +enum MessageType { transaction, expiry, price, announcement } + +class _MockMessage { + final String title; + final String body; + final String time; + final MessageType type; + final bool isRead; + + const _MockMessage(this.title, this.body, this.time, this.type, this.isRead); +} + +const _mockMessages = [ + _MockMessage( + '购买成功', + '您已成功购买 星巴克 \$25 礼品卡,共花费 \$21.25', + '14:32', + MessageType.transaction, + false, + ), + _MockMessage( + '券即将到期', + '您持有的 Target \$30 折扣券 将于3天后到期,请及时使用', + '10:15', + MessageType.expiry, + false, + ), + _MockMessage( + '价格提醒', + '您关注的 Amazon \$100 购物券 当前价格已降至 \$82,低于您设定的提醒价格', + '昨天', + MessageType.price, + true, + ), + _MockMessage( + '出售成交', + '您挂单出售的 Nike \$80 运动券 已成功售出,收入 \$68.00', + '02/07', + MessageType.transaction, + true, + ), + _MockMessage( + '核销成功', + 'Walmart \$50 生活券 已在门店核销成功', + '02/06', + MessageType.transaction, + true, + ), +]; diff --git a/frontend/genex-mobile/lib/features/profile/presentation/pages/kyc_page.dart b/frontend/genex-mobile/lib/features/profile/presentation/pages/kyc_page.dart new file mode 100644 index 0000000..56236ba --- /dev/null +++ b/frontend/genex-mobile/lib/features/profile/presentation/pages/kyc_page.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// KYC认证页面 +/// +/// 分级认证:L0(无认证) → L1(手机+邮箱) → L2(身份证) → L3(高级验证) +/// 每级解锁不同额度和功能 +class KycPage extends StatelessWidget { + const KycPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('身份认证')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Current Level + _buildCurrentLevel(), + const SizedBox(height: 24), + + // KYC Levels + _buildLevel( + 'L1 基础认证', + '手机号 + 邮箱验证', + ['每日购买限额 \$500', '可购买券、出示核销'], + true, + AppColors.success, + ), + _buildLevel( + 'L2 身份认证', + '身份证/护照验证', + ['每日购买限额 \$5,000', '解锁二级市场交易、P2P转赠'], + false, + AppColors.info, + ), + _buildLevel( + 'L3 高级认证', + '视频面审 + 地址证明', + ['无限额', '解锁大额交易、提现无限制'], + false, + AppColors.primary, + ), + ], + ), + ), + ); + } + + Widget _buildCurrentLevel() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(Icons.verified_user_rounded, color: Colors.white, size: 28), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('当前认证等级', style: AppTypography.bodySmall.copyWith(color: Colors.white70)), + const SizedBox(height: 4), + Text('L1 基础认证', style: AppTypography.h1.copyWith(color: Colors.white)), + const SizedBox(height: 4), + Text('每日购买限额 \$500', style: AppTypography.bodySmall.copyWith(color: Colors.white60)), + ], + ), + ), + ], + ), + ); + } + + Widget _buildLevel( + String title, + String requirement, + List benefits, + bool completed, + Color color, + ) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: completed ? color.withValues(alpha: 0.3) : AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + completed ? Icons.check_circle_rounded : Icons.lock_outlined, + color: color, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.labelLarge), + Text(requirement, style: AppTypography.caption), + ], + ), + ), + if (completed) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text('已完成', style: AppTypography.caption.copyWith(color: AppColors.success)), + ) + else + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + minimumSize: Size.zero, + ), + child: const Text('去认证', style: TextStyle(fontSize: 13)), + ), + ], + ), + const SizedBox(height: 12), + ...benefits.map((b) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + Icon(Icons.check_rounded, size: 14, color: completed ? color : AppColors.textTertiary), + const SizedBox(width: 6), + Text(b, style: AppTypography.bodySmall), + ], + ), + )), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/profile/presentation/pages/payment_management_page.dart b/frontend/genex-mobile/lib/features/profile/presentation/pages/payment_management_page.dart new file mode 100644 index 0000000..00e8daa --- /dev/null +++ b/frontend/genex-mobile/lib/features/profile/presentation/pages/payment_management_page.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 支付管理页面 +/// +/// 管理银行卡、信用卡、支付密码 +class PaymentManagementPage extends StatelessWidget { + const PaymentManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('支付管理')), + body: ListView( + padding: const EdgeInsets.all(20), + children: [ + Text('我的银行卡', style: AppTypography.h3), + const SizedBox(height: 12), + + // Card List + _buildCard('Visa', '•••• •••• •••• 4242', 'CREDIT', AppColors.primary), + const SizedBox(height: 10), + _buildCard('Mastercard', '•••• •••• •••• 8888', 'DEBIT', AppColors.info), + const SizedBox(height: 16), + + // Add Card + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: AppColors.border, style: BorderStyle.solid), + borderRadius: AppSpacing.borderRadiusMd, + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline_rounded, color: AppColors.primary), + SizedBox(width: 8), + Text('添加新银行卡', style: TextStyle(color: AppColors.primary, fontWeight: FontWeight.w600)), + ], + ), + ), + const SizedBox(height: 32), + + // Bank Account + Text('银行账户(提现用)', style: AppTypography.h3), + const SizedBox(height: 12), + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + const Icon(Icons.account_balance_rounded, color: AppColors.textSecondary), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Bank of America', style: AppTypography.labelMedium), + Text('•••• 6789 · 储蓄账户', style: AppTypography.caption), + ], + ), + ), + const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary), + ], + ), + ), + const SizedBox(height: 32), + + // Payment Security + Text('支付安全', style: AppTypography.h3), + const SizedBox(height: 12), + _buildSettingTile('支付密码', '已设置', Icons.password_rounded), + _buildSettingTile('指纹/面容支付', '已开启', Icons.fingerprint_rounded), + _buildSettingTile('免密支付', '单笔≤\$10', Icons.flash_on_rounded), + ], + ), + ); + } + + Widget _buildCard(String brand, String number, String type, Color color) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color, color.withValues(alpha: 0.7)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(brand, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16)), + Text(type, style: TextStyle(color: Colors.white.withValues(alpha: 0.7), fontSize: 11)), + ], + ), + const SizedBox(height: 20), + Text(number, style: const TextStyle(color: Colors.white, fontSize: 18, letterSpacing: 2)), + ], + ), + ); + } + + Widget _buildSettingTile(String title, String value, IconData icon) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: ListTile( + leading: Icon(icon, color: AppColors.textSecondary, size: 22), + title: Text(title, style: AppTypography.bodyMedium), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(value, style: AppTypography.caption), + const SizedBox(width: 4), + const Icon(Icons.chevron_right_rounded, size: 18, color: AppColors.textTertiary), + ], + ), + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/profile/presentation/pages/pro_mode_page.dart b/frontend/genex-mobile/lib/features/profile/presentation/pages/pro_mode_page.dart new file mode 100644 index 0000000..b5b3971 --- /dev/null +++ b/frontend/genex-mobile/lib/features/profile/presentation/pages/pro_mode_page.dart @@ -0,0 +1,456 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 高级模式(Pro Mode)设置页 +/// +/// KYC L2+ 用户可开启,展示链上信息: +/// - WalletConnect 连接外部钱包 +/// - 链上地址展示 +/// - 交易Hash查看器 +/// - 链上资产同步 +/// 注:默认关闭,普通用户完全无感知区块链 +class ProModePage extends StatefulWidget { + const ProModePage({super.key}); + + @override + State createState() => _ProModePageState(); +} + +class _ProModePageState extends State { + bool _proModeEnabled = false; + bool _showChainAddress = false; + bool _showTxHash = false; + bool _walletConnected = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('高级模式')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Pro Mode Toggle + _buildProModeCard(), + const SizedBox(height: 20), + + if (_proModeEnabled) ...[ + // WalletConnect + _buildWalletConnectCard(), + const SizedBox(height: 16), + + // Chain Address Display + _buildChainSettingsCard(), + const SizedBox(height: 16), + + // Transaction Explorer + _buildTxExplorerCard(), + const SizedBox(height: 16), + + // Chain Assets + _buildChainAssetsCard(), + const SizedBox(height: 16), + + // Track Selection + _buildTrackCard(), + ], + + if (!_proModeEnabled) _buildProModeInfo(), + ], + ), + ), + ); + } + + Widget _buildProModeCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: _proModeEnabled ? AppColors.cardGradient : null, + color: _proModeEnabled ? null : AppColors.surface, + borderRadius: AppSpacing.borderRadiusLg, + border: _proModeEnabled ? null : Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.code_rounded, + color: _proModeEnabled ? Colors.white : AppColors.primary, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '高级模式 (Pro)', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: _proModeEnabled ? Colors.white : AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + '开启后可查看链上信息和连接外部钱包', + style: TextStyle( + fontSize: 12, + color: _proModeEnabled ? Colors.white70 : AppColors.textSecondary, + ), + ), + ], + ), + ), + Switch( + value: _proModeEnabled, + onChanged: (v) => setState(() => _proModeEnabled = v), + activeColor: Colors.white, + activeTrackColor: Colors.white24, + ), + ], + ), + if (_proModeEnabled) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: const Text( + '需要 KYC L2 及以上认证', + style: TextStyle(fontSize: 11, color: Colors.white70), + ), + ), + ], + ], + ), + ); + } + + Widget _buildWalletConnectCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.account_balance_wallet_rounded, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('WalletConnect', style: AppTypography.labelLarge), + const Spacer(), + if (_walletConnected) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: AppColors.successLight, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: const Text('已连接', style: TextStyle(fontSize: 11, color: AppColors.success, fontWeight: FontWeight.w600)), + ), + ], + ), + const SizedBox(height: 12), + if (_walletConnected) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + const CircleAvatar(radius: 16, backgroundColor: AppColors.primaryContainer, child: Text('M', style: TextStyle(fontSize: 14, color: AppColors.primary))), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('MetaMask', style: AppTypography.labelMedium), + Text('0x7a3b...c4f2', style: AppTypography.caption.copyWith(fontFamily: 'monospace')), + ], + ), + ), + TextButton( + onPressed: () => setState(() => _walletConnected = false), + child: const Text('断开', style: TextStyle(color: AppColors.error, fontSize: 13)), + ), + ], + ), + ), + ] else + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => setState(() => _walletConnected = true), + icon: const Icon(Icons.link_rounded, size: 18), + label: const Text('连接外部钱包'), + ), + ), + const SizedBox(height: 8), + Text( + '连接外部钱包后可将平台资产提取至自有地址', + style: AppTypography.caption, + ), + ], + ), + ); + } + + Widget _buildChainSettingsCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + SwitchListTile( + title: Text('显示链上地址', style: AppTypography.labelMedium), + subtitle: Text('在券详情中展示合约地址', style: AppTypography.caption), + value: _showChainAddress, + onChanged: (v) => setState(() => _showChainAddress = v), + activeColor: AppColors.primary, + contentPadding: EdgeInsets.zero, + ), + const Divider(height: 1), + SwitchListTile( + title: Text('显示交易Hash', style: AppTypography.labelMedium), + subtitle: Text('在交易记录中展示链上Hash', style: AppTypography.caption), + value: _showTxHash, + onChanged: (v) => setState(() => _showTxHash = v), + activeColor: AppColors.primary, + contentPadding: EdgeInsets.zero, + ), + ], + ), + ); + } + + Widget _buildTxExplorerCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.explore_rounded, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('交易浏览器', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + _buildTxItem('购买 星巴克 \$25 礼品卡', '0xabc1...def3', '已确认', AppColors.success), + _buildTxItem('出售 Amazon \$100 券', '0x789a...bc12', '已确认', AppColors.success), + _buildTxItem('转赠给 Alice', '0xdef4...5678', '确认中', AppColors.warning), + const SizedBox(height: 8), + Center( + child: TextButton( + onPressed: () {}, + child: const Text('查看全部链上交易'), + ), + ), + ], + ), + ); + } + + Widget _buildTxItem(String title, String hash, String status, Color color) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.bodyMedium), + Text(hash, style: AppTypography.caption.copyWith(fontFamily: 'monospace', color: AppColors.textLink)), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text(status, style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600)), + ), + ], + ), + ); + } + + Widget _buildChainAssetsCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.token_rounded, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('链上资产', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + _buildAssetRow('平台托管钱包', '0x1234...abcd', '5 张券'), + if (_walletConnected) _buildAssetRow('外部钱包 (MetaMask)', '0x7a3b...c4f2', '0 张券'), + const SizedBox(height: 12), + if (_walletConnected) + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () {}, + child: const Text('提取至外部钱包'), + ), + ), + ], + ), + ); + } + + Widget _buildAssetRow(String label, String address, String count) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.labelMedium), + Text(address, style: AppTypography.caption.copyWith(fontFamily: 'monospace')), + ], + ), + ), + Text(count, style: AppTypography.labelMedium.copyWith(color: AppColors.primary)), + ], + ), + ); + } + + Widget _buildTrackCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.swap_horiz_rounded, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Text('交易轨道', style: AppTypography.labelLarge), + ], + ), + const SizedBox(height: 12), + _buildTrackOption('Utility Track', '券有效期≤12个月,无需证券牌照', AppColors.success, true), + const SizedBox(height: 8), + _buildTrackOption('Securities Track', '长期投资型券产品(即将推出)', AppColors.warning, false), + const SizedBox(height: 8), + Text( + '当前MVP版本仅支持Utility Track', + style: AppTypography.caption.copyWith(color: AppColors.textTertiary), + ), + ], + ), + ); + } + + Widget _buildTrackOption(String name, String desc, Color color, bool active) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: active ? color.withValues(alpha: 0.05) : AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: active ? color : AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: active ? color : AppColors.textTertiary, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: AppTypography.labelMedium), + Text(desc, style: AppTypography.caption), + ], + ), + ), + if (active) Icon(Icons.check_circle_rounded, color: color, size: 20), + if (!active) Text('敬请期待', style: AppTypography.caption.copyWith(color: AppColors.textTertiary)), + ], + ), + ); + } + + Widget _buildProModeInfo() { + return Container( + margin: const EdgeInsets.only(top: 20), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + const Icon(Icons.info_outline_rounded, color: AppColors.textTertiary, size: 40), + const SizedBox(height: 12), + Text('什么是高级模式?', style: AppTypography.h3), + const SizedBox(height: 8), + Text( + '高级模式面向有区块链经验的用户,开启后可以:\n' + '• 连接外部钱包(MetaMask等)\n' + '• 查看链上地址和交易Hash\n' + '• 将资产提取至自有钱包\n' + '• 查看底层链上数据\n\n' + '需要完成 KYC L2 认证后方可开启。', + style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary, height: 1.6), + ), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart b/frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart new file mode 100644 index 0000000..aa90437 --- /dev/null +++ b/frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/kyc_badge.dart'; + +/// A7. 个人中心 +/// +/// 头像、昵称、KYC等级标识、信用积分 +/// KYC认证、支付管理、设置、Pro模式 +class ProfilePage extends StatelessWidget { + const ProfilePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + slivers: [ + // Profile Header + SliverToBoxAdapter(child: _buildProfileHeader(context)), + + // Quick Stats + SliverToBoxAdapter(child: _buildQuickStats()), + + // Menu Sections + SliverToBoxAdapter(child: _buildMenuSection('账户', [ + _MenuItem(Icons.verified_user_outlined, 'KYC 认证', '已完成 L1 认证', true, + onTap: () => Navigator.pushNamed(context, '/kyc')), + _MenuItem(Icons.credit_card_rounded, '支付管理', '已绑定 2 张卡', true, + onTap: () => Navigator.pushNamed(context, '/payment/manage')), + _MenuItem(Icons.account_balance_wallet_outlined, '我的余额', '\$1,234.56', true, + onTap: () => Navigator.pushNamed(context, '/wallet')), + ])), + + SliverToBoxAdapter(child: _buildMenuSection('交易', [ + _MenuItem(Icons.receipt_long_rounded, '交易记录', '', true, + onTap: () => Navigator.pushNamed(context, '/trading')), + _MenuItem(Icons.storefront_rounded, '我的挂单', '2笔出售中', true, + onTap: () => Navigator.pushNamed(context, '/trading')), + _MenuItem(Icons.favorite_border_rounded, '我的收藏', '', true), + ])), + + SliverToBoxAdapter(child: _buildMenuSection('设置', [ + _MenuItem(Icons.notifications_outlined, '通知设置', '', true), + _MenuItem(Icons.language_rounded, '语言', '简体中文', true), + _MenuItem(Icons.shield_outlined, '安全设置', '', true), + _MenuItem(Icons.tune_rounded, '高级设置', 'Pro模式', true, + onTap: () => Navigator.pushNamed(context, '/pro-mode')), + _MenuItem(Icons.info_outline_rounded, '关于 Genex', 'v1.0.0', true), + ])), + + // Logout + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 40), + child: TextButton( + onPressed: () { + Navigator.of(context).pushNamedAndRemoveUntil('/', (_) => false); + }, + child: Text('退出登录', style: AppTypography.labelMedium.copyWith( + color: AppColors.error, + )), + ), + ), + ), + + const SliverPadding(padding: EdgeInsets.only(bottom: 80)), + ], + ), + ); + } + + Widget _buildProfileHeader(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 60, 20, 24), + decoration: const BoxDecoration( + gradient: AppColors.primaryGradient, + ), + child: Row( + children: [ + // Avatar + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: Colors.white24, + shape: BoxShape.circle, + border: Border.all(color: Colors.white38, width: 2), + ), + child: const Icon(Icons.person_rounded, color: Colors.white, size: 32), + ), + const SizedBox(width: 16), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('用户昵称', style: AppTypography.h2.copyWith(color: Colors.white)), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.shield_rounded, size: 10, color: Colors.white), + const SizedBox(width: 2), + Text('L1', style: AppTypography.caption.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + )), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + Text('信用积分: 750', style: AppTypography.bodySmall.copyWith( + color: Colors.white70, + )), + ], + ), + ), + + // Settings icon + IconButton( + icon: const Icon(Icons.settings_outlined, color: Colors.white), + onPressed: () { + Navigator.pushNamed(context, '/settings'); + }, + ), + ], + ), + ); + } + + Widget _buildQuickStats() { + final stats = [ + ('持券', '12'), + ('交易', '28'), + ('节省', '\$156'), + ('信用', '750'), + ]; + + return Container( + margin: const EdgeInsets.fromLTRB(20, 16, 20, 8), + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + boxShadow: AppSpacing.shadowSm, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: stats.map((stat) { + return Column( + children: [ + Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)), + const SizedBox(height: 4), + Text(stat.$1, style: AppTypography.caption), + ], + ); + }).toList(), + ), + ); + } + + Widget _buildMenuSection(String title, List<_MenuItem> items) { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.labelSmall.copyWith(color: AppColors.textTertiary)), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: items.asMap().entries.map((entry) { + final item = entry.value; + final isLast = entry.key == items.length - 1; + return Column( + children: [ + ListTile( + leading: Icon(item.icon, color: AppColors.textPrimary, size: 22), + title: Text(item.title, style: AppTypography.bodyMedium), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (item.subtitle.isNotEmpty) + Text(item.subtitle, style: AppTypography.caption), + if (item.hasArrow) ...[ + const SizedBox(width: 4), + const Icon(Icons.chevron_right_rounded, + color: AppColors.textTertiary, size: 20), + ], + ], + ), + onTap: item.onTap ?? () {}, + ), + if (!isLast) + const Divider(indent: 56, height: 1), + ], + ); + }).toList(), + ), + ), + ], + ), + ); + } +} + +class _MenuItem { + final IconData icon; + final String title; + final String subtitle; + final bool hasArrow; + final VoidCallback? onTap; + + const _MenuItem(this.icon, this.title, this.subtitle, this.hasArrow, {this.onTap}); +} diff --git a/frontend/genex-mobile/lib/features/profile/presentation/pages/settings_page.dart b/frontend/genex-mobile/lib/features/profile/presentation/pages/settings_page.dart new file mode 100644 index 0000000..7249d8a --- /dev/null +++ b/frontend/genex-mobile/lib/features/profile/presentation/pages/settings_page.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; + +/// 设置页面 +/// +/// 账号安全、通知、支付管理、语言、关于 +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('设置')), + body: ListView( + children: [ + // Account & Security + _buildSection('账号与安全', [ + _buildTile('手机号', subtitle: '138****8888', icon: Icons.phone_rounded), + _buildTile('邮箱', subtitle: 'u***@email.com', icon: Icons.email_rounded), + _buildTile('修改密码', icon: Icons.lock_rounded), + _buildTile('身份认证', subtitle: 'L1 基础认证', icon: Icons.verified_user_rounded, onTap: () { + Navigator.pushNamed(context, '/kyc'); + }), + ]), + + // Payment + _buildSection('支付管理', [ + _buildTile('支付方式', subtitle: 'Visa •••• 4242', icon: Icons.credit_card_rounded), + _buildTile('银行账户', subtitle: 'BoA •••• 6789', icon: Icons.account_balance_rounded), + _buildTile('支付密码', icon: Icons.password_rounded), + ]), + + // Notifications + _buildSection('通知设置', [ + _buildSwitchTile('交易通知', true), + _buildSwitchTile('到期提醒', true), + _buildSwitchTile('行情变动', false), + _buildSwitchTile('营销推送', false), + ]), + + // General + _buildSection('通用', [ + _buildTile('语言', subtitle: '简体中文', icon: Icons.language_rounded), + _buildTile('货币', subtitle: 'USD', icon: Icons.attach_money_rounded), + _buildTile('清除缓存', icon: Icons.cleaning_services_rounded), + ]), + + // About + _buildSection('关于', [ + _buildTile('版本', subtitle: 'v1.0.0', icon: Icons.info_outline_rounded), + _buildTile('用户协议', icon: Icons.description_rounded), + _buildTile('隐私政策', icon: Icons.privacy_tip_rounded), + _buildTile('帮助中心', icon: Icons.help_outline_rounded), + ]), + + // Logout + Padding( + padding: const EdgeInsets.all(20), + child: OutlinedButton( + onPressed: () { + Navigator.of(context).pushNamedAndRemoveUntil('/', (_) => false); + }, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.error, + side: const BorderSide(color: AppColors.error), + minimumSize: const Size(double.infinity, 48), + ), + child: const Text('退出登录'), + ), + ), + ], + ), + ); + } + + Widget _buildSection(String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), + child: Text(title, style: AppTypography.labelSmall), + ), + Container( + color: AppColors.surface, + child: Column(children: children), + ), + ], + ); + } + + Widget _buildTile(String title, {String? subtitle, IconData? icon, VoidCallback? onTap}) { + return ListTile( + leading: icon != null ? Icon(icon, size: 22, color: AppColors.textSecondary) : null, + title: Text(title, style: AppTypography.bodyMedium), + subtitle: subtitle != null ? Text(subtitle, style: AppTypography.caption) : null, + trailing: const Icon(Icons.chevron_right_rounded, size: 20, color: AppColors.textTertiary), + onTap: onTap ?? () {}, + ); + } + + Widget _buildSwitchTile(String title, bool value) { + return SwitchListTile( + title: Text(title, style: AppTypography.bodyMedium), + value: value, + onChanged: (_) {}, + activeColor: AppColors.primary, + ); + } +} diff --git a/frontend/genex-mobile/lib/features/trading/presentation/pages/sell_order_page.dart b/frontend/genex-mobile/lib/features/trading/presentation/pages/sell_order_page.dart new file mode 100644 index 0000000..e43f91c --- /dev/null +++ b/frontend/genex-mobile/lib/features/trading/presentation/pages/sell_order_page.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// A10. 挂单出售页面 +/// +/// 消费者将持有的券挂到二级市场出售 +/// 自定义定价 + AI推荐价格 + 手续费预览 +class SellOrderPage extends StatefulWidget { + const SellOrderPage({super.key}); + + @override + State createState() => _SellOrderPageState(); +} + +class _SellOrderPageState extends State { + final _priceController = TextEditingController(text: '22.50'); + double _faceValue = 25.0; + + double get _price => double.tryParse(_priceController.text) ?? 0; + double get _discount => _faceValue > 0 ? _price / _faceValue * 100 : 0; + double get _fee => _price * 0.015; // 1.5% 手续费 + double get _receive => _price - _fee; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('挂单出售')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Coupon Info + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: const Icon(Icons.confirmation_number_rounded, color: AppColors.primary, size: 28), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('星巴克 \$25 礼品卡', style: AppTypography.labelLarge), + const SizedBox(height: 4), + Text('面值 \$$_faceValue · 信用 AAA', style: AppTypography.bodySmall), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Price Input + Text('设定售价', style: AppTypography.h3), + const SizedBox(height: 12), + TextField( + controller: _priceController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + prefixText: '\$ ', + labelText: '售价', + suffixText: 'USD', + ), + style: AppTypography.priceLarge, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 8), + + // AI Suggestion + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + const Icon(Icons.auto_awesome_rounded, color: AppColors.primary, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + 'AI建议售价:\$22.50(9折),此价格成交概率最高', + style: AppTypography.caption.copyWith(color: AppColors.primary), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Fee Breakdown + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + _buildRow('售价', '\$${_price.toStringAsFixed(2)}'), + _buildRow('折扣率', '${_discount.toStringAsFixed(1)}%'), + _buildRow('平台手续费 (1.5%)', '-\$${_fee.toStringAsFixed(2)}'), + const Divider(height: 24), + _buildRow('预计到账', '\$${_receive.toStringAsFixed(2)}', isBold: true), + ], + ), + ), + const SizedBox(height: 16), + + // Market Info + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.infoLight, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + const Icon(Icons.info_outline_rounded, color: AppColors.info, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + '当前市场均价 \$22.80 · 最近24小时成交 42 笔', + style: AppTypography.caption.copyWith(color: AppColors.info), + ), + ), + ], + ), + ), + ], + ), + ), + bottomNavigationBar: Container( + padding: const EdgeInsets.all(20), + child: SizedBox( + height: AppSpacing.buttonHeight, + child: ElevatedButton( + onPressed: () => _confirmSell(context), + child: const Text('确认挂单'), + ), + ), + ), + ); + } + + Widget _buildRow(String label, String value, {bool isBold = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + Text(value, style: isBold ? AppTypography.priceSmall : AppTypography.labelMedium), + ], + ), + ); + } + + void _confirmSell(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('挂单成功'), + content: const Text('您的券已挂到市场,当有买家下单时将自动成交。'), + actions: [ + TextButton(onPressed: () { Navigator.pop(ctx); Navigator.pop(context); }, child: const Text('确定')), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/trading/presentation/pages/trading_detail_page.dart b/frontend/genex-mobile/lib/features/trading/presentation/pages/trading_detail_page.dart new file mode 100644 index 0000000..d7e983c --- /dev/null +++ b/frontend/genex-mobile/lib/features/trading/presentation/pages/trading_detail_page.dart @@ -0,0 +1,796 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// 交易对详情页 - 币安风格 +/// +/// K线图 + OHLC + 交易深度 + 买卖盘口 + 下单 +class TradingDetailPage extends StatefulWidget { + const TradingDetailPage({super.key}); + + @override + State createState() => _TradingDetailPageState(); +} + +class _TradingDetailPageState extends State + with SingleTickerProviderStateMixin { + String _selectedPeriod = '1D'; + bool _isBuy = true; + String _orderType = 'limit'; // limit, market + late TabController _bottomTabController; + + @override + void initState() { + super.initState(); + _bottomTabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _bottomTabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('SBUX / USDT'), + actions: [ + IconButton( + icon: const Icon(Icons.star_border_rounded, size: 22), + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.more_horiz_rounded, size: 22), + onPressed: () {}, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Price header + _buildPriceHeader(), + + // K-line chart + _buildKlineChart(), + + // Period selector + _buildPeriodSelector(), + + const SizedBox(height: 16), + + // Order book + Recent trades + _buildOrderBookSection(), + + const SizedBox(height: 16), + + // Buy/Sell order form + _buildOrderForm(), + + const SizedBox(height: 16), + + // My orders + _buildMyOrders(), + + const SizedBox(height: 100), + ], + ), + ), + ), + ], + ), + + // Bottom buy/sell bar + bottomNavigationBar: _buildBottomBar(), + ); + } + + // ============================================================ + // Price Header - OHLC + 24h stats + // ============================================================ + Widget _buildPriceHeader() { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Current price + change + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '21.30', + style: AppTypography.priceLarge.copyWith( + color: AppColors.success, + fontSize: 32, + ), + ), + const SizedBox(width: 8), + Text( + '≈ \$21.30', + style: AppTypography.bodySmall.copyWith( + color: AppColors.textTertiary, + ), + ), + const Spacer(), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.success, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Text( + '+2.05%', + style: AppTypography.labelSmall.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // 24h OHLC stats + Row( + children: [ + _buildOhlcStat('24h高', '21.75', AppColors.error), + _buildOhlcStat('24h低', '20.85', AppColors.success), + _buildOhlcStat('开盘', '20.87', AppColors.textPrimary), + _buildOhlcStat('24h量', '342.5K', AppColors.textPrimary), + ], + ), + ], + ), + ); + } + + Widget _buildOhlcStat(String label, String value, Color valueColor) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.caption), + const SizedBox(height: 2), + Text( + value, + style: AppTypography.labelSmall.copyWith( + color: valueColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + // ============================================================ + // K-line Chart (placeholder) + // ============================================================ + Widget _buildKlineChart() { + return Container( + height: 220, + margin: const EdgeInsets.fromLTRB(20, 16, 20, 0), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Stack( + children: [ + // Mock candlestick chart + CustomPaint( + size: const Size(double.infinity, 220), + painter: _CandlestickPainter(), + ), + // Label + Positioned( + top: 8, + left: 12, + child: Text( + 'SBUX/USDT · $_selectedPeriod', + style: AppTypography.caption.copyWith( + color: AppColors.textTertiary, + ), + ), + ), + ], + ), + ); + } + + Widget _buildPeriodSelector() { + final periods = ['1m', '5m', '15m', '1h', '4h', '1D', '1W']; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 0), + child: Row( + children: periods.map((period) { + final isSelected = _selectedPeriod == period; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _selectedPeriod = period), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 6), + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: + isSelected ? AppColors.primary : Colors.transparent, + borderRadius: AppSpacing.borderRadiusSm, + ), + alignment: Alignment.center, + child: Text( + period, + style: AppTypography.caption.copyWith( + color: + isSelected ? Colors.white : AppColors.textTertiary, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + ), + ); + }).toList(), + ), + ); + } + + // ============================================================ + // Order Book (Bid/Ask depth) + // ============================================================ + Widget _buildOrderBookSection() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('交易深度', style: AppTypography.h3), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Asks (sell orders) - red + Expanded(child: _buildOrderBookSide(isAsk: true)), + const SizedBox(width: 12), + // Bids (buy orders) - green + Expanded(child: _buildOrderBookSide(isAsk: false)), + ], + ), + ], + ), + ); + } + + Widget _buildOrderBookSide({required bool isAsk}) { + final color = isAsk ? AppColors.error : AppColors.success; + final label = isAsk ? '卖盘' : '买盘'; + final prices = isAsk + ? [21.45, 21.42, 21.40, 21.38, 21.35] + : [21.28, 21.25, 21.22, 21.20, 21.18]; + final amounts = isAsk + ? [120, 85, 200, 45, 310] + : [95, 150, 180, 60, 250]; + + final maxAmount = [...amounts].reduce((a, b) => a > b ? a : b); + + return Column( + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.caption.copyWith(color: color)), + Text('数量', style: AppTypography.caption), + ], + ), + const SizedBox(height: 6), + ...List.generate(prices.length, (i) { + final ratio = amounts[i] / maxAmount; + return Container( + margin: const EdgeInsets.only(bottom: 2), + child: Stack( + children: [ + // Depth bar background + Positioned.fill( + child: FractionallySizedBox( + alignment: + isAsk ? Alignment.centerRight : Alignment.centerLeft, + widthFactor: ratio, + child: Container( + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + // Price + Amount + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + prices[i].toStringAsFixed(2), + style: AppTypography.caption.copyWith( + color: color, + fontWeight: FontWeight.w500, + fontFamily: 'monospace', + ), + ), + Text( + '${amounts[i]}', + style: AppTypography.caption.copyWith( + fontFamily: 'monospace', + ), + ), + ], + ), + ), + ], + ), + ); + }), + ], + ); + } + + // ============================================================ + // Order Form (Buy / Sell) + // ============================================================ + Widget _buildOrderForm() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('下单', style: AppTypography.h3), + const SizedBox(height: 12), + + // Buy/Sell toggle + Container( + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _isBuy = true), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: _isBuy ? AppColors.success : Colors.transparent, + borderRadius: AppSpacing.borderRadiusFull, + ), + alignment: Alignment.center, + child: Text( + '买入', + style: AppTypography.labelMedium.copyWith( + color: _isBuy ? Colors.white : AppColors.textTertiary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _isBuy = false), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: + !_isBuy ? AppColors.error : Colors.transparent, + borderRadius: AppSpacing.borderRadiusFull, + ), + alignment: Alignment.center, + child: Text( + '卖出', + style: AppTypography.labelMedium.copyWith( + color: + !_isBuy ? Colors.white : AppColors.textTertiary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Order type selector + Row( + children: [ + _buildOrderTypeChip('限价单', 'limit'), + const SizedBox(width: 8), + _buildOrderTypeChip('市价单', 'market'), + ], + ), + + const SizedBox(height: 12), + + // Price input (hidden for market orders) + if (_orderType == 'limit') ...[ + _buildInputField('价格', '21.30', 'USDT'), + const SizedBox(height: 8), + ], + + // Amount input + _buildInputField('数量', '', '张'), + + const SizedBox(height: 8), + + // Amount slider (25% / 50% / 75% / 100%) + Row( + children: [ + _buildPercentChip('25%'), + const SizedBox(width: 6), + _buildPercentChip('50%'), + const SizedBox(width: 6), + _buildPercentChip('75%'), + const SizedBox(width: 6), + _buildPercentChip('100%'), + ], + ), + + const SizedBox(height: 12), + + // Available balance + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('可用', style: AppTypography.caption), + Text( + _isBuy ? '1,234.56 USDT' : '3 张 SBUX', + style: AppTypography.caption.copyWith( + color: AppColors.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Submit button + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: + _isBuy ? AppColors.success : AppColors.error, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + ), + ), + child: Text( + _isBuy ? '买入 SBUX' : '卖出 SBUX', + style: AppTypography.labelLarge.copyWith(color: Colors.white), + ), + ), + ), + ], + ), + ); + } + + Widget _buildOrderTypeChip(String label, String type) { + final isSelected = _orderType == type; + return GestureDetector( + onTap: () => setState(() => _orderType = type), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? AppColors.primarySurface : AppColors.gray50, + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all( + color: isSelected + ? AppColors.primary.withValues(alpha: 0.3) + : AppColors.borderLight, + ), + ), + child: Text( + label, + style: AppTypography.labelSmall.copyWith( + color: isSelected ? AppColors.primary : AppColors.textSecondary, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + ); + } + + Widget _buildInputField(String label, String hint, String suffix) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + SizedBox( + width: 40, + child: Text(label, style: AppTypography.caption), + ), + Expanded( + child: TextField( + decoration: InputDecoration( + hintText: hint, + hintStyle: AppTypography.bodyMedium.copyWith( + color: AppColors.textTertiary, + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + style: AppTypography.bodyMedium.copyWith( + fontFamily: 'monospace', + ), + ), + ), + Text( + suffix, + style: AppTypography.caption.copyWith( + color: AppColors.textSecondary, + ), + ), + ], + ), + ); + } + + Widget _buildPercentChip(String label) { + return Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + alignment: Alignment.center, + child: Text(label, style: AppTypography.caption), + ), + ), + ); + } + + // ============================================================ + // My Orders + // ============================================================ + Widget _buildMyOrders() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('当前委托', style: AppTypography.h3), + GestureDetector( + onTap: () {}, + child: Text('历史委托', + style: AppTypography.labelSmall + .copyWith(color: AppColors.primary)), + ), + ], + ), + const SizedBox(height: 12), + + // Mock orders + _buildOrderItem('买入', 'SBUX/USDT', '21.20', '5', '限价', + AppColors.success), + const Divider(height: 1), + _buildOrderItem('卖出', 'SBUX/USDT', '21.50', '2', '限价', + AppColors.error), + ], + ), + ); + } + + Widget _buildOrderItem(String side, String pair, String price, + String amount, String type, Color sideColor) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: sideColor.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + side, + style: AppTypography.caption.copyWith( + color: sideColor, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(pair, style: AppTypography.labelSmall), + Text('$type · $amount张 @ $price', + style: AppTypography.caption), + ], + ), + ), + GestureDetector( + onTap: () {}, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + border: Border.all(color: AppColors.borderLight), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text('撤销', + style: AppTypography.caption + .copyWith(color: AppColors.textSecondary)), + ), + ), + ], + ), + ); + } + + // ============================================================ + // Bottom Bar + // ============================================================ + Widget _buildBottomBar() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + decoration: const BoxDecoration( + color: AppColors.surface, + border: Border(top: BorderSide(color: AppColors.borderLight)), + ), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 44, + child: ElevatedButton( + onPressed: () => setState(() => _isBuy = true), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.success, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusSm, + ), + ), + child: Text('买入', + style: AppTypography.labelMedium + .copyWith(color: Colors.white)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: SizedBox( + height: 44, + child: ElevatedButton( + onPressed: () => setState(() => _isBuy = false), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.error, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusSm, + ), + ), + child: Text('卖出', + style: AppTypography.labelMedium + .copyWith(color: Colors.white)), + ), + ), + ), + ], + ), + ); + } +} + +// ============================================================ +// Mock Candlestick Chart Painter +// ============================================================ +class _CandlestickPainter extends CustomPainter { + final _random = Random(42); + + @override + void paint(Canvas canvas, Size size) { + final candleWidth = size.width / 30; + final padding = 16.0; + final chartHeight = size.height - padding * 2; + + double prevClose = 21.0; + + for (int i = 0; i < 28; i++) { + final x = padding + i * candleWidth; + + // Generate OHLC + final open = prevClose + (_random.nextDouble() - 0.5) * 0.3; + final close = open + (_random.nextDouble() - 0.5) * 0.4; + final high = max(open, close) + _random.nextDouble() * 0.2; + final low = min(open, close) - _random.nextDouble() * 0.2; + prevClose = close; + + final isGreen = close >= open; + final color = isGreen ? AppColors.success : AppColors.error; + + // Normalize to chart coordinates (price range 20.5 - 22.0) + final priceRange = 1.5; + final minPrice = 20.5; + double priceToY(double price) { + return padding + (1 - (price - minPrice) / priceRange) * chartHeight; + } + + // Draw wick + canvas.drawLine( + Offset(x + candleWidth / 2, priceToY(high)), + Offset(x + candleWidth / 2, priceToY(low)), + Paint() + ..color = color + ..strokeWidth = 1, + ); + + // Draw body + final bodyTop = priceToY(max(open, close)); + final bodyBottom = priceToY(min(open, close)); + canvas.drawRect( + Rect.fromLTRB(x + 2, bodyTop, x + candleWidth - 2, bodyBottom), + Paint() + ..color = color + ..style = isGreen ? PaintingStyle.stroke : PaintingStyle.fill + ..strokeWidth = 1, + ); + } + + // Volume bars at bottom + for (int i = 0; i < 28; i++) { + final x = padding + i * candleWidth; + final vol = _random.nextDouble() * 0.15 * chartHeight; + canvas.drawRect( + Rect.fromLTRB( + x + 2, + size.height - padding - vol * 0.3, + x + candleWidth - 2, + size.height - padding, + ), + Paint()..color = AppColors.gray200, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/frontend/genex-mobile/lib/features/trading/presentation/pages/trading_page.dart b/frontend/genex-mobile/lib/features/trading/presentation/pages/trading_page.dart new file mode 100644 index 0000000..0d3605e --- /dev/null +++ b/frontend/genex-mobile/lib/features/trading/presentation/pages/trading_page.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/status_tag.dart'; +import '../../../../shared/widgets/empty_state.dart'; + +/// A5. 交易模块(二级市场) +/// +/// 我的挂单、我的交易记录 +class TradingPage extends StatefulWidget { + const TradingPage({super.key}); + + @override + State createState() => _TradingPageState(); +} + +class _TradingPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('我的交易'), + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: '我的挂单'), + Tab(text: '交易记录'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildMyListings(), + _buildTransactionHistory(), + ], + ), + ); + } + + Widget _buildMyListings() { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 100), + itemCount: 3, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final statuses = [ + StatusTags.onSale(), + StatusTags.completed(), + StatusTags.cancelled(), + ]; + return Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon(Icons.confirmation_number_outlined, + color: AppColors.primary.withValues(alpha: 0.4), size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(['星巴克 \$25', 'Amazon \$50', 'Nike \$80'][index], + style: AppTypography.labelMedium), + const SizedBox(height: 4), + Row( + children: [ + Text('挂单价 ', style: AppTypography.caption), + Text('\$${[21.25, 42.50, 68.00][index]}', + style: AppTypography.priceSmall.copyWith(fontSize: 14)), + ], + ), + ], + ), + ), + statuses[index], + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('挂单时间: 2026/02/${9 - index}', + style: AppTypography.caption), + if (index == 0) + GestureDetector( + onTap: () {}, + child: Text('撤单', style: AppTypography.labelSmall.copyWith( + color: AppColors.error, + )), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildTransactionHistory() { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 100), + itemCount: 6, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final isBuy = index % 2 == 0; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: isBuy ? AppColors.successLight : AppColors.errorLight, + shape: BoxShape.circle, + ), + child: Icon( + isBuy ? Icons.arrow_downward_rounded : Icons.arrow_upward_rounded, + size: 18, + color: isBuy ? AppColors.success : AppColors.error, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isBuy ? '买入' : '卖出', + style: AppTypography.labelMedium, + ), + Text( + '品牌 ${index + 1} 礼品卡', + style: AppTypography.caption, + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${isBuy ? "-" : "+"}\$${(index + 1) * 15}.00', + style: AppTypography.labelMedium.copyWith( + color: isBuy ? AppColors.textPrimary : AppColors.success, + ), + ), + Text('02/${10 - index} 14:${30 + index}', + style: AppTypography.caption), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/frontend/genex-mobile/lib/features/trading/presentation/pages/transfer_page.dart b/frontend/genex-mobile/lib/features/trading/presentation/pages/transfer_page.dart new file mode 100644 index 0000000..be60098 --- /dev/null +++ b/frontend/genex-mobile/lib/features/trading/presentation/pages/transfer_page.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// A9. P2P转赠页面 +/// +/// 选择好友 → 确认转赠 → 转赠成功 +/// 零区块链术语:使用"转赠"而非"转移NFT" +class TransferPage extends StatefulWidget { + const TransferPage({super.key}); + + @override + State createState() => _TransferPageState(); +} + +class _TransferPageState extends State { + final _searchController = TextEditingController(); + String? _selectedFriend; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('转赠给好友')), + body: Column( + children: [ + // Coupon Info + Container( + margin: const EdgeInsets.all(20), + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.primarySurface, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Icon(Icons.card_giftcard_rounded, color: AppColors.primary), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('星巴克 \$25 礼品卡', style: AppTypography.labelMedium), + Text('面值 \$25.00', style: AppTypography.bodySmall), + ], + ), + ), + ], + ), + ), + + // Search Friend + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextField( + controller: _searchController, + decoration: const InputDecoration( + hintText: '搜索好友(手机号/用户名)', + prefixIcon: Icon(Icons.search_rounded), + ), + ), + ), + const SizedBox(height: 16), + + // Friends List + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 20), + children: [ + _buildFriendTile('Alice', 'alice@example.com', 'A'), + _buildFriendTile('Bob', 'bob@example.com', 'B'), + _buildFriendTile('Charlie', 'charlie@example.com', 'C'), + _buildFriendTile('Diana', 'diana@example.com', 'D'), + ], + ), + ), + + // Transfer Button + Container( + padding: const EdgeInsets.all(20), + child: SizedBox( + width: double.infinity, + height: AppSpacing.buttonHeight, + child: ElevatedButton( + onPressed: _selectedFriend != null ? () => _showConfirm(context) : null, + child: Text(_selectedFriend != null ? '转赠给 $_selectedFriend' : '请选择好友'), + ), + ), + ), + ], + ), + ); + } + + Widget _buildFriendTile(String name, String email, String avatar) { + final isSelected = _selectedFriend == name; + return GestureDetector( + onTap: () => setState(() => _selectedFriend = name), + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all( + color: isSelected ? AppColors.primary : AppColors.borderLight, + width: isSelected ? 1.5 : 1, + ), + ), + child: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: AppColors.primaryContainer, + child: Text(avatar, style: const TextStyle(color: AppColors.primary, fontWeight: FontWeight.w600)), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: AppTypography.labelMedium), + Text(email, style: AppTypography.caption), + ], + ), + ), + if (isSelected) const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 22), + ], + ), + ), + ); + } + + void _showConfirm(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (ctx) => Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.card_giftcard_rounded, color: AppColors.primary, size: 48), + const SizedBox(height: 16), + Text('确认转赠', style: AppTypography.h2), + const SizedBox(height: 8), + Text('将 星巴克 \$25 礼品卡 转赠给 $_selectedFriend?', style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + const SizedBox(height: 8), + Text('转赠后您将不再持有此券', style: AppTypography.caption.copyWith(color: AppColors.warning)), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('取消'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.pop(ctx); + _showSuccess(context); + }, + child: const Text('确认转赠'), + ), + ), + ], + ), + ], + ), + ), + ); + } + + void _showSuccess(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.check_circle_rounded, color: AppColors.success, size: 56), + const SizedBox(height: 16), + Text('转赠成功', style: AppTypography.h2), + const SizedBox(height: 8), + Text('$_selectedFriend 已收到您的券', style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(ctx); + Navigator.pop(context); + }, + child: const Text('确定'), + ), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/wallet/presentation/pages/deposit_page.dart b/frontend/genex-mobile/lib/features/wallet/presentation/pages/deposit_page.dart new file mode 100644 index 0000000..adc19ed --- /dev/null +++ b/frontend/genex-mobile/lib/features/wallet/presentation/pages/deposit_page.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 充值页面 +/// +/// 法币充值到平台账户余额 +/// 支付方式:银行卡、Apple Pay、Google Pay +class DepositPage extends StatefulWidget { + const DepositPage({super.key}); + + @override + State createState() => _DepositPageState(); +} + +class _DepositPageState extends State { + final _amountController = TextEditingController(); + final _presets = [50, 100, 200, 500]; + int? _selectedPreset; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('充值')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Current Balance + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusLg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('当前余额', style: AppTypography.bodySmall.copyWith(color: Colors.white70)), + const SizedBox(height: 4), + Text('\$128.50', style: AppTypography.displayLarge.copyWith(color: Colors.white)), + ], + ), + ), + const SizedBox(height: 24), + + Text('充值金额', style: AppTypography.h3), + const SizedBox(height: 12), + + // Preset Amounts + Row( + children: _presets.map((amount) { + final isSelected = _selectedPreset == amount; + return Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _selectedPreset = amount; + _amountController.text = amount.toString(); + }); + }, + child: Container( + margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryContainer : AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all( + color: isSelected ? AppColors.primary : AppColors.border, + ), + ), + child: Center( + child: Text( + '\$$amount', + style: AppTypography.labelMedium.copyWith( + color: isSelected ? AppColors.primary : AppColors.textPrimary, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + const SizedBox(height: 16), + + // Custom Amount + TextField( + controller: _amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: '自定义金额', + prefixText: '\$ ', + ), + onChanged: (_) => setState(() => _selectedPreset = null), + ), + const SizedBox(height: 24), + + // Payment Method + Text('支付方式', style: AppTypography.h3), + const SizedBox(height: 12), + _buildPaymentOption('Visa •••• 4242', Icons.credit_card_rounded, true), + _buildPaymentOption('Apple Pay', Icons.apple_rounded, false), + ], + ), + ), + bottomNavigationBar: Container( + padding: const EdgeInsets.all(20), + child: SizedBox( + height: AppSpacing.buttonHeight, + child: ElevatedButton( + onPressed: _amountController.text.isNotEmpty ? () {} : null, + child: Text('充值 \$${_amountController.text.isNotEmpty ? _amountController.text : '0'}'), + ), + ), + ), + ); + } + + Widget _buildPaymentOption(String name, IconData icon, bool selected) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: selected ? AppColors.primary : AppColors.borderLight), + ), + child: Row( + children: [ + Icon(icon, color: AppColors.textSecondary), + const SizedBox(width: 12), + Expanded(child: Text(name, style: AppTypography.labelMedium)), + if (selected) const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 20), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/features/wallet/presentation/pages/transaction_records_page.dart b/frontend/genex-mobile/lib/features/wallet/presentation/pages/transaction_records_page.dart new file mode 100644 index 0000000..6df4cfc --- /dev/null +++ b/frontend/genex-mobile/lib/features/wallet/presentation/pages/transaction_records_page.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 交易记录页面 +/// +/// 所有交易明细:购买、出售、转赠、充值、提现 +/// 按时间/类型筛选 +class TransactionRecordsPage extends StatelessWidget { + const TransactionRecordsPage({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 4, + child: Scaffold( + appBar: AppBar( + title: const Text('交易记录'), + bottom: const TabBar( + isScrollable: true, + tabs: [ + Tab(text: '全部'), + Tab(text: '购买'), + Tab(text: '出售'), + Tab(text: '转赠'), + ], + ), + ), + body: TabBarView( + children: [ + _buildList(_allRecords), + _buildList(_allRecords.where((r) => r.type == '购买').toList()), + _buildList(_allRecords.where((r) => r.type == '出售').toList()), + _buildList(_allRecords.where((r) => r.type == '转赠').toList()), + ], + ), + ), + ); + } + + Widget _buildList(List<_TxRecord> records) { + if (records.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.receipt_long_rounded, size: 48, color: AppColors.textTertiary), + const SizedBox(height: 12), + Text('暂无记录', style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary)), + ], + ), + ); + } + + return ListView.separated( + padding: const EdgeInsets.all(20), + itemCount: records.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final r = records[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: r.color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Icon(r.icon, color: r.color, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(r.title, style: AppTypography.labelMedium), + const SizedBox(height: 2), + Text(r.subtitle, style: AppTypography.caption), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + r.amount, + style: AppTypography.labelMedium.copyWith( + color: r.amount.startsWith('+') ? AppColors.success : AppColors.textPrimary, + ), + ), + Text(r.time, style: AppTypography.caption), + ], + ), + ], + ), + ); + }, + ); + } +} + +class _TxRecord { + final String type; + final String title; + final String subtitle; + final String amount; + final String time; + final IconData icon; + final Color color; + + const _TxRecord({ + required this.type, + required this.title, + required this.subtitle, + required this.amount, + required this.time, + required this.icon, + required this.color, + }); +} + +const _allRecords = [ + _TxRecord(type: '购买', title: '购买 星巴克 \$25 礼品卡', subtitle: '订单号 GNX20260210001', amount: '-\$21.25', time: '今天 14:32', icon: Icons.shopping_cart_rounded, color: AppColors.primary), + _TxRecord(type: '出售', title: '出售 Amazon \$100 购物券', subtitle: '订单号 GNX20260210002', amount: '+\$92.00', time: '今天 12:15', icon: Icons.sell_rounded, color: AppColors.success), + _TxRecord(type: '转赠', title: '转赠给 Alice', subtitle: 'Nike \$80 运动券', amount: '\$0', time: '昨天 18:45', icon: Icons.card_giftcard_rounded, color: AppColors.info), + _TxRecord(type: '购买', title: '购买 Target \$30 折扣券', subtitle: '订单号 GNX20260209001', amount: '-\$24.00', time: '昨天 10:20', icon: Icons.shopping_cart_rounded, color: AppColors.primary), + _TxRecord(type: '出售', title: '出售 Walmart \$50 生活券', subtitle: '订单号 GNX20260208003', amount: '+\$46.50', time: '2天前', icon: Icons.sell_rounded, color: AppColors.success), +]; diff --git a/frontend/genex-mobile/lib/features/wallet/presentation/pages/wallet_page.dart b/frontend/genex-mobile/lib/features/wallet/presentation/pages/wallet_page.dart new file mode 100644 index 0000000..00559ef --- /dev/null +++ b/frontend/genex-mobile/lib/features/wallet/presentation/pages/wallet_page.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; +import '../../../../shared/widgets/genex_button.dart'; + +/// A6. 账户模块 - 我的余额 +/// +/// 总余额(美元显示)、可提现金额、冻结金额、充值/提现 +/// 交易记录时间线 +class WalletPage extends StatelessWidget { + const WalletPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('我的余额'), + ), + body: SingleChildScrollView( + child: Column( + children: [ + // Balance Card + _buildBalanceCard(), + + // Quick Actions + Padding( + padding: AppSpacing.pagePadding, + child: Row( + children: [ + Expanded( + child: GenexButton( + label: '充值', + icon: Icons.add_rounded, + variant: GenexButtonVariant.primary, + onPressed: () { + Navigator.pushNamed(context, '/wallet/deposit'); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: GenexButton( + label: '提现', + icon: Icons.account_balance_rounded, + variant: GenexButtonVariant.outline, + onPressed: () { + Navigator.pushNamed(context, '/wallet/withdraw'); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Transaction History + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('交易记录', style: AppTypography.h3), + GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/wallet/records'); + }, + child: Row( + children: [ + Text('筛选', style: AppTypography.labelSmall.copyWith( + color: AppColors.textTertiary, + )), + const Icon(Icons.filter_list_rounded, size: 16, + color: AppColors.textTertiary), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 12), + + // Transaction List + _buildTransactionList(), + + const SizedBox(height: 80), + ], + ), + ), + ); + } + + Widget _buildBalanceCard() { + return Container( + margin: const EdgeInsets.fromLTRB(20, 16, 20, 16), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: AppColors.cardGradient, + borderRadius: AppSpacing.borderRadiusLg, + boxShadow: AppSpacing.shadowPrimary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('总余额', style: AppTypography.bodySmall.copyWith( + color: Colors.white70, + )), + const SizedBox(height: 8), + Text( + '\$1,234.56', + style: AppTypography.displayLarge.copyWith( + color: Colors.white, + fontSize: 36, + ), + ), + const SizedBox(height: 20), + Row( + children: [ + _balanceItem('可提现', '\$1,034.56'), + const SizedBox(width: 32), + _balanceItem('冻结中', '\$200.00'), + ], + ), + ], + ), + ); + } + + Widget _balanceItem(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.caption.copyWith(color: Colors.white54)), + const SizedBox(height: 4), + Text(value, style: AppTypography.labelMedium.copyWith(color: Colors.white)), + ], + ); + } + + Widget _buildTransactionList() { + final transactions = [ + ('买入 星巴克 \$25 礼品卡', '-\$21.25', Icons.shopping_cart_rounded, AppColors.textPrimary, '今天 14:32'), + ('卖出 Amazon \$50 购物券', '+\$42.50', Icons.sell_rounded, AppColors.success, '今天 10:15'), + ('充值', '+\$500.00', Icons.add_circle_outline_rounded, AppColors.info, '昨天 09:20'), + ('转赠 Target 券', '-\$30.00', Icons.card_giftcard_rounded, AppColors.textPrimary, '02/07 16:45'), + ('核销 Nike 运动券', '使用', Icons.check_circle_outline_rounded, AppColors.success, '02/06 12:00'), + ('提现', '-\$200.00', Icons.account_balance_rounded, AppColors.textPrimary, '02/05 08:30'), + ]; + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: transactions.length, + separatorBuilder: (_, __) => const Divider(indent: 56), + itemBuilder: (context, index) { + final (title, amount, icon, color, time) = transactions[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, size: 20, color: color), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.labelMedium), + Text(time, style: AppTypography.caption), + ], + ), + ), + Text( + amount, + style: AppTypography.labelMedium.copyWith( + color: amount.startsWith('+') ? AppColors.success : AppColors.textPrimary, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/frontend/genex-mobile/lib/features/wallet/presentation/pages/withdraw_page.dart b/frontend/genex-mobile/lib/features/wallet/presentation/pages/withdraw_page.dart new file mode 100644 index 0000000..37b9885 --- /dev/null +++ b/frontend/genex-mobile/lib/features/wallet/presentation/pages/withdraw_page.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import '../../../../app/theme/app_colors.dart'; +import '../../../../app/theme/app_typography.dart'; +import '../../../../app/theme/app_spacing.dart'; + +/// 提现页面 +/// +/// 将平台余额提现到银行账户 +/// 展示可提现余额、手续费、到账时间 +class WithdrawPage extends StatefulWidget { + const WithdrawPage({super.key}); + + @override + State createState() => _WithdrawPageState(); +} + +class _WithdrawPageState extends State { + final _amountController = TextEditingController(); + double _balance = 128.50; + + double get _amount => double.tryParse(_amountController.text) ?? 0; + double get _fee => _amount * 0.005; // 0.5% + double get _receive => _amount - _fee; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('提现')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Balance + Text('可提现余额', style: AppTypography.bodySmall), + const SizedBox(height: 4), + Text('\$${_balance.toStringAsFixed(2)}', style: AppTypography.displayMedium), + const SizedBox(height: 24), + + // Amount Input + Text('提现金额', style: AppTypography.h3), + const SizedBox(height: 12), + TextField( + controller: _amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + prefixText: '\$ ', + suffixIcon: TextButton( + onPressed: () { + _amountController.text = _balance.toStringAsFixed(2); + setState(() {}); + }, + child: const Text('全部'), + ), + ), + style: AppTypography.priceLarge, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + + // Withdraw To + Text('提现到', style: AppTypography.h3), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.primary), + ), + child: Row( + children: [ + const Icon(Icons.account_balance_rounded, color: AppColors.primary), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Bank of America •••• 6789', style: AppTypography.labelMedium), + Text('储蓄账户', style: AppTypography.caption), + ], + ), + ), + const Icon(Icons.check_circle_rounded, color: AppColors.primary, size: 20), + ], + ), + ), + const SizedBox(height: 24), + + // Fee Details + if (_amount > 0) ...[ + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + children: [ + _buildRow('提现金额', '\$${_amount.toStringAsFixed(2)}'), + _buildRow('手续费 (0.5%)', '-\$${_fee.toStringAsFixed(2)}'), + const Divider(height: 16), + _buildRow('实际到账', '\$${_receive.toStringAsFixed(2)}', bold: true), + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.schedule_rounded, size: 14, color: AppColors.textTertiary), + const SizedBox(width: 4), + Text('预计 1-2 个工作日到账', style: AppTypography.caption), + ], + ), + ], + ), + ), + ], + ], + ), + ), + bottomNavigationBar: Container( + padding: const EdgeInsets.all(20), + child: SizedBox( + height: AppSpacing.buttonHeight, + child: ElevatedButton( + onPressed: _amount > 0 && _amount <= _balance ? () {} : null, + child: Text('确认提现 \$${_amount.toStringAsFixed(2)}'), + ), + ), + ), + ); + } + + Widget _buildRow(String label, String value, {bool bold = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)), + Text(value, style: bold ? AppTypography.priceSmall : AppTypography.labelMedium), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/main.dart b/frontend/genex-mobile/lib/main.dart new file mode 100644 index 0000000..4973021 --- /dev/null +++ b/frontend/genex-mobile/lib/main.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'app/theme/app_theme.dart'; +import 'app/main_shell.dart'; +import 'features/auth/presentation/pages/login_page.dart'; +import 'features/auth/presentation/pages/welcome_page.dart'; +import 'features/auth/presentation/pages/register_page.dart'; +import 'features/auth/presentation/pages/forgot_password_page.dart'; +import 'features/coupons/presentation/pages/coupon_detail_page.dart'; +import 'features/coupons/presentation/pages/order_confirm_page.dart'; +import 'features/coupons/presentation/pages/payment_page.dart'; +import 'features/coupons/presentation/pages/payment_success_page.dart'; +import 'features/coupons/presentation/pages/search_page.dart'; +import 'features/coupons/presentation/pages/my_coupon_detail_page.dart'; +import 'features/coupons/presentation/pages/redeem_qr_page.dart'; +import 'features/trading/presentation/pages/trading_page.dart'; +import 'features/trading/presentation/pages/transfer_page.dart'; +import 'features/trading/presentation/pages/sell_order_page.dart'; +import 'features/wallet/presentation/pages/wallet_page.dart'; +import 'features/wallet/presentation/pages/deposit_page.dart'; +import 'features/wallet/presentation/pages/withdraw_page.dart'; +import 'features/wallet/presentation/pages/transaction_records_page.dart'; +import 'features/profile/presentation/pages/kyc_page.dart'; +import 'features/profile/presentation/pages/settings_page.dart'; +import 'features/profile/presentation/pages/payment_management_page.dart'; +import 'features/profile/presentation/pages/pro_mode_page.dart'; +import 'features/ai_agent/presentation/pages/agent_chat_page.dart'; +import 'features/message/presentation/pages/message_detail_page.dart'; +import 'features/issuer/presentation/pages/issuer_main_page.dart'; +import 'features/merchant/presentation/pages/merchant_home_page.dart'; +import 'features/trading/presentation/pages/trading_detail_page.dart'; + +void main() { + runApp(const GenexConsumerApp()); +} + +/// Genex Mobile - 券的生命周期管理平台 +/// +/// 券钱包/交易所/消息/个人中心 +/// 持有/接收/转赠/交易/核销数字券 +class GenexConsumerApp extends StatelessWidget { + const GenexConsumerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Genex', + theme: AppTheme.light, + debugShowCheckedModeBanner: false, + initialRoute: '/', + onGenerateRoute: _generateRoute, + ); + } + + Route _generateRoute(RouteSettings settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute(builder: (_) => const WelcomePage()); + case '/login': + return MaterialPageRoute(builder: (_) => const LoginPage()); + case '/register': + return MaterialPageRoute(builder: (_) => const RegisterPage()); + case '/forgot-password': + return MaterialPageRoute(builder: (_) => const ForgotPasswordPage()); + case '/main': + return MaterialPageRoute(builder: (_) => const MainShell()); + case '/coupon/detail': + return MaterialPageRoute(builder: (_) => const CouponDetailPage()); + case '/order/confirm': + return MaterialPageRoute(builder: (_) => const OrderConfirmPage()); + case '/payment': + return MaterialPageRoute(builder: (_) => const PaymentPage()); + case '/payment/success': + return MaterialPageRoute(builder: (_) => const PaymentSuccessPage()); + case '/search': + return MaterialPageRoute(builder: (_) => const SearchPage()); + case '/coupon/mine/detail': + return MaterialPageRoute(builder: (_) => const MyCouponDetailPage()); + case '/redeem': + return MaterialPageRoute(builder: (_) => const RedeemQrPage()); + case '/trading': + return MaterialPageRoute(builder: (_) => const TradingPage()); + case '/transfer': + return MaterialPageRoute(builder: (_) => const TransferPage()); + case '/sell': + return MaterialPageRoute(builder: (_) => const SellOrderPage()); + case '/wallet': + return MaterialPageRoute(builder: (_) => const WalletPage()); + case '/wallet/deposit': + return MaterialPageRoute(builder: (_) => const DepositPage()); + case '/wallet/withdraw': + return MaterialPageRoute(builder: (_) => const WithdrawPage()); + case '/wallet/records': + return MaterialPageRoute(builder: (_) => const TransactionRecordsPage()); + case '/kyc': + return MaterialPageRoute(builder: (_) => const KycPage()); + case '/settings': + return MaterialPageRoute(builder: (_) => const SettingsPage()); + case '/payment/manage': + return MaterialPageRoute(builder: (_) => const PaymentManagementPage()); + case '/pro-mode': + return MaterialPageRoute(builder: (_) => const ProModePage()); + case '/ai-chat': + return MaterialPageRoute(builder: (_) => const AgentChatPage()); + case '/message/detail': + return MaterialPageRoute(builder: (_) => const MessageDetailPage()); + case '/issuer': + return MaterialPageRoute(builder: (_) => const IssuerMainPage()); + case '/merchant': + return MaterialPageRoute(builder: (_) => const MerchantHomePage()); + case '/trading/detail': + return MaterialPageRoute(builder: (_) => const TradingDetailPage()); + default: + return MaterialPageRoute( + builder: (_) => Scaffold( + body: Center(child: Text('Route not found: ${settings.name}')), + ), + ); + } + } +} diff --git a/frontend/genex-mobile/lib/shared/widgets/ai_confirm_dialog.dart b/frontend/genex-mobile/lib/shared/widgets/ai_confirm_dialog.dart new file mode 100644 index 0000000..8a6eb06 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/ai_confirm_dialog.dart @@ -0,0 +1,317 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; +import 'genex_button.dart'; + +/// AI操作确认弹窗组件 +/// +/// AI Agent 执行操作前的二次确认弹窗 +/// 场景:AI帮你出售、AI帮你购买、AI帮你转赠 等需要确认的代理操作 +/// +/// 展示内容: +/// - AI建议的操作描述 +/// - 操作详情(金额/数量/对象等) +/// - 风险提示 +/// - 确认/取消按钮 +class AiConfirmDialog extends StatelessWidget { + final String actionTitle; + final String actionDescription; + final List details; + final String? riskWarning; + final String confirmText; + final String? cancelText; + final VoidCallback onConfirm; + final VoidCallback? onCancel; + final AiConfirmLevel level; + + const AiConfirmDialog({ + super.key, + required this.actionTitle, + required this.actionDescription, + required this.details, + this.riskWarning, + this.confirmText = '确认执行', + this.cancelText = '取消', + required this.onConfirm, + this.onCancel, + this.level = AiConfirmLevel.normal, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusLg, + boxShadow: const [ + BoxShadow( + color: Color(0x1A000000), + blurRadius: 24, + offset: Offset(0, 8), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header with AI icon + _buildHeader(), + + // Body + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Action Description + Text( + actionDescription, + style: AppTypography.bodyMedium.copyWith( + color: AppColors.textSecondary, + height: 1.5, + ), + ), + const SizedBox(height: 16), + + // Detail Items + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + children: details.asMap().entries.map((entry) { + final isLast = entry.key == details.length - 1; + final detail = entry.value; + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + detail.label, + style: AppTypography.bodySmall.copyWith( + color: AppColors.textSecondary, + ), + ), + Text( + detail.value, + style: detail.isHighlight + ? AppTypography.labelMedium.copyWith( + color: AppColors.primary, + ) + : AppTypography.labelMedium, + ), + ], + ), + if (!isLast) ...[ + const SizedBox(height: 10), + Divider(color: AppColors.gray200, height: 1), + const SizedBox(height: 10), + ], + ], + ); + }).toList(), + ), + ), + + // Risk Warning + if (riskWarning != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: level == AiConfirmLevel.high + ? AppColors.errorLight + : AppColors.warningLight, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + level == AiConfirmLevel.high + ? Icons.warning_amber_rounded + : Icons.info_outline_rounded, + size: 16, + color: level == AiConfirmLevel.high + ? AppColors.error + : AppColors.warning, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + riskWarning!, + style: AppTypography.bodySmall.copyWith( + color: AppColors.gray700, + height: 1.4, + ), + ), + ), + ], + ), + ), + ], + + const SizedBox(height: 20), + + // Buttons + GenexButton( + label: confirmText, + onPressed: () { + Navigator.of(context).pop(true); + onConfirm(); + }, + ), + const SizedBox(height: 8), + GenexButton( + label: cancelText ?? '取消', + variant: GenexButtonVariant.text, + onPressed: () { + Navigator.of(context).pop(false); + onCancel?.call(); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + child: Row( + children: [ + // AI Avatar + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: const Center( + child: Text( + '✨', + style: TextStyle(fontSize: 20), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('AI助手请求确认', style: AppTypography.labelMedium), + const SizedBox(height: 2), + Text( + actionTitle, + style: AppTypography.h3.copyWith(color: AppColors.primary), + ), + ], + ), + ), + // Level indicator + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: _levelColor.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + _levelText, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: _levelColor, + ), + ), + ), + ], + ), + ); + } + + Color get _levelColor { + switch (level) { + case AiConfirmLevel.low: + return AppColors.success; + case AiConfirmLevel.normal: + return AppColors.warning; + case AiConfirmLevel.high: + return AppColors.error; + } + } + + String get _levelText { + switch (level) { + case AiConfirmLevel.low: + return '低风险'; + case AiConfirmLevel.normal: + return '需确认'; + case AiConfirmLevel.high: + return '高风险'; + } + } + + /// 显示AI确认弹窗的便捷方法 + static Future show( + BuildContext context, { + required String actionTitle, + required String actionDescription, + required List details, + String? riskWarning, + String confirmText = '确认执行', + String? cancelText, + AiConfirmLevel level = AiConfirmLevel.normal, + }) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AiConfirmDialog( + actionTitle: actionTitle, + actionDescription: actionDescription, + details: details, + riskWarning: riskWarning, + confirmText: confirmText, + cancelText: cancelText, + level: level, + onConfirm: () {}, + ), + ); + } +} + +/// AI确认弹窗的详情项 +class AiConfirmDetail { + final String label; + final String value; + final bool isHighlight; + + const AiConfirmDetail({ + required this.label, + required this.value, + this.isHighlight = false, + }); +} + +/// AI操作风险等级 +enum AiConfirmLevel { + /// 低风险:查看信息、获取建议等 + low, + + /// 需确认:购买、出售、转赠等涉及资产操作 + normal, + + /// 高风险:大额操作、提现到外部钱包等 + high, +} diff --git a/frontend/genex-mobile/lib/shared/widgets/confirm_sheet.dart b/frontend/genex-mobile/lib/shared/widgets/confirm_sheet.dart new file mode 100644 index 0000000..c028e87 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/confirm_sheet.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; +import 'genex_button.dart'; + +/// 底部确认Sheet组件 +/// +/// 支付确认、转赠确认等操作确认 +/// 使用场景:购买、转赠、出售、提现等需要确认的操作 +class ConfirmSheet extends StatelessWidget { + final String title; + final List items; + final String confirmText; + final String? cancelText; + final VoidCallback onConfirm; + final VoidCallback? onCancel; + final Widget? header; + final String? warning; + + const ConfirmSheet({ + super.key, + required this.title, + required this.items, + required this.confirmText, + this.cancelText, + required this.onConfirm, + this.onCancel, + this.header, + this.warning, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 24), + decoration: const BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Drag Handle + Center( + child: Container( + margin: const EdgeInsets.only(top: 8, bottom: 16), + width: 36, + height: 4, + decoration: BoxDecoration( + color: AppColors.gray300, + borderRadius: AppSpacing.borderRadiusFull, + ), + ), + ), + + // Title + Text(title, style: AppTypography.h2), + const SizedBox(height: 20), + + // Optional Header (e.g., coupon card preview) + if (header != null) ...[ + header!, + const SizedBox(height: 16), + ], + + // Detail Items + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.gray50, + borderRadius: AppSpacing.borderRadiusMd, + ), + child: Column( + children: items.asMap().entries.map((entry) { + final isLast = entry.key == items.length - 1; + final item = entry.value; + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(item.label, style: AppTypography.bodyMedium.copyWith( + color: AppColors.textSecondary, + )), + Text( + item.value, + style: item.isHighlight + ? AppTypography.labelMedium.copyWith(color: AppColors.primary) + : AppTypography.labelMedium, + ), + ], + ), + if (!isLast) ...[ + const SizedBox(height: 12), + const Divider(), + const SizedBox(height: 12), + ], + ], + ); + }).toList(), + ), + ), + + // Warning + if (warning != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.warningLight, + borderRadius: AppSpacing.borderRadiusSm, + ), + child: Row( + children: [ + const Icon(Icons.info_outline_rounded, size: 16, color: AppColors.warning), + const SizedBox(width: 8), + Expanded( + child: Text( + warning!, + style: AppTypography.bodySmall.copyWith(color: AppColors.gray700), + ), + ), + ], + ), + ), + ], + + const SizedBox(height: 24), + + // Buttons + GenexButton( + label: confirmText, + onPressed: onConfirm, + ), + if (cancelText != null) ...[ + const SizedBox(height: 8), + GenexButton( + label: cancelText!, + variant: GenexButtonVariant.text, + onPressed: onCancel ?? () => Navigator.of(context).pop(), + ), + ], + ], + ), + ); + } + + static Future show( + BuildContext context, { + required String title, + required List items, + required String confirmText, + String? cancelText, + Widget? header, + String? warning, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => ConfirmSheet( + title: title, + items: items, + confirmText: confirmText, + cancelText: cancelText, + header: header, + warning: warning, + onConfirm: () => Navigator.of(ctx).pop(true), + onCancel: () => Navigator.of(ctx).pop(false), + ), + ); + } +} + +class ConfirmSheetItem { + final String label; + final String value; + final bool isHighlight; + + const ConfirmSheetItem({ + required this.label, + required this.value, + this.isHighlight = false, + }); +} diff --git a/frontend/genex-mobile/lib/shared/widgets/coupon_card.dart b/frontend/genex-mobile/lib/shared/widgets/coupon_card.dart new file mode 100644 index 0000000..ebe5447 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/coupon_card.dart @@ -0,0 +1,344 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; + +/// 券卡片组件 - 全端通用核心组件 +/// +/// 展示:券封面图 + 品牌 + 面值 + 折扣率 + 到期时间 +/// 使用场景:首页推荐、市场列表、我的券列表、搜索结果 +class CouponCard extends StatelessWidget { + final String brandName; + final String couponName; + final double faceValue; + final double currentPrice; + final String? imageUrl; + final String? brandLogoUrl; + final DateTime? expiryDate; + final String? creditRating; + final CouponStatus status; + final CouponCardStyle style; + final VoidCallback? onTap; + + const CouponCard({ + super.key, + required this.brandName, + required this.couponName, + required this.faceValue, + required this.currentPrice, + this.imageUrl, + this.brandLogoUrl, + this.expiryDate, + this.creditRating, + this.status = CouponStatus.active, + this.style = CouponCardStyle.list, + this.onTap, + }); + + double get discountRate => currentPrice / faceValue; + String get discountText => '${(discountRate * 10).toStringAsFixed(1)}折'; + + @override + Widget build(BuildContext context) { + return style == CouponCardStyle.grid ? _buildGridCard() : _buildListCard(); + } + + Widget _buildListCard() { + return GestureDetector( + onTap: onTap, + child: Container( + height: AppSpacing.couponCardHeight, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + boxShadow: AppSpacing.shadowSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + // Left: Coupon Image with Ticket Notch + _buildCouponImage(width: 110, height: AppSpacing.couponCardHeight), + + // Ticket Divider (锯齿分割线) + _buildTicketDivider(), + + // Right: Info + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Brand + Name + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + brandName, + style: AppTypography.caption.copyWith( + color: AppColors.textTertiary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + couponName, + style: AppTypography.labelMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + + // Price + Discount + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '\$${currentPrice.toStringAsFixed(2)}', + style: AppTypography.priceSmall, + ), + const SizedBox(width: 6), + Text( + '\$${faceValue.toStringAsFixed(0)}', + style: AppTypography.priceOriginal, + ), + const Spacer(), + _buildDiscountBadge(), + ], + ), + + // Expiry + Status + Row( + children: [ + if (expiryDate != null) ...[ + Icon(Icons.access_time_rounded, size: 12, color: _expiryColor), + const SizedBox(width: 3), + Text( + _expiryText, + style: AppTypography.caption.copyWith(color: _expiryColor), + ), + ], + const Spacer(), + if (creditRating != null) _buildCreditBadge(), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildGridCard() { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + boxShadow: AppSpacing.shadowSm, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image + _buildCouponImage(width: double.infinity, height: 100), + + // Info + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + couponName, + style: AppTypography.labelSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + '\$${currentPrice.toStringAsFixed(2)}', + style: AppTypography.priceSmall.copyWith(fontSize: 15), + ), + const SizedBox(width: 4), + _buildDiscountBadge(), + ], + ), + const SizedBox(height: 4), + Text( + brandName, + style: AppTypography.caption, + maxLines: 1, + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildCouponImage({required double width, required double height}) { + return ClipRRect( + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(12), + bottomLeft: style == CouponCardStyle.list + ? const Radius.circular(12) + : Radius.zero, + topRight: style == CouponCardStyle.grid + ? const Radius.circular(12) + : Radius.zero, + ), + child: Container( + width: width, + height: height, + color: AppColors.primarySurface, + child: imageUrl != null + ? Image.network(imageUrl!, fit: BoxFit.cover) + : Center( + child: Icon( + Icons.confirmation_number_outlined, + size: 32, + color: AppColors.primary.withValues(alpha: 0.4), + ), + ), + ), + ); + } + + Widget _buildTicketDivider() { + return SizedBox( + width: 16, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildNotch(isTop: true), + CustomPaint( + size: const Size(1, 80), + painter: _DashedLinePainter(color: AppColors.border), + ), + _buildNotch(isTop: false), + ], + ), + ); + } + + Widget _buildNotch({required bool isTop}) { + return Container( + width: 16, + height: 8, + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.vertical( + top: isTop ? Radius.zero : const Radius.circular(8), + bottom: isTop ? const Radius.circular(8) : Radius.zero, + ), + ), + ); + } + + Widget _buildDiscountBadge() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text(discountText, style: AppTypography.discountBadge), + ); + } + + Widget _buildCreditBadge() { + final color = _creditColor(creditRating!); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all(color: color.withValues(alpha: 0.3), width: 0.5), + ), + child: Text( + creditRating!, + style: AppTypography.caption.copyWith( + color: color, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Color _creditColor(String rating) { + switch (rating) { + case 'AAA': + return AppColors.creditAAA; + case 'AA': + return AppColors.creditAA; + case 'A': + return AppColors.creditA; + case 'BBB': + return AppColors.creditBBB; + default: + return AppColors.creditBB; + } + } + + String get _expiryText { + if (expiryDate == null) return ''; + final days = expiryDate!.difference(DateTime.now()).inDays; + if (days < 0) return '已过期'; + if (days == 0) return '今天到期'; + if (days <= 3) return '$days天后到期'; + if (days <= 30) return '$days天'; + return '${expiryDate!.month}/${expiryDate!.day}到期'; + } + + Color get _expiryColor { + if (expiryDate == null) return AppColors.textTertiary; + final days = expiryDate!.difference(DateTime.now()).inDays; + if (days <= 3) return AppColors.error; + if (days <= 7) return AppColors.warning; + return AppColors.textTertiary; + } +} + +/// 虚线画笔 +class _DashedLinePainter extends CustomPainter { + final Color color; + _DashedLinePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 1; + const dashHeight = 4.0; + const gapHeight = 3.0; + double startY = 0; + while (startY < size.height) { + canvas.drawLine( + Offset(0, startY), + Offset(0, startY + dashHeight), + paint, + ); + startY += dashHeight + gapHeight; + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +enum CouponStatus { active, pending, expired, used } +enum CouponCardStyle { list, grid } diff --git a/frontend/genex-mobile/lib/shared/widgets/credit_badge.dart b/frontend/genex-mobile/lib/shared/widgets/credit_badge.dart new file mode 100644 index 0000000..a734b51 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/credit_badge.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; + +/// 信用等级徽章组件 +/// +/// AAA/AA/A/BBB/BB 颜色标识 +/// 使用场景:券详情、发行方信息、发行方列表 +class CreditBadge extends StatelessWidget { + final String rating; + final CreditBadgeSize size; + + const CreditBadge({ + super.key, + required this.rating, + this.size = CreditBadgeSize.medium, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: size == CreditBadgeSize.large ? 10 : 6, + vertical: size == CreditBadgeSize.large ? 4 : 2, + ), + decoration: BoxDecoration( + color: _color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all( + color: _color.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.verified_rounded, + size: size == CreditBadgeSize.large ? 14 : 10, + color: _color, + ), + SizedBox(width: size == CreditBadgeSize.large ? 4 : 2), + Text( + rating, + style: _textStyle, + ), + ], + ), + ); + } + + Color get _color { + switch (rating.toUpperCase()) { + case 'AAA': + return AppColors.creditAAA; + case 'AA': + return AppColors.creditAA; + case 'A': + return AppColors.creditA; + case 'BBB': + return AppColors.creditBBB; + case 'BB': + default: + return AppColors.creditBB; + } + } + + TextStyle get _textStyle { + final baseStyle = size == CreditBadgeSize.large + ? AppTypography.labelSmall + : AppTypography.caption; + return baseStyle.copyWith( + color: _color, + fontWeight: FontWeight.w700, + fontSize: size == CreditBadgeSize.large ? 13 : 10, + ); + } +} + +enum CreditBadgeSize { small, medium, large } diff --git a/frontend/genex-mobile/lib/shared/widgets/empty_state.dart b/frontend/genex-mobile/lib/shared/widgets/empty_state.dart new file mode 100644 index 0000000..c4c4425 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/empty_state.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; + +/// 空状态页组件 +/// +/// 各场景的空状态插画 + 引导操作 +/// 使用场景:列表为空、搜索无结果、网络错误 +class EmptyState extends StatelessWidget { + final IconData icon; + final String title; + final String? subtitle; + final String? actionText; + final VoidCallback? onAction; + + const EmptyState({ + super.key, + required this.icon, + required this.title, + this.subtitle, + this.actionText, + this.onAction, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 60), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: AppColors.primarySurface, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 36, color: AppColors.primary.withValues(alpha: 0.5)), + ), + const SizedBox(height: AppSpacing.xxl), + Text( + title, + style: AppTypography.h3.copyWith(color: AppColors.textSecondary), + textAlign: TextAlign.center, + ), + if (subtitle != null) ...[ + const SizedBox(height: AppSpacing.sm), + Text( + subtitle!, + style: AppTypography.bodySmall, + textAlign: TextAlign.center, + ), + ], + if (actionText != null && onAction != null) ...[ + const SizedBox(height: AppSpacing.xxl), + ElevatedButton( + onPressed: onAction, + child: Text(actionText!), + ), + ], + ], + ), + ), + ); + } + + // 快捷工厂方法 + factory EmptyState.noCoupons({VoidCallback? onBrowse}) => EmptyState( + icon: Icons.confirmation_number_outlined, + title: '还没有券', + subtitle: '去市场看看有什么好券吧', + actionText: '去逛逛', + onAction: onBrowse, + ); + + factory EmptyState.noOrders() => const EmptyState( + icon: Icons.receipt_long_outlined, + title: '暂无交易记录', + subtitle: '完成首笔交易后这里会显示记录', + ); + + factory EmptyState.noResults() => const EmptyState( + icon: Icons.search_off_rounded, + title: '没有找到结果', + subtitle: '换个关键词试试', + ); + + factory EmptyState.noMessages() => const EmptyState( + icon: Icons.notifications_none_rounded, + title: '暂无消息', + subtitle: '交易通知和系统公告会显示在这里', + ); + + factory EmptyState.networkError({VoidCallback? onRetry}) => EmptyState( + icon: Icons.wifi_off_rounded, + title: '网络连接失败', + subtitle: '请检查网络设置后重试', + actionText: '重试', + onAction: onRetry, + ); +} diff --git a/frontend/genex-mobile/lib/shared/widgets/genex_button.dart b/frontend/genex-mobile/lib/shared/widgets/genex_button.dart new file mode 100644 index 0000000..f155903 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/genex_button.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; + +/// Genex 按钮组件 +/// +/// 统一的按钮样式,支持 primary/secondary/outline/text 4种变体 +class GenexButton extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + final GenexButtonVariant variant; + final GenexButtonSize size; + final IconData? icon; + final bool isLoading; + final bool fullWidth; + + const GenexButton({ + super.key, + required this.label, + this.onPressed, + this.variant = GenexButtonVariant.primary, + this.size = GenexButtonSize.large, + this.icon, + this.isLoading = false, + this.fullWidth = true, + }); + + @override + Widget build(BuildContext context) { + final child = Row( + mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isLoading) ...[ + SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(_foregroundColor), + ), + ), + const SizedBox(width: 8), + ], + if (icon != null && !isLoading) ...[ + Icon(icon, size: 20), + const SizedBox(width: 6), + ], + Text(label), + ], + ); + + final effectiveOnPressed = isLoading ? null : onPressed; + + Widget button; + switch (variant) { + case GenexButtonVariant.primary: + button = ElevatedButton( + onPressed: effectiveOnPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.4), + disabledForegroundColor: Colors.white70, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + ), + textStyle: _textStyle, + ), + child: child, + ); + + case GenexButtonVariant.secondary: + button = ElevatedButton( + onPressed: effectiveOnPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primarySurface, + foregroundColor: AppColors.primary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + ), + textStyle: _textStyle, + ), + child: child, + ); + + case GenexButtonVariant.outline: + button = OutlinedButton( + onPressed: effectiveOnPressed, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primary, + side: const BorderSide(color: AppColors.primary, width: 1.5), + shape: RoundedRectangleBorder( + borderRadius: AppSpacing.borderRadiusMd, + ), + textStyle: _textStyle, + ), + child: child, + ); + + case GenexButtonVariant.text: + button = TextButton( + onPressed: effectiveOnPressed, + style: TextButton.styleFrom( + foregroundColor: AppColors.primary, + textStyle: _textStyle, + ), + child: child, + ); + } + + // Use LayoutBuilder to safely handle fullWidth in unbounded contexts (e.g. Row) + if (fullWidth) { + return LayoutBuilder( + builder: (context, constraints) => SizedBox( + width: constraints.hasBoundedWidth ? double.infinity : null, + height: _height, + child: button, + ), + ); + } + return SizedBox(height: _height, child: button); + } + + double get _height { + switch (size) { + case GenexButtonSize.large: + return AppSpacing.buttonHeight; + case GenexButtonSize.medium: + return AppSpacing.buttonHeightSm; + case GenexButtonSize.small: + return 32; + } + } + + TextStyle get _textStyle { + switch (size) { + case GenexButtonSize.large: + return AppTypography.labelLarge; + case GenexButtonSize.medium: + return AppTypography.labelMedium; + case GenexButtonSize.small: + return AppTypography.labelSmall; + } + } + + Color get _foregroundColor { + switch (variant) { + case GenexButtonVariant.primary: + return Colors.white; + default: + return AppColors.primary; + } + } +} + +enum GenexButtonVariant { primary, secondary, outline, text } +enum GenexButtonSize { large, medium, small } diff --git a/frontend/genex-mobile/lib/shared/widgets/kyc_badge.dart b/frontend/genex-mobile/lib/shared/widgets/kyc_badge.dart new file mode 100644 index 0000000..9121866 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/kyc_badge.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; + +/// KYC等级标识组件 +/// +/// L0/L1/L2/L3 徽章 +/// 使用场景:个人中心、用户详情 +class KycBadge extends StatelessWidget { + final int level; + final bool showLabel; + + const KycBadge({ + super.key, + required this.level, + this.showLabel = true, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: _color.withValues(alpha: 0.1), + borderRadius: AppSpacing.borderRadiusFull, + border: Border.all(color: _color.withValues(alpha: 0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.shield_rounded, size: 12, color: _color), + const SizedBox(width: 3), + Text( + showLabel ? 'L$level 认证' : 'L$level', + style: AppTypography.caption.copyWith( + color: _color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Color get _color { + switch (level) { + case 0: + return AppColors.gray400; + case 1: + return AppColors.info; + case 2: + return AppColors.primary; + case 3: + return AppColors.success; + default: + return AppColors.gray400; + } + } +} diff --git a/frontend/genex-mobile/lib/shared/widgets/price_tag.dart b/frontend/genex-mobile/lib/shared/widgets/price_tag.dart new file mode 100644 index 0000000..6f30a9d --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/price_tag.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; + +/// 价格标签组件 +/// +/// 当前价格(大字)+ 原价删除线 + 折扣标签 +/// 使用场景:券详情页、确认订单、我的券 +class PriceTag extends StatelessWidget { + final double currentPrice; + final double faceValue; + final PriceTagSize size; + final bool showDiscount; + + const PriceTag({ + super.key, + required this.currentPrice, + required this.faceValue, + this.size = PriceTagSize.medium, + this.showDiscount = true, + }); + + double get discountRate => currentPrice / faceValue; + String get discountText => '${(discountRate * 10).toStringAsFixed(1)}折'; + double get savedAmount => faceValue - currentPrice; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + // Dollar sign + Text( + '\$', + style: _priceStyle.copyWith(fontSize: _priceStyle.fontSize! * 0.6), + ), + // Current price + Text( + currentPrice.toStringAsFixed(2), + style: _priceStyle, + ), + const SizedBox(width: 8), + // Original price (strikethrough) + if (currentPrice < faceValue) + Text( + '\$${faceValue.toStringAsFixed(0)}', + style: _originalStyle, + ), + if (showDiscount && currentPrice < faceValue) ...[ + const SizedBox(width: 8), + _buildDiscountChip(), + ], + ], + ); + } + + Widget _buildDiscountChip() { + return Container( + padding: EdgeInsets.symmetric( + horizontal: size == PriceTagSize.large ? 8 : 6, + vertical: size == PriceTagSize.large ? 3 : 2, + ), + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + discountText, + style: AppTypography.discountBadge.copyWith( + fontSize: size == PriceTagSize.large ? 13 : 11, + ), + ), + ); + } + + TextStyle get _priceStyle { + switch (size) { + case PriceTagSize.large: + return AppTypography.priceLarge; + case PriceTagSize.medium: + return AppTypography.priceMedium; + case PriceTagSize.small: + return AppTypography.priceSmall; + } + } + + TextStyle get _originalStyle { + switch (size) { + case PriceTagSize.large: + return AppTypography.priceOriginal.copyWith(fontSize: 16); + case PriceTagSize.medium: + return AppTypography.priceOriginal; + case PriceTagSize.small: + return AppTypography.priceOriginal.copyWith(fontSize: 11); + } + } +} + +enum PriceTagSize { large, medium, small } diff --git a/frontend/genex-mobile/lib/shared/widgets/skeleton_loader.dart b/frontend/genex-mobile/lib/shared/widgets/skeleton_loader.dart new file mode 100644 index 0000000..48163e7 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/skeleton_loader.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_spacing.dart'; + +/// 骨架屏加载组件 +/// +/// 列表加载、详情加载的骨架占位 +class SkeletonLoader extends StatefulWidget { + final double width; + final double height; + final double borderRadius; + + const SkeletonLoader({ + super.key, + this.width = double.infinity, + required this.height, + this.borderRadius = 8, + }); + + @override + State createState() => _SkeletonLoaderState(); +} + +class _SkeletonLoaderState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(); + _animation = Tween(begin: -1.0, end: 2.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [ + (_animation.value - 0.3).clamp(0.0, 1.0), + _animation.value.clamp(0.0, 1.0), + (_animation.value + 0.3).clamp(0.0, 1.0), + ], + colors: const [ + AppColors.gray100, + AppColors.gray50, + AppColors.gray100, + ], + ), + ), + ); + }, + ); + } +} + +/// 券卡片骨架屏 +class CouponCardSkeleton extends StatelessWidget { + const CouponCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: AppSpacing.couponCardHeight, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + const SkeletonLoader(width: 110, height: double.infinity, borderRadius: 12), + const SizedBox(width: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SkeletonLoader(width: 60, height: 10, borderRadius: 4), + const SizedBox(height: 6), + SkeletonLoader(width: 140, height: 14, borderRadius: 4), + ], + ), + SkeletonLoader(width: 100, height: 16, borderRadius: 4), + SkeletonLoader(width: 80, height: 10, borderRadius: 4), + ], + ), + ), + ), + const SizedBox(width: 12), + ], + ), + ); + } +} diff --git a/frontend/genex-mobile/lib/shared/widgets/status_tag.dart b/frontend/genex-mobile/lib/shared/widgets/status_tag.dart new file mode 100644 index 0000000..e620e83 --- /dev/null +++ b/frontend/genex-mobile/lib/shared/widgets/status_tag.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import '../../app/theme/app_colors.dart'; +import '../../app/theme/app_typography.dart'; +import '../../app/theme/app_spacing.dart'; + +/// 状态标签组件 +/// +/// 处理中/已完成/已取消/退款中 颜色标签 +/// 使用场景:订单列表、我的券、交易记录 +class StatusTag extends StatelessWidget { + final String label; + final StatusType type; + + const StatusTag({ + super.key, + required this.label, + required this.type, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: _bgColor, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + label, + style: AppTypography.caption.copyWith( + color: _textColor, + fontWeight: FontWeight.w500, + fontSize: 11, + ), + ), + ); + } + + Color get _bgColor { + switch (type) { + case StatusType.success: + return AppColors.successLight; + case StatusType.pending: + return AppColors.warningLight; + case StatusType.error: + return AppColors.errorLight; + case StatusType.info: + return AppColors.infoLight; + case StatusType.neutral: + return AppColors.gray100; + } + } + + Color get _textColor { + switch (type) { + case StatusType.success: + return AppColors.success; + case StatusType.pending: + return AppColors.warning; + case StatusType.error: + return AppColors.error; + case StatusType.info: + return AppColors.info; + case StatusType.neutral: + return AppColors.textSecondary; + } + } +} + +enum StatusType { success, pending, error, info, neutral } + +/// 快捷构造 +class StatusTags { + StatusTags._(); + + static StatusTag active() => const StatusTag(label: '可使用', type: StatusType.success); + static StatusTag pending() => const StatusTag(label: '待核销', type: StatusType.pending); + static StatusTag expired() => const StatusTag(label: '已过期', type: StatusType.neutral); + static StatusTag used() => const StatusTag(label: '已使用', type: StatusType.neutral); + static StatusTag processing() => const StatusTag(label: '处理中', type: StatusType.info); + static StatusTag completed() => const StatusTag(label: '已完成', type: StatusType.success); + static StatusTag cancelled() => const StatusTag(label: '已取消', type: StatusType.neutral); + static StatusTag refunding() => const StatusTag(label: '退款中', type: StatusType.pending); + static StatusTag onSale() => const StatusTag(label: '出售中', type: StatusType.info); +} diff --git a/frontend/genex-mobile/pubspec.yaml b/frontend/genex-mobile/pubspec.yaml new file mode 100644 index 0000000..ea85c4b --- /dev/null +++ b/frontend/genex-mobile/pubspec.yaml @@ -0,0 +1,19 @@ +name: genex_consumer +description: Genex Consumer App - 券金融消费者端 +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + uses-material-design: true