feat: add Flutter mobile clients and staff delivery API

This commit is contained in:
david
2026-07-30 21:47:41 +08:00
parent 550efb3812
commit 36a5ced1c0
228 changed files with 17159 additions and 22 deletions

45
apps/service_app/.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-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "058e0af2c2b57e369d905a03ac9748b0ebf543c6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: android
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: ios
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,23 @@
# 瓶安芯服务工作台
配送员、安装维修员和安检员共用的 Android/iOS Flutter 客户端。首期一账号一角色,统一访问 `/heqi/client/v1/staff`
## 本地运行
```bash
flutter pub get
flutter run --dart-define=API_BASE_URL=http://10.0.2.2:12426
```
Android 模拟器访问宿主机使用 `10.0.2.2`iOS Simulator 使用 `http://127.0.0.1:12426`。Release 环境必须注入 HTTPS 地址。
现场照片、签名和表单先按账号使用 AES-GCM 加密暂存。离线只表示“已暂存”,打卡、开始、异常、恢复、到达、签收与最终提交都必须获得服务端在线确认。
## 质量检查
```bash
flutter analyze
flutter test
flutter build apk --debug
flutter build ios --simulator --no-codesign
```

View File

@@ -0,0 +1,16 @@
include: package:flutter_lints/flutter.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
linter:
rules:
avoid_print: true
use_super_parameters: true
formatter:
page_width: 100
trailing_commas: preserve

14
apps/service_app/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,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.heqi.service_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.heqi.service_app"
// 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")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}

View File

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

View File

@@ -0,0 +1,5 @@
package com.heqi.service_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")

34
apps/service_app/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,24 @@
<?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>
</dict>
</plist>

View File

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

View File

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

View File

@@ -0,0 +1,644 @@
// !$*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 */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
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>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; 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 = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
);
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 = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
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 */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.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;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
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;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
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 */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.heqi.serviceApp;
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.heqi.serviceApp.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.heqi.serviceApp.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.heqi.serviceApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.heqi.serviceApp;
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.heqi.serviceApp;
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 */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency 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,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
<?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>NSCameraUsageDescription</key>
<string>用于配送签收、安装维修和安检现场取证。</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>用于上班打卡、配送轨迹和到达服务地址校验。</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>用于选择现场作业凭证。</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>瓶安芯服务工作台</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>service_app</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>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</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>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

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,26 @@
import 'package:flutter/material.dart';
import '../ui/core/app_theme.dart';
import 'dependencies.dart';
import 'router.dart';
class ServiceClientApp extends StatefulWidget {
const ServiceClientApp({required this.dependencies, super.key});
final AppDependencies dependencies;
@override
State<ServiceClientApp> createState() => _ServiceClientAppState();
}
class _ServiceClientAppState extends State<ServiceClientApp> {
late final _router = createRouter(widget.dependencies);
@override
Widget build(BuildContext context) => MaterialApp.router(
title: '瓶安芯服务工作台',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
routerConfig: _router,
);
}

View File

@@ -0,0 +1,96 @@
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';
import '../data/offline/encrypted_draft_store.dart';
import '../data/repositories/service_repository.dart';
import '../data/services/api_client.dart';
import '../data/services/location_service.dart';
import '../data/services/secure_session_store.dart';
class AppDependencies {
AppDependencies._({
required this.session,
required this.repository,
required this.drafts,
});
final StaffSession session;
final ServiceRepository repository;
final EncryptedDraftStore drafts;
static Future<AppDependencies> create() async {
final store = SecureSessionStore();
final session = StaffSession(store);
await session.restore();
final api = ApiClient(() => session.token);
return AppDependencies._(
session: session,
repository: ServiceRepository(api, GeolocatorLocationService()),
drafts: EncryptedDraftStore(),
);
}
}
class StaffSession extends ChangeNotifier {
StaffSession(this._store);
static const _root = '/heqi/client/v1/staff';
static const _tokenKey = 'service_app_access_token';
static const _identityKey = 'service_app_identity';
static const _roleKey = 'service_app_role';
static const _deviceKey = 'service_app_device';
final SecureSessionStore _store;
String _token = '';
String _identity = '';
String _roleCode = '';
String _deviceIdentity = '';
String get token => _token;
String get identity => _identity;
String get roleCode => _roleCode;
String get deviceIdentity => _deviceIdentity;
bool get isAuthenticated => _token.isNotEmpty;
Future<void> restore() async {
_token = await _store.read(_tokenKey) ?? '';
_identity = await _store.read(_identityKey) ?? '';
_roleCode = await _store.read(_roleKey) ?? '';
_deviceIdentity = await _store.read(_deviceKey) ?? '';
if (_deviceIdentity.isEmpty) {
_deviceIdentity = const Uuid().v7();
await _store.write(_deviceKey, _deviceIdentity);
}
}
Future<void> login(String phone, String password) async {
final api = ApiClient(() => '');
final details = jsonMap(
await api.post(
'$_root/auth/login',
authenticated: false,
body: {'phone': phone, 'mode': 'password', 'password': password},
),
);
_token = details['access_token'] as String? ?? '';
_identity = details['identity'] as String? ?? '';
_roleCode = details['role_code'] as String? ?? '';
if (_token.isEmpty || _identity.isEmpty || _roleCode.isEmpty) {
throw const ApiException(500, '工作人员登录上下文缺失');
}
await _store.write(_tokenKey, _token);
await _store.write(_identityKey, _identity);
await _store.write(_roleKey, _roleCode);
notifyListeners();
}
Future<void> logout() async {
_token = '';
_identity = '';
_roleCode = '';
await _store.delete(_tokenKey);
await _store.delete(_identityKey);
await _store.delete(_roleKey);
notifyListeners();
}
}

View File

@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../ui/features/auth/login_page.dart';
import '../ui/features/evidence/evidence_page.dart';
import '../ui/features/preflight/preflight_page.dart';
import '../ui/features/profile/profile_page.dart';
import '../ui/features/work/work_detail_page.dart';
import '../ui/features/work/work_list_page.dart';
import 'dependencies.dart';
GoRouter createRouter(AppDependencies dependencies) => GoRouter(
initialLocation: '/preflight',
refreshListenable: dependencies.session,
redirect: (context, state) {
final login = state.matchedLocation == '/login';
if (!dependencies.session.isAuthenticated && !login) return '/login';
if (dependencies.session.isAuthenticated && login) return '/preflight';
return null;
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => LoginPage(session: dependencies.session),
),
GoRoute(
path: '/preflight',
builder: (context, state) => PreflightPage(
session: dependencies.session,
repository: dependencies.repository,
),
),
GoRoute(
path: '/tasks/:identity',
builder: (context, state) => WorkDetailPage(
session: dependencies.session,
repository: dependencies.repository,
drafts: dependencies.drafts,
identity: state.pathParameters['identity']!,
),
routes: [
GoRoute(
path: 'evidence',
builder: (context, state) => EvidencePage(
session: dependencies.session,
repository: dependencies.repository,
drafts: dependencies.drafts,
taskIdentity: state.pathParameters['identity']!,
),
),
],
),
StatefulShellRoute.indexedStack(
builder: (context, state, shell) => _ServiceShell(
navigationShell: shell,
roleCode: dependencies.session.roleCode,
),
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/work',
builder: (context, state) => WorkListPage(
session: dependencies.session,
repository: dependencies.repository,
completed: false,
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/records',
builder: (context, state) => WorkListPage(
session: dependencies.session,
repository: dependencies.repository,
completed: true,
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/me',
builder: (context, state) => ProfilePage(
session: dependencies.session,
repository: dependencies.repository,
drafts: dependencies.drafts,
),
),
],
),
],
),
],
);
class _ServiceShell extends StatelessWidget {
const _ServiceShell({required this.navigationShell, required this.roleCode});
final StatefulNavigationShell navigationShell;
final String roleCode;
@override
Widget build(BuildContext context) => Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: navigationShell.currentIndex,
onDestinationSelected: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
destinations: [
NavigationDestination(
icon: Icon(
roleCode == 'delivery' ? Icons.local_shipping_outlined : Icons.assignment_outlined,
),
selectedIcon: Icon(roleCode == 'delivery' ? Icons.local_shipping : Icons.assignment),
label: roleCode == 'delivery'
? '配送'
: roleCode == 'operations'
? '安检'
: '工单',
),
const NavigationDestination(
icon: Icon(Icons.history),
selectedIcon: Icon(Icons.history_toggle_off),
label: '记录',
),
const NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}

View File

@@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path_provider/path_provider.dart';
class EncryptedDraftStore {
EncryptedDraftStore({
FlutterSecureStorage? storage,
AesGcm? algorithm,
}) : _storage = storage ?? const FlutterSecureStorage(),
_algorithm = algorithm ?? AesGcm.with256bits();
static const _keyPrefix = 'service_draft_key_';
final FlutterSecureStorage _storage;
final AesGcm _algorithm;
Future<void> saveDraft({
required String accountIdentity,
required String taskIdentity,
required Map<String, Object?> value,
}) async {
final directory = await _accountDirectory(accountIdentity);
final key = await _key(accountIdentity);
final nonce = List<int>.generate(12, (_) => Random.secure().nextInt(256));
final box = await _algorithm.encrypt(
utf8.encode(jsonEncode(value)),
secretKey: key,
nonce: nonce,
);
final payload = jsonEncode({
'nonce': base64Encode(box.nonce),
'cipherText': base64Encode(box.cipherText),
'mac': base64Encode(box.mac.bytes),
});
await File('${directory.path}/$taskIdentity.draft').writeAsString(payload, flush: true);
}
Future<String> sealAttachment({
required String accountIdentity,
required String taskIdentity,
required String stage,
required String sourcePath,
}) async {
final source = File(sourcePath);
final bytes = await source.readAsBytes();
final box = await _encrypt(accountIdentity, bytes);
final name = '$taskIdentity-$stage-${DateTime.now().microsecondsSinceEpoch}.evidence';
final file = File('${(await _accountDirectory(accountIdentity)).path}/$name');
await file.writeAsString(_boxJson(box), flush: true);
if (source.existsSync()) await source.delete();
return name;
}
Future<String> materializeAttachment({
required String accountIdentity,
required String sealedName,
}) async {
final source = File('${(await _accountDirectory(accountIdentity)).path}/$sealedName');
final clear = await _decrypt(accountIdentity, await source.readAsString());
final temporary = await getTemporaryDirectory();
final file = File('${temporary.path}/${sealedName.replaceAll('.evidence', '.jpg')}');
await file.writeAsBytes(clear, flush: true);
return file.path;
}
Future<Map<String, Object?>?> readDraft(String accountIdentity, String taskIdentity) async {
final file = File('${(await _accountDirectory(accountIdentity)).path}/$taskIdentity.draft');
if (!file.existsSync()) return null;
final payload = jsonDecode(await file.readAsString());
if (payload is! Map) return null;
final clear = await _decrypt(accountIdentity, await file.readAsString());
final decoded = jsonDecode(utf8.decode(clear));
if (decoded is! Map) return null;
return decoded.map<String, Object?>((key, value) => MapEntry(key.toString(), value));
}
Future<int> count(String accountIdentity) async {
final directory = await _accountDirectory(accountIdentity);
return directory
.listSync()
.whereType<File>()
.where((file) => file.path.endsWith('.draft'))
.length;
}
Future<void> deleteDraft(String accountIdentity, String taskIdentity) async {
final directory = await _accountDirectory(accountIdentity);
final file = File('${directory.path}/$taskIdentity.draft');
if (file.existsSync()) await file.delete();
for (final evidence in directory.listSync().whereType<File>().where(
(item) => item.uri.pathSegments.last.startsWith('$taskIdentity-'),
)) {
await evidence.delete();
}
}
Future<void> discardAccount(String accountIdentity) async {
final directory = await _accountDirectory(accountIdentity);
if (directory.existsSync()) await directory.delete(recursive: true);
await _storage.delete(key: '$_keyPrefix$accountIdentity');
}
Future<Directory> _accountDirectory(String accountIdentity) async {
final root = await getApplicationSupportDirectory();
final directory = Directory('${root.path}/drafts/$accountIdentity');
if (!directory.existsSync()) await directory.create(recursive: true);
return directory;
}
Future<SecretKey> _key(String accountIdentity) async {
final storageKey = '$_keyPrefix$accountIdentity';
var encoded = await _storage.read(key: storageKey);
if (encoded == null) {
encoded = base64Encode(await (await _algorithm.newSecretKey()).extractBytes());
await _storage.write(key: storageKey, value: encoded);
}
return SecretKey(base64Decode(encoded));
}
Future<SecretBox> _encrypt(String accountIdentity, List<int> clear) async {
final nonce = List<int>.generate(12, (_) => Random.secure().nextInt(256));
return _algorithm.encrypt(clear, secretKey: await _key(accountIdentity), nonce: nonce);
}
Future<List<int>> _decrypt(String accountIdentity, String encodedPayload) async {
final payload = jsonDecode(encodedPayload);
if (payload is! Map) throw const FormatException('Invalid encrypted draft');
return _algorithm.decrypt(
SecretBox(
base64Decode(payload['cipherText'] as String),
nonce: base64Decode(payload['nonce'] as String),
mac: Mac(base64Decode(payload['mac'] as String)),
),
secretKey: await _key(accountIdentity),
);
}
String _boxJson(SecretBox box) => jsonEncode({
'nonce': base64Encode(box.nonce),
'cipherText': base64Encode(box.cipherText),
'mac': base64Encode(box.mac.bytes),
});
}

View File

@@ -0,0 +1,166 @@
import 'package:uuid/uuid.dart';
import '../../domain/models/service_models.dart';
import '../services/api_client.dart';
import '../services/location_service.dart';
class ServiceRepository {
ServiceRepository(this._api, this._location);
static const root = '/heqi/client/v1/staff';
final ApiClient _api;
final LocationService _location;
Future<StaffProfile> profile() async =>
StaffProfile.fromJson(jsonMap(await _api.get('$root/auth/profile')));
Future<PreflightResult> preflight() async =>
PreflightResult.fromJson(jsonMap(await _api.get('$root/preflight')));
Future<Map<String, Object?>> wallet() async => jsonMap(await _api.get('$root/wallet'));
Future<void> attendance(String action, String deviceIdentity) async {
final location = await _location.current();
await _api.post(
'$root/attendance',
body: {
'action': action,
'occurred_at': location.occurredAt.toUtc().toIso8601String(),
'longitude': location.longitude,
'latitude': location.latitude,
'device_identity': deviceIdentity,
'request_no': const Uuid().v7(),
},
);
}
Future<List<WorkItem>> tasks(String roleCode) async {
if (roleCode == 'delivery') {
return jsonList(
await _api.get('$root/delivery/orders'),
).map(WorkItem.delivery).toList(growable: false);
}
return jsonList(
await _api.get('$root/tickets'),
).map(WorkItem.ticket).toList(growable: false);
}
Future<WorkItem> deliveryDetail(String identity) async {
final details = jsonMap(await _api.get('$root/delivery/orders/$identity'));
return WorkItem.delivery(jsonMap(details['order']));
}
Future<WorkItem> ticketDetail(String identity) async =>
WorkItem.ticket(jsonMap(await _api.get('$root/tickets/$identity')));
Future<void> start(WorkItem item, String roleCode) async {
final path = roleCode == 'delivery'
? '$root/delivery/orders/${item.identity}/start'
: '$root/tickets/${item.identity}/start';
await _api.post(path, body: {'reason': '工作人员开始执行'});
}
Future<void> exception(WorkItem item, String roleCode, String reason) async {
final path = roleCode == 'delivery'
? '$root/delivery/orders/${item.identity}/exception'
: '$root/tickets/${item.identity}/exception';
await _api.post(path, body: {'reason': reason});
}
Future<void> recover(WorkItem item, String roleCode, String reason) async {
final path = roleCode == 'delivery'
? '$root/delivery/orders/${item.identity}/recover'
: '$root/tickets/${item.identity}/recover';
await _api.post(path, body: {'reason': reason});
}
Future<void> appendCurrentTrack(String identity) async {
final point = await _location.current();
await _api.post(
'$root/delivery/orders/$identity/tracks',
body: {
'points': [_pointJson(point)],
},
);
}
Future<void> arrive(String identity) async {
final point = await _location.current();
await _api.post('$root/delivery/orders/$identity/arrive', body: _pointJson(point));
}
Future<void> submitDeliveryReceipt({
required String identity,
required String recipientName,
required String recipientPhone,
required String proofFile,
}) async {
final proofUri = await _api.upload(proofFile);
await _api.post(
'$root/delivery/orders/$identity/submit-receipt',
body: {
'request_no': const Uuid().v7(),
'confirm_type': 'signature',
'recipient_name': recipientName,
'recipient_phone': recipientPhone,
'proof_uri': proofUri,
},
);
}
Future<void> submitTicketResult({
required String identity,
required String result,
required String conclusion,
required List<EvidenceInput> evidence,
}) async {
final location = await _location.current();
final uploaded = <Map<String, Object?>>[];
for (final item in evidence) {
final uri = await _api.upload(
item.filePath,
contentType: item.mediaType == 'video' ? 'video/mp4' : 'image/jpeg',
);
uploaded.add({
'evidence_type': item.evidenceType,
'media_type': item.mediaType,
'file_uri': uri,
'captured_at': item.capturedAt.toUtc().toIso8601String(),
'longitude': location.longitude,
'latitude': location.latitude,
'request_no': item.requestNo,
});
}
await _api.post(
'$root/tickets/$identity/submit-result',
body: {'result': result, 'conclusion': conclusion, 'evidences': uploaded},
);
}
Map<String, Object?> _pointJson(LocationPoint point) => {
'request_no': const Uuid().v7(),
'longitude': point.longitude,
'latitude': point.latitude,
'occurred_at': point.occurredAt.toUtc().toIso8601String(),
'source': 'gps',
'accuracy': point.accuracy,
'speed': '',
'direction': '',
};
}
class EvidenceInput {
const EvidenceInput({
required this.evidenceType,
required this.mediaType,
required this.filePath,
required this.capturedAt,
required this.requestNo,
});
final String evidenceType;
final String mediaType;
final String filePath;
final DateTime capturedAt;
final String requestNo;
}

View File

@@ -0,0 +1,93 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
class ApiException implements Exception {
const ApiException(this.code, this.message);
final int code;
final String message;
@override
String toString() => message;
}
class ApiClient {
ApiClient(this._tokenProvider, {http.Client? client, String? baseUrl})
: _client = client ?? http.Client(),
baseUrl =
baseUrl ??
const String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://10.0.2.2:12426',
);
final String baseUrl;
final String Function() _tokenProvider;
final http.Client _client;
Future<Object?> get(String path, {bool authenticated = true}) =>
_send('GET', path, authenticated: authenticated);
Future<Object?> post(
String path, {
Map<String, Object?>? body,
bool authenticated = true,
}) => _send('POST', path, body: body, authenticated: authenticated);
Future<Object?> put(String path, {Map<String, Object?>? body}) => _send('PUT', path, body: body);
Future<String> upload(String filePath, {String contentType = 'image/jpeg'}) async {
final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/upload/file'));
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
request.fields['declared_content_type'] = contentType;
request.files.add(await http.MultipartFile.fromPath('file', filePath));
final response = await http.Response.fromStream(await request.send());
final details = _decode(response);
return jsonMap(details)['uri'] as String? ?? '';
}
Future<Object?> _send(
String method,
String path, {
Map<String, Object?>? body,
bool authenticated = true,
}) async {
final request = http.Request(method, Uri.parse('$baseUrl$path'));
request.headers[HttpHeaders.acceptHeader] = 'application/json';
if (authenticated && _tokenProvider().isNotEmpty) {
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
}
if (body != null) {
request.headers[HttpHeaders.contentTypeHeader] = 'application/json; charset=UTF-8';
request.body = jsonEncode(body);
}
final response = await http.Response.fromStream(await _client.send(request));
return _decode(response);
}
Object? _decode(http.Response response) {
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ApiException(response.statusCode, '网络请求失败(${response.statusCode}');
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, Object?>) {
throw const ApiException(500, '服务端响应格式错误');
}
final code = (decoded['code'] as num?)?.toInt() ?? 500;
if (code != 0) throw ApiException(code, decoded['message'] as String? ?? '操作失败');
return decoded['details'];
}
}
Map<String, Object?> jsonMap(Object? value) {
if (value is Map<String, Object?>) return value;
if (value is Map) return value.map((key, item) => MapEntry(key.toString(), item));
throw const ApiException(500, '服务端数据格式错误');
}
List<Map<String, Object?>> jsonList(Object? value) {
if (value is! List) return const [];
return value.map<Map<String, Object?>>(jsonMap).toList(growable: false);
}

View File

@@ -0,0 +1,42 @@
import 'package:geolocator/geolocator.dart';
class LocationPoint {
const LocationPoint({
required this.longitude,
required this.latitude,
required this.accuracy,
required this.occurredAt,
});
final String longitude;
final String latitude;
final String accuracy;
final DateTime occurredAt;
}
abstract interface class LocationService {
Future<LocationPoint> current();
}
class GeolocatorLocationService implements LocationService {
@override
Future<LocationPoint> current() async {
if (!await Geolocator.isLocationServiceEnabled()) {
throw StateError('请先开启系统定位服务');
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
throw StateError('定位权限未授权,无法完成该在线动作');
}
final position = await Geolocator.getCurrentPosition();
return LocationPoint(
longitude: position.longitude.toStringAsFixed(7),
latitude: position.latitude.toStringAsFixed(7),
accuracy: position.accuracy.toStringAsFixed(1),
occurredAt: position.timestamp,
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureSessionStore {
SecureSessionStore({FlutterSecureStorage? storage})
: _storage = storage ?? const FlutterSecureStorage();
final FlutterSecureStorage _storage;
Future<String?> read(String key) => _storage.read(key: key);
Future<void> write(String key, String value) => _storage.write(key: key, value: value);
Future<void> delete(String key) => _storage.delete(key: key);
}

View File

@@ -0,0 +1,125 @@
class StaffProfile {
const StaffProfile({
required this.identity,
required this.name,
required this.phone,
required this.roleCode,
required this.workStatus,
});
final String identity;
final String name;
final String phone;
final String roleCode;
final String workStatus;
factory StaffProfile.fromJson(Map<String, Object?> json) => StaffProfile(
identity: json['identity'] as String? ?? '',
name: json['name'] as String? ?? '',
phone: json['phone'] as String? ?? '',
roleCode: json['role_code'] as String? ?? '',
workStatus: json['work_status'] as String? ?? 'off_duty',
);
}
class PreflightResult {
const PreflightResult({
required this.roleCode,
required this.workStatus,
required this.canWork,
required this.checks,
});
final String roleCode;
final String workStatus;
final bool canWork;
final Map<String, Object?> checks;
factory PreflightResult.fromJson(Map<String, Object?> json) => PreflightResult(
roleCode: json['role_code'] as String? ?? '',
workStatus: json['work_status'] as String? ?? 'off_duty',
canWork: json['can_work'] as bool? ?? false,
checks: _map(json['checks']),
);
}
class WorkItem {
const WorkItem({
required this.identity,
required this.number,
required this.title,
required this.address,
required this.status,
required this.allowedActions,
required this.raw,
});
final String identity;
final String number;
final String title;
final String address;
final int status;
final List<String> allowedActions;
final Map<String, Object?> raw;
factory WorkItem.delivery(Map<String, Object?> json) => WorkItem(
identity: json['identity'] as String? ?? '',
number: json['order_no'] as String? ?? '',
title: '燃气配送',
address: json['address'] as String? ?? '',
status: (json['order_status'] as num?)?.toInt() ?? 0,
allowedActions: (json['allowed_actions'] as List? ?? const [])
.map((item) => item.toString())
.toList(growable: false),
raw: json,
);
factory WorkItem.ticket(Map<String, Object?> json) => WorkItem(
identity: json['identity'] as String? ?? '',
number: json['ticket_no'] as String? ?? '',
title: _ticketTitle(json['category'] as String? ?? ''),
address: json['address'] as String? ?? '',
status: (json['ticket_status'] as num?)?.toInt() ?? 0,
allowedActions: _ticketActions((json['ticket_status'] as num?)?.toInt() ?? 0),
raw: json,
);
}
Map<String, Object?> _map(Object? value) {
if (value is Map<String, Object?>) return value;
if (value is Map) return value.map((key, item) => MapEntry(key.toString(), item));
return const {};
}
String _ticketTitle(String category) => switch (category) {
'installation' => '安装任务',
'repair' => '维修任务',
'inspection' => '安全检查',
'reinspection' => '复检任务',
_ => '服务任务',
};
List<String> _ticketActions(int status) => switch (status) {
18 => const ['start'],
11 => const ['submit_result', 'exception'],
21 => const ['recover'],
_ => const [],
};
String roleName(String role) => switch (role) {
'delivery' => '配送员',
'installer' => '安装维修员',
'operations' => '安检员',
_ => '工作人员',
};
String statusName(int status) => switch (status) {
11 => '处理中',
18 => '已分派',
20 => '已就绪',
21 => '异常',
23 => '已完成',
33 => '配送中',
34 => '待确认',
_ => '状态 $status',
};

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'app/app.dart';
import 'app/dependencies.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final dependencies = await AppDependencies.create();
runApp(ServiceClientApp(dependencies: dependencies));
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class AppTheme {
static ThemeData light() {
final scheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6C4CF1),
primary: const Color(0xFF6C4CF1),
secondary: const Color(0xFF16A66A),
surface: Colors.white,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: const Color(0xFFF5F4FA),
cardTheme: CardThemeData(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
),
);
}
}

View File

@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import '../../../app/dependencies.dart';
class LoginPage extends StatefulWidget {
const LoginPage({required this.session, super.key});
final StaffSession session;
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _phone = TextEditingController();
final _password = TextEditingController();
bool _busy = false;
String? _error;
Future<void> _login() async {
setState(() {
_busy = true;
_error = null;
});
try {
await widget.session.login(_phone.text.trim(), _password.text);
} catch (error) {
if (mounted) setState(() => _error = error.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
void dispose() {
_phone.dispose();
_password.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.engineering_rounded,
size: 72,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 20),
Text(
'瓶安芯服务工作台',
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w900),
),
const SizedBox(height: 8),
const Text('配送、安装维修与安检共用入口', textAlign: TextAlign.center),
const SizedBox(height: 34),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: '工作人员手机号',
prefixIcon: Icon(Icons.phone_outlined),
),
),
const SizedBox(height: 14),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(
labelText: '登录密码',
prefixIcon: Icon(Icons.lock_outline),
),
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _busy ? null : _login,
child: _busy
? const SizedBox.square(
dimension: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('进入工作台'),
),
const SizedBox(height: 12),
const Text(
'账号与角色由平台审核分配,不提供自助注册',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
],
),
),
),
),
),
);
}

View File

@@ -0,0 +1,225 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:uuid/uuid.dart';
import '../../../app/dependencies.dart';
import '../../../data/offline/encrypted_draft_store.dart';
import '../../../data/repositories/service_repository.dart';
class EvidencePage extends StatefulWidget {
const EvidencePage({
required this.session,
required this.repository,
required this.drafts,
required this.taskIdentity,
super.key,
});
final StaffSession session;
final ServiceRepository repository;
final EncryptedDraftStore drafts;
final String taskIdentity;
@override
State<EvidencePage> createState() => _EvidencePageState();
}
class _EvidencePageState extends State<EvidencePage> {
final _picker = ImagePicker();
final _result = TextEditingController();
final Map<String, Map<String, Object?>> _evidence = {};
bool _busy = false;
String _conclusion = 'qualified';
List<String> get _requiredStages => widget.session.roleCode == 'operations'
? const ['inspection', 'signature']
: const ['before', 'during', 'after', 'signature'];
@override
void initState() {
super.initState();
_restore();
}
Future<void> _restore() async {
final draft = await widget.drafts.readDraft(widget.session.identity, widget.taskIdentity);
if (draft == null || !mounted) return;
final values = draft['evidence'];
if (values is List) {
for (final value in values.whereType<Map<Object?, Object?>>()) {
final mapped = value.map<String, Object?>((key, item) => MapEntry(key.toString(), item));
final stage = mapped['stage'] as String? ?? '';
if (stage.isNotEmpty) _evidence[stage] = mapped;
}
}
_result.text = draft['result'] as String? ?? '';
_conclusion = draft['conclusion'] as String? ?? 'qualified';
setState(() {});
}
Future<void> _capture(String stage) async {
final image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 82,
maxWidth: 1800,
);
if (image == null) return;
final sealedName = await widget.drafts.sealAttachment(
accountIdentity: widget.session.identity,
taskIdentity: widget.taskIdentity,
stage: stage,
sourcePath: image.path,
);
_evidence[stage] = {
'stage': stage,
'sealed_name': sealedName,
'captured_at': DateTime.now().toUtc().toIso8601String(),
'request_no': const Uuid().v7(),
'media_type': stage == 'signature' ? 'signature' : 'image',
};
await _save();
if (mounted) setState(() {});
}
Future<void> _save() => widget.drafts.saveDraft(
accountIdentity: widget.session.identity,
taskIdentity: widget.taskIdentity,
value: {
'task_identity': widget.taskIdentity,
'result': _result.text,
'conclusion': _conclusion,
'updated_at': DateTime.now().toUtc().toIso8601String(),
'evidence': _evidence.values.toList(),
},
);
Future<void> _submit() async {
if (!_requiredStages.every(_evidence.containsKey) || _result.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请完成结果说明和全部必需取证项')));
return;
}
setState(() => _busy = true);
final temporaryFiles = <String>[];
try {
await _save();
final inputs = <EvidenceInput>[];
for (final item in _evidence.values) {
final path = await widget.drafts.materializeAttachment(
accountIdentity: widget.session.identity,
sealedName: item['sealed_name'] as String,
);
temporaryFiles.add(path);
inputs.add(
EvidenceInput(
evidenceType: item['stage'] as String,
mediaType: item['media_type'] as String,
filePath: path,
capturedAt: DateTime.parse(item['captured_at'] as String),
requestNo: item['request_no'] as String,
),
);
}
await widget.repository.submitTicketResult(
identity: widget.taskIdentity,
result: _result.text.trim(),
conclusion: _conclusion,
evidence: inputs,
);
await widget.drafts.deleteDraft(widget.session.identity, widget.taskIdentity);
if (mounted) Navigator.pop(context, true);
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('提交失败,草稿仍安全保留:$error')));
}
} finally {
for (final path in temporaryFiles) {
final file = File(path);
if (file.existsSync()) await file.delete();
}
if (mounted) setState(() => _busy = false);
}
}
@override
void dispose() {
_result.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('现场取证')),
body: ListView(
padding: const EdgeInsets.all(18),
children: [
const Card(
child: Padding(
padding: EdgeInsets.all(18),
child: Row(
children: [
Icon(Icons.lock_outline),
SizedBox(width: 12),
Expanded(child: Text('照片与签名先按当前账号加密暂存;上传成功前仅显示“已暂存”。')),
],
),
),
),
const SizedBox(height: 10),
..._requiredStages.map(
(stage) => Card(
child: ListTile(
leading: Icon(
_evidence.containsKey(stage) ? Icons.check_circle : Icons.camera_alt_outlined,
color: _evidence.containsKey(stage) ? Colors.green : null,
),
title: Text(_stageName(stage)),
subtitle: Text(_evidence.containsKey(stage) ? '已加密暂存' : '尚未采集'),
trailing: TextButton(
onPressed: _busy ? null : () => _capture(stage),
child: Text(_evidence.containsKey(stage) ? '重拍' : '拍摄'),
),
),
),
),
const SizedBox(height: 12),
TextField(
controller: _result,
maxLines: 4,
onChanged: (_) => _save(),
decoration: const InputDecoration(labelText: '现场结果说明'),
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _conclusion,
decoration: const InputDecoration(labelText: '结论'),
items: const [
DropdownMenuItem(value: 'qualified', child: Text('合格')),
DropdownMenuItem(value: 'noncompliant', child: Text('不合格')),
DropdownMenuItem(value: 'high_risk', child: Text('高风险')),
],
onChanged: (value) {
if (value == null) return;
setState(() => _conclusion = value);
_save();
},
),
const SizedBox(height: 22),
ElevatedButton(
onPressed: _busy ? null : _submit,
child: Text(_busy ? '正在上传并等待服务端确认…' : '在线提交结果'),
),
],
),
);
String _stageName(String stage) => switch (stage) {
'before' => '作业前照片',
'during' => '作业中照片',
'after' => '作业后照片',
'inspection' => '检查现场照片',
'signature' => '用户签名图片',
_ => stage,
};
}

View File

@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../app/dependencies.dart';
import '../../../data/repositories/service_repository.dart';
import '../../../domain/models/service_models.dart';
class PreflightPage extends StatefulWidget {
const PreflightPage({required this.session, required this.repository, super.key});
final StaffSession session;
final ServiceRepository repository;
@override
State<PreflightPage> createState() => _PreflightPageState();
}
class _PreflightPageState extends State<PreflightPage> {
late Future<PreflightResult> _future;
bool _busy = false;
@override
void initState() {
super.initState();
_future = widget.repository.preflight();
}
Future<void> _attendance(String action) async {
setState(() => _busy = true);
try {
await widget.repository.attendance(action, widget.session.deviceIdentity);
setState(() => _future = widget.repository.preflight());
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error.toString())));
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('作业前检查')),
body: FutureBuilder<PreflightResult>(
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.hasError) return Center(child: Text(snapshot.error.toString()));
return const Center(child: CircularProgressIndicator());
}
final result = snapshot.data!;
return ListView(
padding: const EdgeInsets.all(18),
children: [
Card(
color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(roleName(result.roleCode), style: const TextStyle(color: Colors.white70)),
const SizedBox(height: 6),
Text(
result.canWork ? '可以开始今日作业' : '仍有前置条件未完成',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 24,
),
),
],
),
),
),
const SizedBox(height: 12),
...result.checks.entries.map((entry) {
final detail = entry.value is Map
? Map<Object?, Object?>.from(entry.value as Map)
: const <Object?, Object?>{};
final status = detail['status']?.toString() ?? 'blocked';
final passed = status == 'passed';
return Card(
child: ListTile(
leading: Icon(
passed
? Icons.check_circle
: status == 'not_configured'
? Icons.info_outline
: Icons.cancel,
color: passed
? Colors.green
: status == 'not_configured'
? Colors.orange
: Colors.red,
),
title: Text(_label(entry.key)),
subtitle: Text(
status == 'not_configured'
? '平台暂未启用,不冒充校验通过'
: passed
? '已通过服务端校验'
: '未通过',
),
),
);
}),
const SizedBox(height: 18),
if (result.workStatus != 'on_duty')
ElevatedButton.icon(
onPressed: _busy ? null : () => _attendance('clock_in'),
icon: const Icon(Icons.location_on),
label: const Text('定位并上班打卡'),
)
else ...[
ElevatedButton(
onPressed: result.canWork ? () => context.go('/work') : null,
child: const Text('进入工作台'),
),
TextButton(
onPressed: _busy ? null : () => _attendance('clock_out'),
child: const Text('下班打卡'),
),
],
],
);
},
),
);
String _label(String value) => switch (value) {
'account' => '账号状态',
'role' => '岗位',
'organization' => '所属组织',
'credential' => '人员资质',
'attendance' => '上班状态',
'daily_training' => '每日培训',
'service_area' => '服务区域',
'authorized_device' => '授权设备',
_ => value,
};
}

View File

@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../app/dependencies.dart';
import '../../../data/offline/encrypted_draft_store.dart';
import '../../../data/repositories/service_repository.dart';
import '../../../domain/models/service_models.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({
required this.session,
required this.repository,
required this.drafts,
super.key,
});
final StaffSession session;
final ServiceRepository repository;
final EncryptedDraftStore drafts;
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
late Future<(StaffProfile, Map<String, Object?>, int)> _future;
@override
void initState() {
super.initState();
_future = _load();
}
Future<(StaffProfile, Map<String, Object?>, int)> _load() async => (
await widget.repository.profile(),
await widget.repository.wallet(),
await widget.drafts.count(widget.session.identity),
);
Future<void> _logout(int draftCount) async {
if (draftCount > 0) {
final discard = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('仍有未同步现场草稿'),
content: Text('当前账号有 $draftCount 份加密草稿。建议返回任务页完成上传;若确认放弃,将安全删除草稿和附件。'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('返回上传')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('放弃并删除')),
],
),
);
if (discard != true) return;
await widget.drafts.discardAccount(widget.session.identity);
}
await widget.session.logout();
if (mounted) context.go('/login');
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('我的工作台')),
body: FutureBuilder<(StaffProfile, Map<String, Object?>, int)>(
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.hasError) return Center(child: Text(snapshot.error.toString()));
return const Center(child: CircularProgressIndicator());
}
final (profile, wallet, drafts) = snapshot.data!;
final balance = (wallet['balance'] as num?)?.toInt() ?? 0;
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
CircleAvatar(
radius: 30,
child: Text(profile.name.isEmpty ? '' : profile.name.substring(0, 1)),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.name,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900),
),
Text('${roleName(profile.roleCode)} · ${profile.phone}'),
],
),
),
Chip(label: Text(profile.workStatus == 'on_duty' ? '在岗' : '离岗')),
],
),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.account_balance_wallet_outlined),
title: const Text('钱包余额'),
trailing: Text(
'¥${(balance / 100).toStringAsFixed(2)}',
style: const TextStyle(fontWeight: FontWeight.w900),
),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.lock_outline),
title: const Text('加密现场草稿'),
subtitle: const Text('仅当前账号重新认证后可恢复'),
trailing: Text('$drafts'),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.verified_user_outlined),
title: const Text('重新执行作业前检查'),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.go('/preflight'),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.logout),
title: const Text('退出登录'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _logout(drafts),
),
),
],
);
},
),
);
}

View File

@@ -0,0 +1,290 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:uuid/uuid.dart';
import '../../../app/dependencies.dart';
import '../../../data/offline/encrypted_draft_store.dart';
import '../../../data/repositories/service_repository.dart';
import '../../../domain/models/service_models.dart';
class WorkDetailPage extends StatefulWidget {
const WorkDetailPage({
required this.session,
required this.repository,
required this.drafts,
required this.identity,
super.key,
});
final StaffSession session;
final ServiceRepository repository;
final EncryptedDraftStore drafts;
final String identity;
@override
State<WorkDetailPage> createState() => _WorkDetailPageState();
}
class _WorkDetailPageState extends State<WorkDetailPage> {
late Future<WorkItem> _future;
bool _busy = false;
@override
void initState() {
super.initState();
_future = _load();
}
Future<WorkItem> _load() => widget.session.roleCode == 'delivery'
? widget.repository.deliveryDetail(widget.identity)
: widget.repository.ticketDetail(widget.identity);
Future<void> _run(Future<void> Function(WorkItem) action, WorkItem item) async {
setState(() => _busy = true);
try {
await action(item);
setState(() => _future = _load());
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error.toString())));
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<String?> _reason() async {
final controller = TextEditingController();
final value = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('填写原因'),
content: TextField(
controller: controller,
maxLines: 3,
decoration: const InputDecoration(labelText: '原因'),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
FilledButton(
onPressed: () => Navigator.pop(context, controller.text.trim()),
child: const Text('确认'),
),
],
),
);
controller.dispose();
return value;
}
Future<void> _receipt(WorkItem item) async {
final existing = await widget.drafts.readDraft(widget.session.identity, item.identity);
if (!mounted) return;
String? sealedName;
if (existing?['kind'] == 'delivery_receipt' && existing?['sealed_name'] is String) {
final reuse = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('发现未提交签收草稿'),
content: const Text('是否继续提交上次加密保存的签收凭证?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('重新拍摄')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('继续提交')),
],
),
);
if (reuse == true) sealedName = existing!['sealed_name'] as String;
}
if (sealedName == null) {
final image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 82);
if (image == null) return;
sealedName = await widget.drafts.sealAttachment(
accountIdentity: widget.session.identity,
taskIdentity: item.identity,
stage: 'receipt',
sourcePath: image.path,
);
}
setState(() => _busy = true);
String? temporaryPath;
try {
await widget.drafts.saveDraft(
accountIdentity: widget.session.identity,
taskIdentity: item.identity,
value: {
'task_identity': item.identity,
'kind': 'delivery_receipt',
'sealed_name': sealedName,
'request_no': const Uuid().v7(),
'captured_at': DateTime.now().toUtc().toIso8601String(),
},
);
temporaryPath = await widget.drafts.materializeAttachment(
accountIdentity: widget.session.identity,
sealedName: sealedName,
);
await widget.repository.submitDeliveryReceipt(
identity: item.identity,
recipientName: item.raw['contact_name'] as String? ?? '收货人',
recipientPhone: item.raw['contact_phone'] as String? ?? '',
proofFile: temporaryPath,
);
await widget.drafts.deleteDraft(widget.session.identity, item.identity);
if (mounted) setState(() => _future = _load());
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('签收提交失败,加密草稿已保留:$error')));
}
} finally {
if (temporaryPath != null) {
final file = File(temporaryPath);
if (file.existsSync()) await file.delete();
}
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('任务详情')),
body: FutureBuilder<WorkItem>(
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.hasError) return Center(child: Text(snapshot.error.toString()));
return const Center(child: CircularProgressIndicator());
}
final item = snapshot.data!;
return ListView(
padding: const EdgeInsets.all(18),
children: [
Card(
color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.number, style: const TextStyle(color: Colors.white70)),
const SizedBox(height: 8),
Text(
item.title,
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 12),
Chip(label: Text(statusName(item.status))),
],
),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.location_on_outlined),
title: const Text('服务地址'),
subtitle: Text(item.address.isEmpty ? '未提供地址' : item.address),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.person_outline),
title: Text(item.raw['contact_name'] as String? ?? '服务用户'),
subtitle: Text(item.raw['contact_phone'] as String? ?? ''),
),
),
const SizedBox(height: 18),
if (item.allowedActions.contains('start'))
ElevatedButton(
onPressed: _busy
? null
: () => _run(
(value) => widget.repository.start(value, widget.session.roleCode),
item,
),
child: const Text('开始处理'),
),
if (item.allowedActions.contains('append_tracks'))
OutlinedButton(
onPressed: _busy
? null
: () => _run(
(value) => widget.repository.appendCurrentTrack(value.identity),
item,
),
child: const Text('上报当前位置'),
),
if (item.allowedActions.contains('arrive'))
ElevatedButton(
onPressed: _busy
? null
: () => _run((value) => widget.repository.arrive(value.identity), item),
child: const Text('到达并校验围栏'),
),
if (item.allowedActions.contains('submit_receipt'))
ElevatedButton(
onPressed: _busy ? null : () => _receipt(item),
child: const Text('拍摄签收凭证并提交'),
),
if (item.allowedActions.contains('submit_result'))
ElevatedButton(
onPressed: _busy
? null
: () async {
final changed = await context.push<bool>(
'/tasks/${item.identity}/evidence',
);
if (changed == true) setState(() => _future = _load());
},
child: const Text('现场取证与提交'),
),
if (item.allowedActions.contains('exception'))
TextButton(
onPressed: _busy
? null
: () async {
final reason = await _reason();
if (reason != null && reason.isNotEmpty) {
await _run(
(value) =>
widget.repository.exception(value, widget.session.roleCode, reason),
item,
);
}
},
child: const Text('标记异常'),
),
if (item.allowedActions.contains('recover'))
ElevatedButton(
onPressed: _busy
? null
: () async {
final reason = await _reason();
if (reason != null && reason.isNotEmpty) {
await _run(
(value) =>
widget.repository.recover(value, widget.session.roleCode, reason),
item,
);
}
},
child: const Text('恢复任务'),
),
if (_busy)
const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
],
);
},
),
);
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../app/dependencies.dart';
import '../../../data/repositories/service_repository.dart';
import '../../../domain/models/service_models.dart';
import 'work_list_view_model.dart';
class WorkListPage extends StatefulWidget {
const WorkListPage({
required this.session,
required this.repository,
required this.completed,
super.key,
});
final StaffSession session;
final ServiceRepository repository;
final bool completed;
@override
State<WorkListPage> createState() => _WorkListPageState();
}
class _WorkListPageState extends State<WorkListPage> {
late final WorkListViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = WorkListViewModel(
widget.repository,
widget.session.roleCode,
completed: widget.completed,
)..load();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(widget.completed ? '作业记录' : '${roleName(widget.session.roleCode)}工作台'),
),
body: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
if (_viewModel.loading && _viewModel.items.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (_viewModel.error != null && _viewModel.items.isEmpty) {
return Center(child: Text(_viewModel.error.toString()));
}
return RefreshIndicator(
onRefresh: _viewModel.load,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 10, 16, 28),
children: [
Card(
color: widget.session.roleCode == 'operations'
? const Color(0xFFE9F8F0)
: const Color(0xFFEEEAFE),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Icon(
widget.session.roleCode == 'delivery'
? Icons.local_shipping
: widget.session.roleCode == 'installer'
? Icons.handyman
: Icons.health_and_safety,
size: 38,
),
const SizedBox(width: 14),
Expanded(
child: Text(
widget.completed ? '服务端确认完成的历史记录' : '仅展示服务端分派给本账号的任务',
style: const TextStyle(fontWeight: FontWeight.w800),
),
),
],
),
),
),
if (_viewModel.items.isEmpty)
const Padding(
padding: EdgeInsets.all(42),
child: Center(child: Text('暂无任务')),
)
else
..._viewModel.items.map(
(item) => Card(
child: ListTile(
contentPadding: const EdgeInsets.all(16),
title: Text(item.title, style: const TextStyle(fontWeight: FontWeight.w800)),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8),
child: Text('${item.number}\n${item.address}'),
),
trailing: Chip(label: Text(statusName(item.status))),
onTap: () => context.push('/tasks/${item.identity}'),
),
),
),
],
),
);
},
),
);
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/foundation.dart';
import '../../../data/repositories/service_repository.dart';
import '../../../domain/models/service_models.dart';
class WorkListViewModel extends ChangeNotifier {
WorkListViewModel(this._repository, this._roleCode, {required this.completed});
final ServiceRepository _repository;
final String _roleCode;
final bool completed;
List<WorkItem> _items = const [];
Object? _error;
bool _loading = false;
List<WorkItem> get items => _items;
Object? get error => _error;
bool get loading => _loading;
Future<void> load() async {
_loading = true;
_error = null;
notifyListeners();
try {
final all = await _repository.tasks(_roleCode);
_items = all
.where((item) => completed ? item.status == 23 : item.status != 23 && item.status != 22)
.toList();
} catch (error) {
_error = error;
} finally {
_loading = false;
notifyListeners();
}
}
}

View File

@@ -0,0 +1,786 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.1"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "762c99f890ca8bf87f7337236f99edd42793843bc6c3631da294a76653a54bd0"
url: "https://pub.dev"
source: hosted
version: "7.3.1"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
url: "https://pub.dev"
source: hosted
version: "0.3.5+4"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cryptography:
dependency: "direct main"
description:
name: cryptography
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dbus:
dependency: transitive
description:
name: dbus
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
url: "https://pub.dev"
source: hosted
version: "0.7.14"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
url: "https://pub.dev"
source: hosted
version: "10.3.1"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
url: "https://pub.dev"
source: hosted
version: "3.0.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "06686df417f34fe9f963ed217b7fdce250e2f637ddd6c374d7eb054549770633"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
geoclue:
dependency: transitive
description:
name: geoclue
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
url: "https://pub.dev"
source: hosted
version: "0.1.1"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: e146a6d63776582651e97a79cbe459f8e1211b100101fadcd84db83361fa599f
url: "https://pub.dev"
source: hosted
version: "14.0.3"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: "86ea1654e4f61ff51466848e91c116b422d6010ea269fda0fbe1af7e9e742ce1"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
url: "https://pub.dev"
source: hosted
version: "2.3.14"
geolocator_linux:
dependency: transitive
description:
name: geolocator_linux
sha256: "3da7420f11c3496511a5bd3c18fd67b88e5659f12e46b7ce00a788f6996e850a"
url: "https://pub.dev"
source: hosted
version: "0.2.6"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
url: "https://pub.dev"
source: hosted
version: "4.2.8"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: "19e485a0f8d6a88abcf9c53cba3a4105e14b7435ed8ac1c108c067b938fe8429"
url: "https://pub.dev"
source: hosted
version: "4.1.4"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
url: "https://pub.dev"
source: hosted
version: "17.3.0"
gsettings:
dependency: transitive
description:
name: gsettings
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
url: "https://pub.dev"
source: hosted
version: "0.2.8"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
http:
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
url: "https://pub.dev"
source: hosted
version: "1.2.3"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
url: "https://pub.dev"
source: hosted
version: "0.8.13+19"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
jni:
dependency: transitive
description:
name: jni
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
url: "https://pub.dev"
source: hosted
version: "1.0.3"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
jni_util:
dependency: transitive
description:
name: jni_util
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
url: "https://pub.dev"
source: hosted
version: "9.5.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
url: "https://pub.dev"
source: hosted
version: "3.0.0"
package_info_plus:
dependency: transitive
description:
name: package_info_plus
sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481"
url: "https://pub.dev"
source: hosted
version: "10.2.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
url: "https://pub.dev"
source: hosted
version: "4.1.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
url: "https://pub.dev"
source: hosted
version: "6.3.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.12.2 <4.0.0"
flutter: ">=3.44.0"

View File

@@ -0,0 +1,98 @@
name: service_app
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.12.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
go_router: ^17.3.0
http: ^1.6.0
flutter_secure_storage: ^10.3.1
uuid: ^4.6.0
path_provider: ^2.1.6
cryptography: ^2.9.0
image_picker: ^1.2.3
geolocator: ^14.0.3
connectivity_plus: ^7.3.1
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

View File

@@ -0,0 +1,31 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:service_app/domain/models/service_models.dart';
void main() {
group('WorkItem', () {
test('maps delivery allowed actions from server', () {
final item = WorkItem.delivery({
'identity': 'order-1',
'order_no': 'GAS001',
'order_status': 33,
'allowed_actions': ['append_tracks', 'arrive', 'exception'],
});
expect(item.identity, 'order-1');
expect(item.allowedActions, contains('arrive'));
expect(statusName(item.status), '配送中');
});
test('derives ticket actions from server status', () {
final item = WorkItem.ticket({
'identity': 'ticket-1',
'ticket_no': 'TK001',
'ticket_status': 11,
'category': 'inspection',
});
expect(item.title, '安全检查');
expect(item.allowedActions, contains('submit_result'));
});
});
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:service_app/app/dependencies.dart';
import 'package:service_app/data/services/secure_session_store.dart';
import 'package:service_app/ui/features/auth/login_page.dart';
void main() {
testWidgets('staff login explains the single shared entry', (tester) async {
final session = StaffSession(SecureSessionStore());
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
expect(find.text('瓶安芯服务工作台'), findsOneWidget);
expect(find.text('配送、安装维修与安检共用入口'), findsOneWidget);
expect(find.byType(TextField), findsNWidgets(2));
});
}

45
apps/user_app/.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-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

33
apps/user_app/.metadata Normal file
View File

@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "058e0af2c2b57e369d905a03ac9748b0ebf543c6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: android
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
- platform: ios
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

23
apps/user_app/README.md Normal file
View File

@@ -0,0 +1,23 @@
# 瓶安芯用户端
Android/iOS Flutter 客户端。首期只接入 `/heqi/client/v1/user` 的真实能力:首页安全内容与服务归属、商城、订单、合同、工单、钱包、地址和个人资料。
## 本地运行
```bash
flutter pub get
flutter run --dart-define=API_BASE_URL=http://10.0.2.2:12426
```
Android 模拟器访问宿主机使用 `10.0.2.2`iOS Simulator 使用 `http://127.0.0.1:12426`。Release 环境必须注入 HTTPS 地址。
## 质量检查
```bash
flutter analyze
flutter test
flutter build apk --debug
flutter build ios --simulator --no-codesign
```
充值 Mock 确认、设备控制、收藏、押金、消息和发票均不在 Release UI 暴露。

View File

@@ -0,0 +1,16 @@
include: package:flutter_lints/flutter.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
linter:
rules:
avoid_print: true
use_super_parameters: true
formatter:
page_width: 100
trailing_commas: preserve

14
apps/user_app/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,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.heqi.user_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.heqi.user_app"
// 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")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}

View File

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

View File

@@ -0,0 +1,5 @@
package com.heqi.user_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

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