1. 架构设计:

* 业务逻辑与UI解耦合
* 生产者消费者模式
* 仓库模式
* 观察者模式
2. 页面开发:
* 空载具入库
* 手动码盘入库
* 呼叫空托
This commit is contained in:
李宇奇 2025-04-14 23:28:20 +08:00
commit da66e8e96a
165 changed files with 7469 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# 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
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# 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

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,44 @@
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")
}
android {
namespace = "com.example.pda_template"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.pda_template"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

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

View File

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

View File

@ -0,0 +1,6 @@
package com.example.pda_template;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

21
android/build.gradle.kts Normal file
View File

@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

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

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=file\:///D:/gradleZip/gradle-8.10.2-all.zip

View File

@ -0,0 +1,25 @@
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.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

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

View File

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

View File

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

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.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 = com.example.pdaTemplete;
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 = com.example.pdaTemplete.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 = com.example.pdaTemplete.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 = com.example.pdaTemplete.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 = 12.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 = 12.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 = com.example.pdaTemplete;
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 = com.example.pdaTemplete;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,99 @@
<?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"
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"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

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

View File

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

49
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Pda Templete</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>pda_templete</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

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

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@ -0,0 +1,5 @@
wms:
require_stock_out: "/wms/task/requireStockOut"
require_stock_in: "/wms/task/requireStockIn"
test: "/test/test"
baseUrl: "http://10.0.2.2:12315"

View File

@ -0,0 +1,6 @@
# 项目UI主题配置
theme:
primaryColor: 0xff05dcef # 主题色
backgroundColor: 0xffffffff # 背景色
textColor: 0xff333333 # 文本颜色
accentColor: 0xff03a9f4 # 强调色

View File

@ -0,0 +1,8 @@
enum ConfigPathEnum {
uiConfig(code: 1, configPath: 'lib/app/config/uiConfig.yaml'),
endPoints(code: 2, configPath: 'lib/app/config/endpoints.yaml');
final int code;
final String configPath;
const ConfigPathEnum({required this.code, required this.configPath});
}

25
lib/app/enum/stand.dart Normal file
View File

@ -0,0 +1,25 @@
enum Stand {
stand1(point: "P1", message: "1号拣选站台"),
stand2(point: "P2", message: "2号拣选站台"),
stand3(point: "P3", message: "3号拣选站台"),
stand4(point: "R1", message: "1号入库站台");
final String point;
final String message;
const Stand({required this.point, required this.message});
static Stand? getStandByCode(String point) {
return Stand.values.firstWhere(
(stand) => stand.point == point,
orElse: () => throw Exception('未找到对应的站台'),
);
}
static Stand? getStandByMessage(String message) {
return Stand.values.firstWhere(
(stand) => stand.message == message,
orElse: () => throw Exception('未找到对应的站台'),
);
}
}

View File

@ -0,0 +1,11 @@
enum WmsOutType {
empty(code: 1, desc: "空箱出库"),
nomal(code: 2, desc: "普通出库"),
allOut(code: 4, desc: "完整出库"),
forIn(code: 5, desc: "入库使用"),
emerge(code: 9, desc: "紧急出库");
final int code;
final String desc;
const WmsOutType({required this.code, required this.desc});
}

View File

@ -0,0 +1,185 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import '../../config/api_config.dart';
import '../../models/requests/stock_in_request_dto.dart';
import '../../models/requests/stock_out_request_dto.dart';
import '../../models/responses/base_wms_api_response_dto.dart';
import '../wms_api_client.dart';
class WmsV1ApiClient implements WmsApiClient {
final Dio _dio;
final int _timeOut;
final ApiConfig _apiConfig;
WmsV1ApiClient({Dio? dio, int timeOut = 10000, required ApiConfig apiConfig})
: _dio = dio ?? _createDio(apiConfig.baseUrl, timeOut),
_timeOut = timeOut,
_apiConfig = apiConfig;
@override
Future<BaseWmsApiResponseDto> requireStockOut(StockOutRequestDto request) {
final path = _apiConfig.getPath('require_stock_out');
return _post(
path: path,
data: request.toJson(),
mapper: BaseWmsApiResponseDto.fromJson,
);
}
@override
Future<BaseWmsApiResponseDto> requireStockIn(StockInRequestDto request) {
final path = _apiConfig.getPath('require_stock_in');
return _post(
path: path,
data: request.toJson(),
mapper: BaseWmsApiResponseDto.fromJson,
);
}
static Dio _createDio(String baseUrl, int timeout) {
return Dio(
BaseOptions(
baseUrl: baseUrl,
connectTimeout: Duration(milliseconds: timeout),
receiveTimeout: Duration(milliseconds: timeout),
),
);
}
Future<T> _post<T>({
required String path,
required Object? data,
required T Function(Map<String, dynamic>) mapper,
}) async {
return _RequestHandler(_dio, _timeOut).execute(
request:
() => _dio.post<String>(
path,
data: data != null ? jsonEncode(data) : null,
options: _requestOptions,
),
mapper: mapper,
);
}
Future<T> _get<T>({
required String path,
required T Function(Map<String, dynamic>) mapper,
}) async {
return _RequestHandler(_dio, _timeOut).execute(
request: () => _dio.get<String>(path, options: _requestOptions),
mapper: mapper,
);
}
Options get _requestOptions => Options(
headers: {'Content-Type': 'application/json'},
responseType: ResponseType.json,
sendTimeout: Duration(milliseconds: _timeOut),
receiveTimeout: Duration(milliseconds: _timeOut),
);
@override
Future<BaseWmsApiResponseDto> testLink() {
final path = _apiConfig.getPath('test');
return _get(path: path, mapper: BaseWmsApiResponseDto.fromJson);
}
}
// ====== ======
class _RequestHandler {
final Dio _dio;
final int _timeOut;
_RequestHandler(this._dio, this._timeOut);
Future<T> execute<T>({
required Future<Response<String>> Function() request,
required T Function(Map<String, dynamic>) mapper,
}) async {
try {
final response = await request().timeout(
Duration(milliseconds: _timeOut),
onTimeout:
() =>
throw DioException(
requestOptions: RequestOptions(),
error: 'Request timed out after $_timeOut ms',
),
);
return _ResponseResolver.resolve(response: response, mapper: mapper);
} on DioException catch (e) {
throw _WmsApiError.fromDioException(e);
} catch (e) {
throw _WmsApiError.generic(e.toString());
}
}
}
// ====== ======
class _ResponseResolver {
static T resolve<T>({
required Response<String> response,
required T Function(Map<String, dynamic>) mapper,
}) {
_validateStatusCode(response.statusCode);
final json = _parseJson(response.data);
return _mapResponse(json, mapper);
}
static void _validateStatusCode(int? statusCode) {
if (statusCode != 200) {
throw _WmsApiError(
code: statusCode ?? 500,
message: 'Unexpected HTTP status: $statusCode',
);
}
}
static Map<String, dynamic> _parseJson(String? data) {
try {
return jsonDecode(data ?? '{}') as Map<String, dynamic>;
} catch (e) {
throw _WmsApiError(
code: 422,
message: 'Invalid JSON format: ${e.toString()}',
);
}
}
static T _mapResponse<T>(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) mapper,
) {
try {
return mapper(json);
} catch (e) {
throw _WmsApiError(
code: 422,
message: 'Response mapping failed: ${e.toString()}',
);
}
}
}
// ====== ======
class _WmsApiError implements Exception {
final int code;
final String message;
_WmsApiError({required this.code, required this.message});
factory _WmsApiError.fromDioException(DioException e) {
return _WmsApiError(
code: e.response?.statusCode ?? 500,
message: e.message ?? 'Unknown network error',
);
}
factory _WmsApiError.generic(String message) {
return _WmsApiError(code: 500, message: message);
}
@override
String toString() => '[$code] $message';
}

View File

@ -0,0 +1,11 @@
import '/core/api/models/requests/stock_out_request_dto.dart';
import '../models/requests/stock_in_request_dto.dart';
import '../models/responses/base_wms_api_response_dto.dart';
abstract class WmsApiClient {
Future<BaseWmsApiResponseDto> requireStockIn(StockInRequestDto request);
Future<BaseWmsApiResponseDto> requireStockOut(StockOutRequestDto request);
Future<BaseWmsApiResponseDto> testLink();
}

View File

@ -0,0 +1,43 @@
import 'package:yaml/yaml.dart';
class ApiConfig {
final Map<String, String> _paths;
final String baseUrl;
ApiConfig._(this._paths, this.baseUrl);
factory ApiConfig.fromYaml(String yamlContent) {
final yamlMap = loadYaml(yamlContent) as YamlMap;
final wmsConfig = yamlMap['wms'] as YamlMap;
final paths = _parsePaths(wmsConfig);
final baseUrl = wmsConfig['baseUrl'] as String? ?? 'http://10.0.0.2:12315';
return ApiConfig._(paths, baseUrl);
}
//
static Map<String, String> _parsePaths(YamlMap wmsConfig) {
final Map<String, String> paths = {};
wmsConfig.forEach((key, value) {
if (value is String && key.toString() != 'baseUrl') {
paths[key.toString()] = value;
}
});
return paths;
}
String getPath(String endpointKey, {Map<String, String>? params}) {
final pathTemplate = _paths[endpointKey];
if (pathTemplate == null) {
throw Exception('Endpoint $endpointKey not found in config');
}
return _replacePathParams(pathTemplate, params ?? {});
}
// {version}
String _replacePathParams(String template, Map<String, String> params) {
return params.entries.fold(
template,
(path, entry) => path.replaceAll('{${entry.key}}', entry.value),
);
}
}

View File

@ -0,0 +1,22 @@
class GoodsInfoForTaskDto {
final String goodsId;
final int opNum;
final DateTime produceTime;
final String remark;
GoodsInfoForTaskDto({
required this.goodsId,
required this.opNum,
required this.produceTime,
required this.remark,
});
Map<String, dynamic> toJson() {
return {
"goodsId": goodsId,
"opNum": opNum,
"produceTime": produceTime.toIso8601String(),
"remark": remark,
};
}
}

View File

@ -0,0 +1,13 @@
class BaseWmsRequestDto {
final String standId;
final String? username;
BaseWmsRequestDto({required this.standId, this.username = "PDA"});
Map<String, dynamic> toJson() {
return {
'standId': standId,
'username': username,
};
}
}

View File

@ -0,0 +1,28 @@
import 'base_wms_request_dto.dart';
import '../entries/goods_info_for_task_dto.dart';
class StockInRequestDto extends BaseWmsRequestDto {
final bool emptyTask;
final String vehicleId;
final String origin;
final List<GoodsInfoForTaskDto> goodsInfo;
StockInRequestDto({
required this.emptyTask,
required this.vehicleId,
required this.origin,
required super.standId,
required this.goodsInfo,
});
@override
Map<String, dynamic> toJson() {
return {
'standId': standId,
'emptyTask': emptyTask,
'vehicleId': vehicleId,
'origin': origin,
'goodsInfo': goodsInfo.map((e) => e.toJson()).toList(),
};
}
}

View File

@ -0,0 +1,33 @@
import 'package:pda_template/core/api/models/requests/base_wms_request_dto.dart';
class StockOutRequestDto extends BaseWmsRequestDto {
final int outType;
final String? goodsId;
final String vehicleId;
final int needNum;
final String destination;
final String? reason;
StockOutRequestDto({
required this.outType,
this.goodsId,
required this.vehicleId,
required this.needNum,
required this.destination,
this.reason,
required super.standId
});
@override
Map<String, dynamic> toJson() {
return {
"outType": outType,
"goodsId": goodsId ?? "0",
"vehicleId": vehicleId,
"needNum": needNum,
"destination": destination,
"reason": reason ?? "empty",
"standId": standId
};
}
}

View File

@ -0,0 +1,13 @@
class BaseWmsApiResponseDto {
final int code;
final String message;
BaseWmsApiResponseDto({required this.code, required this.message});
factory BaseWmsApiResponseDto.fromJson(Map<String, dynamic> json) {
return BaseWmsApiResponseDto(
code: int.parse(json['code'].toString()),
message: json['message'].toString(),
);
}
}

View File

@ -0,0 +1,81 @@
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '/features/page/business_logic/notifiers/page_repository_notifier.dart';
import '/features/page/data/repositories/page_repository_impl.dart';
import '/features/page/domain/repositories/page_repository.dart';
import '/features/stock/business_logic/notifiers/stock_in_manual_notifier.dart';
import '/features/stock/business_logic/notifiers/stock_out_empty_notifier.dart';
import '../../app/enum/config_path_enum.dart';
import '../api/clients/wms_api_client.dart';
import '../api/clients/impl/wms_v1_api_client.dart';
import '../api/config/api_config.dart';
import '../../features/stock/data/repositories/stock_repository_impl.dart';
import '../../features/stock/domain/repositories/stock_repository.dart';
import '../../features/stock/business_logic/notifiers/stock_in_empty_notifier.dart';
final _defaultApiConfig = ApiConfig.fromYaml('''
wms:
require_stock_out: "/wms/task/requireStockOut"
require_stock_in: "/wms/task/requireStockIn"
test: "/test/test"
baseUrl: "http://10.0.2.2:12315"
''');
final apiConfigProvider = FutureProvider<ApiConfig>((ref) async {
final configString = await rootBundle.loadString(
ConfigPathEnum.endPoints.configPath,
);
return ApiConfig.fromYaml(configString);
});
final wmsApiClientProvider = Provider<WmsApiClient>((ref) {
final configAsync = ref.watch(apiConfigProvider);
return configAsync.when(
data: (config) => WmsV1ApiClient(apiConfig: config),
loading: () => WmsV1ApiClient(apiConfig: _defaultApiConfig),
error: (err, stack) => WmsV1ApiClient(apiConfig: _defaultApiConfig),
);
});
final stockRepositoryProvider = Provider<StockRepository>((ref) {
final apiClient = ref.watch(wmsApiClientProvider);
return StockRepositoryImpl(wmsApiClient: apiClient);
});
final pageRepositoryProvider = Provider<PageRepository>((ref) {
final apiClient = ref.watch(wmsApiClientProvider);
return PageRepositoryImpl(wmsApiClient: apiClient);
});
final stockInEmptyNotifierProvider =
StateNotifierProvider.autoDispose<StockInEmptyNotifier, AsyncValue<void>>((
ref,
) {
final repository = ref.watch(stockRepositoryProvider);
return StockInEmptyNotifier(repository);
});
final stockInNotifierProvider =
StateNotifierProvider.autoDispose<StockInManualNotifier, AsyncValue<void>>((
ref,
) {
final repository = ref.watch(stockRepositoryProvider);
return StockInManualNotifier(repository);
});
final pageNotifierProvider =
StateNotifierProvider.autoDispose<PageRepositoryNotifier, AsyncValue<void>>(
(ref) {
final repository = ref.watch(pageRepositoryProvider);
return PageRepositoryNotifier(repository);
},
);
final stockOutEmptyNotifierProvider =
StateNotifierProvider.autoDispose<StockOutEmptyNotifier, AsyncValue<void>>((
ref,
) {
final repository = ref.watch(stockRepositoryProvider);
return StockOutEmptyNotifier(repository);
});

View File

@ -0,0 +1,220 @@
import 'package:bruno/bruno.dart';
import 'package:flutter/material.dart';
class DialogUtils {
///
static void showMessage(
BuildContext context,
String title,
String message, {
String btnLabel = '确定',
}) {
BrnDialogManager.showSingleButtonDialog(
context,
label: btnLabel,
title: title,
message: message,
onTap: () {
Navigator.of(context).pop();
},
);
}
/// message label msg
static void showMessageList(
BuildContext context,
String title,
List<dynamic> message, {
String btnLabel = '确定',
}) {
List<BrnInfoModal> msg = [];
for (var msgData in message) {
msg.add(
BrnInfoModal(
keyPart: " ${msgData["label"]}",
valuePart: msgData["msg"].toString(),
),
);
}
BrnDialogManager.showSingleButtonDialog(
context,
label: btnLabel,
title: title,
messageWidget: BrnPairInfoTable(
expandAtIndex: 4,
isFolded: false,
children: msg,
),
onTap: () {
Navigator.of(context).pop();
},
);
}
///
static void showSuccessMessage(
BuildContext context,
String title,
String message, {
String btnLabel = '确定',
}) {
BrnDialogManager.showSingleButtonDialog(
context,
showIcon: true,
iconWidget: Image.asset("lib/images/ico/round_success.png"),
label: btnLabel,
title: title,
message: message,
onTap: () {
Navigator.of(context).pop();
},
);
}
///
static void showWarningMessage(
BuildContext context,
String title,
String message, {
String btnLabel = '确定',
}) {
BrnDialogManager.showSingleButtonDialog(
context,
showIcon: true,
iconWidget: Image.asset("lib/images/ico/round_warning.png"),
label: btnLabel,
title: title,
message: message,
onTap: () {
Navigator.of(context).pop();
},
);
}
///
static void showErrorMessage(
BuildContext context,
String title,
String message, {
String btnLabel = '确定',
}) {
BrnDialogManager.showSingleButtonDialog(
context,
showIcon: true,
iconWidget: Image.asset("lib/images/ico/round_error.png"),
label: btnLabel,
title: title,
message: message,
onTap: () {
Navigator.of(context).pop();
},
);
}
///
static void showConfirmMessage(
BuildContext context,
String title,
String message, {
Function()? cancel,
Function()? confirm,
String cancelLabel = '取消',
String confirmBtn = '确定',
}) {
BrnDialogManager.showConfirmDialog(
context,
showIcon: true,
iconWidget: Image.asset("lib/images/ico/round_question.png"),
title: title,
confirm: confirmBtn,
cancel: cancelLabel,
message: message,
onConfirm: () {
Navigator.of(context).pop();
if (confirm != null) {
confirm();
}
},
onCancel: () {
Navigator.of(context).pop();
if (cancel != null) {
cancel();
}
},
);
}
///
static void showInputMessage(
BuildContext context,
String title, {
String? hintText,
String? message,
Function()? cancel,
Function(String)? confirm,
String cancelLabel = '取消',
String confirmBtn = '确定',
}) {
BrnMiddleInputDialog(
title: title,
message: message,
hintText: hintText,
cancelText: cancelLabel,
confirmText: confirmBtn,
autoFocus: true,
maxLength: 1000,
maxLines: 1,
dismissOnActionsTap: false,
barrierDismissible: true,
onConfirm: (value) {
Navigator.pop(context);
if (confirm != null) {
confirm(value);
}
},
onCancel: () {
Navigator.pop(context);
if (cancel != null) {
cancel();
}
},
).show(context);
}
///
static void showSingleSelectDialog(
BuildContext context,
String title,
List<String> conditions, {
Function(int, String?)? onSubmitClick,
String msg = "",
String submitText = "确定",
}) {
int selectedIndex = 0;
showDialog(
context: context,
builder:
(_) => StatefulBuilder(
builder: (context, state) {
return BrnSingleSelectDialog(
isClose: true,
title: title,
messageText: msg,
checkedItem: conditions[selectedIndex],
submitText: submitText,
isCustomFollowScroll: true,
conditions: conditions,
onSubmitClick: (data) {
if (onSubmitClick != null) {
onSubmitClick(selectedIndex, data);
}
},
onItemClick: (BuildContext context, int index) {
selectedIndex = index;
},
);
},
),
);
}
}

View File

@ -0,0 +1,20 @@
class StringUtils {
static bool isEmpty(String? value) {
if (value == null || value == "") {
return true;
}
return false;
}
static bool isNumber(String? value) {
if (isEmpty(value)) {
return false;
}
try {
double num = double.parse(value!);
return true;
} catch (e) {
return false;
}
}
}

View File

@ -0,0 +1,27 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pda_template/features/page/domain/repositories/page_repository.dart';
import '../../../../core/di/providers.dart';
class PageRepositoryNotifier extends StateNotifier<AsyncValue<void>> {
final PageRepository _repository;
PageRepositoryNotifier(this._repository) : super(const AsyncValue.data(null));
Future<void> submit() async {
state = const AsyncValue.loading();
try {
await _repository.testLink();
state = const AsyncValue.data(null);
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
final pageNotifierProvider =
StateNotifierProvider.autoDispose<PageRepositoryNotifier, AsyncValue<void>>((
ref,
) {
final repository = ref.watch(pageRepositoryProvider);
return PageRepositoryNotifier(repository);
});

View File

@ -0,0 +1,15 @@
import 'package:pda_template/core/api/clients/wms_api_client.dart';
import 'package:pda_template/features/page/domain/repositories/page_repository.dart';
import '../../../stock/domain/models/base_wms_api_response.dart';
class PageRepositoryImpl extends PageRepository {
final WmsApiClient wmsApiClient;
PageRepositoryImpl({required this.wmsApiClient});
@override
Future<BaseWmsApiResponse> testLink() {
return wmsApiClient.testLink().then((value) => BaseWmsApiResponse.fromDto(value));
}
}

View File

@ -0,0 +1,22 @@
import '../../../../core/api/models/responses/base_wms_api_response_dto.dart';
class BaseWmsApiResponse {
final int code;
final String message;
BaseWmsApiResponse({required this.code, required this.message});
factory BaseWmsApiResponse.fromJson(Map<String, dynamic> json) {
return BaseWmsApiResponse(
code: int.parse(json['code'].toString()),
message: json['message'].toString(),
);
}
factory BaseWmsApiResponse.fromDto(BaseWmsApiResponseDto dto) {
return BaseWmsApiResponse(
code: dto.code,
message: dto.message,
);
}
}

View File

@ -0,0 +1,5 @@
import '../../../stock/domain/models/base_wms_api_response.dart';
abstract class PageRepository {
Future<BaseWmsApiResponse> testLink();
}

View File

@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import 'package:bruno/bruno.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yaml/yaml.dart';
import '../../../app/enum/config_path_enum.dart';
import '../../../core/di/providers.dart';
import '../../../core/utils/extensions/dialogUtils.dart';
import '../../stock/presentation/screens/stock_in_empty_screen.dart';
import '../../stock/presentation/screens/stock_in_manual_screen.dart';
import '../../stock/presentation/screens/stock_out_empty_screen.dart';
class Home extends ConsumerStatefulWidget {
const Home({super.key});
@override
ConsumerState<Home> createState() => _HomePageState();
}
class _HomePageState extends ConsumerState<Home> {
List<BrnDoughnutDataItem> stockChartsData = [];
late Color primaryColor;
bool _isLoading = true;
AsyncValue<void>? _previousState;
bool _hasSubmitted = false;
@override
void initState() {
super.initState();
_loadConfig();
setState(() {
stockChartsData = [
BrnDoughnutDataItem(value: 40, title: "空闲", color: Colors.green),
BrnDoughnutDataItem(value: 60, title: "占用", color: Colors.orange),
];
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentState = ref.read(pageNotifierProvider);
if (_previousState != currentState && _hasSubmitted) {
_previousState = currentState;
Future.microtask(() => _handleState(currentState));
} else {
_previousState = currentState;
}
}
Future<void> _loadConfig() async {
final configString = await rootBundle.loadString(
ConfigPathEnum.uiConfig.configPath,
);
final config = loadYaml(configString);
setState(() {
primaryColor = Color(config['theme']['primaryColor']);
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
ref.listen(pageNotifierProvider, (previous, next) {
if (previous != next && _hasSubmitted) {
_previousState = next;
Future.microtask(() => _handleState(next));
}
});
return Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: Colors.white),
centerTitle: true,
backgroundColor: primaryColor,
title: const Text("WMS移动终端模板", style: TextStyle(color: Colors.white)),
),
drawer: Drawer(
backgroundColor: Colors.white,
child: ListView(
padding: const EdgeInsets.all(0),
children: [
UserAccountsDrawerHeader(
accountName: const Text("模板"),
accountEmail: const Text("欢迎使用WMS移动终端"),
decoration: BoxDecoration(color: primaryColor),
),
ListTile(
title: const Text("空载具入库"),
trailing: const Icon(Icons.grain),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const StockInEmpty()),
);
},
),
ListTile(
title: const Text("手动码盘入库"),
trailing: const Icon(Icons.add_box),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const StockInManual(),
),
);
},
),
// ListTile(
// title: const Text("EBS码盘入库"),
// trailing: const Icon(Icons.add_box),
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const StockInEBS(),
// ),
// );
// },
// ),
ListTile(
title: const Text("呼叫空托"),
trailing: const Icon(Icons.ac_unit),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const StockOutEmpty(),
),
);
},
),
// ListTile(
// title: const Text("出库拣货"),
// trailing: const Icon(Icons.back_hand),
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(builder: (context) => const Pick()),
// );
// },
// ),
// ListTile(title: const Text("库存盘点"), trailing: const Icon(Icons.checklist), onTap: () {
// Navigator.push(context, MaterialPageRoute(builder: (context) => const StockCheck()));
// }),
// ListTile(title: const Text("库存查询"), trailing: const Icon(Icons.list_alt), onTap: () {
// Navigator.push(context, MaterialPageRoute(builder: (context) => const StockSearch()));
// })
],
),
),
body: Padding(
padding: const EdgeInsets.only(top: 10, left: 20, right: 20),
child: ListView(
children: [
const Text("库存占用情况:"),
Row(
children: [
Column(
children: [
BrnDoughnutChart(
padding: const EdgeInsets.all(50),
width: 150,
height: 150,
data: stockChartsData,
showTitleWhenSelected: false,
),
],
),
Column(
children: [
DoughnutChartLegend(
data: stockChartsData,
legendStyle: BrnDoughnutChartLegendStyle.list,
),
],
),
],
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: test,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(primaryColor),
),
child: const Text(
"网络检测",
style: TextStyle(color: Colors.white),
),
),
),
),
],
),
),
);
}
void test() {
//
_hasSubmitted = true;
ref.read(pageNotifierProvider.notifier).submit();
}
void _handleState(AsyncValue<void> state) {
state.whenOrNull(
loading: () => BrnLoadingDialog.show(context, content: "正在请求"),
error: (error, _) {
BrnLoadingDialog.dismiss(context);
DialogUtils.showErrorMessage(context, "请求发生错误", error.toString());
},
data: (_) {
BrnLoadingDialog.dismiss(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("请求成功"),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.green,
margin: const EdgeInsets.only(bottom: 100, left: 20, right: 20),
),
);
},
);
}
}

View File

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart';
import '../../../app/enum/config_path_enum.dart';
import 'home.dart';
class Login extends ConsumerStatefulWidget {
const Login({super.key});
@override
ConsumerState<Login> createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState<Login> {
final dio = Dio();
final _userPwd = TextEditingController();
final _userId = TextEditingController();
late Color primaryColor;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadConfig();
}
Future<void> _loadConfig() async {
final configString = await rootBundle.loadString(
ConfigPathEnum.uiConfig.configPath,
);
final config = loadYaml(configString);
setState(() {
primaryColor = Color(config['theme']['primaryColor']);
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
return Scaffold(
appBar: AppBar(
title: const Text("请登录(模板)", style: TextStyle(color: Colors.white)),
centerTitle: true,
backgroundColor: primaryColor,
),
body: Center(
child: Column(
children: [
//const Text("测试程序,禁止用于正式环境", style: TextStyle(color: Colors.redAccent)),
// Padding(
// padding: const EdgeInsets.only(
// top: 120,
// left: 50,
// right: 50,
// bottom: 10
// ),
// child: TextField(
// controller: _userId,
// decoration: const InputDecoration(
// labelText: "用户名:",
// ),
// ),
// ),
// Padding(
// padding: const EdgeInsets.only(
// top: 0,
// left: 50,
// right: 50,
// bottom: 10
// ),
// child: TextField(
// controller: _userPwd,
// decoration: const InputDecoration(
// labelText: "密码:",
// ),
// obscureText: true,
// ),
// ),
Padding(
padding: const EdgeInsets.only(top: 15),
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: login,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(primaryColor),
),
child: const Text(
"进入系统",
style: TextStyle(color: Colors.white),
),
),
),
),
],
),
),
);
}
void login() {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const Home()),
(router) => false,
);
}
@override
void dispose() {
_userPwd.dispose();
_userId.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/providers.dart';
import '../../domain/models/stock_in_request.dart';
import '../../domain/repositories/stock_repository.dart';
class StockInEmptyNotifier extends StateNotifier<AsyncValue<void>> {
final StockRepository _repository;
StockInEmptyNotifier(this._repository) : super(const AsyncValue.data(null));
Future<void> submit({
required String vehicleId,
required String standId,
}) async {
state = const AsyncValue.loading();
try {
await _repository.requireStockIn(
StockInRequest(
emptyTask: true,
vehicleId: vehicleId,
standId: standId,
goodsInfo: [],
origin: standId,
),
);
state = const AsyncValue.data(null);
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
final stockEmptyInNotifierProvider =
StateNotifierProvider.autoDispose<StockInEmptyNotifier, AsyncValue<void>>((
ref,
) {
final repository = ref.watch(stockRepositoryProvider);
return StockInEmptyNotifier(repository);
});

View File

@ -0,0 +1,41 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/providers.dart';
import '../../domain/models/goodsInfo_for_task.dart';
import '/features/stock/domain/repositories/stock_repository.dart';
import '../../domain/models/stock_in_request.dart';
class StockInManualNotifier extends StateNotifier<AsyncValue<void>> {
final StockRepository _repository;
StockInManualNotifier(this._repository) : super(const AsyncValue.data(null));
Future<void> submit({
required String vehicleId,
required String standId,
required List<GoodsInfoForTask> goodsInfo,
}) async {
state = const AsyncValue.loading();
try {
await _repository.requireStockIn(
StockInRequest(
emptyTask: false,
vehicleId: vehicleId,
standId: standId,
goodsInfo: goodsInfo,
origin: standId,
),
);
state = const AsyncValue.data(null);
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
final stockInNotifierProvider =
StateNotifierProvider.autoDispose<StockInManualNotifier, AsyncValue<void>>((
ref,
) {
final repository = ref.watch(stockRepositoryProvider);
return StockInManualNotifier(repository);
});

View File

@ -0,0 +1,33 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pda_template/features/stock/domain/repositories/stock_repository.dart';
import '/core/di/providers.dart';
class StockOutEmptyNotifier extends StateNotifier<AsyncValue<void>> {
final StockRepository repository;
StockOutEmptyNotifier(this.repository) : super(const AsyncValue.data(null));
Future<void> submit({
required int needNum,
required String destination,
}) async {
state = const AsyncValue.loading();
try {
await repository.emptyStockOut(
needNum: needNum,
destination: destination,
);
state = const AsyncValue.data(null);
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
final stockEmptyOutNotifierProvider =
StateNotifierProvider.autoDispose<StockOutEmptyNotifier, AsyncValue<void>>((
ref,
) {
final repository = ref.watch(stockRepositoryProvider);
return StockOutEmptyNotifier(repository);
});

View File

@ -0,0 +1,56 @@
import '/app/enum/wms_out_type.dart';
import '/features/stock/domain/models/stock_out_request.dart';
import '../../../../core/api/clients/wms_api_client.dart';
import '../../domain/models/base_wms_api_response.dart';
import '../../domain/models/stock_in_request.dart';
import '../../domain/repositories/stock_repository.dart';
class StockRepositoryImpl implements StockRepository {
final WmsApiClient wmsApiClient;
StockRepositoryImpl({required this.wmsApiClient});
@override
Future<BaseWmsApiResponse> requireStockIn(StockInRequest request) {
return wmsApiClient
.requireStockIn(request.toDto())
.then((value) => BaseWmsApiResponse.fromDto(value));
}
@override
Future<BaseWmsApiResponse> emptyStockIn({
required String vehicleId,
required String standId,
required String origin,
}) {
final request = StockInRequest(
emptyTask: true,
vehicleId: vehicleId,
standId: standId,
origin: origin,
goodsInfo: [],
);
return wmsApiClient
.requireStockIn(request.toDto())
.then((value) => BaseWmsApiResponse.fromDto(value));
}
@override
Future<BaseWmsApiResponse> emptyStockOut({
required int needNum,
required String destination,
}) {
final request = StockOutRequest(
outType: WmsOutType.empty.code,
goodsId: "0",
vehicleId: "",
needNum: needNum,
destination: destination,
reason: "empty",
standId: destination,
);
return wmsApiClient
.requireStockOut(request.toDto())
.then((value) => BaseWmsApiResponse.fromDto(value));
}
}

View File

@ -0,0 +1,22 @@
import '../../../../core/api/models/responses/base_wms_api_response_dto.dart';
class BaseWmsApiResponse {
final int code;
final String message;
BaseWmsApiResponse({required this.code, required this.message});
factory BaseWmsApiResponse.fromJson(Map<String, dynamic> json) {
return BaseWmsApiResponse(
code: int.parse(json['code'].toString()),
message: json['message'].toString(),
);
}
factory BaseWmsApiResponse.fromDto(BaseWmsApiResponseDto dto) {
return BaseWmsApiResponse(
code: dto.code,
message: dto.message,
);
}
}

View File

@ -0,0 +1,13 @@
class BaseWmsRequest {
final String standId;
final String? username;
BaseWmsRequest({required this.standId, this.username = "PDA"});
Map<String, dynamic> toJson() {
return {
'standId': standId,
'username': username,
};
}
}

View File

@ -0,0 +1,33 @@
import '../../../../core/api/models/entries/goods_info_for_task_dto.dart';
class GoodsInfoForTask {
final String goodsId;
int opNum;
final DateTime produceTime;
final String remark;
GoodsInfoForTask({
required this.goodsId,
required this.opNum,
required this.produceTime,
required this.remark,
});
Map<String, dynamic> toJson() {
return {
"goodsId": goodsId,
"opNum": opNum,
"produceTime": produceTime,
"remark": remark,
};
}
GoodsInfoForTaskDto toDto() {
return GoodsInfoForTaskDto(
goodsId: goodsId,
opNum: opNum,
produceTime: produceTime,
remark: remark,
);
}
}

View File

@ -0,0 +1,39 @@
import '/core/api/models/requests/stock_in_request_dto.dart';
import 'base_wms_request.dart';
import 'goodsInfo_for_task.dart';
class StockInRequest extends BaseWmsRequest {
final bool emptyTask;
final String vehicleId;
final String origin;
final List<GoodsInfoForTask> goodsInfo;
StockInRequest({
required this.emptyTask,
required this.vehicleId,
required this.origin,
required super.standId,
required this.goodsInfo,
});
@override
Map<String, dynamic> toJson() {
return {
'standId': standId,
'emptyTask': emptyTask,
'vehicleId': vehicleId,
'origin': origin,
'goodsInfo': goodsInfo.map((e) => e.toJson()).toList(),
};
}
StockInRequestDto toDto() {
return StockInRequestDto(
emptyTask: emptyTask,
vehicleId: vehicleId,
origin: origin,
standId: standId,
goodsInfo: goodsInfo.map((e) => e.toDto()).toList(),
);
}
}

View File

@ -0,0 +1,46 @@
import '/core/api/models/requests/stock_out_request_dto.dart';
import '/features/stock/domain/models/base_wms_request.dart';
class StockOutRequest extends BaseWmsRequest {
final int outType;
final String? goodsId;
final String vehicleId;
final int needNum;
final String destination;
final String? reason;
StockOutRequest({
required this.outType,
this.goodsId,
required this.vehicleId,
required this.needNum,
required this.destination,
this.reason,
required super.standId,
});
@override
Map<String, dynamic> toJson() {
return {
"outType": outType,
"goodsId": goodsId ?? "0",
"vehicleId": vehicleId,
"needNum": needNum,
"destination": destination,
"reason": reason ?? "empty",
"standId": standId,
};
}
StockOutRequestDto toDto() {
return StockOutRequestDto(
outType: outType,
goodsId: goodsId,
vehicleId: vehicleId,
needNum: needNum,
destination: destination,
reason: reason,
standId: standId,
);
}
}

View File

@ -0,0 +1,10 @@
import '../models/stock_in_request.dart';
import '../models/base_wms_api_response.dart';
abstract class StockRepository {
Future<BaseWmsApiResponse> emptyStockIn({required String vehicleId, required String standId, required String origin});
Future<BaseWmsApiResponse> requireStockIn(StockInRequest request);
Future<BaseWmsApiResponse> emptyStockOut({required int needNum, required String destination});
}

View File

@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:bruno/bruno.dart';
import 'package:pda_template/core/di/providers.dart' as app_providers;
import 'package:flutter/services.dart' show rootBundle;
import 'package:yaml/yaml.dart';
import '../../../../app/enum/config_path_enum.dart';
import '../../../../app/enum/stand.dart';
import '../../../../core/utils/extensions/dialogUtils.dart';
class StockInEmpty extends ConsumerStatefulWidget {
const StockInEmpty({super.key});
@override
ConsumerState<StockInEmpty> createState() => _StockInEmptyPageState();
}
class _StockInEmptyPageState extends ConsumerState<StockInEmpty> {
final _vehicleTextController = TextEditingController();
Stand? _selectedStand;
late Color primaryColor;
bool _isLoading = true;
AsyncValue<void>? _previousState;
bool _hasSubmitted = false;
@override
void initState() {
super.initState();
_loadConfig();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//
final currentState = ref.read(app_providers.stockInEmptyNotifierProvider);
//
if (_previousState != currentState && _hasSubmitted) {
_previousState = currentState;
// 使Future.microtask确保在build完成后处理状态
Future.microtask(() => _handleState(currentState));
} else {
//
_previousState = currentState;
}
}
Future<void> _loadConfig() async {
final configString = await rootBundle.loadString(
ConfigPathEnum.uiConfig.configPath,
);
final config = loadYaml(configString);
setState(() {
primaryColor = Color(config['theme']['primaryColor']);
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
// build中处理
ref.listen(app_providers.stockInEmptyNotifierProvider, (previous, next) {
if (previous != next && _hasSubmitted) {
_previousState = next;
// 使Future.microtask确保在build完成后处理状态
Future.microtask(() => _handleState(next));
}
});
return Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: Colors.white),
leading: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
),
centerTitle: true,
backgroundColor: primaryColor,
title: const Text("空载具入库", style: TextStyle(color: Colors.white)),
),
body: Center(
child: Padding(
padding: const EdgeInsets.only(top: 5, left: 10, right: 10),
child: ListView(
children: [
BrnTextInputFormItem(
controller: _vehicleTextController,
title: "载具号:",
hint: "请扫描或输入",
isRequire: true,
themeData: BrnFormItemConfig(
titleTextStyle: BrnTextStyle(fontSize: 16),
contentTextStyle: BrnTextStyle(fontSize: 16),
hintTextStyle: BrnTextStyle(fontSize: 16),
errorTextStyle: BrnTextStyle(fontSize: 16),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"*",
style: TextStyle(color: Colors.red, fontSize: 16),
),
SizedBox(width: 2),
Text(
"站台号:",
style: TextStyle(fontSize: 16, color: Color(0xFF222222)),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: DropdownButton<Stand>(
isDense: true,
icon: Icon(Icons.arrow_drop_down, size: 24),
iconSize: 24,
underline: Container(),
hint: Text("请选择站台", style: TextStyle(fontSize: 16)),
value: _selectedStand,
style: TextStyle(fontSize: 16, color: Colors.black),
items:
Stand.values.map((Stand stand) {
return DropdownMenuItem<Stand>(
value: stand,
child: Text(
stand.message,
style: TextStyle(fontSize: 16),
),
);
}).toList(),
onChanged: (Stand? value) {
setState(() {
_selectedStand = value;
});
},
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 5),
child: ElevatedButton(
onPressed: _submit,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(primaryColor),
),
child: const Text(
"空载具入库",
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
),
);
}
void _submit() {
final vehicleId = _vehicleTextController.text.trim();
final standId = _selectedStand?.point;
if (vehicleId == "") {
DialogUtils.showWarningMessage(context, "请先填写载具号", "", btnLabel: "返回填写");
return;
}
if (_selectedStand == null) {
DialogUtils.showWarningMessage(context, "请先选择站台", "", btnLabel: "返回选择");
return;
}
//
_hasSubmitted = true;
ref
.read(app_providers.stockInEmptyNotifierProvider.notifier)
.submit(vehicleId: vehicleId, standId: standId!);
}
void _handleState(AsyncValue<void> state) {
state.whenOrNull(
loading: () => BrnLoadingDialog.show(context, content: "正在请求"),
error: (error, _) {
BrnLoadingDialog.dismiss(context);
DialogUtils.showErrorMessage(context, "请求发生错误", error.toString());
},
data: (_) {
BrnLoadingDialog.dismiss(context);
DialogUtils.showSuccessMessage(context, "成功", "入库成功", btnLabel: "我知道了");
_vehicleTextController.clear();
setState(() => _selectedStand = null);
},
);
}
@override
void dispose() {
_vehicleTextController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,460 @@
import 'package:bruno/bruno.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:yaml/yaml.dart';
import '../../../../app/enum/config_path_enum.dart';
import '../../../../app/enum/stand.dart';
import '../../../../core/di/providers.dart' as app_providers;
import '../../../../core/utils/extensions/dialogUtils.dart';
import '../../../../core/utils/extensions/stringUtils.dart';
import '../../domain/models/goodsInfo_for_task.dart';
class StockInManual extends ConsumerStatefulWidget {
const StockInManual({super.key});
@override
ConsumerState<StockInManual> createState() => _StockInManualPageState();
}
class _StockInManualPageState extends ConsumerState<StockInManual> {
final _goodsCodeController = TextEditingController();
final _vehicleIdController = TextEditingController();
List<GoodsInfoForTask> goodsInfoList = [];
Stand? _selectedStand;
late Color primaryColor;
bool _isLoading = true;
AsyncValue<void>? _previousState;
bool _hasSubmitted = false;
@override
void initState() {
super.initState();
_loadConfig();
_goodsCodeController.addListener(() {
if (_goodsCodeController.text.isNotEmpty) {
resolveCode();
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentState = ref.read(app_providers.stockInNotifierProvider);
if (_previousState != currentState && _hasSubmitted) {
_previousState = currentState;
Future.microtask(() => _handleState(currentState));
} else {
_previousState = currentState;
}
}
void resolveCode() {
String code = _goodsCodeController.text;
if (StringUtils.isEmpty(code)) {
DialogUtils.showWarningMessage(context, "警告", "条码文本框内无数据,请先扫描或者输入数据");
return;
}
List<String> codeData = code.split(",");
if (codeData.length != 6 && codeData.length != 8) {
DialogUtils.showWarningMessage(context, "警告", "条码格式错误");
return;
}
DialogUtils.showSuccessMessage(context, "物料明细", code, btnLabel: "扫码成功");
setState(() {
goodsInfoList.add(
GoodsInfoForTask(
goodsId: codeData[1],
opNum: int.parse(codeData[3]),
produceTime: DateTime.parse(codeData[4]),
remark: "",
),
);
_goodsCodeController.clear();
});
return;
}
Future<void> _loadConfig() async {
final configString = await rootBundle.loadString(
ConfigPathEnum.uiConfig.configPath,
);
final config = loadYaml(configString);
setState(() {
primaryColor = Color(config['theme']['primaryColor']);
_isLoading = false;
});
}
void _handleState(AsyncValue<void> state) {
state.whenOrNull(
loading: () => BrnLoadingDialog.show(context, content: "正在请求"),
error: (error, _) {
BrnLoadingDialog.dismiss(context);
DialogUtils.showErrorMessage(context, "请求发生错误", error.toString());
_hasSubmitted = false;
},
data: (_) {
BrnLoadingDialog.dismiss(context);
DialogUtils.showSuccessMessage(context, "成功", "入库成功", btnLabel: "我知道了");
_vehicleIdController.clear();
_goodsCodeController.clear();
goodsInfoList = [];
setState(() {
_selectedStand = null;
_hasSubmitted = false;
});
},
);
}
void clickLine(index, dynamic, cell) {
if (cell.colKey == "action") {
delete(index);
return;
}
if (cell.colKey == "opNum") {
modifyNumber(index);
return;
}
showDetails(index, goodsInfoList[index]);
}
void delete(int index) {
setState(() {
if (index >= 0 && index < goodsInfoList.length) {
goodsInfoList.removeAt(index);
}
});
}
void modifyNumber(int index) {
if (index < 0 || index >= goodsInfoList.length) return;
DialogUtils.showInputMessage(
context,
"请输入要修改的数量",
message: "仅支持数字",
confirm: (value) {
if (!StringUtils.isNumber(value)) {
DialogUtils.showWarningMessage(context, "警告", "该文本框仅支持数字");
return;
}
setState(() {
goodsInfoList[index].opNum = int.parse(value);
});
},
);
}
///
void showDetails(int index, GoodsInfoForTask item) {
List<dynamic> message = [];
message.add({"label": "物料号:", "msg": item.goodsId});
message.add({"label": "数量:", "msg": item.opNum.toString()});
message.add({"label": "生产日期:", "msg": item.produceTime.toIso8601String()});
message.add({"label": "备注:", "msg": item.remark});
DialogUtils.showMessageList(context, "数据详情", message, btnLabel: "我知道了");
}
void _submit() {
if (goodsInfoList.isEmpty) {
DialogUtils.showWarningMessage(context, "警告", "您的码盘数据为空", btnLabel: "确定");
return;
}
if (StringUtils.isEmpty(_vehicleIdController.text)) {
DialogUtils.showWarningMessage(
context,
"警告",
"请先扫描载具号",
btnLabel: "返回填写",
);
return;
}
if (_selectedStand == null) {
DialogUtils.showWarningMessage(context, "警告", "请先选择站台", btnLabel: "返回选择");
return;
}
int dataCount = goodsInfoList.length;
DialogUtils.showConfirmMessage(
context,
"码盘完成",
"载具:${_vehicleIdController.text} 码盘 $dataCount 条数据,是否继续?",
confirmBtn: "继续",
confirm: () {
_hasSubmitted = true;
ref
.read(app_providers.stockInNotifierProvider.notifier)
.submit(
vehicleId: _vehicleIdController.text,
standId: _selectedStand!.point,
goodsInfo: goodsInfoList,
);
},
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
ref.listen(app_providers.stockInNotifierProvider, (previous, next) {
if (_hasSubmitted) {
_previousState = next;
Future.microtask(() => _handleState(next));
}
});
return Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: Colors.white),
leading: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
),
centerTitle: true,
backgroundColor: primaryColor,
title: const Text("手动码盘入库", style: TextStyle(color: Colors.white)),
),
body: Center(
child: Padding(
padding: const EdgeInsets.only(top: 5, left: 10, right: 10),
child: ListView(
children: [
BrnTextInputFormItem(
controller: _vehicleIdController,
title: "载具号:",
hint: "请扫描或输入",
isRequire: true,
themeData: BrnFormItemConfig(
titleTextStyle: BrnTextStyle(fontSize: 16),
contentTextStyle: BrnTextStyle(fontSize: 16),
hintTextStyle: BrnTextStyle(fontSize: 16),
errorTextStyle: BrnTextStyle(fontSize: 16),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"*",
style: TextStyle(color: Colors.red, fontSize: 16),
),
SizedBox(width: 2),
Text(
"站台号:",
style: TextStyle(fontSize: 16, color: Color(0xFF222222)),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: DropdownButton<Stand>(
isDense: true,
icon: Icon(Icons.arrow_drop_down, size: 24),
iconSize: 24,
underline: Container(),
hint: Text("请选择站台", style: TextStyle(fontSize: 16)),
value: _selectedStand,
style: TextStyle(fontSize: 16, color: Colors.black),
items:
Stand.values.map((Stand stand) {
return DropdownMenuItem<Stand>(
value: stand,
child: Text(
stand.message,
style: TextStyle(fontSize: 16),
),
);
}).toList(),
onChanged: (Stand? value) {
setState(() {
_selectedStand = value;
});
},
),
),
),
],
),
),
BrnTextInputFormItem(
controller: _goodsCodeController,
title: "条码:",
hint: "请扫描物料二维码",
isRequire: true,
themeData: BrnFormItemConfig(
titleTextStyle: BrnTextStyle(fontSize: 16),
contentTextStyle: BrnTextStyle(fontSize: 16),
hintTextStyle: BrnTextStyle(fontSize: 16),
errorTextStyle: BrnTextStyle(fontSize: 16),
),
),
Padding(
padding: const EdgeInsets.only(top: 5),
child: ElevatedButton(
onPressed: resolveCode,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(primaryColor),
),
child: const Text(
"添加物料",
style: TextStyle(color: Colors.white),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 0),
child: ElevatedButton(
onPressed: _submit,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(primaryColor),
),
child: const Text(
"码盘完成",
style: TextStyle(color: Colors.white),
),
),
),
const Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: Text("已码物料:"),
),
Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color(0x4D0C0C05),
width: 0.3,
), // border
borderRadius: BorderRadius.circular((5)), //
),
child: ListView.builder(
shrinkWrap: true, // 使ListView适应内容高度
physics:
const NeverScrollableScrollPhysics(), // ListView的滚动
itemCount: goodsInfoList.length,
itemBuilder: (context, index) {
var item = goodsInfoList[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
elevation: 2,
child: Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
colorScheme: Theme.of(
context,
).colorScheme.copyWith(primary: primaryColor),
),
child: ExpansionTile(
title: Text(
'物料号: ${item.goodsId}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
subtitle: Text(
'生产日期: ${item.produceTime.toIso8601String()}',
style: const TextStyle(fontSize: 13),
),
leading: IconButton(
icon: const Icon(
TDIcons.delete,
color: Colors.redAccent,
),
onPressed: () => delete(index),
),
collapsedBackgroundColor: Colors.white,
backgroundColor: Colors.white,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => modifyNumber(index),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'数量: ${item.opNum}',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
Icon(
Icons.edit,
size: 16,
color: primaryColor,
),
],
),
),
),
const SizedBox(height: 4),
Text('备注: ${item.remark}'),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => showDetails(index, item),
style: ButtonStyle(
backgroundColor:
WidgetStateProperty.all(
primaryColor,
),
),
child: const Text(
"查看详情",
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
],
),
),
);
},
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,203 @@
import 'package:bruno/bruno.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:yaml/yaml.dart';
import '/app/enum/config_path_enum.dart';
import '/app/enum/stand.dart';
import '/core/di/providers.dart' as app_providers;
import '/core/utils/extensions/dialogUtils.dart';
class StockOutEmpty extends ConsumerStatefulWidget {
const StockOutEmpty({super.key});
@override
ConsumerState<StockOutEmpty> createState() => _StockOutEmptyPageState();
}
class _StockOutEmptyPageState extends ConsumerState<StockOutEmpty> {
int availableVehiclesNum = 0;
bool isDataLoaded = false;
String goodsId = "0";
String outStand = Stand.stand1.point;
int selectedQuantity = 1;
List<BrnCommonActionSheetItem> actions =
Stand.values
.map((stand) => BrnCommonActionSheetItem(stand.message))
.toList();
bool _hasSubmitted = false;
AsyncValue<void>? _previousState;
late Color primaryColor;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadConfig();
}
Future<void> _loadConfig() async {
final configString = await rootBundle.loadString(
ConfigPathEnum.uiConfig.configPath,
);
final config = loadYaml(configString);
setState(() {
primaryColor = Color(config['theme']['primaryColor']);
_isLoading = false;
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentState = ref.read(app_providers.stockOutEmptyNotifierProvider);
if (_previousState != currentState && _hasSubmitted) {
_previousState = currentState;
Future.microtask(() => _handleState(currentState));
} else {
_previousState = currentState;
}
}
void _handleState(AsyncValue<void> state) {
state.whenOrNull(
loading: () => BrnLoadingDialog.show(context, content: "正在请求"),
error: (error, _) {
BrnLoadingDialog.dismiss(context);
DialogUtils.showErrorMessage(context, "请求发生错误", error.toString());
_hasSubmitted = false;
},
data: (_) {
BrnLoadingDialog.dismiss(context);
DialogUtils.showSuccessMessage(
context,
"成功",
"空托已呼叫",
btnLabel: "我知道了",
);
setState(() {
selectedQuantity = 1;
_hasSubmitted = false;
});
},
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
ref.listen(app_providers.stockOutEmptyNotifierProvider, (previous, next) {
if (_hasSubmitted) {
_previousState = next;
Future.microtask(() => _handleState(next));
}
});
return Scaffold(
appBar: AppBar(
iconTheme: const IconThemeData(color: Colors.white),
leading: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
),
centerTitle: true,
backgroundColor: primaryColor,
title: const Text("呼叫空托", style: TextStyle(color: Colors.white)),
),
body: Padding(
padding: const EdgeInsets.only(left: 5, right: 5),
child: ListView(
children: [
BrnTextSelectFormItem(
title: "请选择站台:",
value: outStand,
isRequire: true,
onTap: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return BrnCommonActionSheet(
actions: actions,
clickCallBack: (
int index,
BrnCommonActionSheetItem actionEle,
) {
String message = actionEle.title;
final stand = Stand.values.firstWhere(
(s) => s.message == message,
);
setState(() {
outStand = stand.point;
});
},
);
},
);
},
),
BrnStepInputFormItem(
title: "需要的空载具数量:",
subTitle: "请酌情填写选择后不可取消最大数量5",
value: 1,
maxLimit: 5,
minLimit: 1,
isRequire: true,
onChanged: (oldValue, newValue) {
setState(() {
selectedQuantity = newValue;
});
},
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: ElevatedButton(
onPressed: callEmptyCart,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(primaryColor),
),
child: const Text(
"开始呼叫空托盘",
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
);
}
void callEmptyCart() {
if (_hasSubmitted) {
DialogUtils.showWarningMessage(
context,
"警告",
"有请求正在进行中,请稍后再试",
btnLabel: "我知道了",
);
return;
}
final standMessage =
Stand.values.firstWhere((element) => element.point == outStand).message;
DialogUtils.showConfirmMessage(
context,
"确认呼叫",
"您确定要在站台 $standMessage 呼叫 $selectedQuantity 个空托盘吗?",
confirmBtn: "确认",
confirm: () {
_hasSubmitted = true;
ref
.read(app_providers.stockOutEmptyNotifierProvider.notifier)
.submit(needNum: selectedQuantity, destination: outStand);
},
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

36
lib/main.dart Normal file
View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:yaml/yaml.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app/enum/config_path_enum.dart';
import 'features/page/presentation/login.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final configString = await rootBundle.loadString(
ConfigPathEnum.uiConfig.configPath,
);
final config = loadYaml(configString);
final primaryColor = Color(config['theme']['primaryColor']);
runApp(ProviderScope(child: MyApp(primaryColor)));
}
class MyApp extends StatelessWidget {
final Color primaryColor;
const MyApp(this.primaryColor, {super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WMS移动终端',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: primaryColor),
useMaterial3: true,
// extensions: [TDThemeData.fromJson('main', AppConfig.tdThemeConfig)!],
),
home: const Login(),
);
}
}

1
linux/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flutter/ephemeral

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