feat: Create genex-mobile app with coupon lifecycle management redesign

基于 frontend/mobile 创建全新的 genex-mobile 应用,重新设计为券的生命周期管理平台。

## 底部导航重构 (5 tabs → 4 tabs)
- 首页 / 交易 / 消息 / 我的
- 移除独立的"我的券"Tab,功能合并到首页券钱包中
- "市场"重命名为"交易",图标改为行情图标

## 首页改造
### 券钱包(替代原Banner轮播区域)
- 紫色渐变卡片展示"我的钱包",含券数量统计(可使用/待核销/已过期)
- 水平滚动的券迷你卡片列表,支持Tab筛选(全部/可使用/待核销/已过期)
- 快捷操作栏:接收 / 转赠 / 出售 / 核销
- 接收功能:点击弹出底部Sheet,展示接收ID和接收二维码
  - 对方可通过扫码或输入ID转赠券到钱包
  - 接收ID支持一键复制

### 分类网格重新设计(从商品品类改为省钱机制)
- 原8个商品分类(餐饮/购物/娱乐/出行/生活/品牌/折扣/全部)
- 改为6个省钱导向分类:限时抢购 / 新券首发 / 折扣排行 / 即将到期 / 比价 / 全部分类
- 3列×2行布局,每个入口强调"怎么省"而非"卖什么"

### 其他区域保持不变
- AI智能推荐卡片
- 精选好券列表
- AI FAB浮动按钮

## 交易页(币安交易所风格)
### 一级市场(打新申购 / Launchpad风格)
- 券发行卡片:品牌信息 / 发行价 / 面值 / 折扣 / 发行量
- 销售进度条和百分比
- 状态标签:即将开始(含倒计时)/ 申购中 / 已结束

### 二级市场(交易所行情列表)
- 交易对分类Tab:券/法币 | 券/数字货币 | 券/稳定币 | 收藏
- 行情列表:交易对名称 / 最新价格 / 24h涨跌幅(红绿色块)
- 成交量和USD等价显示
- 支持的交易对示例:SBUX/USD, NIKE/BTC, AMZN/USDT 等

### 交易对详情页(K线 + 盘口 + 下单)
- 价格头部:当前价 / 24h涨跌 / OHLC数据(高/低/开盘/成交量)
- K线图(含模拟蜡烛图渲染 + 成交量柱状图)
- 时间周期选择器:1m / 5m / 15m / 1h / 4h / 1D / 1W
- 交易深度:买卖盘口(Bid/Ask)带深度条可视化
- 下单表单:买入/卖出切换 / 限价单/市价单 / 价格数量输入 / 比例快选(25%/50%/75%/100%)
- 当前委托和历史委托列表
- 底部买入/卖出快捷按钮

## 券详情页增强
- 新增"附近可用门店"区域(LBS定位功能入口)
- 展示附近门店列表:门店名 / 距离 / 营业状态

## 技术细节
- 保持原有设计系统:紫色主色调 #6C5CE7 / Material 3 / 亮色模式
- Flutter analyze 零错误通过
- 所有新增页面使用 mock 数据,便于后续接入真实API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-11 17:57:16 -08:00
parent 0ca11bddb1
commit b63414542b
116 changed files with 14846 additions and 0 deletions

56
frontend/genex-mobile/.gitignore vendored Normal file
View File

@ -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

View File

@ -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'

View File

@ -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.

View File

@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

View File

@ -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

View File

@ -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 = "../.."
}

View File

@ -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.**

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,46 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="Genex"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package cn.gogenex.genex_consumer
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@ -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

View File

@ -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")

34
frontend/genex-mobile/ios/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -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)
}
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -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.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Genex Consumer</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>genex_consumer</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -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.
}
}

View File

@ -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<String, Map<String, String>> _localizedValues = {
'zh-CN': _zhCN,
'en-US': _enUS,
'ja-JP': _jaJP,
};
static const Map<String, String> _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<String, String> _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<String, String> _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の提案',
};
}

View File

@ -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<MainShell> createState() => _MainShellState();
}
class _MainShellState extends State<MainShell> {
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,
);
}
}

View File

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

View File

@ -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<BoxShadow> shadowSm = [
BoxShadow(
color: Color(0x0A000000),
blurRadius: 8,
offset: Offset(0, 2),
),
];
static const List<BoxShadow> shadowMd = [
BoxShadow(
color: Color(0x0F000000),
blurRadius: 16,
offset: Offset(0, 4),
),
];
static const List<BoxShadow> shadowLg = [
BoxShadow(
color: Color(0x14000000),
blurRadius: 24,
offset: Offset(0, 8),
),
];
static const List<BoxShadow> 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;
}

View File

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

View File

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

View File

@ -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<AgentChatPage> createState() => _AgentChatPageState();
}
class _AgentChatPageState extends State<AgentChatPage> {
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.258.5折信用AAA\n2. Amazon \$100 购物券 - 当前售价 \$858.5折信用AA\n\n这两张券的折扣率在同类中最优,且发行方信用等级高。'));
});
}
});
}
}
class _Msg {
final bool isAi;
final String text;
_Msg(this.isAi, this.text);
}

View File

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

View File

@ -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<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
}
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
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),
),
),
],
),
);
}
}

View File

@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> 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');
},
),
],
);
}
}

View File

@ -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<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
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,
),
),
],
);
}
}

View File

@ -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. - + /
///
/// LogoSloganGoogle/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),
],
),
);
}
}

View File

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

View File

@ -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<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
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'];

View File

@ -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<MarketPage> createState() => _MarketPageState();
}
class _MarketPageState extends State<MarketPage>
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<Color>(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
];

View File

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

View File

@ -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<MyCouponsPage> createState() => _MyCouponsPageState();
}
class _MyCouponsPageState extends State<MyCouponsPage>
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;
}
}

View File

@ -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<OrderConfirmPage> createState() => _OrderConfirmPageState();
}
class _OrderConfirmPageState extends State<OrderConfirmPage> {
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');
},
),
],
),
),
);
}
}

View File

@ -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 PayGoogle Pay
///
class PaymentPage extends StatefulWidget {
const PaymentPage({super.key});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
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);
}

View File

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

View File

@ -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<RedeemQrPage> createState() => _RedeemQrPageState();
}
class _RedeemQrPageState extends State<RedeemQrPage> {
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),
),
),
],
),
),
],
),
),
);
}
}

View File

@ -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<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
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');
},
),
);
},
);
}
}

View File

@ -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;
}
}

View File

@ -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<IssuerMainPage> createState() => _IssuerMainPageState();
}
class _IssuerMainPageState extends State<IssuerMainPage> {
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: () {},
),
);
}
}

View File

@ -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<MerchantAiAssistantPage> createState() =>
_MerchantAiAssistantPageState();
}
class _MerchantAiAssistantPageState extends State<MerchantAiAssistantPage>
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')),
],
),
);
}
}

View File

@ -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<Widget> _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),
],
),
),
);
}
}

View File

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

View File

@ -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<MessagePage> createState() => _MessagePageState();
}
class _MessagePageState extends State<MessagePage>
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,
),
];

View File

@ -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<String> 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),
],
),
)),
],
),
);
}
}

View File

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

View File

@ -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<ProModePage> createState() => _ProModePageState();
}
class _ProModePageState extends State<ProModePage> {
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),
),
],
),
);
}
}

View File

@ -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});
}

View File

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

View File

@ -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<SellOrderPage> createState() => _SellOrderPageState();
}
class _SellOrderPageState extends State<SellOrderPage> {
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.509折此价格成交概率最高',
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('确定')),
],
),
);
}
}

View File

@ -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<TradingDetailPage> createState() => _TradingDetailPageState();
}
class _TradingDetailPageState extends State<TradingDetailPage>
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;
}

View File

@ -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<TradingPage> createState() => _TradingPageState();
}
class _TradingPageState extends State<TradingPage>
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),
],
),
],
),
);
},
);
}
}

View File

@ -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<TransferPage> createState() => _TransferPageState();
}
class _TransferPageState extends State<TransferPage> {
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('确定'),
),
],
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More