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>
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = "../.."
|
||||
}
|
||||
|
|
@ -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.**
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package cn.gogenex.genex_consumer
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -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 */;
|
||||
}
|
||||
7
frontend/genex-mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
frontend/genex-mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal 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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#import "GeneratedPluginRegistrant.h"
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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の提案',
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)],
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
@ -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.25(8.5折),信用AAA\n2. Amazon \$100 购物券 - 当前售价 \$85(8.5折),信用AA\n\n这两张券的折扣率在同类中最优,且发行方信用等级高。'));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _Msg {
|
||||
final bool isAi;
|
||||
final String text;
|
||||
_Msg(this.isAi, this.text);
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../../../app/theme/app_colors.dart';
|
||||
import '../../../../app/theme/app_typography.dart';
|
||||
import '../../../../app/theme/app_spacing.dart';
|
||||
import '../../../../shared/widgets/genex_button.dart';
|
||||
|
||||
/// A1. 欢迎页 - 品牌展示 + 注册/登录入口
|
||||
///
|
||||
/// 品牌Logo、Slogan、手机号注册、邮箱注册、社交登录入口(Google/Apple)
|
||||
class WelcomePage extends StatelessWidget {
|
||||
const WelcomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(flex: 2),
|
||||
|
||||
// Brand Logo
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: AppSpacing.borderRadiusXl,
|
||||
boxShadow: AppSpacing.shadowPrimary,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.diamond_rounded,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Brand Name
|
||||
Text(
|
||||
'Genex',
|
||||
style: AppTypography.displayLarge.copyWith(
|
||||
color: AppColors.primary,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Slogan
|
||||
Text(
|
||||
'让每一张券都有价值',
|
||||
style: AppTypography.bodyLarge.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(flex: 3),
|
||||
|
||||
// Phone Register
|
||||
GenexButton(
|
||||
label: '手机号注册',
|
||||
icon: Icons.phone_android_rounded,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/register');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Email Register
|
||||
GenexButton(
|
||||
label: '邮箱注册',
|
||||
icon: Icons.email_outlined,
|
||||
variant: GenexButtonVariant.outline,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/register');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Social Login Divider
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider(color: AppColors.border)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text('其他方式登录', style: AppTypography.caption),
|
||||
),
|
||||
const Expanded(child: Divider(color: AppColors.border)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Social Login Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_SocialLoginButton(
|
||||
icon: Icons.g_mobiledata_rounded,
|
||||
label: 'Google',
|
||||
onTap: () {
|
||||
Navigator.pushReplacementNamed(context, '/main');
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_SocialLoginButton(
|
||||
icon: Icons.apple_rounded,
|
||||
label: 'Apple',
|
||||
onTap: () {
|
||||
Navigator.pushReplacementNamed(context, '/main');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Already have account
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('已有账号?', style: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/login');
|
||||
},
|
||||
child: Text('登录', style: AppTypography.labelMedium.copyWith(
|
||||
color: AppColors.primary,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Terms
|
||||
Text(
|
||||
'注册即表示同意《用户协议》和《隐私政策》',
|
||||
style: AppTypography.caption.copyWith(fontSize: 10),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SocialLoginButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _SocialLoginButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.gray50,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Icon(icon, size: 28, color: AppColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(label, style: AppTypography.caption),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'];
|
||||
|
|
@ -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
|
||||
];
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../../../app/theme/app_colors.dart';
|
||||
import '../../../../app/theme/app_typography.dart';
|
||||
import '../../../../app/theme/app_spacing.dart';
|
||||
|
||||
/// A6. 支付方式选择页
|
||||
///
|
||||
/// 选择支付方式:信用卡/借记卡、Apple Pay、Google Pay
|
||||
/// 后端自动完成:法币→稳定币→链上原子交换(消费者无感知)
|
||||
class PaymentPage extends StatefulWidget {
|
||||
const PaymentPage({super.key});
|
||||
|
||||
@override
|
||||
State<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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
];
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.50(9折),此价格成交概率最高',
|
||||
style: AppTypography.caption.copyWith(color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Fee Breakdown
|
||||
Container(
|
||||
padding: AppSpacing.cardPadding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildRow('售价', '\$${_price.toStringAsFixed(2)}'),
|
||||
_buildRow('折扣率', '${_discount.toStringAsFixed(1)}%'),
|
||||
_buildRow('平台手续费 (1.5%)', '-\$${_fee.toStringAsFixed(2)}'),
|
||||
const Divider(height: 24),
|
||||
_buildRow('预计到账', '\$${_receive.toStringAsFixed(2)}', isBold: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Market Info
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.infoLight,
|
||||
borderRadius: AppSpacing.borderRadiusSm,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline_rounded, color: AppColors.info, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'当前市场均价 \$22.80 · 最近24小时成交 42 笔',
|
||||
style: AppTypography.caption.copyWith(color: AppColors.info),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SizedBox(
|
||||
height: AppSpacing.buttonHeight,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _confirmSell(context),
|
||||
child: const Text('确认挂单'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(String label, String value, {bool isBold = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary)),
|
||||
Text(value, style: isBold ? AppTypography.priceSmall : AppTypography.labelMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmSell(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('挂单成功'),
|
||||
content: const Text('您的券已挂到市场,当有买家下单时将自动成交。'),
|
||||
actions: [
|
||||
TextButton(onPressed: () { Navigator.pop(ctx); Navigator.pop(context); }, child: const Text('确定')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||