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 0000000..db77bb4
Binary files /dev/null and b/frontend/genex-mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
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 0000000..17987b7
Binary files /dev/null and b/frontend/genex-mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/frontend/genex-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/genex-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/frontend/genex-mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
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 0000000..d5f1c8d
Binary files /dev/null and b/frontend/genex-mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
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 0000000..4d6372e
Binary files /dev/null and b/frontend/genex-mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
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 0000000..dc9ada4
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..7353c41
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
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 0000000..797d452
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
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 0000000..6ed2d93
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
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 0000000..4cd7b00
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
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 0000000..fe73094
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..321773c
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
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 0000000..502f463
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
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 0000000..0ec3034
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
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 0000000..e9f5fea
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..84ac32a
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..8953cba
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
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 0000000..0467bf1
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
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 0000000..9da19ea
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
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 0000000..9da19ea
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
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 0000000..9da19ea
Binary files /dev/null and b/frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
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