From 480ce836bbaba17c0ef56de055998328c2730c70 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 7 Jun 2026 07:55:33 +0800 Subject: [PATCH] chore: release v1.0.18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动端响应式适配(抽屉导航/列表卡片/弹窗自适应)、Android 正式签名与 APK 发布、 iOS(TestFlight) 工程与 CI、多平台构建流水线、相关文档同步。 Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/deploy.yml | 66 +- .gitea/workflows/export-keystore.yml | 30 + CHANGELOG.md | 9 + CLAUDE.md | 17 +- backend/config/version.yaml | 2 +- client/.metadata | 12 +- client/android/app/build.gradle.kts | 35 +- .../android/app/src/main/AndroidManifest.xml | 2 +- client/android/gradle.properties | 4 + client/ios/.gitignore | 34 + client/ios/Flutter/AppFrameworkInfo.plist | 24 + client/ios/Flutter/Debug.xcconfig | 2 + client/ios/Flutter/Release.xcconfig | 2 + client/ios/Podfile | 43 + client/ios/Podfile.lock | 22 + client/ios/Runner.xcodeproj/project.pbxproj | 756 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/swiftpm/Package.resolved | 59 ++ .../xcshareddata/xcschemes/Runner.xcscheme | 119 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/swiftpm/Package.resolved | 59 ++ client/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + client/ios/Runner/Base.lproj/Main.storyboard | 26 + client/ios/Runner/Info.plist | 70 ++ client/ios/Runner/Runner-Bridging-Header.h | 1 + client/ios/Runner/SceneDelegate.swift | 6 + client/ios/RunnerTests/RunnerTests.swift | 12 + client/lib/core/responsive/responsive.dart | 15 + client/lib/screens/about/about_screen.dart | 3 +- client/lib/screens/auth/login_screen.dart | 5 +- .../lib/screens/finance/finance_screen.dart | 123 ++- .../inventory/inventory_list_screen.dart | 218 ++++- .../lib/screens/partners/partners_screen.dart | 40 +- .../lib/screens/products/products_screen.dart | 87 +- .../lib/screens/settings/settings_screen.dart | 15 +- client/lib/screens/shell/app_shell.dart | 200 +++-- .../stock_in/stock_in_form_screen.dart | 9 +- .../stock_in/stock_in_list_screen.dart | 191 +++-- .../stock_out/stock_out_form_screen.dart | 11 +- .../stock_out/stock_out_list_screen.dart | 153 ++-- client/lib/widgets/data_table_card.dart | 79 +- client/lib/widgets/mobile_list_card.dart | 131 +++ .../lib/widgets/searchable_option_field.dart | 3 +- client/lib/widgets/select_product_dialog.dart | 3 +- client/pubspec.lock | 32 +- docs/TODO.md | 14 + docs/android-signing.md | 51 ++ docs/context/project.md | 71 +- docs/dev-setup.md | 13 + docs/ios-signing.md | 73 ++ scripts/ci/compile-android.sh | 64 ++ scripts/ci/compile-ios.sh | 124 +++ scripts/ci/deploy.sh | 4 +- scripts/ci/release.sh | 6 +- web/download.njk | 60 +- 81 files changed, 3096 insertions(+), 366 deletions(-) create mode 100644 .gitea/workflows/export-keystore.yml create mode 100644 client/ios/.gitignore create mode 100644 client/ios/Flutter/AppFrameworkInfo.plist create mode 100644 client/ios/Flutter/Debug.xcconfig create mode 100644 client/ios/Flutter/Release.xcconfig create mode 100644 client/ios/Podfile create mode 100644 client/ios/Podfile.lock create mode 100644 client/ios/Runner.xcodeproj/project.pbxproj create mode 100644 client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 client/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 client/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 client/ios/Runner/AppDelegate.swift create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 client/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 client/ios/Runner/Base.lproj/Main.storyboard create mode 100644 client/ios/Runner/Info.plist create mode 100644 client/ios/Runner/Runner-Bridging-Header.h create mode 100644 client/ios/Runner/SceneDelegate.swift create mode 100644 client/ios/RunnerTests/RunnerTests.swift create mode 100644 client/lib/core/responsive/responsive.dart create mode 100644 client/lib/widgets/mobile_list_card.dart create mode 100644 docs/TODO.md create mode 100644 docs/android-signing.md create mode 100644 docs/ios-signing.md create mode 100755 scripts/ci/compile-android.sh create mode 100755 scripts/ci/compile-ios.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index ded42af..8883fa8 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -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: diff --git a/.gitea/workflows/export-keystore.yml b/.gitea/workflows/export-keystore.yml new file mode 100644 index 0000000..cc49918 --- /dev/null +++ b/.gitea/workflows/export-keystore.yml @@ -0,0 +1,30 @@ +# 一次性:把 Android release keystore 从 CI secret 导出为 artifact,便于本地/Bitwarden 备份。 +# 用完务必删除:① 本 workflow 文件;② 跑出来的这次 run 的 artifact(含密钥)。 +# 触发:Forgejo 网页 → Actions → Export Keystore → Run workflow(手动)。 +name: Export Keystore + +on: + workflow_dispatch: + +jobs: + export: + runs-on: mac + steps: + - name: Restore keystore from secret + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + run: | + set -e + mkdir -p out + # 原始 base64(方便直接粘进 Bitwarden Secure Note) + printf '%s' "$ANDROID_KEYSTORE_BASE64" > out/keystore.b64.txt + # 解码回二进制 keystore + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > out/jiu-release.jks + echo "base64 长度: $(wc -c < out/keystore.b64.txt)" + echo "jks 字节数: $(wc -c < out/jiu-release.jks)" + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: keystore-backup + path: out/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 56fabf0..1388e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ 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.18] - 2026-06-06 + +### 新功能 +- 移动端(Android)界面适配:窄屏(手机)自动切换为移动布局——侧边栏改为侧滑抽屉导航,各列表(库存/出入库/财务/往来单位/基础数据)由宽表格自动切换为卡片流,弹窗与表单自适应屏宽不再溢出;宽屏(桌面/Web)布局保持不变 +- Android 客户端正式上线:CI/CD 新增 Android APK 构建(正式 release 签名、versionCode 单调递增),随发版自动发布到下载页;下载页 Android 卡片已激活 + +### 改进 +- 表单字段在手机上占满整行,便于点选;底部状态栏在手机端隐藏 + ## [1.0.17] - 2026-06-05 ### 新功能 diff --git a/CLAUDE.md b/CLAUDE.md index b1666bc..4c146d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/backend/config/version.yaml b/backend/config/version.yaml index dcf6363..5a0a91e 100644 --- a/backend/config/version.yaml +++ b/backend/config/version.yaml @@ -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" diff --git a/client/.metadata b/client/.metadata index a8104ff..344170c 100644 --- a/client/.metadata +++ b/client/.metadata @@ -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 diff --git a/client/android/app/build.gradle.kts b/client/android/app/build.gradle.kts index c7c02c6..02791c7 100644 --- a/client/android/app/build.gradle.kts +++ b/client/android/app/build.gradle.kts @@ -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") + } } } } diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml index 4bc45e1..d2d2d8b 100644 --- a/client/android/app/src/main/AndroidManifest.xml +++ b/client/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/client/ios/Flutter/Debug.xcconfig b/client/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/client/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/client/ios/Flutter/Release.xcconfig b/client/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/client/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/client/ios/Podfile b/client/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/client/ios/Podfile @@ -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 diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock new file mode 100644 index 0000000..4d61a45 --- /dev/null +++ b/client/ios/Podfile.lock @@ -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 diff --git a/client/ios/Runner.xcodeproj/project.pbxproj b/client/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4318cd5 --- /dev/null +++ b/client/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; +/* 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 = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + D5753B5E6E2CB7979B358C42 /* Pods */, + AA598329361F3C077ABDB4F2 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; + AA598329361F3C077ABDB4F2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 086A871143ACEBCC07E71A24 /* Pods_Runner.framework */, + 852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 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 = ""; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4d7193e --- /dev/null +++ b/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 +} diff --git a/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/ios/Runner.xcworkspace/contents.xcworkspacedata b/client/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/client/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/client/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4d7193e --- /dev/null +++ b/client/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 +} diff --git a/client/ios/Runner/AppDelegate.swift b/client/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/client/ios/Runner/AppDelegate.swift @@ -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) + } +} diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/client/ios/Runner/Base.lproj/LaunchScreen.storyboard b/client/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/client/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/ios/Runner/Base.lproj/Main.storyboard b/client/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/client/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/ios/Runner/Info.plist b/client/ios/Runner/Info.plist new file mode 100644 index 0000000..5dd1c1c --- /dev/null +++ b/client/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 岩美酒库 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + jiu_client + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/client/ios/Runner/Runner-Bridging-Header.h b/client/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/client/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/client/ios/Runner/SceneDelegate.swift b/client/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/client/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/client/ios/RunnerTests/RunnerTests.swift b/client/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/client/ios/RunnerTests/RunnerTests.swift @@ -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. + } + +} diff --git a/client/lib/core/responsive/responsive.dart b/client/lib/core/responsive/responsive.dart new file mode 100644 index 0000000..3e67198 --- /dev/null +++ b/client/lib/core/responsive/responsive.dart @@ -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; + } +} diff --git a/client/lib/screens/about/about_screen.dart b/client/lib/screens/about/about_screen.dart index d25b57b..d0793f2 100644 --- a/client/lib/screens/about/about_screen.dart +++ b/client/lib/screens/about/about_screen.dart @@ -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, diff --git a/client/lib/screens/auth/login_screen.dart b/client/lib/screens/auth/login_screen.dart index ca4b4e2..a3e9255 100644 --- a/client/lib/screens/auth/login_screen.dart +++ b/client/lib/screens/auth/login_screen.dart @@ -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 { 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 { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8)), child: Container( - width: 400, + width: context.dialogWidth(400), padding: const EdgeInsets.all(40), child: Form( key: _formKey, diff --git a/client/lib/screens/finance/finance_screen.dart b/client/lib/screens/finance/finance_screen.dart index 20d97a1..5fcdbf8 100644 --- a/client/lib/screens/finance/finance_screen.dart +++ b/client/lib/screens/finance/finance_screen.dart @@ -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); } } diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 71a993f..8d73050 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -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 { 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 { } } + Future _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 { 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 { ), ], ), + mobileCards: + filteredItems.map(_inventoryCard).toList(), columns: [ const DataColumn(label: Text('商品编码')), const DataColumn(label: Text('商品名称')), @@ -580,6 +721,7 @@ class _InventoryListScreenState extends ConsumerState { ), ], ), + mobileCards: warnings.map(_warningCard).toList(), columns: const [ DataColumn(label: Text('商品编码')), DataColumn(label: Text('商品名称')), @@ -717,6 +859,7 @@ class _InventoryListScreenState extends ConsumerState { ), ], ), + 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), ), ), diff --git a/client/lib/screens/partners/partners_screen.dart b/client/lib/screens/partners/partners_screen.dart index 8a5c745..4e128aa 100644 --- a/client/lib/screens/partners/partners_screen.dart +++ b/client/lib/screens/partners/partners_screen.dart @@ -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 { 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, diff --git a/client/lib/screens/products/products_screen.dart b/client/lib/screens/products/products_screen.dart index e8d72d9..0caaea3 100644 --- a/client/lib/screens/products/products_screen.dart +++ b/client/lib/screens/products/products_screen.dart @@ -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 { 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 { 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 { 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 { ); } + /// 基础数据:窄屏卡片 + 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 { builder: (ctx) => AlertDialog( title: Text(title), content: SizedBox( - width: 360, + width: context.dialogWidth(360), child: Form( key: formKey, child: Column( diff --git a/client/lib/screens/settings/settings_screen.dart b/client/lib/screens/settings/settings_screen.dart index b62937b..05a6d5f 100644 --- a/client/lib/screens/settings/settings_screen.dart +++ b/client/lib/screens/settings/settings_screen.dart @@ -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 { 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 { 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: [ diff --git a/client/lib/screens/shell/app_shell.dart b/client/lib/screens/shell/app_shell.dart index b75e2e7..dada9f5 100644 --- a/client/lib/screens/shell/app_shell.dart +++ b/client/lib/screens/shell/app_shell.dart @@ -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 { bool _sidebarExpanded = true; final String _loginTime = DateFormat('HH:mm:ss').format(DateTime.now()); bool _forceDialogShown = false; + final GlobalKey _scaffoldKey = GlobalKey(); void _showForceUpdateDialog( BuildContext context, AppUpdateInfo info) { @@ -69,11 +71,86 @@ class _AppShellState extends ConsumerState { _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 { 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 { 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( icon: const Icon(Icons.keyboard_arrow_down, color: Colors.white70), @@ -172,32 +264,33 @@ class _AppShellState extends ConsumerState { 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 { ), ), 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, diff --git a/client/lib/screens/stock_in/stock_in_form_screen.dart b/client/lib/screens/stock_in/stock_in_form_screen.dart index d3d3ef9..b27c399 100644 --- a/client/lib/screens/stock_in/stock_in_form_screen.dart +++ b/client/lib/screens/stock_in/stock_in_form_screen.dart @@ -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: [ diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index fab3e5a..9c16995 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -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 { 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 { ), columns: columns, rows: rows, + mobileCards: orders.map((o) => _orderCard(context, o)).toList(), + ); + } + + /// 操作按钮列表,表格与移动端卡片共用。 + List _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, diff --git a/client/lib/screens/stock_out/stock_out_form_screen.dart b/client/lib/screens/stock_out/stock_out_form_screen.dart index f216007..fda8778 100644 --- a/client/lib/screens/stock_out/stock_out_form_screen.dart +++ b/client/lib/screens/stock_out/stock_out_form_screen.dart @@ -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: [ diff --git a/client/lib/screens/stock_out/stock_out_list_screen.dart b/client/lib/screens/stock_out/stock_out_list_screen.dart index 90d6311..c64f519 100644 --- a/client/lib/screens/stock_out/stock_out_list_screen.dart +++ b/client/lib/screens/stock_out/stock_out_list_screen.dart @@ -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 { 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 { ), columns: columns, rows: rows, + mobileCards: orders.map((o) => _orderCard(context, o)).toList(), + ); + } + + /// 操作按钮列表,表格与移动端卡片共用。 + List _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, diff --git a/client/lib/widgets/data_table_card.dart b/client/lib/widgets/data_table_card.dart index 0c3e5e6..b6d12ea 100644 --- a/client/lib/widgets/data_table_card.dart +++ b/client/lib/widgets/data_table_card.dart @@ -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? onPageChanged; final ValueChanged? onPageSizeChanged; + /// 窄屏(手机)卡片列表。与 [rows] 同序、同数量;提供后窄屏渲染卡片流, + /// 宽屏仍渲染表格。为 null 时窄屏也保持表格(横向滚动)。 + final List? 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 { ), 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 { ); } + 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)), diff --git a/client/lib/widgets/mobile_list_card.dart b/client/lib/widgets/mobile_list_card.dart new file mode 100644 index 0000000..1f20019 --- /dev/null +++ b/client/lib/widgets/mobile_list_card.dart @@ -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 fields; + final List? 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), + ), + ), + ], + ), + ); + } +} diff --git a/client/lib/widgets/searchable_option_field.dart b/client/lib/widgets/searchable_option_field.dart index 1a4b156..177d07b 100644 --- a/client/lib/widgets/searchable_option_field.dart +++ b/client/lib/widgets/searchable_option_field.dart @@ -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: [ diff --git a/client/lib/widgets/select_product_dialog.dart b/client/lib/widgets/select_product_dialog.dart index 002a8f3..98f66d6 100644 --- a/client/lib/widgets/select_product_dialog.dart +++ b/client/lib/widgets/select_product_dialog.dart @@ -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 { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: SizedBox( - width: 480, + width: context.dialogWidth(480), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/client/pubspec.lock b/client/pubspec.lock index 922c788..3131bc6 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -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" diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..e232e6a --- /dev/null +++ b/docs/TODO.md @@ -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/邮件通知,后续可加。 diff --git a/docs/android-signing.md b/docs/android-signing.md new file mode 100644 index 0000000..82c8822 --- /dev/null +++ b/docs/android-signing.md @@ -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` 中忽略。 diff --git a/docs/context/project.md b/docs/context/project.md index e099207..4c09150 100644 --- a/docs/context/project.md +++ b/docs/context/project.md @@ -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 _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 安装、平板布局等) | diff --git a/docs/dev-setup.md b/docs/dev-setup.md index 9ac6390..edc6924 100644 --- a/docs/dev-setup.md +++ b/docs/dev-setup.md @@ -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 规范」。 + --- ## 五、后端测试架构 diff --git a/docs/ios-signing.md b/docs/ios-signing.md new file mode 100644 index 0000000..1921641 --- /dev/null +++ b/docs/ios-signing.md @@ -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`,应用名「岩美酒库」)。 diff --git a/scripts/ci/compile-android.sh b/scripts/ci/compile-android.sh new file mode 100755 index 0000000..d0d289c --- /dev/null +++ b/scripts/ci/compile-android.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# compile-android.sh — 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 --- +if [ -n "${ANDROID_KEYSTORE_BASE64:-}" ]; then + 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 < compile-android: WARNING — no ANDROID_KEYSTORE_BASE64, falling back to debug signing" +fi + +# 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/ diff --git a/scripts/ci/compile-ios.sh b/scripts/ci/compile-ios.sh new file mode 100755 index 0000000..1d4f104 --- /dev/null +++ b/scripts/ci/compile-ios.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# compile-ios.sh — 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" < + + + + methodapp-store + teamID${IOS_TEAM_ID} + signingStylemanual + uploadBitcode + uploadSymbols + provisioningProfiles + + ${BUNDLE_ID}${PROFILE_NAME} + + + +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" diff --git a/scripts/ci/deploy.sh b/scripts/ci/deploy.sh index 17dc796..c1b316c 100755 --- a/scripts/ci/deploy.sh +++ b/scripts/ci/deploy.sh @@ -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 diff --git a/scripts/ci/release.sh b/scripts/ci/release.sh index 223151b..16cd5c7 100755 --- a/scripts/ci/release.sh +++ b/scripts/ci/release.sh @@ -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" diff --git a/web/download.njk b/web/download.njk index 06905d4..f363568 100644 --- a/web/download.njk +++ b/web/download.njk @@ -258,7 +258,7 @@ pageStyle: |

随时随地,
轻松管理酒库。

- Web 版与 Windows 桌面版已正式上线,macOS、iOS、Android 客户端即将推出。 + Web 版、Windows / macOS 桌面版与 Android 客户端均已上线,iOS 客户端即将推出。

@@ -271,7 +271,7 @@ pageStyle: |
可用平台
-
Web / Windows
+
Web / Windows / macOS / Android
@@ -308,7 +308,7 @@ pageStyle: |

支持的平台

-

Web 版与 Windows 桌面版现已可用,其余平台正在开发中。

+

Web 版、Windows / macOS 桌面版与 Android 客户端现已可用,iOS 正在开发中。

@@ -345,25 +345,25 @@ pageStyle: |
-
+

iOS

-

iPhone 与 iPad 通用,支持扫码与推送通知。

- 敬请期待 - - 即将推出 - +

iPhone 与 iPad 通用,通过 TestFlight 安装。

+ 敬请期待 + + 即将推出 +
-
+

Android

-

手机与平板通用,支持主流应用商店及 APK 安装。

- 敬请期待 - - 即将推出 - +

手机与平板通用,APK 直接安装,支持 Android 6.0 及以上。

+ ✓ 已支持 + + 下载 Android 版 +
@@ -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 = '通过 TestFlight 安装'; + if (window.lucide) lucide.createIcons(); + } + if (detected === 'ios') updateDetectCard(detected); + } }) .catch(function() {}); });