Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfeb8f0bba | |||
| 480ce836bb |
@@ -52,6 +52,64 @@ jobs:
|
||||
name: macos
|
||||
path: dist/
|
||||
|
||||
build-android:
|
||||
# Same physical mac runner (label: mac, capacity 1) — it already has the
|
||||
# Android SDK. Serialize after build-macos via needs so it doesn't sit
|
||||
# queued and time out.
|
||||
needs: build-macos
|
||||
runs-on: mac
|
||||
env:
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Provision (idempotent — auto-runs if host not initialized)
|
||||
run: sh scripts/ci/provision-mac.sh
|
||||
|
||||
- name: Compile (Flutter Android APK)
|
||||
env:
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: sh scripts/ci/compile-android.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Upload android artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: android
|
||||
path: dist/
|
||||
|
||||
build-ios:
|
||||
# Same physical mac runner (label: mac, capacity 1) with Xcode. Serialize
|
||||
# after build-android. Builds a signed IPA and uploads to TestFlight; if the
|
||||
# iOS signing secrets are absent the script skips gracefully (exit 0), so this
|
||||
# job is safe to keep before the Apple Developer account is configured.
|
||||
needs: build-android
|
||||
runs-on: mac
|
||||
env:
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Provision (idempotent — auto-runs if host not initialized)
|
||||
run: sh scripts/ci/provision-mac.sh
|
||||
|
||||
- name: Compile & upload to TestFlight (Flutter iOS)
|
||||
env:
|
||||
IOS_DIST_CERT_P12_BASE64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }}
|
||||
IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
|
||||
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
|
||||
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
|
||||
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
|
||||
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
|
||||
APPSTORE_API_KEY_P8_BASE64: ${{ secrets.APPSTORE_API_KEY_P8_BASE64 }}
|
||||
run: sh scripts/ci/compile-ios.sh "${{ gitea.ref_name }}"
|
||||
|
||||
build-windows:
|
||||
runs-on: windows
|
||||
env:
|
||||
@@ -77,7 +135,7 @@ jobs:
|
||||
path: dist/
|
||||
|
||||
release-deploy:
|
||||
needs: [build-linux-web, build-macos, build-windows]
|
||||
needs: [build-linux-web, build-macos, build-android, build-ios, build-windows]
|
||||
runs-on: mac
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
@@ -99,6 +157,12 @@ jobs:
|
||||
name: macos
|
||||
path: dist/
|
||||
|
||||
- name: Download android artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: android
|
||||
path: dist/
|
||||
|
||||
- name: Download windows artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.19] - 2026-06-07
|
||||
|
||||
### 改进
|
||||
- 优化 Android 构建与部署流程(CI/CD 内部调整),功能与界面无变化
|
||||
|
||||
## [1.0.18] - 2026-06-06
|
||||
|
||||
### 新功能
|
||||
- 移动端(Android)界面适配:窄屏(手机)自动切换为移动布局——侧边栏改为侧滑抽屉导航,各列表(库存/出入库/财务/往来单位/基础数据)由宽表格自动切换为卡片流,弹窗与表单自适应屏宽不再溢出;宽屏(桌面/Web)布局保持不变
|
||||
- Android 客户端正式上线:CI/CD 新增 Android APK 构建(正式 release 签名、versionCode 单调递增),随发版自动发布到下载页;下载页 Android 卡片已激活
|
||||
|
||||
### 改进
|
||||
- 表单字段在手机上占满整行,便于点选;底部状态栏在手机端隐藏
|
||||
|
||||
## [1.0.17] - 2026-06-05
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -94,7 +94,7 @@ doc-writer
|
||||
| linter | 格式自动修复(gofmt/dart format),docs/review/lint-report.md | 逻辑代码 |
|
||||
| code-reviewer | docs/review/*-review.md | 任何代码 |
|
||||
| security-auditor | docs/security/ | 任何代码 |
|
||||
| devops | deploy/, .github/workflows/ | 业务代码 |
|
||||
| devops | deploy/, .gitea/workflows/, scripts/ci/ | 业务代码 |
|
||||
| sre | docs/runbooks/ | 任何代码 |
|
||||
| doc-writer | docs/(除 context/ 外) | 任何代码 |
|
||||
|
||||
@@ -240,6 +240,13 @@ cd client && flutter test
|
||||
- 不要在 toolbar 放独立的筛选按钮,筛选入口应内嵌在列头
|
||||
- 列定义使用 `ColDef`,可隐藏列用 `ColumnToggleButton` 控制
|
||||
|
||||
### 响应式 / 移动端适配
|
||||
- 断点判定统一用 `context.isMobile`(`client/lib/core/responsive/responsive.dart`,宽度 < 600 为窄屏/手机),**禁止**在各处散落 `MediaQuery.width < 600` 这类魔法数
|
||||
- **列表屏**:用 `DataTableCard` 时必须同时传 `mobileCards`(窄屏渲染卡片流),卡片复用 `MobileListCard` / `MobileCardField`(`widgets/mobile_list_card.dart`);宽屏仍走原表格,行为不变
|
||||
- **弹窗固定宽度**:一律用 `context.dialogWidth(X)`(≤ 屏宽 92%),**禁止**裸写 `width: <固定值>` 导致窄屏溢出
|
||||
- **导航**:窄屏(手机)用 Drawer 抽屉(`app_shell.dart`),宽屏保持侧边栏;新增页面无需单独处理,挂在 shell 下即可
|
||||
- 平台判断仍遵守上面「前端平台判断」规则(`kIsWeb` 先于 `dart:io`)
|
||||
|
||||
### 权限中间件(已全局挂载)
|
||||
- `middleware.ReadOnly()`:已挂载到主 `api` 路由组,只读用户(role=readonly)所有写操作自动返回 403
|
||||
- `middleware.AdminOnly()`:挂载在 `/users` 路由组,仅管理员可管理用户
|
||||
@@ -261,7 +268,13 @@ cd client && flutter test
|
||||
```
|
||||
|
||||
执行顺序:本地 build → test → 更新 CHANGELOG.md → git commit → tag → push main+tag。
|
||||
CI/CD(Forgejo)收到 tag 后自动:编译 → 测试 → 创建 Release → 部署 EC2 → Telegram 通知。
|
||||
CI/CD(Forgejo,`.gitea/workflows/deploy.yml`)收到 tag 后自动:编译 → 测试 → 创建 Release → 部署 EC2 → Telegram 通知。
|
||||
|
||||
**多平台构建矩阵**(脚本在 `scripts/ci/`):
|
||||
- mac runner(容量 1)串行链:`build-linux-web`(后端+Web+官网)→ `build-macos` → `build-android` → `build-ios` → `release-deploy`
|
||||
- windows runner 并行:`build-windows`(Inno Setup 打包 setup.exe)
|
||||
- 分发:桌面(Win/macOS) 发布到下载页 `/downloads/` + 应用内更新;**Android** APK 同样挂 `/downloads/`,需 `ANDROID_*` 签名 secrets(见 `docs/android-signing.md`);**iOS** 走 TestFlight,需 `IOS_*` / `APPSTORE_*` secrets(见 `docs/ios-signing.md`)
|
||||
- Android/iOS 的 job 在对应 secrets 未配置时**优雅跳过**(exit 0,不阻塞发版);本地无 `client/android/key.properties` 时 APK 自动回退 debug 签名
|
||||
|
||||
**CHANGELOG 格式**(Keep a Changelog):
|
||||
```markdown
|
||||
|
||||
@@ -6,5 +6,5 @@ download_urls:
|
||||
macos: ""
|
||||
windows: ""
|
||||
ios: ""
|
||||
android: ""
|
||||
android: "https://jiu.51yanmei.com/downloads/jiu-android.apk"
|
||||
web: "https://jiu.51yanmei.com/app"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
|
||||
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,11 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
- platform: android
|
||||
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
- platform: ios
|
||||
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
@@ -5,6 +8,15 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// 读取 release 签名配置(key.properties 不入库,由本地或 CI 生成)。
|
||||
// 缺失时回退 debug 签名,保证本地 `flutter run --release` 与无密钥环境可用。
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
val hasReleaseSigning = keystorePropertiesFile.exists()
|
||||
if (hasReleaseSigning) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.yanmei.jiu"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -20,21 +32,32 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.yanmei.jiu"
|
||||
// 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
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (hasReleaseSigning) {
|
||||
create("release") {
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
signingConfig = if (hasReleaseSigning) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
// 无正式密钥时回退 debug,便于本地构建/调试
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="jiu_client"
|
||||
android:label="岩美酒库"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -0,0 +1,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>
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,43 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- printing (1.0.0):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- printing (from `.symlinks/plugins/printing/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
printing:
|
||||
:path: ".symlinks/plugins/printing/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -0,0 +1,756 @@
|
||||
// !$*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 */; };
|
||||
3460B9FE6E275516D150453E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 086A871143ACEBCC07E71A24 /* Pods_Runner.framework */; };
|
||||
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 */; };
|
||||
CBD53FA90DB3F4E143A654CC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */; };
|
||||
/* 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 */
|
||||
086A871143ACEBCC07E71A24 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
1EE111A264F501C30DD0A904 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; 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>"; };
|
||||
5A9159D36C6D9B7DF22F0FBF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; 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>"; };
|
||||
852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
9C64D02D6BED1C5F196D6192 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
0CE49F0D076B3E115DB6BF6D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CBD53FA90DB3F4E143A654CC /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
3460B9FE6E275516D150453E /* Pods_Runner.framework 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 */,
|
||||
D5753B5E6E2CB7979B358C42 /* Pods */,
|
||||
AA598329361F3C077ABDB4F2 /* Frameworks */,
|
||||
);
|
||||
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>";
|
||||
};
|
||||
AA598329361F3C077ABDB4F2 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
086A871143ACEBCC07E71A24 /* Pods_Runner.framework */,
|
||||
852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D5753B5E6E2CB7979B358C42 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9C64D02D6BED1C5F196D6192 /* Pods-Runner.debug.xcconfig */,
|
||||
5A9159D36C6D9B7DF22F0FBF /* Pods-Runner.release.xcconfig */,
|
||||
1EE111A264F501C30DD0A904 /* Pods-Runner.profile.xcconfig */,
|
||||
08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */,
|
||||
F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
B21EFFE7BABA33CD7CB4E616 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
0CE49F0D076B3E115DB6BF6D /* Frameworks */,
|
||||
);
|
||||
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 = (
|
||||
3052FA6A8375458CBCA9F94D /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
DB40772CF39C21BA0E695053 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
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 "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 */
|
||||
3052FA6A8375458CBCA9F94D /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
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";
|
||||
};
|
||||
B21EFFE7BABA33CD7CB4E616 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
DB40772CF39C21BA0E695053 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* 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.yanmei.jiu;
|
||||
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;
|
||||
baseConfigurationReference = 08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
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.yanmei.jiu.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;
|
||||
baseConfigurationReference = 2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */;
|
||||
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.yanmei.jiu.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;
|
||||
baseConfigurationReference = F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
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.yanmei.jiu.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.yanmei.jiu;
|
||||
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.yanmei.jiu;
|
||||
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 "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 */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||
"version" : "5.21.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -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 "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||
"version" : "5.21.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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>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>jiu_client</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>
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -0,0 +1,6 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// 移动端断点:宽度 < 600 视为窄屏(手机)。
|
||||
const double kMobileBreakpoint = 600;
|
||||
|
||||
extension ResponsiveX on BuildContext {
|
||||
/// 是否为窄屏(手机)布局。宽屏(桌面 / Web / 平板)返回 false。
|
||||
bool get isMobile => MediaQuery.sizeOf(this).width < kMobileBreakpoint;
|
||||
|
||||
/// 固定宽度弹窗的安全宽度:不超过屏宽的 92%,避免窄屏溢出。
|
||||
double dialogWidth(double preferred) {
|
||||
final max = MediaQuery.sizeOf(this).width * 0.92;
|
||||
return preferred < max ? preferred : max;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -340,7 +341,7 @@ class _FeedbackDialogState extends ConsumerState<_FeedbackDialog> {
|
||||
return AlertDialog(
|
||||
title: Text(isBug ? '反馈 Bug' : '功能建议'),
|
||||
content: SizedBox(
|
||||
width: 440,
|
||||
width: context.dialogWidth(440),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -125,7 +126,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(bottom: Radius.circular(4)),
|
||||
child: SizedBox(
|
||||
width: 320,
|
||||
width: context.dialogWidth(320),
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
@@ -263,7 +264,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Container(
|
||||
width: 400,
|
||||
width: context.dialogWidth(400),
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/finance.dart';
|
||||
import '../../providers/finance_provider.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../providers/connectivity_provider.dart';
|
||||
@@ -129,6 +131,47 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 财务记录:窄屏卡片
|
||||
Widget _financeCard(FinanceRecord r) {
|
||||
final canClose = (r.type == 'payable' || r.type == 'receivable') &&
|
||||
r.status == 'open';
|
||||
final showStatus = r.type == 'payable' || r.type == 'receivable';
|
||||
return MobileListCard(
|
||||
title: Text(r.partnerName?.isNotEmpty == true ? r.partnerName! : r.typeLabel),
|
||||
subtitle: Text(r.recordDate?.substring(0, 10) ?? '-'),
|
||||
trailing: _TypeBadge(r.typeLabel),
|
||||
fields: [
|
||||
MobileCardField('金额', '¥${r.amount.toStringAsFixed(2)}'),
|
||||
MobileCardField('余额', null,
|
||||
valueWidget: Text(
|
||||
'¥${r.balance.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: r.balance > 0
|
||||
? AppTheme.danger
|
||||
: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)),
|
||||
if (r.refType != null && r.refId != null)
|
||||
MobileCardField(
|
||||
'关联单据', '${r.refType!.replaceAll('_', '-')}#${r.refId}'),
|
||||
if (showStatus)
|
||||
MobileCardField('状态', null, valueWidget: _StatusBadge(r.status)),
|
||||
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
|
||||
],
|
||||
actions: canClose
|
||||
? [
|
||||
TextButton(
|
||||
onPressed: () => _closeRecord(r),
|
||||
child: const Text('结清',
|
||||
style: TextStyle(fontSize: 13, color: AppTheme.success)),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen(networkRecoveryCountProvider, (_, __) => _refetch());
|
||||
@@ -302,34 +345,44 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.typeFilter.isNotEmpty && records.isNotEmpty)
|
||||
Container(
|
||||
color: AppTheme.background,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
_SummaryCard(
|
||||
title: '合计金额',
|
||||
value: '¥${(totalAmount / 10000).toStringAsFixed(2)}万',
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '未结清',
|
||||
value: '¥${(openAmount / 10000).toStringAsFixed(2)}万',
|
||||
icon: Icons.pending_actions,
|
||||
color: AppTheme.danger,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '已结清',
|
||||
value: '¥${(closedAmount / 10000).toStringAsFixed(2)}万',
|
||||
icon: Icons.check_circle,
|
||||
color: AppTheme.success,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final w = context.isMobile ? 150.0 : null;
|
||||
final cards = [
|
||||
_SummaryCard(
|
||||
title: '合计金额',
|
||||
value: '¥${(totalAmount / 10000).toStringAsFixed(2)}万',
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: AppTheme.primary,
|
||||
width: w,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '未结清',
|
||||
value: '¥${(openAmount / 10000).toStringAsFixed(2)}万',
|
||||
icon: Icons.pending_actions,
|
||||
color: AppTheme.danger,
|
||||
width: w,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '已结清',
|
||||
value: '¥${(closedAmount / 10000).toStringAsFixed(2)}万',
|
||||
icon: Icons.check_circle,
|
||||
color: AppTheme.success,
|
||||
width: w,
|
||||
),
|
||||
];
|
||||
return Container(
|
||||
color: AppTheme.background,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: context.isMobile
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: cards),
|
||||
)
|
||||
: Row(children: cards),
|
||||
);
|
||||
}),
|
||||
if (widget.typeFilter.isNotEmpty && records.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
@@ -405,6 +458,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
|
||||
),
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
mobileCards: records.map(_financeCard).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -487,7 +541,7 @@ class _AddPaymentDialogState extends State<_AddPaymentDialog> {
|
||||
return AlertDialog(
|
||||
title: Text(_title),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
width: context.dialogWidth(360),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -611,18 +665,19 @@ class _SummaryCard extends StatelessWidget {
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final double? width;
|
||||
|
||||
const _SummaryCard({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
final card = Container(
|
||||
height: 72,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
@@ -659,8 +714,10 @@ class _SummaryCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
return width != null
|
||||
? SizedBox(width: width, child: card)
|
||||
: Expanded(child: card);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/inventory.dart';
|
||||
import '../../core/auth/auth_state.dart';
|
||||
import '../../providers/inventory_provider.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/multi_select_dropdown.dart' show FilterableColumnHeader;
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../core/utils/export_util.dart';
|
||||
@@ -47,7 +49,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('修改备注'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
width: context.dialogWidth(360),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
@@ -188,6 +190,133 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _printLabel(BuildContext context, Inventory item) async {
|
||||
if (item.productId == null) return;
|
||||
final qrBytes =
|
||||
await ref.read(productRepositoryProvider).getQRCodeBytes(item.productId!);
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
if (!context.mounted) return;
|
||||
await safePrint(
|
||||
context,
|
||||
() => printProductLabel(
|
||||
qrBytes: qrBytes,
|
||||
name: item.productName,
|
||||
code: item.productCode,
|
||||
series: item.series.isEmpty ? null : item.series,
|
||||
spec: item.spec.isEmpty ? null : item.spec,
|
||||
batchNo: item.batchNo.isEmpty ? null : item.batchNo,
|
||||
productionDate: item.productionDate,
|
||||
remark: item.remark.isEmpty ? null : item.remark,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 库存查询:窄屏卡片
|
||||
Widget _inventoryCard(Inventory item) {
|
||||
return MobileListCard(
|
||||
onTap: item.productId != null
|
||||
? () => context.push('/products/${item.productId}')
|
||||
: null,
|
||||
title: Text(item.productName.isEmpty ? '-' : item.productName),
|
||||
subtitle: item.productCode.isEmpty ? null : Text(item.productCode),
|
||||
trailing: _InventoryStatusBadge(item),
|
||||
fields: [
|
||||
if (item.spec.isNotEmpty) MobileCardField('规格', item.spec),
|
||||
if (item.series.isNotEmpty) MobileCardField('系列', item.series),
|
||||
if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo),
|
||||
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
|
||||
MobileCardField('库存', null,
|
||||
valueWidget: Text(
|
||||
'${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: item.quantity == 0
|
||||
? AppTheme.danger
|
||||
: (item.minStock != null && item.quantity < item.minStock!)
|
||||
? AppTheme.accent
|
||||
: AppTheme.textPrimary,
|
||||
),
|
||||
)),
|
||||
if (item.unitPrice != null)
|
||||
MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'),
|
||||
if (item.supplierName.isNotEmpty)
|
||||
MobileCardField('供应商', item.supplierName),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => _editRemark(context, item),
|
||||
child: const Text('备注', style: TextStyle(fontSize: 13)),
|
||||
),
|
||||
if (item.productId != null)
|
||||
TextButton(
|
||||
onPressed: () => _printLabel(context, item),
|
||||
child: const Text('打标签', style: TextStyle(fontSize: 13)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 库存预警:窄屏卡片
|
||||
Widget _warningCard(Inventory item) {
|
||||
return MobileListCard(
|
||||
onTap: item.productId != null
|
||||
? () => context.push('/products/${item.productId}')
|
||||
: null,
|
||||
title: Text(item.productName.isEmpty ? '-' : item.productName),
|
||||
subtitle: item.productCode.isEmpty ? null : Text(item.productCode),
|
||||
trailing: _InventoryStatusBadge(item),
|
||||
fields: [
|
||||
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
|
||||
MobileCardField('当前库存', null,
|
||||
valueWidget: Text(
|
||||
item.quantity.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: item.quantity == 0 ? AppTheme.danger : AppTheme.accent),
|
||||
)),
|
||||
MobileCardField('安全库存', '${item.minStock}'),
|
||||
MobileCardField('缺口', null,
|
||||
valueWidget: Text(
|
||||
'${item.minStock! - item.quantity.toInt()}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.danger,
|
||||
fontWeight: FontWeight.w600),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 流水记录:窄屏卡片
|
||||
Widget _logCard(dynamic log) {
|
||||
return MobileListCard(
|
||||
title: Text(log.productName ?? '-'),
|
||||
subtitle: Text(log.createdAt?.substring(0, 19) ?? '-'),
|
||||
trailing: _DirectionBadge(log.direction),
|
||||
fields: [
|
||||
MobileCardField('仓库', log.warehouseName ?? '-'),
|
||||
MobileCardField('数量', null,
|
||||
valueWidget: Text(
|
||||
log.quantity.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: log.direction == 'in'
|
||||
? AppTheme.success
|
||||
: AppTheme.danger),
|
||||
)),
|
||||
MobileCardField('变前', log.qtyBefore?.toStringAsFixed(0) ?? '-'),
|
||||
MobileCardField('变后', log.qtyAfter?.toStringAsFixed(0) ?? '-'),
|
||||
if (log.refType != null) MobileCardField('来源', log.refType),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageScaffold(
|
||||
@@ -251,35 +380,45 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Summary cards
|
||||
Container(
|
||||
color: AppTheme.background,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
_SummaryCard(
|
||||
title: '商品总数',
|
||||
value: '${result.total}',
|
||||
unit: '种',
|
||||
icon: Icons.inventory_2,
|
||||
color: AppTheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '库存预警',
|
||||
value: '$warningCount',
|
||||
unit: '种',
|
||||
icon: Icons.warning_amber,
|
||||
color: AppTheme.accent),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '缺货商品',
|
||||
value: '$emptyCount',
|
||||
unit: '种',
|
||||
icon: Icons.remove_shopping_cart,
|
||||
color: AppTheme.danger),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Summary cards(窄屏横向滚动,宽屏等分)
|
||||
Builder(builder: (context) {
|
||||
final w = context.isMobile ? 150.0 : null;
|
||||
final cards = [
|
||||
_SummaryCard(
|
||||
title: '商品总数',
|
||||
value: '${result.total}',
|
||||
unit: '种',
|
||||
icon: Icons.inventory_2,
|
||||
color: AppTheme.primary,
|
||||
width: w),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '库存预警',
|
||||
value: '$warningCount',
|
||||
unit: '种',
|
||||
icon: Icons.warning_amber,
|
||||
color: AppTheme.accent,
|
||||
width: w),
|
||||
const SizedBox(width: 12),
|
||||
_SummaryCard(
|
||||
title: '缺货商品',
|
||||
value: '$emptyCount',
|
||||
unit: '种',
|
||||
icon: Icons.remove_shopping_cart,
|
||||
color: AppTheme.danger,
|
||||
width: w),
|
||||
];
|
||||
return Container(
|
||||
color: AppTheme.background,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: context.isMobile
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: cards),
|
||||
)
|
||||
: Row(children: cards),
|
||||
);
|
||||
}),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: DataTableCard(
|
||||
@@ -346,6 +485,8 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
mobileCards:
|
||||
filteredItems.map(_inventoryCard).toList(),
|
||||
columns: [
|
||||
const DataColumn(label: Text('商品编码')),
|
||||
const DataColumn(label: Text('商品名称')),
|
||||
@@ -580,6 +721,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
mobileCards: warnings.map(_warningCard).toList(),
|
||||
columns: const [
|
||||
DataColumn(label: Text('商品编码')),
|
||||
DataColumn(label: Text('商品名称')),
|
||||
@@ -717,6 +859,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
mobileCards: logs.map(_logCard).toList(),
|
||||
columns: const [
|
||||
DataColumn(label: Text('商品名称')),
|
||||
DataColumn(label: Text('仓库')),
|
||||
@@ -788,18 +931,21 @@ class _SummaryCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
/// 固定宽度(窄屏横向滚动用);为 null 时占据等分宽度(Expanded,宽屏用)。
|
||||
final double? width;
|
||||
|
||||
const _SummaryCard({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
final card = Container(
|
||||
height: 72,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
@@ -849,8 +995,10 @@ class _SummaryCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
return width != null
|
||||
? SizedBox(width: width, child: card)
|
||||
: Expanded(child: card);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,7 +1098,7 @@ class _ImportProgressDialog extends StatelessWidget {
|
||||
content: ValueListenableBuilder<_ImportState>(
|
||||
valueListenable: stateNotifier,
|
||||
builder: (_, state, __) => SizedBox(
|
||||
width: 320,
|
||||
width: context.dialogWidth(320),
|
||||
child: _buildContent(context, state),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,10 +3,12 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/models/page_result.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/partner.dart';
|
||||
import '../../providers/partner_provider.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../core/utils/export_util.dart';
|
||||
|
||||
@@ -145,12 +147,48 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
required void Function(Partner) onDelete,
|
||||
}) {
|
||||
final partners = result.data;
|
||||
|
||||
Widget partnerCard(Partner p) {
|
||||
return MobileListCard(
|
||||
title: Text(p.name),
|
||||
subtitle: p.code != null ? Text(p.code!) : null,
|
||||
trailing: Text(
|
||||
p.status == 'enabled' ? '正常' : '禁用',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: p.status == 'enabled'
|
||||
? AppTheme.success
|
||||
: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
fields: [
|
||||
if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact),
|
||||
if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone),
|
||||
if (p.address?.isNotEmpty == true) MobileCardField('地址', p.address),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
key: Key('btn_edit_${p.id}'),
|
||||
onPressed: () => onEdit(p),
|
||||
child: const Text('编辑', style: TextStyle(fontSize: 13)),
|
||||
),
|
||||
TextButton(
|
||||
key: Key('btn_delete_${p.id}'),
|
||||
onPressed: () => onDelete(p),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return DataTableCard(
|
||||
totalCount: result.total,
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
onPageChanged: onPageChanged,
|
||||
onPageSizeChanged: onPageSizeChanged,
|
||||
mobileCards: partners.map(partnerCard).toList(),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
@@ -433,7 +471,7 @@ class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
|
||||
final typeName = widget.isSupplier ? '供应商' : '客户';
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 520,
|
||||
width: context.dialogWidth(520),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../providers/product_option_provider.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../core/utils/export_util.dart';
|
||||
|
||||
@@ -84,6 +86,23 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
|
||||
),
|
||||
),
|
||||
mobileCards: paged
|
||||
.map((it) => _optionCard(
|
||||
code: it.code,
|
||||
name: it.name,
|
||||
remark: it.remark,
|
||||
onEdit: () => _showOptionDialog(
|
||||
title: '编辑商品名称',
|
||||
hasQuantity: false,
|
||||
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||
onSave: (data) => ref.read(productNameListProvider.notifier).updateItem(it.id, data),
|
||||
),
|
||||
onDelete: () => _confirmDelete(
|
||||
'删除名称「${it.name}」?',
|
||||
() => ref.read(productNameListProvider.notifier).delete(it.id),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
columns: const [
|
||||
DataColumn(label: Text('编号')),
|
||||
DataColumn(label: Text('名称')),
|
||||
@@ -156,6 +175,23 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
|
||||
),
|
||||
),
|
||||
mobileCards: paged
|
||||
.map((it) => _optionCard(
|
||||
code: it.code,
|
||||
name: it.name,
|
||||
remark: it.remark,
|
||||
onEdit: () => _showOptionDialog(
|
||||
title: '编辑系列',
|
||||
hasQuantity: false,
|
||||
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||
onSave: (data) => ref.read(productSeriesListProvider.notifier).updateItem(it.id, data),
|
||||
),
|
||||
onDelete: () => _confirmDelete(
|
||||
'删除系列「${it.name}」?',
|
||||
() => ref.read(productSeriesListProvider.notifier).delete(it.id),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
columns: const [
|
||||
DataColumn(label: Text('编号')),
|
||||
DataColumn(label: Text('系列名称')),
|
||||
@@ -228,6 +264,24 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
rows: items.map((it) => [it.code ?? '', it.name, it.quantity, it.remark ?? '']).toList(),
|
||||
),
|
||||
),
|
||||
mobileCards: paged
|
||||
.map((it) => _optionCard(
|
||||
code: it.code,
|
||||
name: it.name,
|
||||
remark: it.remark,
|
||||
quantity: it.quantity,
|
||||
onEdit: () => _showOptionDialog(
|
||||
title: '编辑规格',
|
||||
hasQuantity: true,
|
||||
initial: {'code': it.code ?? '', 'name': it.name, 'quantity': it.quantity, 'remark': it.remark ?? ''},
|
||||
onSave: (data) => ref.read(productSpecListProvider.notifier).updateItem(it.id, data),
|
||||
),
|
||||
onDelete: () => _confirmDelete(
|
||||
'删除规格「${it.name}」?',
|
||||
() => ref.read(productSpecListProvider.notifier).delete(it.id),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
columns: const [
|
||||
DataColumn(label: Text('编号')),
|
||||
DataColumn(label: Text('规格名称')),
|
||||
@@ -321,6 +375,37 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 基础数据:窄屏卡片
|
||||
Widget _optionCard({
|
||||
required String? code,
|
||||
required String name,
|
||||
required String? remark,
|
||||
int? quantity,
|
||||
required VoidCallback onEdit,
|
||||
required VoidCallback onDelete,
|
||||
}) {
|
||||
return MobileListCard(
|
||||
title: Text(name),
|
||||
subtitle: (code?.isNotEmpty == true) ? Text(code!) : null,
|
||||
fields: [
|
||||
if (quantity != null && quantity > 0)
|
||||
MobileCardField('单品数量', '$quantity'),
|
||||
if (remark?.isNotEmpty == true) MobileCardField('备注', remark),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: onEdit,
|
||||
child: const Text('编辑', style: TextStyle(fontSize: 13)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onDelete,
|
||||
child: const Text('删除',
|
||||
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionButtons({required VoidCallback onEdit, required VoidCallback onDelete}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -386,7 +471,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
width: context.dialogWidth(360),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/auth/auth_state.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/number_rule.dart';
|
||||
import '../../models/user.dart';
|
||||
@@ -536,7 +537,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('编辑编号规则 — ${rule.typeLabel}'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
width: context.dialogWidth(360),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -708,7 +709,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('修改$label'),
|
||||
content: SizedBox(
|
||||
width: 320,
|
||||
width: context.dialogWidth(320),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
@@ -838,7 +839,7 @@ class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
|
||||
return AlertDialog(
|
||||
title: Text(widget.warehouse != null ? '编辑仓库' : '新建仓库'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
width: context.dialogWidth(400),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
@@ -971,7 +972,7 @@ class _UserFormDialogState extends ConsumerState<_UserFormDialog> {
|
||||
return AlertDialog(
|
||||
title: Text(_isEdit ? '编辑用户' : '新增用户'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
width: context.dialogWidth(400),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
@@ -1084,7 +1085,7 @@ class _ResetPasswordDialogState extends ConsumerState<_ResetPasswordDialog> {
|
||||
return AlertDialog(
|
||||
title: Text('重置密码 — ${widget.user.username}'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
width: context.dialogWidth(360),
|
||||
child: TextField(
|
||||
controller: _ctrl,
|
||||
obscureText: true,
|
||||
@@ -1625,7 +1626,7 @@ class _BatchImportWidgetState extends ConsumerState<_BatchImportWidget> {
|
||||
const Text('确认清空数据', style: TextStyle(color: Colors.red)),
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
width: context.dialogWidth(400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -2035,7 +2036,7 @@ class _ShopEditDialogState extends ConsumerState<_ShopEditDialog> {
|
||||
return AlertDialog(
|
||||
title: const Text('编辑酒行信息'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
width: context.dialogWidth(400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
|
||||
import 'dart:async';
|
||||
import '../../core/auth/auth_state.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../providers/connectivity_provider.dart';
|
||||
import '../../providers/shop_provider.dart';
|
||||
@@ -24,6 +25,7 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
bool _sidebarExpanded = true;
|
||||
final String _loginTime = DateFormat('HH:mm:ss').format(DateTime.now());
|
||||
bool _forceDialogShown = false;
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
void _showForceUpdateDialog(
|
||||
BuildContext context, AppUpdateInfo info) {
|
||||
@@ -69,11 +71,86 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
_NavItem(icon: Icons.info_outline, label: '关于我们', path: '/about'),
|
||||
];
|
||||
|
||||
/// 窄屏(手机)侧滑抽屉导航,容纳全部菜单项。
|
||||
Widget _buildDrawer(
|
||||
BuildContext context, AuthUser? user, String location) {
|
||||
final shopAsync = ref.watch(shopInfoProvider);
|
||||
final shopName = shopAsync.valueOrNull?.name ?? user?.shopNo ?? '';
|
||||
final logoUrl = shopAsync.valueOrNull?.logoUrl ?? '';
|
||||
return Drawer(
|
||||
child: Container(
|
||||
color: AppTheme.primaryDark,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 抽屉头部:门店 Logo + 名称 + 编号
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
||||
color: AppTheme.primary,
|
||||
child: Row(
|
||||
children: [
|
||||
_ShopLogo(
|
||||
logoUrl: logoUrl, shopName: shopName, size: 40),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
shopName.isEmpty ? '—' : shopName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (user != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${user.shopNo} · ${user.username}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: _navItems.map((item) {
|
||||
final isActive = location.startsWith(item.path);
|
||||
return _SidebarItem(
|
||||
item: item,
|
||||
isActive: isActive,
|
||||
expanded: true,
|
||||
onTap: () {
|
||||
Navigator.pop(context); // 关闭抽屉
|
||||
context.go(item.path);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(authStateProvider).user;
|
||||
final isOnline = ref.watch(connectivityProvider);
|
||||
final location = GoRouterState.of(context).matchedLocation;
|
||||
final isMobile = context.isMobile;
|
||||
final sidebarWidth = _sidebarExpanded ? 200.0 : 56.0;
|
||||
final updateNotifier = ref.watch(updateProvider.notifier);
|
||||
final updateInfo = ref.watch(updateProvider).valueOrNull;
|
||||
@@ -82,6 +159,8 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
|
||||
return SelectionArea(
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: isMobile ? _buildDrawer(context, user, location) : null,
|
||||
body: Column(
|
||||
children: [
|
||||
// Top Bar
|
||||
@@ -93,41 +172,54 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_sidebarExpanded ? Icons.menu_open : Icons.menu,
|
||||
isMobile
|
||||
? Icons.menu
|
||||
: (_sidebarExpanded ? Icons.menu_open : Icons.menu),
|
||||
color: Colors.white),
|
||||
onPressed: () =>
|
||||
setState(() => _sidebarExpanded = !_sidebarExpanded),
|
||||
tooltip: _sidebarExpanded ? '收起侧边栏' : '展开侧边栏',
|
||||
onPressed: () {
|
||||
if (isMobile) {
|
||||
_scaffoldKey.currentState?.openDrawer();
|
||||
} else {
|
||||
setState(() => _sidebarExpanded = !_sidebarExpanded);
|
||||
}
|
||||
},
|
||||
tooltip: isMobile
|
||||
? '菜单'
|
||||
: (_sidebarExpanded ? '收起侧边栏' : '展开侧边栏'),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_ShopButton(user: user, version: appVersion),
|
||||
const Spacer(),
|
||||
if (user != null) ...[
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showShopPanel(context, user, version: appVersion),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.business,
|
||||
color: Colors.white70, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(user.shopNo,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 13)),
|
||||
],
|
||||
// 窄屏隐藏门店号/用户名文字块,仅保留用户菜单,避免顶栏拥挤
|
||||
if (!isMobile) ...[
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
_showShopPanel(context, user, version: appVersion),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.business,
|
||||
color: Colors.white70, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(user.shopNo,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
const Icon(Icons.person_outline,
|
||||
color: Colors.white70, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(user.username,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 13)),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 20),
|
||||
const Icon(Icons.person_outline,
|
||||
color: Colors.white70, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(user.username,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70, fontSize: 13)),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.keyboard_arrow_down,
|
||||
color: Colors.white70),
|
||||
@@ -172,32 +264,33 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Sidebar
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
width: sidebarWidth,
|
||||
color: AppTheme.primaryDark,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: _navItems.map((item) {
|
||||
final isActive =
|
||||
location.startsWith(item.path);
|
||||
return _SidebarItem(
|
||||
item: item,
|
||||
isActive: isActive,
|
||||
expanded: _sidebarExpanded,
|
||||
onTap: () => context.go(item.path),
|
||||
);
|
||||
}).toList(),
|
||||
// Sidebar(仅宽屏;窄屏改用 Drawer)
|
||||
if (!isMobile)
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
width: sidebarWidth,
|
||||
color: AppTheme.primaryDark,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: _navItems.map((item) {
|
||||
final isActive =
|
||||
location.startsWith(item.path);
|
||||
return _SidebarItem(
|
||||
item: item,
|
||||
isActive: isActive,
|
||||
expanded: _sidebarExpanded,
|
||||
onTap: () => context.go(item.path),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content area
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -278,7 +371,8 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
),
|
||||
),
|
||||
Expanded(child: widget.child),
|
||||
// Status bar
|
||||
// Status bar(仅宽屏;窄屏隐藏,离线已有顶部横幅提示)
|
||||
if (!isMobile)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final w = constraints.maxWidth;
|
||||
@@ -515,7 +609,7 @@ void _showShopPanel(BuildContext context, AuthUser u, {String version = 'v1.0.0'
|
||||
builder: (ctx) => Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: SizedBox(
|
||||
width: 360,
|
||||
width: ctx.dialogWidth(360),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/auth/auth_state.dart';
|
||||
@@ -849,8 +850,14 @@ class _FormField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 窄屏(手机)字段占满整行,便于点选;宽屏沿用固定宽度配合 Wrap 多列。
|
||||
final effectiveWidth = width == double.infinity
|
||||
? double.infinity
|
||||
: (context.isMobile
|
||||
? MediaQuery.sizeOf(context).width - 64
|
||||
: width);
|
||||
return SizedBox(
|
||||
width: width,
|
||||
width: effectiveWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -4,11 +4,13 @@ import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, pri
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../providers/stock_in_provider.dart';
|
||||
import '../../repositories/stock_in_repository.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../widgets/status_badge.dart';
|
||||
@@ -239,88 +241,10 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
return DataCell(SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _showDetail(context, o.id),
|
||||
child: const Text('详情',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||
if (context.mounted) {
|
||||
await safePrint(context, () => printStockInOrder(order));
|
||||
}
|
||||
},
|
||||
child: const Text('打印',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||
if (!context.mounted) return;
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => _LabelPrintDialog(
|
||||
order: order,
|
||||
productRepo: ref.read(productRepositoryProvider),
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('打标签',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
if (o.status == 'approved') ...[
|
||||
TextButton(
|
||||
onPressed: () => _confirmSettle(context, o.id, 'stock_in'),
|
||||
child: const Text('结清',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
|
||||
),
|
||||
],
|
||||
if (o.status == 'draft') ...[
|
||||
TextButton(
|
||||
onPressed: () => context.go('/stock-in/edit/${o.id}'),
|
||||
child: const Text('修改',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmDelete(context, o),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmSubmit(context, o),
|
||||
child: const Text('提交',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
],
|
||||
if (o.status == 'pending') ...[
|
||||
TextButton(
|
||||
key: Key('btn_approve_${o.id}'),
|
||||
onPressed: () => _confirmApprove(context, o),
|
||||
child: const Text('通过',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.success)),
|
||||
),
|
||||
TextButton(
|
||||
key: Key('btn_reject_${o.id}'),
|
||||
onPressed: () => _confirmReject(context, o),
|
||||
child: const Text('拒绝',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
],
|
||||
)));
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _orderActions(context, o),
|
||||
),
|
||||
));
|
||||
default:
|
||||
return const DataCell(SizedBox());
|
||||
}
|
||||
@@ -414,6 +338,105 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
),
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 操作按钮列表,表格与移动端卡片共用。
|
||||
List<Widget> _orderActions(BuildContext context, StockInOrder o) {
|
||||
return [
|
||||
TextButton(
|
||||
onPressed: () => _showDetail(context, o.id),
|
||||
child: const Text('详情',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||
if (context.mounted) {
|
||||
await safePrint(context, () => printStockInOrder(order));
|
||||
}
|
||||
},
|
||||
child: const Text('打印',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||
if (!context.mounted) return;
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => _LabelPrintDialog(
|
||||
order: order,
|
||||
productRepo: ref.read(productRepositoryProvider),
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('打标签',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
if (o.status == 'approved')
|
||||
TextButton(
|
||||
onPressed: () => _confirmSettle(context, o.id, 'stock_in'),
|
||||
child: const Text('结清',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
|
||||
),
|
||||
if (o.status == 'draft') ...[
|
||||
TextButton(
|
||||
onPressed: () => context.go('/stock-in/edit/${o.id}'),
|
||||
child: const Text('修改',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmDelete(context, o),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmSubmit(context, o),
|
||||
child: const Text('提交',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
],
|
||||
if (o.status == 'pending') ...[
|
||||
TextButton(
|
||||
key: Key('btn_approve_${o.id}'),
|
||||
onPressed: () => _confirmApprove(context, o),
|
||||
child: const Text('通过',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.success)),
|
||||
),
|
||||
TextButton(
|
||||
key: Key('btn_reject_${o.id}'),
|
||||
onPressed: () => _confirmReject(context, o),
|
||||
child: const Text('拒绝',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/// 入库单:窄屏卡片
|
||||
Widget _orderCard(BuildContext context, StockInOrder o) {
|
||||
return MobileListCard(
|
||||
onTap: () => _showDetail(context, o.id),
|
||||
title: Text(o.orderNo,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
|
||||
trailing: StatusBadge(_apiStatusToEnum(o.status)),
|
||||
fields: [
|
||||
MobileCardField('供应商', o.partnerName ?? '-'),
|
||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||
MobileCardField(
|
||||
'金额',
|
||||
o.totalAmount != null
|
||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
||||
: '-'),
|
||||
MobileCardField('日期', o.orderDate?.substring(0, 10) ?? '-'),
|
||||
],
|
||||
actions: _orderActions(context, o),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -668,7 +691,7 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 720,
|
||||
width: context.dialogWidth(720),
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -965,7 +988,7 @@ class _LabelPrintDialogState extends State<_LabelPrintDialog> {
|
||||
final items = widget.order.items;
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 520,
|
||||
width: context.dialogWidth(520),
|
||||
constraints: const BoxConstraints(maxHeight: 520),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/auth/auth_state.dart';
|
||||
import '../../core/utils/print_util.dart';
|
||||
@@ -683,8 +684,14 @@ class _FormField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 窄屏(手机)字段占满整行,便于点选;宽屏沿用固定宽度配合 Wrap 多列。
|
||||
final effectiveWidth = width == double.infinity
|
||||
? double.infinity
|
||||
: (context.isMobile
|
||||
? MediaQuery.sizeOf(context).width - 64
|
||||
: width);
|
||||
return SizedBox(
|
||||
width: width,
|
||||
width: effectiveWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -742,7 +749,7 @@ class _InventoryPickerDialogState extends State<_InventoryPickerDialog> {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
child: SizedBox(
|
||||
width: 820,
|
||||
width: context.dialogWidth(820),
|
||||
height: 580,
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
@@ -5,11 +5,13 @@ import '../../core/utils/print_util.dart' show safePrint, printStockOutOrder, pr
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
import '../../providers/stock_out_provider.dart';
|
||||
import '../../repositories/stock_out_repository.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../widgets/status_badge.dart';
|
||||
@@ -246,69 +248,10 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
return DataCell(SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _showDetail(context, o.id),
|
||||
child: const Text('详情',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
|
||||
if (context.mounted) {
|
||||
await safePrint(context, () => printStockOutOrder(order));
|
||||
}
|
||||
},
|
||||
child: const Text('打印',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
if (o.status == 'approved') ...[
|
||||
TextButton(
|
||||
onPressed: () => _confirmSettle(context, o.id, 'stock_out'),
|
||||
child: const Text('结清',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
|
||||
),
|
||||
],
|
||||
if (o.status == 'draft') ...[
|
||||
TextButton(
|
||||
onPressed: () => context.go('/stock-out/edit/${o.id}'),
|
||||
child: const Text('修改',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmDelete(context, o),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmSubmit(context, o),
|
||||
child: const Text('提交',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
],
|
||||
if (o.status == 'pending') ...[
|
||||
TextButton(
|
||||
key: Key('btn_approve_${o.id}'),
|
||||
onPressed: () => _confirmApprove(context, o),
|
||||
child: const Text('通过',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.success)),
|
||||
),
|
||||
TextButton(
|
||||
key: Key('btn_reject_${o.id}'),
|
||||
onPressed: () => _confirmReject(context, o),
|
||||
child: const Text('拒绝',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
],
|
||||
)));
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _orderActions(context, o),
|
||||
),
|
||||
));
|
||||
default:
|
||||
return const DataCell(SizedBox());
|
||||
}
|
||||
@@ -402,6 +345,86 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
),
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 操作按钮列表,表格与移动端卡片共用。
|
||||
List<Widget> _orderActions(BuildContext context, StockOutOrder o) {
|
||||
return [
|
||||
TextButton(
|
||||
onPressed: () => _showDetail(context, o.id),
|
||||
child: const Text('详情',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
|
||||
if (context.mounted) {
|
||||
await safePrint(context, () => printStockOutOrder(order));
|
||||
}
|
||||
},
|
||||
child: const Text('打印',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
if (o.status == 'approved')
|
||||
TextButton(
|
||||
onPressed: () => _confirmSettle(context, o.id, 'stock_out'),
|
||||
child: const Text('结清',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
|
||||
),
|
||||
if (o.status == 'draft') ...[
|
||||
TextButton(
|
||||
onPressed: () => context.go('/stock-out/edit/${o.id}'),
|
||||
child: const Text('修改',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmDelete(context, o),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmSubmit(context, o),
|
||||
child: const Text('提交',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
],
|
||||
if (o.status == 'pending') ...[
|
||||
TextButton(
|
||||
key: Key('btn_approve_${o.id}'),
|
||||
onPressed: () => _confirmApprove(context, o),
|
||||
child: const Text('通过',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.success)),
|
||||
),
|
||||
TextButton(
|
||||
key: Key('btn_reject_${o.id}'),
|
||||
onPressed: () => _confirmReject(context, o),
|
||||
child: const Text('拒绝',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/// 出库单:窄屏卡片
|
||||
Widget _orderCard(BuildContext context, StockOutOrder o) {
|
||||
return MobileListCard(
|
||||
onTap: () => _showDetail(context, o.id),
|
||||
title: Text(o.orderNo,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
|
||||
trailing: StatusBadge(_apiStatusToEnum(o.status)),
|
||||
fields: [
|
||||
MobileCardField('客户', o.partnerName ?? '-'),
|
||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||
MobileCardField(
|
||||
'金额',
|
||||
o.totalAmount != null
|
||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
||||
: '-'),
|
||||
MobileCardField('日期', o.orderDate?.substring(0, 10) ?? '-'),
|
||||
],
|
||||
actions: _orderActions(context, o),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -660,7 +683,7 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 720,
|
||||
width: context.dialogWidth(720),
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -961,7 +984,7 @@ class _LabelPrintDialogState extends State<_LabelPrintDialog> {
|
||||
final items = widget.order.items;
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 520,
|
||||
width: context.dialogWidth(520),
|
||||
constraints: const BoxConstraints(maxHeight: 520),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/responsive/responsive.dart';
|
||||
import '../core/theme/app_theme.dart';
|
||||
|
||||
class DataTableCard extends StatefulWidget {
|
||||
@@ -11,6 +12,10 @@ class DataTableCard extends StatefulWidget {
|
||||
final ValueChanged<int>? onPageChanged;
|
||||
final ValueChanged<int>? onPageSizeChanged;
|
||||
|
||||
/// 窄屏(手机)卡片列表。与 [rows] 同序、同数量;提供后窄屏渲染卡片流,
|
||||
/// 宽屏仍渲染表格。为 null 时窄屏也保持表格(横向滚动)。
|
||||
final List<Widget>? mobileCards;
|
||||
|
||||
const DataTableCard({
|
||||
super.key,
|
||||
required this.columns,
|
||||
@@ -21,6 +26,7 @@ class DataTableCard extends StatefulWidget {
|
||||
this.pageSize = 20,
|
||||
this.onPageChanged,
|
||||
this.onPageSizeChanged,
|
||||
this.mobileCards,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -49,30 +55,9 @@ class _DataTableCardState extends State<DataTableCard> {
|
||||
),
|
||||
if (widget.toolbar != null) const Divider(height: 1),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: Table(
|
||||
defaultColumnWidth: const IntrinsicColumnWidth(),
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
_buildHeaderRow(),
|
||||
for (int i = 0; i < widget.rows.length; i++)
|
||||
_buildDataRow(i, widget.rows[i]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: (context.isMobile && widget.mobileCards != null)
|
||||
? _buildMobileList()
|
||||
: _buildTable(),
|
||||
),
|
||||
// Pagination
|
||||
if (widget.totalCount != null)
|
||||
@@ -102,6 +87,52 @@ class _DataTableCardState extends State<DataTableCard> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTable() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: Table(
|
||||
defaultColumnWidth: const IntrinsicColumnWidth(),
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
_buildHeaderRow(),
|
||||
for (int i = 0; i < widget.rows.length; i++)
|
||||
_buildDataRow(i, widget.rows[i]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileList() {
|
||||
final cards = widget.mobileCards!;
|
||||
if (cards.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Text('暂无数据',
|
||||
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: cards.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (_, i) => cards[i],
|
||||
);
|
||||
}
|
||||
|
||||
TableRow _buildHeaderRow() {
|
||||
return TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/theme/app_theme.dart';
|
||||
|
||||
/// 移动端列表卡片的单个字段(label: value)。
|
||||
class MobileCardField {
|
||||
final String label;
|
||||
final String? value;
|
||||
|
||||
/// 自定义值控件(如带颜色的金额、状态徽章);提供后忽略 [value]。
|
||||
final Widget? valueWidget;
|
||||
const MobileCardField(this.label, this.value, {this.valueWidget});
|
||||
}
|
||||
|
||||
/// 窄屏(手机)列表的通用卡片:标题 + 右上角徽章 + 字段竖排 + 底部操作。
|
||||
/// 替代宽屏的表格行,使一行数据在手机上可一屏读完、操作可点。
|
||||
class MobileListCard extends StatelessWidget {
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
final Widget? trailing;
|
||||
final List<MobileCardField> fields;
|
||||
final List<Widget>? actions;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const MobileListCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.trailing,
|
||||
this.fields = const [],
|
||||
this.actions,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
child: title,
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary),
|
||||
child: subtitle!,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
trailing!,
|
||||
],
|
||||
],
|
||||
),
|
||||
if (fields.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
...fields.map(_buildField),
|
||||
],
|
||||
if (actions != null && actions!.isNotEmpty) ...[
|
||||
const Divider(height: 18),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
children: actions!,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField(MobileCardField f) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Text(
|
||||
f.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppTheme.textSecondary),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: f.valueWidget ??
|
||||
Text(
|
||||
f.value ?? '—',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppTheme.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import '../core/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/responsive/responsive.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
import '../core/theme/app_theme.dart';
|
||||
|
||||
@@ -135,7 +136,7 @@ class _SearchDialogState extends State<_SearchDialog> {
|
||||
title: Text(widget.title, style: const TextStyle(fontSize: 16)),
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
width: context.dialogWidth(300),
|
||||
height: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/responsive/responsive.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
import '../models/product.dart';
|
||||
|
||||
@@ -69,7 +70,7 @@ class _SelectProductDialogState extends State<SelectProductDialog> {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: SizedBox(
|
||||
width: 480,
|
||||
width: context.dialogWidth(480),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
||||
@@ -69,10 +69,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85"
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -183,10 +183,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.34"
|
||||
version: "2.0.35"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -217,10 +217,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -353,10 +353,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -553,10 +553,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||
sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.23"
|
||||
version: "2.4.25"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -654,10 +654,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -678,10 +678,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
|
||||
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.30"
|
||||
version: "6.3.32"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -787,5 +787,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.10.3 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# render-env.sh — 从 Bitwarden 渲染 jiu 的 production.env(方案一)
|
||||
#
|
||||
# 配置基底 = production.env.template(非密钥项的权威来源,随仓库维护)。
|
||||
# 密钥 = 从 Bitwarden 条目 DB_PASSWORD 的同名字段按需取,Mac 上不常驻明文。
|
||||
#
|
||||
# 用法:
|
||||
# sh deploy/render-env.sh --check # 只校验:4 个密钥都能从金库取到、长度非空
|
||||
# sh deploy/render-env.sh --print # 渲染到 stdout 预览(密钥打码,可安全粘贴)
|
||||
# sh deploy/render-env.sh --out FILE # 渲染到指定文件(含明文,慎用,自负保管)
|
||||
# sh deploy/render-env.sh --deploy # 渲染→scp 到 EC2→重启 jiu→健康检查→删本地临时文件
|
||||
#
|
||||
# 前提:rbw 已 unlock。
|
||||
set -euo pipefail
|
||||
|
||||
# ---- 可配置项 ----
|
||||
RBW="${RBW_BIN:-/opt/homebrew/bin/rbw}"
|
||||
BW_ITEM="${JIU_BW_ITEM:-DB_PASSWORD}" # 存放 jiu 密钥字段的 Bitwarden 条目
|
||||
SECRET_KEYS="DATABASE_DSN JWT_SECRET LICENSE_HMAC_SECRET DB_PASSWORD"
|
||||
|
||||
EC2_HOST="${EC2_HOST:-18.136.60.128}"
|
||||
EC2_USER="${EC2_USER:-ec2-user}"
|
||||
EC2_KEY="${EC2_KEY:-$HOME/.ssh/wangjia.pem}"
|
||||
REMOTE_ENV="/opt/jiu/config/production.env"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
TEMPLATE="$SCRIPT_DIR/production.env.template"
|
||||
|
||||
# ---- 工具函数 ----
|
||||
is_secret() {
|
||||
case " $SECRET_KEYS " in *" $1 "*) return 0;; *) return 1;; esac
|
||||
}
|
||||
|
||||
require_unlocked() {
|
||||
if ! "$RBW" unlocked >/dev/null 2>&1; then
|
||||
echo "render-env: 金库未解锁,请先运行: rbw unlock" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 渲染:逐行读模板,密钥行用金库值替换。$1=mask 时密钥打码。
|
||||
render() {
|
||||
local mask="${1:-}"
|
||||
local line key val
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
''|\#*) printf '%s\n' "$line"; continue ;;
|
||||
esac
|
||||
key="${line%%=*}"
|
||||
if is_secret "$key"; then
|
||||
val="$("$RBW" get --field "$key" "$BW_ITEM")"
|
||||
if [ -z "$val" ]; then
|
||||
echo "render-env: 金库条目 '$BW_ITEM' 的字段 '$key' 为空" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$mask" = "mask" ]; then
|
||||
printf '%s=<%d 字符·已打码>\n' "$key" "${#val}"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$val"
|
||||
fi
|
||||
else
|
||||
printf '%s\n' "$line"
|
||||
fi
|
||||
done < "$TEMPLATE"
|
||||
}
|
||||
|
||||
# ---- 主流程 ----
|
||||
[ -f "$TEMPLATE" ] || { echo "render-env: 找不到模板 $TEMPLATE" >&2; exit 1; }
|
||||
MODE="${1:---print}"
|
||||
require_unlocked
|
||||
|
||||
case "$MODE" in
|
||||
--check)
|
||||
for k in $SECRET_KEYS; do
|
||||
v="$("$RBW" get --field "$k" "$BW_ITEM")"
|
||||
printf ' %-22s %s\n' "$k" "$([ -n "$v" ] && echo "OK (${#v} 字符)" || echo "缺失 ❌")"
|
||||
done
|
||||
;;
|
||||
|
||||
--print)
|
||||
render mask
|
||||
;;
|
||||
|
||||
--out)
|
||||
OUT="${2:?用法: render-env.sh --out <文件路径>}"
|
||||
umask 077
|
||||
render > "$OUT"
|
||||
echo "render-env: 已写入 $OUT(含明文,注意保管)"
|
||||
;;
|
||||
|
||||
--deploy)
|
||||
TMP="$(mktemp -t jiu-prod-env)"
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
umask 077
|
||||
render > "$TMP"
|
||||
echo "render-env: 已本地渲染(临时文件,退出即删)"
|
||||
echo "render-env: 上传到 $EC2_USER@$EC2_HOST:$REMOTE_ENV"
|
||||
scp -i "$EC2_KEY" "$TMP" "$EC2_USER@$EC2_HOST:/tmp/production.env.new"
|
||||
ssh -i "$EC2_KEY" "$EC2_USER@$EC2_HOST" bash <<REMOTE
|
||||
set -e
|
||||
mkdir -p /opt/jiu/config
|
||||
mv /tmp/production.env.new "$REMOTE_ENV"
|
||||
chmod 600 "$REMOTE_ENV"
|
||||
sudo systemctl restart jiu
|
||||
sleep 3
|
||||
curl -sf http://localhost:8080/health >/dev/null && echo " 健康检查通过 ✅" || { echo " 健康检查失败 ❌"; exit 1; }
|
||||
REMOTE
|
||||
echo "render-env: 部署完成,EC2 上的 production.env 已更新并重启"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "未知参数: $MODE" >&2
|
||||
sed -n '2,16p' "$0" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,14 @@
|
||||
# TODO(待办 / 后续迭代)
|
||||
|
||||
本文件收集已明确「本期不做、后续再做」的事项,避免遗漏。
|
||||
|
||||
## 移动端适配
|
||||
|
||||
- [ ] **iOS 上架与分发(待 Apple 账号)**:iOS 工程与 TestFlight 流水线已就绪(见 `docs/ios-signing.md`),待注册 Apple Developer 账号、配置证书/Profile/API Key 等 Forgejo secrets 后即可自动上传 TestFlight;后续可考虑 App Store 正式上架。
|
||||
- [ ] **Android 应用内 APK 静默安装**:当前 Android「立即更新」仅打开下载链接;后续做应用内下载 APK + `REQUEST_INSTALL_PACKAGES` 触发安装。
|
||||
- [ ] **平板专属三栏布局**:当前仅手机窄屏 + 桌面两档;平板(600–1200px)后续可做更优的三栏/双栏布局。
|
||||
|
||||
## 意见反馈(v1.0.17 已上线)
|
||||
|
||||
- [ ] **管理端反馈查看 UI**:当前仅有 admin API(`GET /api/v1/admin/feedback`、`PATCH /api/v1/admin/feedback/:id`),app 内尚无超管查看/处理反馈的界面。
|
||||
- [ ] **新反馈通知**:提交反馈后暂无 Telegram/邮件通知,后续可加。
|
||||
@@ -0,0 +1,51 @@
|
||||
# Android 正式签名 — 一次性配置
|
||||
|
||||
CI 用正式 release keystore 给 APK 签名。keystore **不入库**,通过 Forgejo Secrets 注入。
|
||||
本文档是一次性操作,配置完成后每次打 tag 自动签名打包。
|
||||
|
||||
> 本地构建无需做这些:缺少 `key.properties` 时 `flutter build apk` 自动回退 debug 签名。
|
||||
|
||||
## 1. 生成 keystore(本地执行一次)
|
||||
|
||||
```bash
|
||||
keytool -genkeypair -v \
|
||||
-keystore jiu-release.jks \
|
||||
-alias jiu \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-storepass '<你的store密码>' \
|
||||
-keypass '<你的key密码>' \
|
||||
-dname "CN=Yanmei, OU=Jiu, O=Yanmei, L=, ST=, C=CN"
|
||||
```
|
||||
|
||||
**务必妥善保管 `jiu-release.jks` 与两个密码**:一旦丢失,已安装用户将无法收到覆盖升级(签名不匹配)。
|
||||
建议把 `jiu-release.jks` 备份到 `~/.env` 同级的安全位置(勿提交到仓库)。
|
||||
|
||||
## 2. 生成 base64(供 CI 注入)
|
||||
|
||||
```bash
|
||||
base64 -i jiu-release.jks | pbcopy # macOS:已复制到剪贴板
|
||||
```
|
||||
|
||||
## 3. 在 Forgejo 仓库配置 Secrets
|
||||
|
||||
仓库 → Settings → Actions → Secrets,新增 4 项:
|
||||
|
||||
| Secret 名 | 值 |
|
||||
|-----------|-----|
|
||||
| `ANDROID_KEYSTORE_BASE64` | 第 2 步的 base64 字符串 |
|
||||
| `ANDROID_KEYSTORE_PASSWORD` | store 密码 |
|
||||
| `ANDROID_KEY_ALIAS` | `jiu` |
|
||||
| `ANDROID_KEY_PASSWORD` | key 密码 |
|
||||
|
||||
## 4. 验证
|
||||
|
||||
打一个 tag 触发流水线,确认:
|
||||
- `build-android` job 绿,产出 `jiu-android.apk`;
|
||||
- 日志出现 `configuring release signing`(而非 `falling back to debug signing`);
|
||||
- 下载安装后,下次升级能覆盖安装(签名一致)。
|
||||
|
||||
## 工作原理
|
||||
|
||||
- `client/android/app/build.gradle.kts`:存在 `key.properties` 则用正式签名,否则回退 debug。
|
||||
- `scripts/ci/compile-android.sh`:从 `ANDROID_KEYSTORE_BASE64` 还原 keystore,写出 `key.properties`,构建后清理。
|
||||
- `key.properties`、`*.jks`、`*.keystore` 已在 `client/android/.gitignore` 中忽略。
|
||||
@@ -40,7 +40,8 @@ jiu/
|
||||
│ ├── main.go # 入口,启动时执行 AutoMigrate
|
||||
│ ├── config/
|
||||
│ │ ├── config.go # 配置结构体(含 mapstructure 标签)
|
||||
│ │ └── config.yaml # 本地开发配置(不提交 git)
|
||||
│ │ ├── config.yaml # 本地开发配置(不提交 git)
|
||||
│ │ └── version.yaml # 版本清单(version + download_urls,发版时由 release.sh 更新)
|
||||
│ ├── internal/
|
||||
│ │ ├── handler/ # HTTP 处理器(每模块一文件)
|
||||
│ │ │ └── *_test.go # Handler 集成测试(SQLite in-memory)
|
||||
@@ -59,10 +60,13 @@ jiu/
|
||||
│ │ └── S001.sql # 门店 S001 测试种子数据(SQL 命令形式)
|
||||
│ └── schema/schema.sql # 完整建表 SQL(MySQL)
|
||||
├── client/ # Flutter 跨端客户端
|
||||
│ └── android/ ios/ macos/ web/ windows/ # 五个平台目录(均已初始化)
|
||||
├── deploy/
|
||||
│ └── docker-compose.yml # 本地开发:MySQL(3306) + Adminer(8888)
|
||||
├── .gitea/workflows/ # Forgejo Actions(deploy.yml=tag 发版,及备份/重置等)
|
||||
├── scripts/
|
||||
│ ├── dev.sh # 一站式开发脚本(见下方用法)
|
||||
│ ├── ci/ # CI 编译/发布脚本(compile-*.sh / release.sh / deploy.sh / provision-*.sh)
|
||||
│ └── .logs/ # 脚本运行日志(已加入 .gitignore,不提交)
|
||||
└── docs/
|
||||
├── context/project.md # 本文件(项目全貌)
|
||||
@@ -164,6 +168,7 @@ sh scripts/dev.sh seed S001
|
||||
|
||||
版本(无需 JWT):
|
||||
GET /version # 返回最新版本信息(读取 version.yaml,缺失返回 500)
|
||||
GET /api/v1/public/release # 版本 + download_urls(驱动客户端更新提示与下载页)
|
||||
|
||||
商品:
|
||||
GET/POST /api/v1/products
|
||||
@@ -204,6 +209,12 @@ sh scripts/dev.sh seed S001
|
||||
数据导入:
|
||||
POST /api/v1/import/products (Excel/CSV)
|
||||
POST /api/v1/import/partners (Excel/CSV)
|
||||
|
||||
意见反馈:
|
||||
POST /api/v1/feedback # 提交反馈(bug/suggestion,文字+图片,认证)
|
||||
POST /api/v1/feedback/images # 上传反馈附图(认证)
|
||||
GET /api/v1/admin/feedback # 反馈列表(仅 superadmin)
|
||||
PATCH /api/v1/admin/feedback/:id # 标记处理状态(仅 superadmin)
|
||||
```
|
||||
|
||||
## Flutter 客户端结构
|
||||
@@ -218,6 +229,9 @@ client/lib/
|
||||
│ ├── config/app_config.dart # 全局 URL 配置(通过 --dart-define=BASE_URL=... 注入)
|
||||
│ ├── router/app_router.dart # go_router(_RouterNotifier + refreshListenable)
|
||||
│ ├── storage/login_history.dart # 登录历史(shared_preferences,支持候选词自动填充)
|
||||
│ ├── responsive/responsive.dart # 响应式:context.isMobile(<600)、context.dialogWidth(X)
|
||||
│ ├── update/app_updater*.dart # 应用内更新(_io: Win/macOS 装;_web: 刷新;移动端/兜底开下载链接)
|
||||
│ ├── errors/error_reporter.dart # 异常上报(reportError)
|
||||
│ ├── models/page_result.dart # 通用分页结果模型
|
||||
│ └── exceptions.dart # 自定义异常类型
|
||||
├── models/ # 数据模型(与后端 JSON 对应)
|
||||
@@ -239,19 +253,24 @@ client/lib/
|
||||
│ └── number_rule_provider.dart # 编号规则
|
||||
├── screens/
|
||||
│ ├── auth/login_screen.dart # 登录页(含候选词下拉,150ms 延迟防止覆盖 onTap)
|
||||
│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏 + 状态栏 + 更新提示 banner)
|
||||
│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏/窄屏 Drawer 抽屉 + 状态栏 + 更新 banner)
|
||||
│ ├── stock_in/ # 入库管理
|
||||
│ ├── stock_out/ # 出库管理
|
||||
│ ├── inventory/ # 库存管理(含批次追踪 batch_tracking_screen.dart)
|
||||
│ ├── partners/ # 往来单位
|
||||
│ ├── finance/ # 财务管理
|
||||
│ ├── products/ # 商品管理(含分类)
|
||||
│ └── settings/ # 系统设置(用户/仓库/编号规则/关于)
|
||||
│ ├── about/ # 关于我们(含意见反馈:文字+附图直接提交后台)
|
||||
│ ├── public/ # 商品扫码公开展示页(无需登录)
|
||||
│ └── settings/ # 系统设置(用户/仓库/编号规则)
|
||||
└── widgets/
|
||||
├── page_scaffold.dart # Tab 页面封装
|
||||
├── data_table_card.dart # 含工具栏+分页的数据表格
|
||||
│ # StatefulWidget,用 Table + IntrinsicColumnWidth 实现
|
||||
│ # ValueNotifier<int> _hoveredRow 驱动行 hover 高亮
|
||||
│ # mobileCards 入参:窄屏渲染卡片流,宽屏仍表格
|
||||
├── mobile_list_card.dart # 移动端列表卡片:MobileListCard / MobileCardField
|
||||
│ # 标题 + 右上角徽章 + 字段竖排 + 底部操作,替代窄屏表格行
|
||||
├── multi_select_dropdown.dart # 多选筛选组件(多个导出):
|
||||
│ # MultiSelectDropdown — 独立多选按钮(toolbar 使用)
|
||||
│ # FilterableColumnHeader — 列头内嵌筛选图标
|
||||
@@ -327,6 +346,48 @@ final warehouseOptions = _records
|
||||
- `required: true` — 不可隐藏
|
||||
- `minWidth: 1000` — 屏幕宽度不足时自动隐藏
|
||||
|
||||
## 移动端 / 响应式 UI 规范
|
||||
|
||||
客户端同一套代码跑桌面/Web/平板/手机。**窄屏(手机)自动切换为移动布局,宽屏保持桌面布局不变**。
|
||||
|
||||
### 断点判定
|
||||
统一用 `client/lib/core/responsive/responsive.dart` 的扩展,不要散落魔法数:
|
||||
```dart
|
||||
context.isMobile // 宽度 < 600(kMobileBreakpoint)
|
||||
context.dialogWidth(720) // 固定宽度弹窗的安全宽度,≤ 屏宽 92%
|
||||
```
|
||||
|
||||
### 列表 → 卡片
|
||||
列表屏用 `DataTableCard` 时同时传 `mobileCards`,窄屏渲染卡片流、宽屏仍表格:
|
||||
```dart
|
||||
DataTableCard(
|
||||
columns: [...], rows: [...], // 宽屏表格
|
||||
mobileCards: items.map(_buildCard).toList(), // 窄屏卡片(MobileListCard)
|
||||
)
|
||||
```
|
||||
卡片复用 `MobileListCard`(标题/徽章/字段/操作)+ `MobileCardField`(`widgets/mobile_list_card.dart`)。
|
||||
顶部并排汇总卡片在窄屏改为横向滚动。
|
||||
|
||||
### 弹窗宽度
|
||||
固定宽度弹窗一律 `width: context.dialogWidth(X)`,**禁止**裸写 `width: <固定值>`(窄屏会溢出)。
|
||||
表单字段(`_FormField`)窄屏占满整行。
|
||||
|
||||
### 导航
|
||||
窄屏用 Drawer 抽屉(`app_shell.dart` 的 `_buildDrawer`,汉堡按钮打开),隐藏底部状态栏;宽屏保持侧边栏。
|
||||
新增页面挂在 shell 下即可,自动适配,无需单独处理。
|
||||
|
||||
## 跨端分发
|
||||
|
||||
| 平台 | 构建产物 | 分发方式 |
|
||||
|------|----------|----------|
|
||||
| Web | `flutter build web` | 部署到 `jiu.51yanmei.com/app` |
|
||||
| Windows | Inno Setup `setup.exe` | 下载页 `/downloads/` + 应用内更新 |
|
||||
| macOS | `.app` zip | 下载页 `/downloads/` + 应用内更新 |
|
||||
| Android | 签名 `jiu-android.apk` | 下载页 `/downloads/`(见 docs/android-signing.md) |
|
||||
| iOS | 签名 IPA | TestFlight(见 docs/ios-signing.md) |
|
||||
|
||||
版本清单:`backend/config/version.yaml` 的 `download_urls`(含各平台下载地址);客户端 `GET /api/v1/public/release` 读取,驱动更新提示与下载页。
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 路径 | 说明 |
|
||||
@@ -337,3 +398,7 @@ final warehouseOptions = _records
|
||||
| S001 种子 | backend/seeds/S001.sql | 门店 S001 测试数据(含完整入库/出库/库存历史) |
|
||||
| S002 种子 | backend/seeds/S002.sql | 门店 S002 测试数据(基础数据相同,无库存/单据,模拟新门店) |
|
||||
| 用户手册 | docs/user-manual.md | 酒行员工操作手册(登录/入库/出库/库存/财务/设置) |
|
||||
| Android 签名 | docs/android-signing.md | 生成 keystore + 配置 Forgejo secrets,CI 给 APK 正式签名 |
|
||||
| iOS 分发 | docs/ios-signing.md | 证书/Profile/API Key + secrets,CI 构建并上传 TestFlight |
|
||||
| 部署(NAS/Gitea) | docs/deployment-nas-gitea.md | 自建 Forgejo + runner 部署说明 |
|
||||
| 待办 | docs/TODO.md | 后续迭代事项(iOS 上架、应用内 APK 安装、平板布局等) |
|
||||
|
||||
@@ -152,6 +152,19 @@ sh scripts/dev.sh --backend-only
|
||||
| KeyDownEvent 断言失败 | Flutter 3.41 + macOS 26 已知 bug | 改用 Chrome 运行 |
|
||||
| mouse_tracker 断言失败(debug)| Flutter debug 断言 + hover 事件 | 用 `--profile` 模式运行 |
|
||||
|
||||
### 移动端构建
|
||||
|
||||
```bash
|
||||
# Android(本地):无 client/android/key.properties 时自动回退 debug 签名
|
||||
cd client && flutter build apk --release
|
||||
|
||||
# iOS(本地验证编译,不签名)
|
||||
cd client && flutter build ios --release --no-codesign
|
||||
```
|
||||
|
||||
- 正式签名/发布由 CI 完成:Android 见 `docs/android-signing.md`,iOS(TestFlight)见 `docs/ios-signing.md`。
|
||||
- 移动端 UI 自动适配:窄屏走移动布局(Drawer 抽屉 + 列表卡片),规范见 `docs/context/project.md` 的「移动端 / 响应式 UI 规范」。
|
||||
|
||||
---
|
||||
|
||||
## 五、后端测试架构
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# iOS 分发(TestFlight)— 一次性配置
|
||||
|
||||
iOS 不能像安卓那样把安装包挂网页直接侧载。本项目通过 **TestFlight** 分发:CI 自动构建签名 IPA 并上传 App Store Connect,用户装 TestFlight app 后用公开链接安装。
|
||||
|
||||
所有凭证通过 Forgejo Secrets 注入,**不入库**。配置完成前 `build-ios` job 会自动跳过(exit 0),不阻塞发版。
|
||||
|
||||
## 前置:Apple Developer 账号
|
||||
|
||||
- 注册 Apple Developer Program($99/年)。
|
||||
- 在 [App Store Connect](https://appstoreconnect.apple.com) 新建 App,Bundle ID = `com.yanmei.jiu`(与工程一致)。
|
||||
|
||||
## 1. 发行证书(Apple Distribution)
|
||||
|
||||
在钥匙串/开发者后台创建 **Apple Distribution** 证书,导出为 `.p12`(设一个导出密码):
|
||||
|
||||
```bash
|
||||
# 已在 Xcode/钥匙串里有 Distribution 证书时,从「钥匙串访问」导出 .p12
|
||||
base64 -i dist.p12 | pbcopy # → ANDROID 同理,复制到剪贴板
|
||||
```
|
||||
|
||||
## 2. Provisioning Profile(App Store 类型)
|
||||
|
||||
开发者后台 → Profiles → 新建 **App Store** 类型,关联 App ID `com.yanmei.jiu` 与上面的发行证书,下载 `.mobileprovision`:
|
||||
|
||||
```bash
|
||||
base64 -i jiu_appstore.mobileprovision | pbcopy
|
||||
```
|
||||
|
||||
## 3. App Store Connect API Key
|
||||
|
||||
App Store Connect → Users and Access → Integrations → App Store Connect API → 新建密钥(角色 App Manager 即可),下载 `AuthKey_XXXX.p8`,记下 **Key ID** 与 **Issuer ID**:
|
||||
|
||||
```bash
|
||||
base64 -i AuthKey_XXXX.p8 | pbcopy
|
||||
```
|
||||
|
||||
## 4. 在 Forgejo 仓库配置 Secrets
|
||||
|
||||
仓库 → Settings → Actions → Secrets,新增 7 项:
|
||||
|
||||
| Secret 名 | 值 |
|
||||
|-----------|-----|
|
||||
| `IOS_DIST_CERT_P12_BASE64` | 第 1 步 .p12 的 base64 |
|
||||
| `IOS_DIST_CERT_PASSWORD` | .p12 导出密码 |
|
||||
| `IOS_PROVISIONING_PROFILE_BASE64` | 第 2 步 .mobileprovision 的 base64 |
|
||||
| `IOS_TEAM_ID` | Apple Developer Team ID(10 位) |
|
||||
| `APPSTORE_API_KEY_ID` | 第 3 步 Key ID |
|
||||
| `APPSTORE_API_ISSUER_ID` | 第 3 步 Issuer ID |
|
||||
| `APPSTORE_API_KEY_P8_BASE64` | 第 3 步 .p8 的 base64 |
|
||||
|
||||
## 5. 配置 TestFlight 公开链接(下载页)
|
||||
|
||||
TestFlight 设置「公开链接」后,把链接填到 `backend/config/version.yaml`:
|
||||
|
||||
```yaml
|
||||
download_urls:
|
||||
ios: "https://testflight.apple.com/join/XXXXXXXX"
|
||||
```
|
||||
|
||||
填上后下载页的 iOS 卡片自动激活为「通过 TestFlight 安装」;客户端「立即更新」在 iOS 上也会跳到该链接。release.sh 不会覆盖此行(仅改 macos/windows/android)。
|
||||
|
||||
## 6. 验证
|
||||
|
||||
打 tag 触发流水线,确认 `build-ios` job:
|
||||
- 日志非 `SKIP`,出现 `uploading to TestFlight`;
|
||||
- App Store Connect → TestFlight 出现新 build(处理需几分钟);
|
||||
- 在 TestFlight 里设置测试组/公开链接后即可分发。
|
||||
|
||||
## 工作原理
|
||||
|
||||
- `scripts/ci/compile-ios.sh`:建临时 keychain 导入证书、安装 profile、生成 ExportOptions(manual / app-store)、`flutter build ipa`、`xcrun altool --upload-app` 上传;CFBundleVersion 由版本号推导单调递增;缺 secrets 时跳过。
|
||||
- `.gitea/workflows/deploy.yml` 的 `build-ios` job:mac runner,串行于 build-android。
|
||||
- 工程:`client/ios`(bundle id `com.yanmei.jiu`,应用名「岩美酒库」)。
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# compile-android.sh <tag> — build signed Flutter Android APK, package into dist/
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/_env.sh
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
|
||||
# versionCode 必须单调递增,否则 Android 无法覆盖升级安装。
|
||||
# 由版本号推导:major*10000 + minor*100 + patch(如 1.0.17 -> 10017)。
|
||||
MAJOR="$(echo "$VER" | cut -d. -f1)"
|
||||
MINOR="$(echo "$VER" | cut -d. -f2)"
|
||||
PATCH="$(echo "$VER" | cut -d. -f3)"
|
||||
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||
|
||||
echo "==> compile-android: version=${VER} build=${BUILD}"
|
||||
|
||||
# Sync Flutter pubspec version (BSD sed on macOS)
|
||||
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
|
||||
|
||||
# --- release 签名(强制):从 CI secrets 还原 keystore 与 key.properties ---
|
||||
# 必须 release 签名。缺任一签名 secret 直接失败,绝不回退 debug:
|
||||
# debug 包用公开调试密钥签名,不能正式分发,也无法与正式版互相覆盖升级。
|
||||
: "${ANDROID_KEYSTORE_BASE64:?缺少 ANDROID_KEYSTORE_BASE64:未配置 Android release 签名,构建中止(不回退 debug)}"
|
||||
: "${ANDROID_KEYSTORE_PASSWORD:?缺少 ANDROID_KEYSTORE_PASSWORD:未配置 Android release 签名,构建中止}"
|
||||
: "${ANDROID_KEY_ALIAS:?缺少 ANDROID_KEY_ALIAS:未配置 Android release 签名,构建中止}"
|
||||
: "${ANDROID_KEY_PASSWORD:?缺少 ANDROID_KEY_PASSWORD:未配置 Android release 签名,构建中止}"
|
||||
|
||||
echo "==> compile-android: configuring release signing"
|
||||
KEYSTORE_PATH="$(pwd)/client/android/jiu-release.jks"
|
||||
echo "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${KEYSTORE_PATH}"
|
||||
cat > client/android/key.properties <<EOF
|
||||
storeFile=${KEYSTORE_PATH}
|
||||
storePassword=${ANDROID_KEYSTORE_PASSWORD}
|
||||
keyAlias=${ANDROID_KEY_ALIAS}
|
||||
keyPassword=${ANDROID_KEY_PASSWORD}
|
||||
EOF
|
||||
|
||||
# Build APK (universal, all ABIs — simplest for sideload download)
|
||||
cd client
|
||||
flutter build apk --release \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
cd ..
|
||||
|
||||
# Locate the built APK (path differs across Flutter versions) and copy to dist/
|
||||
mkdir -p dist
|
||||
APK=""
|
||||
for cand in \
|
||||
client/build/app/outputs/flutter-apk/app-release.apk \
|
||||
client/build/app/outputs/apk/release/app-release.apk; do
|
||||
if [ -f "$cand" ]; then APK="$cand"; break; fi
|
||||
done
|
||||
if [ -z "$APK" ]; then
|
||||
echo "ERROR: built APK not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$APK" dist/jiu-android.apk
|
||||
|
||||
# Clean up signing material so it never lingers on the runner workspace
|
||||
rm -f client/android/key.properties client/android/jiu-release.jks 2>/dev/null || true
|
||||
|
||||
echo "==> compile-android: done — dist/ contents:"
|
||||
ls -lh dist/
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# compile-ios.sh <tag> — build signed iOS IPA and upload to TestFlight (App Store Connect)
|
||||
#
|
||||
# 需要的 CI secrets(缺任意一个则优雅跳过,不阻塞流水线):
|
||||
# IOS_DIST_CERT_P12_BASE64 Apple Distribution 证书 (.p12) 的 base64
|
||||
# IOS_DIST_CERT_PASSWORD .p12 导出密码
|
||||
# IOS_PROVISIONING_PROFILE_BASE64 App Store 类型 provisioning profile (.mobileprovision) 的 base64
|
||||
# IOS_TEAM_ID Apple Developer Team ID(10 位)
|
||||
# APPSTORE_API_KEY_ID App Store Connect API Key ID
|
||||
# APPSTORE_API_ISSUER_ID App Store Connect API Issuer ID
|
||||
# APPSTORE_API_KEY_P8_BASE64 API Key (.p8) 的 base64
|
||||
#
|
||||
# 详见 docs/ios-signing.md。
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/_env.sh
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
|
||||
# CFBundleVersion(build 号)必须单调递增,否则 TestFlight 拒绝重复上传。
|
||||
MAJOR="$(echo "$VER" | cut -d. -f1)"
|
||||
MINOR="$(echo "$VER" | cut -d. -f2)"
|
||||
PATCH="$(echo "$VER" | cut -d. -f3)"
|
||||
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||
|
||||
echo "==> compile-ios: version=${VER} build=${BUILD}"
|
||||
|
||||
# --- 凭证缺失则跳过(账号/证书尚未配置时不阻塞发版)---
|
||||
if [ -z "${IOS_DIST_CERT_P12_BASE64:-}" ] || \
|
||||
[ -z "${IOS_PROVISIONING_PROFILE_BASE64:-}" ] || \
|
||||
[ -z "${APPSTORE_API_KEY_P8_BASE64:-}" ]; then
|
||||
echo "==> compile-ios: SKIP — iOS signing secrets not configured (see docs/ios-signing.md)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
cleanup() {
|
||||
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
|
||||
rm -rf "$WORK"
|
||||
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/jiu-ci.mobileprovision" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- 1. 临时 keychain 导入发行证书 ---
|
||||
KEYCHAIN="$WORK/jiu-ci.keychain-db"
|
||||
KEYCHAIN_PWD="ci-temp-$$"
|
||||
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||||
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||||
|
||||
echo "${IOS_DIST_CERT_P12_BASE64}" | base64 --decode > "$WORK/dist.p12"
|
||||
security import "$WORK/dist.p12" -k "$KEYCHAIN" \
|
||||
-P "${IOS_DIST_CERT_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security
|
||||
# 允许 codesign 非交互访问私钥
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
|
||||
# 把临时 keychain 加入搜索列表(保留默认 login keychain)
|
||||
security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
|
||||
|
||||
# --- 2. 安装 provisioning profile ---
|
||||
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$PROFILES_DIR"
|
||||
echo "${IOS_PROVISIONING_PROFILE_BASE64}" | base64 --decode > "$WORK/profile.mobileprovision"
|
||||
# 解出 profile 的 Name 与 UUID(ExportOptions 需要)
|
||||
security cms -D -i "$WORK/profile.mobileprovision" > "$WORK/profile.plist"
|
||||
PROFILE_NAME="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$WORK/profile.plist")"
|
||||
PROFILE_UUID="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$WORK/profile.plist")"
|
||||
cp "$WORK/profile.mobileprovision" "$PROFILES_DIR/${PROFILE_UUID}.mobileprovision"
|
||||
echo "==> compile-ios: profile '${PROFILE_NAME}' (${PROFILE_UUID})"
|
||||
|
||||
# --- 3. App Store Connect API Key(供上传使用)---
|
||||
API_KEYS_DIR="$HOME/.appstoreconnect/private_keys"
|
||||
mkdir -p "$API_KEYS_DIR"
|
||||
echo "${APPSTORE_API_KEY_P8_BASE64}" | base64 --decode > "$API_KEYS_DIR/AuthKey_${APPSTORE_API_KEY_ID}.p8"
|
||||
|
||||
# --- 4. 同步版本号 ---
|
||||
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
|
||||
|
||||
# --- 5. 生成 ExportOptions.plist(manual 签名,app-store 分发)---
|
||||
BUNDLE_ID="com.yanmei.jiu"
|
||||
cat > "$WORK/ExportOptions.plist" <<EOF
|
||||
<?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>method</key><string>app-store</string>
|
||||
<key>teamID</key><string>${IOS_TEAM_ID}</string>
|
||||
<key>signingStyle</key><string>manual</string>
|
||||
<key>uploadBitcode</key><false/>
|
||||
<key>uploadSymbols</key><true/>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>${BUNDLE_ID}</key><string>${PROFILE_NAME}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# --- 6. 构建签名 IPA ---
|
||||
cd client
|
||||
flutter build ipa --release \
|
||||
--build-name="${VER}" \
|
||||
--build-number="${BUILD}" \
|
||||
--export-options-plist="$WORK/ExportOptions.plist" \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
cd ..
|
||||
|
||||
IPA="$(ls client/build/ios/ipa/*.ipa | head -1)"
|
||||
if [ -z "$IPA" ] || [ ! -f "$IPA" ]; then
|
||||
echo "ERROR: IPA not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> compile-ios: built ${IPA}"
|
||||
|
||||
# --- 7. 上传 TestFlight(App Store Connect)---
|
||||
echo "==> compile-ios: uploading to TestFlight"
|
||||
xcrun altool --upload-app -f "$IPA" -t ios \
|
||||
--apiKey "${APPSTORE_API_KEY_ID}" \
|
||||
--apiIssuer "${APPSTORE_API_ISSUER_ID}"
|
||||
|
||||
echo "==> compile-ios: done — uploaded build ${BUILD} to TestFlight"
|
||||
@@ -94,6 +94,7 @@ rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no
|
||||
# Guarded: may be absent in a partial manual deploy.
|
||||
[ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${EC2_USER}@${EC2_HOST}:/tmp/jiu-windows-x64-setup.exe" || true
|
||||
[ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${EC2_USER}@${EC2_HOST}:/tmp/jiu-macos-x64.zip" || true
|
||||
[ -f dist/jiu-android.apk ] && ${SCP} dist/jiu-android.apk "${EC2_USER}@${EC2_HOST}:/tmp/jiu-android.apk" || true
|
||||
|
||||
echo "==> deploy: running remote deploy"
|
||||
|
||||
@@ -134,9 +135,10 @@ rm -rf /tmp/jiu-marketing-new
|
||||
# Publish desktop client installers to the nginx-served downloads dir.
|
||||
# Keep only the latest version: clear old installers, then drop in the new ones.
|
||||
mkdir -p /opt/jiu/downloads
|
||||
rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* 2>/dev/null || true
|
||||
rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* /opt/jiu/downloads/jiu-android-* /opt/jiu/downloads/jiu-android.apk 2>/dev/null || true
|
||||
[ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true
|
||||
[ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true
|
||||
[ -f /tmp/jiu-android.apk ] && mv -f /tmp/jiu-android.apk /opt/jiu/downloads/ || true
|
||||
|
||||
# Update jiu nginx config in the pangolin-edge reverse proxy.
|
||||
# nginx now runs inside the `pangolin-edge` container (host networking), not on
|
||||
|
||||
@@ -51,6 +51,7 @@ repo = sys.argv[5]
|
||||
base = "https://jiu.51yanmei.com/downloads"
|
||||
mac_url = f"{base}/jiu-macos-x64.zip"
|
||||
win_url = f"{base}/jiu-windows-x64-setup.exe"
|
||||
android_url = f"{base}/jiu-android.apk"
|
||||
|
||||
lines = []
|
||||
with open('backend/config/version.yaml') as f:
|
||||
@@ -69,11 +70,13 @@ with open('backend/config/version.yaml') as f:
|
||||
lines.append(' macos: "' + mac_url + '"\n')
|
||||
elif line.startswith(' windows:'):
|
||||
lines.append(' windows: "' + win_url + '"\n')
|
||||
elif line.startswith(' android:'):
|
||||
lines.append(' android: "' + android_url + '"\n')
|
||||
else:
|
||||
lines.append(line)
|
||||
with open('backend/config/version.yaml', 'w') as f:
|
||||
f.writelines(lines)
|
||||
print('version.yaml updated to', ver, '| macos:', mac_url, '| windows:', win_url)
|
||||
print('version.yaml updated to', ver, '| macos:', mac_url, '| windows:', win_url, '| android:', android_url)
|
||||
PYEOF
|
||||
|
||||
# Package configs.tar.gz (includes updated version.yaml)
|
||||
@@ -133,5 +136,6 @@ upload_asset dist/marketing.tar.gz
|
||||
upload_asset dist/configs.tar.gz
|
||||
upload_asset dist/jiu-macos-x64.zip
|
||||
upload_asset dist/jiu-windows-x64-setup.exe
|
||||
upload_asset dist/jiu-android.apk
|
||||
|
||||
echo "==> release: done — Release ${TAG} created"
|
||||
|
||||
@@ -258,7 +258,7 @@ pageStyle: |
|
||||
</div>
|
||||
<h1 class="ph-title">随时随地,<br/>轻松管理酒库。</h1>
|
||||
<p class="ph-lead">
|
||||
Web 版与 Windows 桌面版已正式上线,macOS、iOS、Android 客户端即将推出。
|
||||
Web 版、Windows / macOS 桌面版与 Android 客户端均已上线,iOS 客户端即将推出。
|
||||
</p>
|
||||
<div class="ph-meta">
|
||||
<div class="ph-meta-item">
|
||||
@@ -271,7 +271,7 @@ pageStyle: |
|
||||
</div>
|
||||
<div class="ph-meta-item">
|
||||
<div class="ph-meta-label">可用平台</div>
|
||||
<div class="ph-meta-val">Web / Windows</div>
|
||||
<div class="ph-meta-val">Web / Windows / macOS / Android</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,7 +308,7 @@ pageStyle: |
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2 class="fs-3xl fw-6 text-brand-900 m-0 mb-6" style="letter-spacing: var(--tracking-cn-display);">支持的平台</h2>
|
||||
<p class="fs-md text-muted m-0 mb-8">Web 版与 Windows 桌面版现已可用,其余平台正在开发中。</p>
|
||||
<p class="fs-md text-muted m-0 mb-8">Web 版、Windows / macOS 桌面版与 Android 客户端现已可用,iOS 正在开发中。</p>
|
||||
|
||||
<div class="dl-grid" id="platform-grid">
|
||||
<!-- Web -->
|
||||
@@ -345,25 +345,25 @@ pageStyle: |
|
||||
</div>
|
||||
|
||||
<!-- iOS -->
|
||||
<div class="dl-card coming-soon" data-platform="ios">
|
||||
<div class="dl-card coming-soon" data-platform="ios" id="ios-card">
|
||||
<div class="dl-card-icon"><i data-lucide="smartphone" class="icon"></i></div>
|
||||
<h3 class="dl-card-name">iOS</h3>
|
||||
<p class="dl-card-desc">iPhone 与 iPad 通用,支持扫码与推送通知。</p>
|
||||
<span class="dl-status soon">敬请期待</span>
|
||||
<span class="dl-card-btn disabled">
|
||||
<i data-lucide="clock" class="icon"></i>即将推出
|
||||
</span>
|
||||
<p class="dl-card-desc">iPhone 与 iPad 通用,通过 TestFlight 安装。</p>
|
||||
<span class="dl-status soon" id="ios-status">敬请期待</span>
|
||||
<a href="#" class="dl-card-btn disabled" id="ios-dl-btn">
|
||||
<i data-lucide="clock" class="icon"></i><span>即将推出</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Android -->
|
||||
<div class="dl-card coming-soon" data-platform="android">
|
||||
<div class="dl-card" data-platform="android">
|
||||
<div class="dl-card-icon"><i data-lucide="tablet-smartphone" class="icon"></i></div>
|
||||
<h3 class="dl-card-name">Android</h3>
|
||||
<p class="dl-card-desc">手机与平板通用,支持主流应用商店及 APK 安装。</p>
|
||||
<span class="dl-status soon">敬请期待</span>
|
||||
<span class="dl-card-btn disabled">
|
||||
<i data-lucide="clock" class="icon"></i>即将推出
|
||||
</span>
|
||||
<p class="dl-card-desc">手机与平板通用,APK 直接安装,支持 Android 6.0 及以上。</p>
|
||||
<span class="dl-status available">✓ 已支持</span>
|
||||
<a href="#" class="dl-card-btn primary" id="android-dl-btn">
|
||||
<i data-lucide="download" class="icon"></i>下载 Android 版
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,8 +429,8 @@ pageStyle: |
|
||||
(function() {
|
||||
var platforms = {
|
||||
web: { name: 'Web 版', desc: '浏览器直接访问,功能完整,无需下载安装。', icon: 'globe', btnText: '立即使用 Web 版', btnHref: 'https://jiu.51yanmei.com/app/', available: true },
|
||||
ios: { name: 'iOS', desc: 'iOS 客户端正在开发中,敬请期待。', icon: 'smartphone', btnText: '敬请期待', available: false },
|
||||
android: { name: 'Android', desc: 'Android 客户端正在开发中,敬请期待。', icon: 'tablet-smartphone', btnText: '敬请期待', available: false },
|
||||
ios: { name: 'iOS', desc: 'iPhone 与 iPad 通用,通过 TestFlight 安装。', icon: 'smartphone', btnText: '敬请期待', available: false },
|
||||
android: { name: 'Android', desc: '手机与平板通用,APK 直接安装,支持 Android 6.0 及以上。', icon: 'tablet-smartphone', btnText: '下载 Android 版', btnHref: '#', available: true },
|
||||
windows: { name: 'Windows 桌面', desc: '原生桌面客户端,支持 Win 10/11(64 位),离线模式与本地打印。', icon: 'monitor', btnText: '下载 Windows 版', btnHref: '#', available: true },
|
||||
macos: { name: 'macOS 桌面', desc: '支持 Apple Silicon 与 Intel,解压即用,首次运行右键→打开。', icon: 'laptop', btnText: '下载 macOS 版', btnHref: '#', available: true }
|
||||
};
|
||||
@@ -513,6 +513,32 @@ pageStyle: |
|
||||
if (btn) btn.href = data.download_urls.windows;
|
||||
if (detected === 'windows') updateDetectCard(detected);
|
||||
}
|
||||
if (data && data.download_urls && data.download_urls.android) {
|
||||
platforms.android.btnHref = data.download_urls.android;
|
||||
var andBtn = document.getElementById('android-dl-btn');
|
||||
if (andBtn) andBtn.href = data.download_urls.android;
|
||||
if (detected === 'android') updateDetectCard(detected);
|
||||
}
|
||||
// iOS 走 TestFlight:仅当配置了公开 TestFlight 链接时激活卡片
|
||||
if (data && data.download_urls && data.download_urls.ios) {
|
||||
platforms.ios.available = true;
|
||||
platforms.ios.btnHref = data.download_urls.ios;
|
||||
platforms.ios.btnText = '通过 TestFlight 安装';
|
||||
var iosCard = document.getElementById('ios-card');
|
||||
var iosStatus = document.getElementById('ios-status');
|
||||
var iosBtn = document.getElementById('ios-dl-btn');
|
||||
if (iosCard) iosCard.classList.remove('coming-soon');
|
||||
if (iosStatus) { iosStatus.className = 'dl-status available'; iosStatus.textContent = '✓ 已上线'; }
|
||||
if (iosBtn) {
|
||||
iosBtn.className = 'dl-card-btn primary';
|
||||
iosBtn.href = data.download_urls.ios;
|
||||
iosBtn.target = '_blank';
|
||||
iosBtn.rel = 'noopener';
|
||||
iosBtn.innerHTML = '<i data-lucide="download" class="icon"></i>通过 TestFlight 安装';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
if (detected === 'ios') updateDetectCard(detected);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
});
|
||||
|
||||