Compare commits

..

2 Commits

Author SHA1 Message Date
wangjia cfeb8f0bba chore: release v1.0.19
Deploy / build-linux-web (push) Successful in 54s
Deploy / build-windows (push) Successful in 1m47s
Deploy / build-macos (push) Successful in 1m18s
Deploy / build-android (push) Successful in 1m3s
Deploy / build-ios (push) Successful in 8s
Deploy / release-deploy (push) Successful in 1m35s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:50:34 +08:00
wangjia 480ce836bb chore: release v1.0.18
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-windows (push) Successful in 1m48s
Deploy / build-macos (push) Successful in 1m17s
Deploy / build-android (push) Successful in 4m13s
Deploy / build-ios (push) Successful in 9s
Deploy / release-deploy (push) Successful in 1m37s
移动端响应式适配(抽屉导航/列表卡片/弹窗自适应)、Android 正式签名与 APK 发布、
iOS(TestFlight) 工程与 CI、多平台构建流水线、相关文档同步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:55:33 +08:00
81 changed files with 3190 additions and 366 deletions
+65 -1
View File
@@ -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:
+14
View File
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.19] - 2026-06-07
### 改进
- 优化 Android 构建与部署流程(CI/CD 内部调整),功能与界面无变化
## [1.0.18] - 2026-06-06
### 新功能
- 移动端(Android)界面适配:窄屏(手机)自动切换为移动布局——侧边栏改为侧滑抽屉导航,各列表(库存/出入库/财务/往来单位/基础数据)由宽表格自动切换为卡片流,弹窗与表单自适应屏宽不再溢出;宽屏(桌面/Web)布局保持不变
- Android 客户端正式上线:CI/CD 新增 Android APK 构建(正式 release 签名、versionCode 单调递增),随发版自动发布到下载页;下载页 Android 卡片已激活
### 改进
- 表单字段在手机上占满整行,便于点选;底部状态栏在手机端隐藏
## [1.0.17] - 2026-06-05
### 新功能
+15 -2
View File
@@ -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/CDForgejo)收到 tag 后自动:编译 → 测试 → 创建 Release → 部署 EC2 → Telegram 通知。
CI/CDForgejo`.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
+1 -1
View File
@@ -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"
+6 -6
View File
@@ -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
+29 -6
View File
@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,6 +8,15 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
// 读取 release 签名配置(key.properties 不入库,由本地或 CI 生成)。
// 缺失时回退 debug 签名,保证本地 `flutter run --release` 与无密钥环境可用。
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
val hasReleaseSigning = keystorePropertiesFile.exists()
if (hasReleaseSigning) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.yanmei.jiu"
compileSdk = flutter.compileSdkVersion
@@ -20,21 +32,32 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.yanmei.jiu"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
if (hasReleaseSigning) {
create("release") {
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = if (hasReleaseSigning) {
signingConfigs.getByName("release")
} else {
// 无正式密钥时回退 debug,便于本地构建/调试
signingConfigs.getByName("debug")
}
}
}
}
@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="jiu_client"
android:label="岩美酒库"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
+4
View File
@@ -1,2 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+43
View File
@@ -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
+22
View File
@@ -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
+756
View File
@@ -0,0 +1,756 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3460B9FE6E275516D150453E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 086A871143ACEBCC07E71A24 /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
CBD53FA90DB3F4E143A654CC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
086A871143ACEBCC07E71A24 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1EE111A264F501C30DD0A904 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5A9159D36C6D9B7DF22F0FBF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9C64D02D6BED1C5F196D6192 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0CE49F0D076B3E115DB6BF6D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CBD53FA90DB3F4E143A654CC /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
3460B9FE6E275516D150453E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
D5753B5E6E2CB7979B358C42 /* Pods */,
AA598329361F3C077ABDB4F2 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
AA598329361F3C077ABDB4F2 /* Frameworks */ = {
isa = PBXGroup;
children = (
086A871143ACEBCC07E71A24 /* Pods_Runner.framework */,
852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
D5753B5E6E2CB7979B358C42 /* Pods */ = {
isa = PBXGroup;
children = (
9C64D02D6BED1C5F196D6192 /* Pods-Runner.debug.xcconfig */,
5A9159D36C6D9B7DF22F0FBF /* Pods-Runner.release.xcconfig */,
1EE111A264F501C30DD0A904 /* Pods-Runner.profile.xcconfig */,
08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */,
2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */,
F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
B21EFFE7BABA33CD7CB4E616 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
0CE49F0D076B3E115DB6BF6D /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
3052FA6A8375458CBCA9F94D /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DB40772CF39C21BA0E695053 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3052FA6A8375458CBCA9F94D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B21EFFE7BABA33CD7CB4E616 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DB40772CF39C21BA0E695053 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,59 @@
{
"pins" : [
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,59 @@
{
"pins" : [
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>岩美酒库</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>jiu_client</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
@@ -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;
}
}
+2 -1
View File
@@ -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,
+3 -2
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../core/responsive/responsive.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -125,7 +126,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
borderRadius:
const BorderRadius.vertical(bottom: Radius.circular(4)),
child: SizedBox(
width: 320,
width: context.dialogWidth(320),
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
@@ -263,7 +264,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
child: Container(
width: 400,
width: context.dialogWidth(400),
padding: const EdgeInsets.all(40),
child: Form(
key: _formKey,
+90 -33
View File
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/finance.dart';
import '../../providers/finance_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../providers/connectivity_provider.dart';
@@ -129,6 +131,47 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
);
}
/// 财务记录:窄屏卡片
Widget _financeCard(FinanceRecord r) {
final canClose = (r.type == 'payable' || r.type == 'receivable') &&
r.status == 'open';
final showStatus = r.type == 'payable' || r.type == 'receivable';
return MobileListCard(
title: Text(r.partnerName?.isNotEmpty == true ? r.partnerName! : r.typeLabel),
subtitle: Text(r.recordDate?.substring(0, 10) ?? '-'),
trailing: _TypeBadge(r.typeLabel),
fields: [
MobileCardField('金额', '¥${r.amount.toStringAsFixed(2)}'),
MobileCardField('余额', null,
valueWidget: Text(
'¥${r.balance.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 13,
color: r.balance > 0
? AppTheme.danger
: AppTheme.textSecondary,
fontWeight: FontWeight.w600,
),
)),
if (r.refType != null && r.refId != null)
MobileCardField(
'关联单据', '${r.refType!.replaceAll('_', '-')}#${r.refId}'),
if (showStatus)
MobileCardField('状态', null, valueWidget: _StatusBadge(r.status)),
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
],
actions: canClose
? [
TextButton(
onPressed: () => _closeRecord(r),
child: const Text('结清',
style: TextStyle(fontSize: 13, color: AppTheme.success)),
),
]
: null,
);
}
@override
Widget build(BuildContext context) {
ref.listen(networkRecoveryCountProvider, (_, __) => _refetch());
@@ -302,34 +345,44 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
return Column(
children: [
if (widget.typeFilter.isNotEmpty && records.isNotEmpty)
Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: Row(
children: [
_SummaryCard(
title: '合计金额',
value: '¥${(totalAmount / 10000).toStringAsFixed(2)}',
icon: Icons.account_balance_wallet,
color: AppTheme.primary,
),
const SizedBox(width: 12),
_SummaryCard(
title: '未结清',
value: '¥${(openAmount / 10000).toStringAsFixed(2)}',
icon: Icons.pending_actions,
color: AppTheme.danger,
),
const SizedBox(width: 12),
_SummaryCard(
title: '已结清',
value: '¥${(closedAmount / 10000).toStringAsFixed(2)}',
icon: Icons.check_circle,
color: AppTheme.success,
),
],
),
),
Builder(builder: (context) {
final w = context.isMobile ? 150.0 : null;
final cards = [
_SummaryCard(
title: '合计金额',
value: '¥${(totalAmount / 10000).toStringAsFixed(2)}',
icon: Icons.account_balance_wallet,
color: AppTheme.primary,
width: w,
),
const SizedBox(width: 12),
_SummaryCard(
title: '未结清',
value: '¥${(openAmount / 10000).toStringAsFixed(2)}',
icon: Icons.pending_actions,
color: AppTheme.danger,
width: w,
),
const SizedBox(width: 12),
_SummaryCard(
title: '已结清',
value: '¥${(closedAmount / 10000).toStringAsFixed(2)}',
icon: Icons.check_circle,
color: AppTheme.success,
width: w,
),
];
return Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: context.isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: cards),
)
: Row(children: cards),
);
}),
if (widget.typeFilter.isNotEmpty && records.isNotEmpty)
const Divider(height: 1),
Expanded(
@@ -405,6 +458,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
),
columns: columns,
rows: rows,
mobileCards: records.map(_financeCard).toList(),
),
),
],
@@ -487,7 +541,7 @@ class _AddPaymentDialogState extends State<_AddPaymentDialog> {
return AlertDialog(
title: Text(_title),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -611,18 +665,19 @@ class _SummaryCard extends StatelessWidget {
final String value;
final IconData icon;
final Color color;
final double? width;
const _SummaryCard({
required this.title,
required this.value,
required this.icon,
required this.color,
this.width,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
final card = Container(
height: 72,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@@ -659,8 +714,10 @@ class _SummaryCard extends StatelessWidget {
),
],
),
),
);
);
return width != null
? SizedBox(width: width, child: card)
: Expanded(child: card);
}
}
@@ -5,11 +5,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/inventory.dart';
import '../../core/auth/auth_state.dart';
import '../../providers/inventory_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
@@ -47,7 +49,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
builder: (ctx) => AlertDialog(
title: const Text('修改备注'),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: TextField(
controller: ctrl,
autofocus: true,
@@ -188,6 +190,133 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
}
}
Future<void> _printLabel(BuildContext context, Inventory item) async {
if (item.productId == null) return;
final qrBytes =
await ref.read(productRepositoryProvider).getQRCodeBytes(item.productId!);
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
if (!context.mounted) return;
await safePrint(
context,
() => printProductLabel(
qrBytes: qrBytes,
name: item.productName,
code: item.productCode,
series: item.series.isEmpty ? null : item.series,
spec: item.spec.isEmpty ? null : item.spec,
batchNo: item.batchNo.isEmpty ? null : item.batchNo,
productionDate: item.productionDate,
remark: item.remark.isEmpty ? null : item.remark,
shopName: shopInfo?.name ?? '',
shopAddress: shopInfo?.address ?? '',
shopPhone: shopInfo?.phone ?? '',
),
);
}
/// 库存查询:窄屏卡片
Widget _inventoryCard(Inventory item) {
return MobileListCard(
onTap: item.productId != null
? () => context.push('/products/${item.productId}')
: null,
title: Text(item.productName.isEmpty ? '-' : item.productName),
subtitle: item.productCode.isEmpty ? null : Text(item.productCode),
trailing: _InventoryStatusBadge(item),
fields: [
if (item.spec.isNotEmpty) MobileCardField('规格', item.spec),
if (item.series.isNotEmpty) MobileCardField('系列', item.series),
if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo),
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
MobileCardField('库存', null,
valueWidget: Text(
'${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: item.quantity == 0
? AppTheme.danger
: (item.minStock != null && item.quantity < item.minStock!)
? AppTheme.accent
: AppTheme.textPrimary,
),
)),
if (item.unitPrice != null)
MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'),
if (item.supplierName.isNotEmpty)
MobileCardField('供应商', item.supplierName),
],
actions: [
TextButton(
onPressed: () => _editRemark(context, item),
child: const Text('备注', style: TextStyle(fontSize: 13)),
),
if (item.productId != null)
TextButton(
onPressed: () => _printLabel(context, item),
child: const Text('打标签', style: TextStyle(fontSize: 13)),
),
],
);
}
/// 库存预警:窄屏卡片
Widget _warningCard(Inventory item) {
return MobileListCard(
onTap: item.productId != null
? () => context.push('/products/${item.productId}')
: null,
title: Text(item.productName.isEmpty ? '-' : item.productName),
subtitle: item.productCode.isEmpty ? null : Text(item.productCode),
trailing: _InventoryStatusBadge(item),
fields: [
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
MobileCardField('当前库存', null,
valueWidget: Text(
item.quantity.toStringAsFixed(0),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: item.quantity == 0 ? AppTheme.danger : AppTheme.accent),
)),
MobileCardField('安全库存', '${item.minStock}'),
MobileCardField('缺口', null,
valueWidget: Text(
'${item.minStock! - item.quantity.toInt()}',
style: const TextStyle(
fontSize: 13,
color: AppTheme.danger,
fontWeight: FontWeight.w600),
)),
],
);
}
/// 流水记录:窄屏卡片
Widget _logCard(dynamic log) {
return MobileListCard(
title: Text(log.productName ?? '-'),
subtitle: Text(log.createdAt?.substring(0, 19) ?? '-'),
trailing: _DirectionBadge(log.direction),
fields: [
MobileCardField('仓库', log.warehouseName ?? '-'),
MobileCardField('数量', null,
valueWidget: Text(
log.quantity.toStringAsFixed(0),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: log.direction == 'in'
? AppTheme.success
: AppTheme.danger),
)),
MobileCardField('变前', log.qtyBefore?.toStringAsFixed(0) ?? '-'),
MobileCardField('变后', log.qtyAfter?.toStringAsFixed(0) ?? '-'),
if (log.refType != null) MobileCardField('来源', log.refType),
],
);
}
@override
Widget build(BuildContext context) {
return PageScaffold(
@@ -251,35 +380,45 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
return Column(
children: [
// Summary cards
Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: Row(
children: [
_SummaryCard(
title: '商品总数',
value: '${result.total}',
unit: '',
icon: Icons.inventory_2,
color: AppTheme.primary),
const SizedBox(width: 12),
_SummaryCard(
title: '库存预警',
value: '$warningCount',
unit: '',
icon: Icons.warning_amber,
color: AppTheme.accent),
const SizedBox(width: 12),
_SummaryCard(
title: '缺货商品',
value: '$emptyCount',
unit: '',
icon: Icons.remove_shopping_cart,
color: AppTheme.danger),
],
),
),
// Summary cards(窄屏横向滚动,宽屏等分)
Builder(builder: (context) {
final w = context.isMobile ? 150.0 : null;
final cards = [
_SummaryCard(
title: '商品总数',
value: '${result.total}',
unit: '',
icon: Icons.inventory_2,
color: AppTheme.primary,
width: w),
const SizedBox(width: 12),
_SummaryCard(
title: '库存预警',
value: '$warningCount',
unit: '',
icon: Icons.warning_amber,
color: AppTheme.accent,
width: w),
const SizedBox(width: 12),
_SummaryCard(
title: '缺货商品',
value: '$emptyCount',
unit: '',
icon: Icons.remove_shopping_cart,
color: AppTheme.danger,
width: w),
];
return Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: context.isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: cards),
)
: Row(children: cards),
);
}),
const Divider(height: 1),
Expanded(
child: DataTableCard(
@@ -346,6 +485,8 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards:
filteredItems.map(_inventoryCard).toList(),
columns: [
const DataColumn(label: Text('商品编码')),
const DataColumn(label: Text('商品名称')),
@@ -580,6 +721,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards: warnings.map(_warningCard).toList(),
columns: const [
DataColumn(label: Text('商品编码')),
DataColumn(label: Text('商品名称')),
@@ -717,6 +859,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards: logs.map(_logCard).toList(),
columns: const [
DataColumn(label: Text('商品名称')),
DataColumn(label: Text('仓库')),
@@ -788,18 +931,21 @@ class _SummaryCard extends StatelessWidget {
final IconData icon;
final Color color;
/// 固定宽度(窄屏横向滚动用);为 null 时占据等分宽度(Expanded,宽屏用)。
final double? width;
const _SummaryCard({
required this.title,
required this.value,
required this.unit,
required this.icon,
required this.color,
this.width,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
final card = Container(
height: 72,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@@ -849,8 +995,10 @@ class _SummaryCard extends StatelessWidget {
),
],
),
),
);
);
return width != null
? SizedBox(width: width, child: card)
: Expanded(child: card);
}
}
@@ -950,7 +1098,7 @@ class _ImportProgressDialog extends StatelessWidget {
content: ValueListenableBuilder<_ImportState>(
valueListenable: stateNotifier,
builder: (_, state, __) => SizedBox(
width: 320,
width: context.dialogWidth(320),
child: _buildContent(context, state),
),
),
@@ -3,10 +3,12 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/models/page_result.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/partner.dart';
import '../../providers/partner_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
@@ -145,12 +147,48 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
required void Function(Partner) onDelete,
}) {
final partners = result.data;
Widget partnerCard(Partner p) {
return MobileListCard(
title: Text(p.name),
subtitle: p.code != null ? Text(p.code!) : null,
trailing: Text(
p.status == 'enabled' ? '正常' : '禁用',
style: TextStyle(
fontSize: 12,
color: p.status == 'enabled'
? AppTheme.success
: AppTheme.textSecondary,
),
),
fields: [
if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact),
if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone),
if (p.address?.isNotEmpty == true) MobileCardField('地址', p.address),
],
actions: [
TextButton(
key: Key('btn_edit_${p.id}'),
onPressed: () => onEdit(p),
child: const Text('编辑', style: TextStyle(fontSize: 13)),
),
TextButton(
key: Key('btn_delete_${p.id}'),
onPressed: () => onDelete(p),
child: const Text('删除',
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
),
],
);
}
return DataTableCard(
totalCount: result.total,
page: result.page,
pageSize: result.pageSize,
onPageChanged: onPageChanged,
onPageSizeChanged: onPageSizeChanged,
mobileCards: partners.map(partnerCard).toList(),
toolbar: Row(
children: [
ElevatedButton.icon(
@@ -433,7 +471,7 @@ class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
final typeName = widget.isSupplier ? '供应商' : '客户';
return Dialog(
child: Container(
width: 520,
width: context.dialogWidth(520),
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
@@ -1,9 +1,11 @@
import '../../core/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/product_option_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
@@ -84,6 +86,23 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
),
),
mobileCards: paged
.map((it) => _optionCard(
code: it.code,
name: it.name,
remark: it.remark,
onEdit: () => _showOptionDialog(
title: '编辑商品名称',
hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productNameListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete(
'删除名称「${it.name}」?',
() => ref.read(productNameListProvider.notifier).delete(it.id),
),
))
.toList(),
columns: const [
DataColumn(label: Text('编号')),
DataColumn(label: Text('名称')),
@@ -156,6 +175,23 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
),
),
mobileCards: paged
.map((it) => _optionCard(
code: it.code,
name: it.name,
remark: it.remark,
onEdit: () => _showOptionDialog(
title: '编辑系列',
hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productSeriesListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete(
'删除系列「${it.name}」?',
() => ref.read(productSeriesListProvider.notifier).delete(it.id),
),
))
.toList(),
columns: const [
DataColumn(label: Text('编号')),
DataColumn(label: Text('系列名称')),
@@ -228,6 +264,24 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
rows: items.map((it) => [it.code ?? '', it.name, it.quantity, it.remark ?? '']).toList(),
),
),
mobileCards: paged
.map((it) => _optionCard(
code: it.code,
name: it.name,
remark: it.remark,
quantity: it.quantity,
onEdit: () => _showOptionDialog(
title: '编辑规格',
hasQuantity: true,
initial: {'code': it.code ?? '', 'name': it.name, 'quantity': it.quantity, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productSpecListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete(
'删除规格「${it.name}」?',
() => ref.read(productSpecListProvider.notifier).delete(it.id),
),
))
.toList(),
columns: const [
DataColumn(label: Text('编号')),
DataColumn(label: Text('规格名称')),
@@ -321,6 +375,37 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
);
}
/// 基础数据:窄屏卡片
Widget _optionCard({
required String? code,
required String name,
required String? remark,
int? quantity,
required VoidCallback onEdit,
required VoidCallback onDelete,
}) {
return MobileListCard(
title: Text(name),
subtitle: (code?.isNotEmpty == true) ? Text(code!) : null,
fields: [
if (quantity != null && quantity > 0)
MobileCardField('单品数量', '$quantity'),
if (remark?.isNotEmpty == true) MobileCardField('备注', remark),
],
actions: [
TextButton(
onPressed: onEdit,
child: const Text('编辑', style: TextStyle(fontSize: 13)),
),
TextButton(
onPressed: onDelete,
child: const Text('删除',
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
),
],
);
}
Widget _actionButtons({required VoidCallback onEdit, required VoidCallback onDelete}) {
return Row(
mainAxisSize: MainAxisSize.min,
@@ -386,7 +471,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
builder: (ctx) => AlertDialog(
title: Text(title),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: Form(
key: formKey,
child: Column(
@@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/number_rule.dart';
import '../../models/user.dart';
@@ -536,7 +537,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
builder: (ctx) => AlertDialog(
title: Text('编辑编号规则 — ${rule.typeLabel}'),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -708,7 +709,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
builder: (ctx) => AlertDialog(
title: Text('修改$label'),
content: SizedBox(
width: 320,
width: context.dialogWidth(320),
child: TextField(
controller: ctrl,
autofocus: true,
@@ -838,7 +839,7 @@ class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
return AlertDialog(
title: Text(widget.warehouse != null ? '编辑仓库' : '新建仓库'),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Form(
key: _formKey,
child: Column(
@@ -971,7 +972,7 @@ class _UserFormDialogState extends ConsumerState<_UserFormDialog> {
return AlertDialog(
title: Text(_isEdit ? '编辑用户' : '新增用户'),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Form(
key: _formKey,
child: Column(
@@ -1084,7 +1085,7 @@ class _ResetPasswordDialogState extends ConsumerState<_ResetPasswordDialog> {
return AlertDialog(
title: Text('重置密码 — ${widget.user.username}'),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: TextField(
controller: _ctrl,
obscureText: true,
@@ -1625,7 +1626,7 @@ class _BatchImportWidgetState extends ConsumerState<_BatchImportWidget> {
const Text('确认清空数据', style: TextStyle(color: Colors.red)),
]),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -2035,7 +2036,7 @@ class _ShopEditDialogState extends ConsumerState<_ShopEditDialog> {
return AlertDialog(
title: const Text('编辑酒行信息'),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
+147 -53
View File
@@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
import 'dart:async';
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/connectivity_provider.dart';
import '../../providers/shop_provider.dart';
@@ -24,6 +25,7 @@ class _AppShellState extends ConsumerState<AppShell> {
bool _sidebarExpanded = true;
final String _loginTime = DateFormat('HH:mm:ss').format(DateTime.now());
bool _forceDialogShown = false;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _showForceUpdateDialog(
BuildContext context, AppUpdateInfo info) {
@@ -69,11 +71,86 @@ class _AppShellState extends ConsumerState<AppShell> {
_NavItem(icon: Icons.info_outline, label: '关于我们', path: '/about'),
];
/// 窄屏(手机)侧滑抽屉导航,容纳全部菜单项。
Widget _buildDrawer(
BuildContext context, AuthUser? user, String location) {
final shopAsync = ref.watch(shopInfoProvider);
final shopName = shopAsync.valueOrNull?.name ?? user?.shopNo ?? '';
final logoUrl = shopAsync.valueOrNull?.logoUrl ?? '';
return Drawer(
child: Container(
color: AppTheme.primaryDark,
child: SafeArea(
child: Column(
children: [
// 抽屉头部:门店 Logo + 名称 + 编号
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
color: AppTheme.primary,
child: Row(
children: [
_ShopLogo(
logoUrl: logoUrl, shopName: shopName, size: 40),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
shopName.isEmpty ? '' : shopName,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
if (user != null) ...[
const SizedBox(height: 2),
Text(
'${user.shopNo} · ${user.username}',
style: const TextStyle(
color: Colors.white70, fontSize: 12),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: _navItems.map((item) {
final isActive = location.startsWith(item.path);
return _SidebarItem(
item: item,
isActive: isActive,
expanded: true,
onTap: () {
Navigator.pop(context); // 关闭抽屉
context.go(item.path);
},
);
}).toList(),
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final user = ref.watch(authStateProvider).user;
final isOnline = ref.watch(connectivityProvider);
final location = GoRouterState.of(context).matchedLocation;
final isMobile = context.isMobile;
final sidebarWidth = _sidebarExpanded ? 200.0 : 56.0;
final updateNotifier = ref.watch(updateProvider.notifier);
final updateInfo = ref.watch(updateProvider).valueOrNull;
@@ -82,6 +159,8 @@ class _AppShellState extends ConsumerState<AppShell> {
return SelectionArea(
child: Scaffold(
key: _scaffoldKey,
drawer: isMobile ? _buildDrawer(context, user, location) : null,
body: Column(
children: [
// Top Bar
@@ -93,41 +172,54 @@ class _AppShellState extends ConsumerState<AppShell> {
children: [
IconButton(
icon: Icon(
_sidebarExpanded ? Icons.menu_open : Icons.menu,
isMobile
? Icons.menu
: (_sidebarExpanded ? Icons.menu_open : Icons.menu),
color: Colors.white),
onPressed: () =>
setState(() => _sidebarExpanded = !_sidebarExpanded),
tooltip: _sidebarExpanded ? '收起侧边栏' : '展开侧边栏',
onPressed: () {
if (isMobile) {
_scaffoldKey.currentState?.openDrawer();
} else {
setState(() => _sidebarExpanded = !_sidebarExpanded);
}
},
tooltip: isMobile
? '菜单'
: (_sidebarExpanded ? '收起侧边栏' : '展开侧边栏'),
),
const SizedBox(width: 4),
_ShopButton(user: user, version: appVersion),
const Spacer(),
if (user != null) ...[
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _showShopPanel(context, user, version: appVersion),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.business,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.shopNo,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
],
// 窄屏隐藏门店号/用户名文字块,仅保留用户菜单,避免顶栏拥挤
if (!isMobile) ...[
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () =>
_showShopPanel(context, user, version: appVersion),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.business,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.shopNo,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
],
),
),
),
),
const SizedBox(width: 20),
const Icon(Icons.person_outline,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.username,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
const SizedBox(width: 8),
const SizedBox(width: 20),
const Icon(Icons.person_outline,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.username,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
const SizedBox(width: 8),
],
PopupMenuButton<String>(
icon: const Icon(Icons.keyboard_arrow_down,
color: Colors.white70),
@@ -172,32 +264,33 @@ class _AppShellState extends ConsumerState<AppShell> {
Expanded(
child: Row(
children: [
// Sidebar
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: sidebarWidth,
color: AppTheme.primaryDark,
child: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: _navItems.map((item) {
final isActive =
location.startsWith(item.path);
return _SidebarItem(
item: item,
isActive: isActive,
expanded: _sidebarExpanded,
onTap: () => context.go(item.path),
);
}).toList(),
// Sidebar(仅宽屏;窄屏改用 Drawer
if (!isMobile)
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: sidebarWidth,
color: AppTheme.primaryDark,
child: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: _navItems.map((item) {
final isActive =
location.startsWith(item.path);
return _SidebarItem(
item: item,
isActive: isActive,
expanded: _sidebarExpanded,
onTap: () => context.go(item.path),
);
}).toList(),
),
),
),
],
],
),
),
),
// Content area
Expanded(
child: Column(
@@ -278,7 +371,8 @@ class _AppShellState extends ConsumerState<AppShell> {
),
),
Expanded(child: widget.child),
// Status bar
// Status bar(仅宽屏;窄屏隐藏,离线已有顶部横幅提示)
if (!isMobile)
LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
@@ -515,7 +609,7 @@ void _showShopPanel(BuildContext context, AuthUser u, {String version = 'v1.0.0'
builder: (ctx) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: SizedBox(
width: 360,
width: ctx.dialogWidth(360),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/responsive/responsive.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/auth/auth_state.dart';
@@ -849,8 +850,14 @@ class _FormField extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 窄屏(手机)字段占满整行,便于点选;宽屏沿用固定宽度配合 Wrap 多列。
final effectiveWidth = width == double.infinity
? double.infinity
: (context.isMobile
? MediaQuery.sizeOf(context).width - 64
: width);
return SizedBox(
width: width,
width: effectiveWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -4,11 +4,13 @@ import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, pri
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/stock_in.dart';
import '../../providers/stock_in_provider.dart';
import '../../repositories/stock_in_repository.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
@@ -239,88 +241,10 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
return DataCell(SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _showDetail(context, o.id),
child: const Text('详情',
style:
TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockInRepositoryProvider).get(o.id);
if (context.mounted) {
await safePrint(context, () => printStockInOrder(order));
}
},
child: const Text('打印',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockInRepositoryProvider).get(o.id);
if (!context.mounted) return;
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
await showDialog(
context: context,
builder: (_) => _LabelPrintDialog(
order: order,
productRepo: ref.read(productRepositoryProvider),
shopName: shopInfo?.name ?? '',
shopAddress: shopInfo?.address ?? '',
shopPhone: shopInfo?.phone ?? '',
),
);
},
child: const Text('打标签',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
if (o.status == 'approved') ...[
TextButton(
onPressed: () => _confirmSettle(context, o.id, 'stock_in'),
child: const Text('结清',
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
),
],
if (o.status == 'draft') ...[
TextButton(
onPressed: () => context.go('/stock-in/edit/${o.id}'),
child: const Text('修改',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(
fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
],
],
)));
mainAxisSize: MainAxisSize.min,
children: _orderActions(context, o),
),
));
default:
return const DataCell(SizedBox());
}
@@ -414,6 +338,105 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
),
columns: columns,
rows: rows,
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
);
}
/// 操作按钮列表,表格与移动端卡片共用。
List<Widget> _orderActions(BuildContext context, StockInOrder o) {
return [
TextButton(
onPressed: () => _showDetail(context, o.id),
child: const Text('详情',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockInRepositoryProvider).get(o.id);
if (context.mounted) {
await safePrint(context, () => printStockInOrder(order));
}
},
child: const Text('打印',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockInRepositoryProvider).get(o.id);
if (!context.mounted) return;
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
await showDialog(
context: context,
builder: (_) => _LabelPrintDialog(
order: order,
productRepo: ref.read(productRepositoryProvider),
shopName: shopInfo?.name ?? '',
shopAddress: shopInfo?.address ?? '',
shopPhone: shopInfo?.phone ?? '',
),
);
},
child: const Text('打标签',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
if (o.status == 'approved')
TextButton(
onPressed: () => _confirmSettle(context, o.id, 'stock_in'),
child: const Text('结清',
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
),
if (o.status == 'draft') ...[
TextButton(
onPressed: () => context.go('/stock-in/edit/${o.id}'),
child: const Text('修改',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
],
];
}
/// 入库单:窄屏卡片
Widget _orderCard(BuildContext context, StockInOrder o) {
return MobileListCard(
onTap: () => _showDetail(context, o.id),
title: Text(o.orderNo,
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
trailing: StatusBadge(_apiStatusToEnum(o.status)),
fields: [
MobileCardField('供应商', o.partnerName ?? '-'),
MobileCardField('仓库', o.warehouseName ?? '-'),
MobileCardField(
'金额',
o.totalAmount != null
? '¥${o.totalAmount!.toStringAsFixed(2)}'
: '-'),
MobileCardField('日期', o.orderDate?.substring(0, 10) ?? '-'),
],
actions: _orderActions(context, o),
);
}
@@ -668,7 +691,7 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 720,
width: context.dialogWidth(720),
constraints: const BoxConstraints(maxHeight: 600),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -965,7 +988,7 @@ class _LabelPrintDialogState extends State<_LabelPrintDialog> {
final items = widget.order.items;
return Dialog(
child: Container(
width: 520,
width: context.dialogWidth(520),
constraints: const BoxConstraints(maxHeight: 520),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../core/auth/auth_state.dart';
import '../../core/utils/print_util.dart';
@@ -683,8 +684,14 @@ class _FormField extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 窄屏(手机)字段占满整行,便于点选;宽屏沿用固定宽度配合 Wrap 多列。
final effectiveWidth = width == double.infinity
? double.infinity
: (context.isMobile
? MediaQuery.sizeOf(context).width - 64
: width);
return SizedBox(
width: width,
width: effectiveWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -742,7 +749,7 @@ class _InventoryPickerDialogState extends State<_InventoryPickerDialog> {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: SizedBox(
width: 820,
width: context.dialogWidth(820),
height: 580,
child: Column(
children: [
@@ -5,11 +5,13 @@ import '../../core/utils/print_util.dart' show safePrint, printStockOutOrder, pr
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/stock_out.dart';
import '../../providers/stock_out_provider.dart';
import '../../repositories/stock_out_repository.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
@@ -246,69 +248,10 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
return DataCell(SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _showDetail(context, o.id),
child: const Text('详情',
style:
TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
if (context.mounted) {
await safePrint(context, () => printStockOutOrder(order));
}
},
child: const Text('打印',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
if (o.status == 'approved') ...[
TextButton(
onPressed: () => _confirmSettle(context, o.id, 'stock_out'),
child: const Text('结清',
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
),
],
if (o.status == 'draft') ...[
TextButton(
onPressed: () => context.go('/stock-out/edit/${o.id}'),
child: const Text('修改',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(
fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
],
],
)));
mainAxisSize: MainAxisSize.min,
children: _orderActions(context, o),
),
));
default:
return const DataCell(SizedBox());
}
@@ -402,6 +345,86 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
),
columns: columns,
rows: rows,
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
);
}
/// 操作按钮列表,表格与移动端卡片共用。
List<Widget> _orderActions(BuildContext context, StockOutOrder o) {
return [
TextButton(
onPressed: () => _showDetail(context, o.id),
child: const Text('详情',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
if (context.mounted) {
await safePrint(context, () => printStockOutOrder(order));
}
},
child: const Text('打印',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
if (o.status == 'approved')
TextButton(
onPressed: () => _confirmSettle(context, o.id, 'stock_out'),
child: const Text('结清',
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
),
if (o.status == 'draft') ...[
TextButton(
onPressed: () => context.go('/stock-out/edit/${o.id}'),
child: const Text('修改',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
],
];
}
/// 出库单:窄屏卡片
Widget _orderCard(BuildContext context, StockOutOrder o) {
return MobileListCard(
onTap: () => _showDetail(context, o.id),
title: Text(o.orderNo,
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
trailing: StatusBadge(_apiStatusToEnum(o.status)),
fields: [
MobileCardField('客户', o.partnerName ?? '-'),
MobileCardField('仓库', o.warehouseName ?? '-'),
MobileCardField(
'金额',
o.totalAmount != null
? '¥${o.totalAmount!.toStringAsFixed(2)}'
: '-'),
MobileCardField('日期', o.orderDate?.substring(0, 10) ?? '-'),
],
actions: _orderActions(context, o),
);
}
@@ -660,7 +683,7 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 720,
width: context.dialogWidth(720),
constraints: const BoxConstraints(maxHeight: 600),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -961,7 +984,7 @@ class _LabelPrintDialogState extends State<_LabelPrintDialog> {
final items = widget.order.items;
return Dialog(
child: Container(
width: 520,
width: context.dialogWidth(520),
constraints: const BoxConstraints(maxHeight: 520),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
+55 -24
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../core/responsive/responsive.dart';
import '../core/theme/app_theme.dart';
class DataTableCard extends StatefulWidget {
@@ -11,6 +12,10 @@ class DataTableCard extends StatefulWidget {
final ValueChanged<int>? onPageChanged;
final ValueChanged<int>? onPageSizeChanged;
/// [rows]
/// null
final List<Widget>? mobileCards;
const DataTableCard({
super.key,
required this.columns,
@@ -21,6 +26,7 @@ class DataTableCard extends StatefulWidget {
this.pageSize = 20,
this.onPageChanged,
this.onPageSizeChanged,
this.mobileCards,
});
@override
@@ -49,30 +55,9 @@ class _DataTableCardState extends State<DataTableCard> {
),
if (widget.toolbar != null) const Divider(height: 1),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
border: TableBorder(
horizontalInside: BorderSide(
color: Colors.grey.shade200,
width: 0.5,
),
),
children: [
_buildHeaderRow(),
for (int i = 0; i < widget.rows.length; i++)
_buildDataRow(i, widget.rows[i]),
],
),
),
),
),
),
child: (context.isMobile && widget.mobileCards != null)
? _buildMobileList()
: _buildTable(),
),
// Pagination
if (widget.totalCount != null)
@@ -102,6 +87,52 @@ class _DataTableCardState extends State<DataTableCard> {
);
}
Widget _buildTable() {
return LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
border: TableBorder(
horizontalInside: BorderSide(
color: Colors.grey.shade200,
width: 0.5,
),
),
children: [
_buildHeaderRow(),
for (int i = 0; i < widget.rows.length; i++)
_buildDataRow(i, widget.rows[i]),
],
),
),
),
),
);
}
Widget _buildMobileList() {
final cards = widget.mobileCards!;
if (cards.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text('暂无数据',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14)),
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: cards.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (_, i) => cards[i],
);
}
TableRow _buildHeaderRow() {
return TableRow(
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import '../core/theme/app_theme.dart';
/// label: value
class MobileCardField {
final String label;
final String? value;
/// [value]
final Widget? valueWidget;
const MobileCardField(this.label, this.value, {this.valueWidget});
}
/// + + +
/// 使
class MobileListCard extends StatelessWidget {
final Widget title;
final Widget? subtitle;
final Widget? trailing;
final List<MobileCardField> fields;
final List<Widget>? actions;
final VoidCallback? onTap;
const MobileListCard({
super.key,
required this.title,
this.subtitle,
this.trailing,
this.fields = const [],
this.actions,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade200),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DefaultTextStyle(
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
child: title,
),
if (subtitle != null) ...[
const SizedBox(height: 2),
DefaultTextStyle(
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary),
child: subtitle!,
),
],
],
),
),
if (trailing != null) ...[
const SizedBox(width: 8),
trailing!,
],
],
),
if (fields.isNotEmpty) ...[
const SizedBox(height: 10),
...fields.map(_buildField),
],
if (actions != null && actions!.isNotEmpty) ...[
const Divider(height: 18),
Align(
alignment: Alignment.centerRight,
child: Wrap(
spacing: 4,
children: actions!,
),
),
],
],
),
),
),
);
}
Widget _buildField(MobileCardField f) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 64,
child: Text(
f.label,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary),
),
),
Expanded(
child: f.valueWidget ??
Text(
f.value ?? '',
style: const TextStyle(
fontSize: 13, color: AppTheme.textPrimary),
),
),
],
),
);
}
}
@@ -1,5 +1,6 @@
import '../core/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import '../core/responsive/responsive.dart';
import 'package:lpinyin/lpinyin.dart';
import '../core/theme/app_theme.dart';
@@ -135,7 +136,7 @@ class _SearchDialogState extends State<_SearchDialog> {
title: Text(widget.title, style: const TextStyle(fontSize: 16)),
contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
content: SizedBox(
width: 300,
width: context.dialogWidth(300),
height: 400,
child: Column(
children: [
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../core/responsive/responsive.dart';
import 'package:lpinyin/lpinyin.dart';
import '../models/product.dart';
@@ -69,7 +70,7 @@ class _SelectProductDialogState extends State<SelectProductDialog> {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: SizedBox(
width: 480,
width: context.dialogWidth(480),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
+16 -16
View File
@@ -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"
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# render-env.sh — 从 Bitwarden 渲染 jiu 的 production.env(方案一)
#
# 配置基底 = production.env.template(非密钥项的权威来源,随仓库维护)。
# 密钥 = 从 Bitwarden 条目 DB_PASSWORD 的同名字段按需取,Mac 上不常驻明文。
#
# 用法:
# sh deploy/render-env.sh --check # 只校验:4 个密钥都能从金库取到、长度非空
# sh deploy/render-env.sh --print # 渲染到 stdout 预览(密钥打码,可安全粘贴)
# sh deploy/render-env.sh --out FILE # 渲染到指定文件(含明文,慎用,自负保管)
# sh deploy/render-env.sh --deploy # 渲染→scp 到 EC2→重启 jiu→健康检查→删本地临时文件
#
# 前提:rbw 已 unlock。
set -euo pipefail
# ---- 可配置项 ----
RBW="${RBW_BIN:-/opt/homebrew/bin/rbw}"
BW_ITEM="${JIU_BW_ITEM:-DB_PASSWORD}" # 存放 jiu 密钥字段的 Bitwarden 条目
SECRET_KEYS="DATABASE_DSN JWT_SECRET LICENSE_HMAC_SECRET DB_PASSWORD"
EC2_HOST="${EC2_HOST:-18.136.60.128}"
EC2_USER="${EC2_USER:-ec2-user}"
EC2_KEY="${EC2_KEY:-$HOME/.ssh/wangjia.pem}"
REMOTE_ENV="/opt/jiu/config/production.env"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEMPLATE="$SCRIPT_DIR/production.env.template"
# ---- 工具函数 ----
is_secret() {
case " $SECRET_KEYS " in *" $1 "*) return 0;; *) return 1;; esac
}
require_unlocked() {
if ! "$RBW" unlocked >/dev/null 2>&1; then
echo "render-env: 金库未解锁,请先运行: rbw unlock" >&2
exit 1
fi
}
# 渲染:逐行读模板,密钥行用金库值替换。$1=mask 时密钥打码。
render() {
local mask="${1:-}"
local line key val
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|\#*) printf '%s\n' "$line"; continue ;;
esac
key="${line%%=*}"
if is_secret "$key"; then
val="$("$RBW" get --field "$key" "$BW_ITEM")"
if [ -z "$val" ]; then
echo "render-env: 金库条目 '$BW_ITEM' 的字段 '$key' 为空" >&2
exit 1
fi
if [ "$mask" = "mask" ]; then
printf '%s=<%d 字符·已打码>\n' "$key" "${#val}"
else
printf '%s=%s\n' "$key" "$val"
fi
else
printf '%s\n' "$line"
fi
done < "$TEMPLATE"
}
# ---- 主流程 ----
[ -f "$TEMPLATE" ] || { echo "render-env: 找不到模板 $TEMPLATE" >&2; exit 1; }
MODE="${1:---print}"
require_unlocked
case "$MODE" in
--check)
for k in $SECRET_KEYS; do
v="$("$RBW" get --field "$k" "$BW_ITEM")"
printf ' %-22s %s\n' "$k" "$([ -n "$v" ] && echo "OK (${#v} 字符)" || echo "缺失 ❌")"
done
;;
--print)
render mask
;;
--out)
OUT="${2:?用法: render-env.sh --out <文件路径>}"
umask 077
render > "$OUT"
echo "render-env: 已写入 $OUT(含明文,注意保管)"
;;
--deploy)
TMP="$(mktemp -t jiu-prod-env)"
trap 'rm -f "$TMP"' EXIT
umask 077
render > "$TMP"
echo "render-env: 已本地渲染(临时文件,退出即删)"
echo "render-env: 上传到 $EC2_USER@$EC2_HOST:$REMOTE_ENV"
scp -i "$EC2_KEY" "$TMP" "$EC2_USER@$EC2_HOST:/tmp/production.env.new"
ssh -i "$EC2_KEY" "$EC2_USER@$EC2_HOST" bash <<REMOTE
set -e
mkdir -p /opt/jiu/config
mv /tmp/production.env.new "$REMOTE_ENV"
chmod 600 "$REMOTE_ENV"
sudo systemctl restart jiu
sleep 3
curl -sf http://localhost:8080/health >/dev/null && echo " 健康检查通过 ✅" || { echo " 健康检查失败 ❌"; exit 1; }
REMOTE
echo "render-env: 部署完成,EC2 上的 production.env 已更新并重启"
;;
*)
echo "未知参数: $MODE" >&2
sed -n '2,16p' "$0" >&2
exit 1
;;
esac
+14
View File
@@ -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/邮件通知,后续可加。
+51
View File
@@ -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` 中忽略。
+68 -3
View File
@@ -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 # 完整建表 SQLMySQL
├── client/ # Flutter 跨端客户端
│ └── android/ ios/ macos/ web/ windows/ # 五个平台目录(均已初始化)
├── deploy/
│ └── docker-compose.yml # 本地开发:MySQL(3306) + Adminer(8888)
├── .gitea/workflows/ # Forgejo Actionsdeploy.yml=tag 发版,及备份/重置等)
├── scripts/
│ ├── dev.sh # 一站式开发脚本(见下方用法)
│ ├── ci/ # CI 编译/发布脚本(compile-*.sh / release.sh / deploy.sh / provision-*.sh
│ └── .logs/ # 脚本运行日志(已加入 .gitignore,不提交)
└── docs/
├── context/project.md # 本文件(项目全貌)
@@ -164,6 +168,7 @@ sh scripts/dev.sh seed S001
版本(无需 JWT:
GET /version # 返回最新版本信息(读取 version.yaml,缺失返回 500
GET /api/v1/public/release # 版本 + download_urls(驱动客户端更新提示与下载页)
商品:
GET/POST /api/v1/products
@@ -204,6 +209,12 @@ sh scripts/dev.sh seed S001
数据导入:
POST /api/v1/import/products (Excel/CSV)
POST /api/v1/import/partners (Excel/CSV)
意见反馈:
POST /api/v1/feedback # 提交反馈(bug/suggestion,文字+图片,认证)
POST /api/v1/feedback/images # 上传反馈附图(认证)
GET /api/v1/admin/feedback # 反馈列表(仅 superadmin
PATCH /api/v1/admin/feedback/:id # 标记处理状态(仅 superadmin
```
## Flutter 客户端结构
@@ -218,6 +229,9 @@ client/lib/
│ ├── config/app_config.dart # 全局 URL 配置(通过 --dart-define=BASE_URL=... 注入)
│ ├── router/app_router.dart # go_router_RouterNotifier + refreshListenable
│ ├── storage/login_history.dart # 登录历史(shared_preferences,支持候选词自动填充)
│ ├── responsive/responsive.dart # 响应式:context.isMobile<600)、context.dialogWidth(X)
│ ├── update/app_updater*.dart # 应用内更新(_io: Win/macOS 装;_web: 刷新;移动端/兜底开下载链接)
│ ├── errors/error_reporter.dart # 异常上报(reportError
│ ├── models/page_result.dart # 通用分页结果模型
│ └── exceptions.dart # 自定义异常类型
├── models/ # 数据模型(与后端 JSON 对应)
@@ -239,19 +253,24 @@ client/lib/
│ └── number_rule_provider.dart # 编号规则
├── screens/
│ ├── auth/login_screen.dart # 登录页(含候选词下拉,150ms 延迟防止覆盖 onTap
│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏 + 状态栏 + 更新提示 banner
│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏/窄屏 Drawer 抽屉 + 状态栏 + 更新 banner
│ ├── stock_in/ # 入库管理
│ ├── stock_out/ # 出库管理
│ ├── inventory/ # 库存管理(含批次追踪 batch_tracking_screen.dart
│ ├── partners/ # 往来单位
│ ├── finance/ # 财务管理
│ ├── products/ # 商品管理(含分类)
── settings/ # 系统设置(用户/仓库/编号规则/关于
── about/ # 关于我们(含意见反馈:文字+附图直接提交后台
│ ├── public/ # 商品扫码公开展示页(无需登录)
│ └── settings/ # 系统设置(用户/仓库/编号规则)
└── widgets/
├── page_scaffold.dart # Tab 页面封装
├── data_table_card.dart # 含工具栏+分页的数据表格
│ # StatefulWidget,用 Table + IntrinsicColumnWidth 实现
│ # ValueNotifier<int> _hoveredRow 驱动行 hover 高亮
│ # mobileCards 入参:窄屏渲染卡片流,宽屏仍表格
├── mobile_list_card.dart # 移动端列表卡片:MobileListCard / MobileCardField
│ # 标题 + 右上角徽章 + 字段竖排 + 底部操作,替代窄屏表格行
├── multi_select_dropdown.dart # 多选筛选组件(多个导出):
│ # MultiSelectDropdown — 独立多选按钮(toolbar 使用)
│ # FilterableColumnHeader — 列头内嵌筛选图标
@@ -327,6 +346,48 @@ final warehouseOptions = _records
- `required: true` — 不可隐藏
- `minWidth: 1000` — 屏幕宽度不足时自动隐藏
## 移动端 / 响应式 UI 规范
客户端同一套代码跑桌面/Web/平板/手机。**窄屏(手机)自动切换为移动布局,宽屏保持桌面布局不变**。
### 断点判定
统一用 `client/lib/core/responsive/responsive.dart` 的扩展,不要散落魔法数:
```dart
context.isMobile // 宽度 < 600kMobileBreakpoint
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 secretsCI 给 APK 正式签名 |
| iOS 分发 | docs/ios-signing.md | 证书/Profile/API Key + secretsCI 构建并上传 TestFlight |
| 部署(NAS/Gitea | docs/deployment-nas-gitea.md | 自建 Forgejo + runner 部署说明 |
| 待办 | docs/TODO.md | 后续迭代事项(iOS 上架、应用内 APK 安装、平板布局等) |
+13
View File
@@ -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`iOSTestFlight)见 `docs/ios-signing.md`
- 移动端 UI 自动适配:窄屏走移动布局(Drawer 抽屉 + 列表卡片),规范见 `docs/context/project.md` 的「移动端 / 响应式 UI 规范」。
---
## 五、后端测试架构
+73
View File
@@ -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) 新建 AppBundle ID = `com.yanmei.jiu`(与工程一致)。
## 1. 发行证书(Apple Distribution
在钥匙串/开发者后台创建 **Apple Distribution** 证书,导出为 `.p12`(设一个导出密码):
```bash
# 已在 Xcode/钥匙串里有 Distribution 证书时,从「钥匙串访问」导出 .p12
base64 -i dist.p12 | pbcopy # → ANDROID 同理,复制到剪贴板
```
## 2. Provisioning ProfileApp 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 ID10 位) |
| `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、生成 ExportOptionsmanual / app-store)、`flutter build ipa``xcrun altool --upload-app` 上传;CFBundleVersion 由版本号推导单调递增;缺 secrets 时跳过。
- `.gitea/workflows/deploy.yml``build-ios` jobmac runner,串行于 build-android。
- 工程:`client/ios`bundle id `com.yanmei.jiu`,应用名「岩美酒库」)。
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# compile-android.sh <tag> — build signed Flutter Android APK, package into dist/
set -euo pipefail
# shellcheck source=scripts/ci/_env.sh
. "$(dirname "$0")/_env.sh"
TAG="$1"
VER="${TAG#v}"
# versionCode 必须单调递增,否则 Android 无法覆盖升级安装。
# 由版本号推导:major*10000 + minor*100 + patch(如 1.0.17 -> 10017)。
MAJOR="$(echo "$VER" | cut -d. -f1)"
MINOR="$(echo "$VER" | cut -d. -f2)"
PATCH="$(echo "$VER" | cut -d. -f3)"
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
echo "==> compile-android: version=${VER} build=${BUILD}"
# Sync Flutter pubspec version (BSD sed on macOS)
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
# --- release 签名(强制):从 CI secrets 还原 keystore 与 key.properties ---
# 必须 release 签名。缺任一签名 secret 直接失败,绝不回退 debug:
# debug 包用公开调试密钥签名,不能正式分发,也无法与正式版互相覆盖升级。
: "${ANDROID_KEYSTORE_BASE64:?缺少 ANDROID_KEYSTORE_BASE64:未配置 Android release 签名,构建中止(不回退 debug)}"
: "${ANDROID_KEYSTORE_PASSWORD:?缺少 ANDROID_KEYSTORE_PASSWORD:未配置 Android release 签名,构建中止}"
: "${ANDROID_KEY_ALIAS:?缺少 ANDROID_KEY_ALIAS:未配置 Android release 签名,构建中止}"
: "${ANDROID_KEY_PASSWORD:?缺少 ANDROID_KEY_PASSWORD:未配置 Android release 签名,构建中止}"
echo "==> compile-android: configuring release signing"
KEYSTORE_PATH="$(pwd)/client/android/jiu-release.jks"
echo "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${KEYSTORE_PATH}"
cat > client/android/key.properties <<EOF
storeFile=${KEYSTORE_PATH}
storePassword=${ANDROID_KEYSTORE_PASSWORD}
keyAlias=${ANDROID_KEY_ALIAS}
keyPassword=${ANDROID_KEY_PASSWORD}
EOF
# Build APK (universal, all ABIs — simplest for sideload download)
cd client
flutter build apk --release \
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
"--dart-define=APP_VERSION=${TAG}"
cd ..
# Locate the built APK (path differs across Flutter versions) and copy to dist/
mkdir -p dist
APK=""
for cand in \
client/build/app/outputs/flutter-apk/app-release.apk \
client/build/app/outputs/apk/release/app-release.apk; do
if [ -f "$cand" ]; then APK="$cand"; break; fi
done
if [ -z "$APK" ]; then
echo "ERROR: built APK not found" >&2
exit 1
fi
cp "$APK" dist/jiu-android.apk
# Clean up signing material so it never lingers on the runner workspace
rm -f client/android/key.properties client/android/jiu-release.jks 2>/dev/null || true
echo "==> compile-android: done — dist/ contents:"
ls -lh dist/
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# compile-ios.sh <tag> — build signed iOS IPA and upload to TestFlight (App Store Connect)
#
# 需要的 CI secrets(缺任意一个则优雅跳过,不阻塞流水线):
# IOS_DIST_CERT_P12_BASE64 Apple Distribution 证书 (.p12) 的 base64
# IOS_DIST_CERT_PASSWORD .p12 导出密码
# IOS_PROVISIONING_PROFILE_BASE64 App Store 类型 provisioning profile (.mobileprovision) 的 base64
# IOS_TEAM_ID Apple Developer Team ID10 位)
# 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}"
# CFBundleVersionbuild 号)必须单调递增,否则 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 与 UUIDExportOptions 需要)
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.plistmanual 签名,app-store 分发)---
BUNDLE_ID="com.yanmei.jiu"
cat > "$WORK/ExportOptions.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>app-store</string>
<key>teamID</key><string>${IOS_TEAM_ID}</string>
<key>signingStyle</key><string>manual</string>
<key>uploadBitcode</key><false/>
<key>uploadSymbols</key><true/>
<key>provisioningProfiles</key>
<dict>
<key>${BUNDLE_ID}</key><string>${PROFILE_NAME}</string>
</dict>
</dict>
</plist>
EOF
# --- 6. 构建签名 IPA ---
cd client
flutter build ipa --release \
--build-name="${VER}" \
--build-number="${BUILD}" \
--export-options-plist="$WORK/ExportOptions.plist" \
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
"--dart-define=APP_VERSION=${TAG}"
cd ..
IPA="$(ls client/build/ios/ipa/*.ipa | head -1)"
if [ -z "$IPA" ] || [ ! -f "$IPA" ]; then
echo "ERROR: IPA not found" >&2
exit 1
fi
echo "==> compile-ios: built ${IPA}"
# --- 7. 上传 TestFlightApp 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"
+3 -1
View File
@@ -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
+5 -1
View File
@@ -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"
+43 -17
View File
@@ -258,7 +258,7 @@ pageStyle: |
</div>
<h1 class="ph-title">随时随地,<br/>轻松管理酒库。</h1>
<p class="ph-lead">
Web 版Windows 桌面版已正式上线,macOS、iOS、Android 客户端即将推出。
Web 版Windows / macOS 桌面版与 Android 客户端均已上线,iOS 客户端即将推出。
</p>
<div class="ph-meta">
<div class="ph-meta-item">
@@ -271,7 +271,7 @@ pageStyle: |
</div>
<div class="ph-meta-item">
<div class="ph-meta-label">可用平台</div>
<div class="ph-meta-val">Web / Windows</div>
<div class="ph-meta-val">Web / Windows / macOS / Android</div>
</div>
</div>
</div>
@@ -308,7 +308,7 @@ pageStyle: |
<section class="section">
<div class="container">
<h2 class="fs-3xl fw-6 text-brand-900 m-0 mb-6" style="letter-spacing: var(--tracking-cn-display);">支持的平台</h2>
<p class="fs-md text-muted m-0 mb-8">Web 版Windows 桌面版现已可用,其余平台正在开发中。</p>
<p class="fs-md text-muted m-0 mb-8">Web 版Windows / macOS 桌面版与 Android 客户端现已可用,iOS 正在开发中。</p>
<div class="dl-grid" id="platform-grid">
<!-- Web -->
@@ -345,25 +345,25 @@ pageStyle: |
</div>
<!-- iOS -->
<div class="dl-card coming-soon" data-platform="ios">
<div class="dl-card coming-soon" data-platform="ios" id="ios-card">
<div class="dl-card-icon"><i data-lucide="smartphone" class="icon"></i></div>
<h3 class="dl-card-name">iOS</h3>
<p class="dl-card-desc">iPhone 与 iPad 通用,支持扫码与推送通知。</p>
<span class="dl-status soon">敬请期待</span>
<span class="dl-card-btn disabled">
<i data-lucide="clock" class="icon"></i>即将推出
</span>
<p class="dl-card-desc">iPhone 与 iPad 通用,通过 TestFlight 安装。</p>
<span class="dl-status soon" id="ios-status">敬请期待</span>
<a href="#" class="dl-card-btn disabled" id="ios-dl-btn">
<i data-lucide="clock" class="icon"></i><span>即将推出</span>
</a>
</div>
<!-- Android -->
<div class="dl-card coming-soon" data-platform="android">
<div class="dl-card" data-platform="android">
<div class="dl-card-icon"><i data-lucide="tablet-smartphone" class="icon"></i></div>
<h3 class="dl-card-name">Android</h3>
<p class="dl-card-desc">手机与平板通用,支持主流应用商店及 APK 安装。</p>
<span class="dl-status soon">敬请期待</span>
<span class="dl-card-btn disabled">
<i data-lucide="clock" class="icon"></i>即将推出
</span>
<p class="dl-card-desc">手机与平板通用,APK 直接安装,支持 Android 6.0 及以上。</p>
<span class="dl-status available">✓ 已支持</span>
<a href="#" class="dl-card-btn primary" id="android-dl-btn">
<i data-lucide="download" class="icon"></i>下载 Android 版
</a>
</div>
</div>
</div>
@@ -429,8 +429,8 @@ pageStyle: |
(function() {
var platforms = {
web: { name: 'Web 版', desc: '浏览器直接访问,功能完整,无需下载安装。', icon: 'globe', btnText: '立即使用 Web 版', btnHref: 'https://jiu.51yanmei.com/app/', available: true },
ios: { name: 'iOS', desc: 'iOS 客户端正在开发中,敬请期待。', icon: 'smartphone', btnText: '敬请期待', available: false },
android: { name: 'Android', desc: 'Android 客户端正在开发中,敬请期待。', icon: 'tablet-smartphone', btnText: '敬请期待', available: false },
ios: { name: 'iOS', desc: 'iPhone 与 iPad 通用,通过 TestFlight 安装。', icon: 'smartphone', btnText: '敬请期待', available: false },
android: { name: 'Android', desc: '手机与平板通用,APK 直接安装,支持 Android 6.0 及以上。', icon: 'tablet-smartphone', btnText: '下载 Android 版', btnHref: '#', available: true },
windows: { name: 'Windows 桌面', desc: '原生桌面客户端,支持 Win 10/11(64 位),离线模式与本地打印。', icon: 'monitor', btnText: '下载 Windows 版', btnHref: '#', available: true },
macos: { name: 'macOS 桌面', desc: '支持 Apple Silicon 与 Intel,解压即用,首次运行右键→打开。', icon: 'laptop', btnText: '下载 macOS 版', btnHref: '#', available: true }
};
@@ -513,6 +513,32 @@ pageStyle: |
if (btn) btn.href = data.download_urls.windows;
if (detected === 'windows') updateDetectCard(detected);
}
if (data && data.download_urls && data.download_urls.android) {
platforms.android.btnHref = data.download_urls.android;
var andBtn = document.getElementById('android-dl-btn');
if (andBtn) andBtn.href = data.download_urls.android;
if (detected === 'android') updateDetectCard(detected);
}
// iOS 走 TestFlight:仅当配置了公开 TestFlight 链接时激活卡片
if (data && data.download_urls && data.download_urls.ios) {
platforms.ios.available = true;
platforms.ios.btnHref = data.download_urls.ios;
platforms.ios.btnText = '通过 TestFlight 安装';
var iosCard = document.getElementById('ios-card');
var iosStatus = document.getElementById('ios-status');
var iosBtn = document.getElementById('ios-dl-btn');
if (iosCard) iosCard.classList.remove('coming-soon');
if (iosStatus) { iosStatus.className = 'dl-status available'; iosStatus.textContent = '✓ 已上线'; }
if (iosBtn) {
iosBtn.className = 'dl-card-btn primary';
iosBtn.href = data.download_urls.ios;
iosBtn.target = '_blank';
iosBtn.rel = 'noopener';
iosBtn.innerHTML = '<i data-lucide="download" class="icon"></i>通过 TestFlight 安装';
if (window.lucide) lucide.createIcons();
}
if (detected === 'ios') updateDetectCard(detected);
}
})
.catch(function() {});
});