Compare commits

..

43 Commits

Author SHA1 Message Date
wangjia 5278bea940 chore: release v1.0.20
Deploy / build-linux-web (push) Successful in 47s
Deploy / build-macos (push) Successful in 1m14s
Deploy / build-android (push) Successful in 1m12s
Deploy / build-ios (push) Successful in 8s
Deploy / build-windows (push) Failing after 12m32s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:40:43 +08:00
wangjia 0bc3f4b702 fix(client): Android release 包缺少 INTERNET 权限导致登录页「服务不可达」
release 包走 main AndroidManifest,Flutter 模板默认只在 debug/profile 清单
注入 INTERNET 权限,主清单不带;强制 release 签名后联网权限缺失,所有网络
请求失败。在主清单显式声明 android.permission.INTERNET。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:39:23 +08:00
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
wangjia 94f0fb2095 chore: release v1.0.17
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-windows (push) Successful in 1m45s
Deploy / build-macos (push) Successful in 1m4s
Deploy / release-deploy (push) Successful in 3m37s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:35:09 +08:00
wangjia 36e5677b7c feat(client): 反馈 Bug/功能建议 改为直接提交后台(文字+附图)
- feedback_repository + provider:图片上传 /feedback/images、提交 /feedback
- 关于我们的反馈入口改为表单弹窗:多行文字 + 选图上传(缩略图/可删,最多9张),
  提交到后台,不再走 mailto
- 平台/版本随提交上报(web 安全:defaultTargetPlatform,不引 dart:io)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:31:46 +08:00
wangjia 7a1d8465e5 feat(backend): 意见反馈接口(提交+图片上传+admin列表)
- model/feedback.go: Feedback(多租户 shop_id,images 以 JSON 存)+ StringSlice 类型
- handler/feedback.go: Submit(认证,shop/user 从 JWT,去规范化 username/shop_code)、
  UploadImage(复用 product_image 压缩逻辑,存 /images/feedback/<shop>/)、
  List(SuperAdminOnly,分页过滤)、UpdateStatus(new/handled)
- router 注册 /feedback、/feedback/images、/admin/feedback;main AutoMigrate;schema.sql 加表
- feedback_test 覆盖提交/校验/权限/列表

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:31:46 +08:00
wangjia dd09f1a526 feat(client): 关于→关于我们,移到左侧菜单独立页面
- 新增 screens/about/about_screen.dart(版本信息/授权信息/关于我们/意见反馈)
- 左侧导航新增「关于我们」(/about),从系统设置移除原「关于」Tab(7→6)
- 迁移并清理 settings 中仅关于页使用的 widget/弹窗/import

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:05:12 +08:00
wangjia 53a52cc530 feat(client): 打印任务默认名 类型_时间戳(标签/入库单/出库单)
Printing.layoutPdf 传 name:标签_yyyyMMdd_HHmmss / 入库单_... / 出库单_...,
作为打印任务名与「打印到 PDF」的默认文件名。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:05:12 +08:00
wangjia 337a358d54 docs(changelog): 1.0.16(应用内更新 + 安装程序中文化 + 更新提示优化)
Deploy / build-linux-web (push) Successful in 49s
Deploy / build-macos (push) Successful in 58s
Deploy / build-windows (push) Successful in 1m49s
Deploy / release-deploy (push) Successful in 1m21s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:26:31 +08:00
wangjia c98a90472b feat(client): 更新提示加引导文案,强制更新弹窗文案统一
- banner: 发现新版本 vX,建议立即更新以获得最新功能与修复
- 强制更新弹窗: 去掉 release notes,统一为「需更新后才能继续使用,
  更新以获得最新功能与修复」

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:25:43 +08:00
wangjia 207a9a8343 feat(client): 更新 banner 去掉更新内容 + 安装程序中文化
- 顶部更新 banner 只显示「发现新版本 vX」,不再附带 release notes
- Inno Setup 安装向导改中文:随仓库带 ChineseSimplified.isl(加 UTF-8 BOM),
  .iss 用相对路径引用,compile-windows.sh 把它和 .iss 一起拷到构建目录,
  不依赖 runner 上 Inno Setup 是否自带简中
- (检查更新无更新时的提示已在上个提交加好)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:20:50 +08:00
wangjia d8c0bd74d2 feat(client): 应用内更新(Windows/macOS 下载安装,Web 刷新)
- 新增 lib/core/update/:app_updater(入口+进度对话框) + io/web 条件实现
  - Windows: Dio 下载 setup.exe(无 MOTW) -> 启动安装包 -> 退出,规避浏览器
    SmartScreen 下载页与运行时拦截
  - macOS: 下载 zip -> ditto 解压 -> 后台脚本等退出后替换 /Applications 内
    app 并重启(权限失败回退访达高亮)
  - Web: 刷新页面;其它平台回退浏览器下载
- app_shell 更新 banner/强制更新弹窗、设置页「立即更新」改走应用内更新
- 设置页「检查更新」加反馈(正在检查/已是最新/发现新版本/失败)
- jiu-installer.iss 加 CloseApplications=yes 便于覆盖

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:04:26 +08:00
wangjia 056ae914ce fix(client): Windows main.cpp 用 ASCII 十六进制转义写标题,修复 C4819
Deploy / build-linux-web (push) Successful in 52s
Deploy / build-windows (push) Successful in 1m38s
Deploy / build-macos (push) Successful in 1m3s
Deploy / release-deploy (push) Successful in 1m25s
v1.0.14 Windows 构建失败:main.cpp 含中文(u8 标题+中文注释),MSVC 在
GBK(936) 代码页报 C4819「字符无法表示」,而 runner 开了 /WX 警告即错误。
改为标题用 UTF-8 字节的 \x 十六进制转义、注释改英文,源码纯 ASCII,
运行时 MultiByteToWideChar(CP_UTF8) 解码,窗口标题仍是岩美酒库。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:32:48 +08:00
wangjia 022443476c docs(changelog): 1.0.14(Windows 打印修复 + 关于我们真实信息/配置化)
Deploy / build-linux-web (push) Successful in 50s
Deploy / build-windows (push) Failing after 1m29s
Deploy / build-macos (push) Successful in 1m1s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:48:27 +08:00
wangjia 93f4f87e44 fix(client): 打印字体改 TrueType,修复 Windows 打印卡死
打印字体实为 OpenType/CFF,dart_pdf 只支持 TrueType(glyf),渲染中文
抛 'does not support Unicode';标签打印时异常抛在原生 onLayout 回调内
导致打印插件死等->卡死。用 fonttools 将字体转为 TrueType/glyf 替换。
加回归测试防止退回 CFF 字体。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:08:45 +08:00
wangjia 20e7d07be3 feat(client): 关于我们内容改用配置文件 + 窗口标题改岩美酒库
- 新增 assets/config/app_info.json(服务商/网站/邮箱真实值),启动时
  AppInfo.load() 读取,不硬编码、不走环境变量
- 关于我们/续费/反馈处的服务商、网站、邮箱统一读配置
- Windows 窗口标题 jiu_client -> 岩美酒库(u8 + UTF-8→UTF-16 防乱码)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:08:45 +08:00
wangjia 1834740676 fix(ci): Inno Setup 去掉中文语言段,用内置英文向导
Deploy / build-windows (push) Successful in 1m37s
Deploy / build-linux-web (push) Successful in 46s
Deploy / build-macos (push) Successful in 1m0s
Deploy / release-deploy (push) Successful in 1m21s
ChineseSimplified.isl 默认不随 Inno Setup 安装,引用它导致编译报
找不到文件。去掉 [Languages] 段用内置默认(英文)向导;app 名称与
快捷方式仍为中文。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:34:44 +08:00
wangjia 90a194fc00 fix(ci): ISCC 调用关闭 MSYS 参数路径转换,修复 more than one script filename
Deploy / build-linux-web (push) Successful in 50s
Deploy / build-windows (push) Failing after 1m22s
Deploy / build-macos (push) Successful in 59s
Deploy / release-deploy (push) Has been skipped
Git Bash(MSYS) 把 /D 开头的参数当 POSIX 路径自动转换,导致 ISCC 的
/DAppVer /DSrcDir /DOutDir 丢失 /D 前缀、被当成多个脚本文件名而报错。
调用 ISCC 时设 MSYS_NO_PATHCONV=1 + MSYS2_ARG_CONV_EXCL='*' 关闭转换。
install-innosetup.ps1 顺带加 /LOG 失败时打印安装日志。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:05:09 +08:00
wangjia fba8b0a917 fix(ci): 健壮化 Inno Setup 安装 + 部署仅保留最新安装包
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-macos (push) Successful in 57s
Deploy / build-windows (push) Failing after 1m22s
Deploy / release-deploy (push) Has been skipped
- 新增 install-innosetup.ps1:校验下载大小、检查安装程序退出码、双
  Program Files 目录定位 ISCC,修复原内联命令静默成功但没真正装上的问题
  (SYSTEM 账户下 Invoke-WebRequest 无 -UseBasicParsing + 未检查退出码)
- provision/compile-windows.sh 在两个 Program Files 目录查找 ISCC
- deploy.sh 发布安装包到 /opt/jiu/downloads 前先清旧包,仅保留最新版本

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:42:48 +08:00
wangjia c001f0e8e6 feat(client): Windows 提供 Inno Setup 安装程序 + 下载改同源 HTTPS
Deploy / build-windows (push) Failing after 22s
Deploy / build-linux-web (push) Failing after 14m4s
Deploy / build-macos (push) Has been skipped
Deploy / release-deploy (push) Has been skipped
- Windows 客户端打成 jiu-windows-x64-setup.exe(Inno Setup,中文向导、
  桌面/开始菜单快捷方式、卸载项),替代解压版 zip
- provision-windows.sh 自动静默安装 Inno Setup(缺则装)
- 下载 URL 从内网 HTTP Forgejo 改为 https://jiu.51yanmei.com/downloads/,
  修复 HTTPS 页面下发 http 内网地址被浏览器拦截(无法安全下载),win/mac 均生效
- deploy.sh 将安装包发布到 EC2 /opt/jiu/downloads/,nginx 新增 /downloads/ 路由

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:46:41 +08:00
wangjia f039bd0c61 fix(ci): deploy 适配容器化 nginx(pangolin-edge)
Deploy / build-windows (push) Failing after 25s
Deploy / build-linux-web (push) Successful in 1m14s
Deploy / build-macos (push) Successful in 1m12s
Deploy / release-deploy (push) Has been skipped
nginx 已从主机迁入 pangolin-edge 容器(host 网络模式),其
/etc/nginx/conf.d 由主机 ~/pangolin/edge/conf.d bind-mount。
deploy 改为:jiu.conf 写入该挂载目录 + docker exec pangolin-edge
nginx -t/-s reload,不再 cp 到主机 /etc/nginx 或 systemctl 操作
已废弃的主机 nginx。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:20:50 +08:00
wangjia f4beb90769 fix(ci): nginx reload 改用 systemctl reload-or-restart,避免空 PID 文件报错
Deploy / build-linux-web (push) Successful in 1m1s
Deploy / build-windows (push) Successful in 1m42s
Deploy / build-macos (push) Successful in 1m2s
Deploy / release-deploy (push) Failing after 1m17s
nginx -s reload 依赖 /run/nginx.pid,该文件为空时报 invalid PID number。
改用 systemctl reload-or-restart(经 systemd MAINPID 发 HUP,运行中则 reload、
未运行则启动),并 enable nginx 保证重启后自启。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:33:24 +08:00
wangjia 6f0b273950 fix(ci): Windows 构建清除 cp 复制的插件软链,避免 PathExists(errno 183)
Deploy / build-windows (push) Successful in 1m31s
Deploy / build-linux-web (push) Successful in 55s
Deploy / build-macos (push) Successful in 58s
Deploy / release-deploy (push) Failing after 1m20s
cp -r 把 windows/flutter/ephemeral/.plugin_symlinks 复制成真实目录,
flutter createPluginSymlinks 只清软链、清不掉真目录,重建时撞车。
复制后删除 ephemeral/.dart_tool/build,由 flutter 干净重建。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:06:36 +08:00
wangjia b43601b4fd ci: 运行环境自愈预置 + 发版任务串行 + 删 ci.yml/暂停备份
Deploy / build-windows (push) Failing after 25s
Deploy / build-macos (push) Has been cancelled
Deploy / release-deploy (push) Has been cancelled
Deploy / build-linux-web (push) Has been cancelled
- 新增 scripts/ci/provision-{mac,windows}.sh:幂等预置,按 Flutter 版本
  stamp 短路,pipeline 首跑自动 precache + 预热缓存,运行时不再下载 engine
- scripts/ci/_env.sh:抽出共用镜像 env,compile 脚本统一 source
- deploy.yml:build-macos needs build-linux-web,同一 mac runner 串行
  执行,避免排队超时;各 build job 加 Provision 前置步骤
- 删除 .gitea/workflows/ci.yml
- backup.yml:暂停定时备份(保留 workflow_dispatch 手动触发)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:04:23 +08:00
wangjia 3aa63d6575 fix(ci): Windows 构建改用 C:\jiu-build 短路径,弃用 subst W: 映射
Deploy / build-windows (push) Successful in 1m41s
Deploy / build-linux-web (push) Failing after 14m36s
Deploy / build-macos (push) Successful in 33m24s
Deploy / release-deploy (push) Has been skipped
subst W: 因 cmd/MSYS 引号转义失败(报"找不到路径"),导致 W: 盘
未创建、cd /w/client 失败。改为将 client/ 复制到 System32 之外的
C:\jiu-build 构建,规避 WOW64 重定向,并用 PowerShell Compress-Archive 打包。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:23:09 +08:00
wangjia a3f9f091b4 fix(ci): Windows 构建用 subst W: 绕过 System32 WOW64 路径重定向
Deploy / build-linux-web (push) Successful in 1m6s
Deploy / build-windows (push) Failing after 16s
Deploy / release-deploy (push) Has been cancelled
Deploy / build-macos (push) Has been cancelled
2026-06-01 01:19:02 +08:00
wangjia b3f5a0f083 debug(ci): 添加 Windows 构建目录诊断输出
Deploy / build-windows (push) Failing after 30s
Deploy / build-linux-web (push) Successful in 1m2s
Deploy / build-macos (push) Failing after 14m5s
Deploy / release-deploy (push) Has been skipped
2026-06-01 01:13:48 +08:00
wangjia 0af92e1732 fix(ci): Windows 构建前重新生成 windows/ 平台目录,避免 CMake 路径不存在
Deploy / build-windows (push) Failing after 28s
Deploy / build-linux-web (push) Successful in 1m11s
Deploy / build-macos (push) Failing after 10m10s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 00:11:47 +08:00
wangjia ea5526944b feat(client): 添加 Flutter Windows 桌面平台支持
Deploy / build-macos (push) Successful in 52s
Deploy / build-windows (push) Failing after 4m27s
Deploy / release-deploy (push) Has been cancelled
Deploy / build-linux-web (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 08:44:31 +08:00
wangjia c2f8bda4f3 chore(ci): 移除 Windows pipeline 中的 Git 安装步骤
Deploy / build-linux-web (push) Successful in 1m8s
Deploy / build-macos (push) Successful in 50s
Deploy / build-windows (push) Failing after 38s
Deploy / release-deploy (push) Has been skipped
Git 已在 Windows runner 上手动安装并加入系统 PATH,无需每次 CI 安装。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:17:43 +08:00
wangjia 7f6584ab64 fix(ci): 改从 TUNA 镜像安装 Git,避免访问 GitHub
Deploy / build-windows (push) Failing after 17s
Deploy / build-linux-web (push) Successful in 54s
Deploy / build-macos (push) Successful in 50s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:07:47 +08:00
wangjia 23de243598 fix(ci): Windows 安装 Git 改用 cmd,避免 PowerShell 执行策略限制
Deploy / build-windows (push) Failing after 18s
Deploy / build-macos (push) Successful in 56s
Deploy / build-linux-web (push) Failing after 12m28s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:01:49 +08:00
wangjia 65e2021138 fix: 修复打印函数缺少 try 块导致 macOS 编译失败;CI 自动安装 Git bash
Deploy / build-windows (push) Failing after 14s
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-macos (push) Successful in 52s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:58:05 +08:00
wangjia d94c6b109b chore: release v1.0.4
Deploy / build-windows (push) Failing after 19s
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-macos (push) Failing after 32s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:53:51 +08:00
wangjia 75e3b934bc chore: release v1.0.3
Deploy / build-windows (push) Failing after 9s
Deploy / build-linux-web (push) Successful in 51s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:39:04 +08:00
wangjia ec7596e272 fix(deploy): version.yaml 路径改为 /opt/jiu/config/(与 WorkingDirectory 一致) 2026-05-26 21:34:14 +08:00
wangjia 9fc379f33a chore: release v1.0.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:27:24 +08:00
wangjia ee826eca7f fix(print): 商品标签改为竖向 20mm×38mm 布局
原设计 4in×2in 横向与实际标签纸不符,导致内容旋转 90°。按实测尺寸重设为竖向 20mm×38mm,重新排布:酒行名顶部深色背景→商品名→QR码→规格/批次/生产日期→联系方式底部。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:20:31 +08:00
wangjia 51863dcbd3 feat(deploy): 新增 Windows 桌面客户端构建与发布支持
CI 拆成三个 job(mac 构建 Linux/Web、windows 构建 Windows 桌面、mac 发布部署);新增 compile-windows.sh 用 Flutter 构建并打包为 jiu-windows-x64.zip;release.sh 自动写入 Windows 下载 URL 到 version.yaml 并上传 zip。下载页 Windows 卡片从"敬请期待"改为真实下载按钮,URL 由 API 动态填充。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 18:29:17 +08:00
wangjia eda7d37b64 feat: 新增用户自助注册功能
后端新增 POST /api/v1/public/register 接口,支持门店自助注册并自动生成门店编码(S000001 格式);shops 表加 description 字段。营销站新增 /register/ 注册页,含门店信息和管理员账号表单,注册成功后展示门店编码和登录提示。Flutter 登录页底部加「前往官网注册」跳转链接。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 13:56:34 +08:00
wangjia 471b980179 refactor(web): 营销站重构为 Eleventy SSG,内容迁移至 Markdown
- 引入 Eleventy 静态站点生成器,页面统一使用 Nunjucks 模板
- base.njk 提供公共 nav / footer / CSS,消除三个页面间的重复代码
- 抽取 color.css(设计 token)+ style.css(公共组件 + utility 类)
- index.njk:首页迁移,用 utility class 替换大量页面私有 CSS
- download.njk:下载页重构,版本号/更新日志从 CHANGELOG.md 构建时解析,OS 自动识别
- docs.njk:文档页重构,12 章内容从 web/content/docs.md 构建时渲染,三列布局保留
- docs.css 单独提取,docs 专有样式不污染公共 CSS
- 内容面向非技术用户重写,去除技术术语和内部路径引用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:24:14 +08:00
wangjia 3d1fc0d4ba fix(client): 统一打印错误处理,失败时上报并弹提示
新增 safePrint 封装,所有打印调用点统一 catch 任意异常(含字体加载失败),
自动上报 error_reporter 并展示 SnackBar,不再静默失败。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:55:56 +08:00
180 changed files with 12448 additions and 5212 deletions
+10
View File
@@ -43,6 +43,16 @@ cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
只保留有内容的分类(新功能/改进/修复),根据 commit message 的类型(feat/fix/refactor 等)归类。
**CHANGELOG 撰写原则:**
- 站在**用户视角**总结,描述对使用体验的实际影响,而非技术实现
- **忽略**以下类型的提交(产品感知弱):
- 发版工具、CI/CD、部署脚本相关(chore: release、ci:、devops:
- 内部文档、开发规范、CLAUDE.md 更新(docs(claude):、docs(internal):
- 代码格式、lint、重构(style:、refactor: 纯内部)
- 依赖升级、版本号 bumpchore: bump、chore: upgrade
- **保留并重点描述**:feat、fix、perf 类,以及用户能感知到的 refactor(如界面改版、交互优化)
- 每条描述用一句话说清楚"用户能做什么"或"修了什么问题",不堆砌技术术语
## 5. 提交代码
将所有改动(包括 CHANGELOG.md)用以下格式提交到 main
+3 -2
View File
@@ -1,8 +1,9 @@
name: DB Backup
on:
schedule:
- cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00
# 定时备份已暂停(保留手动触发)。恢复时取消下面 schedule 的注释即可。
# schedule:
# - cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00
workflow_dispatch:
jobs:
-30
View File
@@ -1,30 +0,0 @@
name: CI
on:
push:
branches-ignore: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- name: Go tests
working-directory: backend
run: go test ./...
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Flutter tests
working-directory: client
run: flutter test
+152 -2
View File
@@ -6,7 +6,7 @@ on:
- 'v[0-9]*.[0-9]*.[0-9]*'
jobs:
deploy:
build-linux-web:
runs-on: mac
env:
GOPROXY: https://goproxy.cn,direct
@@ -16,9 +16,159 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Compile
- name: Provision (idempotent — auto-runs if host not initialized)
run: sh scripts/ci/provision-mac.sh
- name: Compile (Linux backend + Flutter Web)
run: sh scripts/ci/compile.sh "${{ gitea.ref_name }}"
- name: Upload linux-web artifacts
uses: actions/upload-artifact@v3
with:
name: linux-web
path: dist/
build-macos:
# Same physical runner as build-linux-web (label: mac, capacity 1). Run them
# serially via needs so the second job doesn't sit queued and time out.
needs: build-linux-web
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 macOS)
run: sh scripts/ci/compile-macos.sh "${{ gitea.ref_name }}"
- name: Upload macos artifacts
uses: actions/upload-artifact@v3
with:
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:
GOPROXY: https://goproxy.cn,direct
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)
shell: bash
run: sh scripts/ci/provision-windows.sh
- name: Compile (Flutter Windows)
shell: bash
run: sh scripts/ci/compile-windows.sh "${{ gitea.ref_name }}"
- name: Upload windows artifacts
uses: actions/upload-artifact@v3
with:
name: windows
path: dist/
release-deploy:
needs: [build-linux-web, build-macos, build-android, build-ios, build-windows]
runs-on: mac
env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- uses: actions/checkout@v4
- name: Download linux-web artifacts
uses: actions/download-artifact@v3
with:
name: linux-web
path: dist/
- name: Download macos artifacts
uses: actions/download-artifact@v3
with:
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:
name: windows
path: dist/
- name: Test
run: sh scripts/ci/test.sh
+7
View File
@@ -23,6 +23,13 @@ client/devtools_options.yaml
# Flutter 构建产物
client/build/
# CI 构建产物
dist/
# 营销站构建产物
web/dist/
web/node_modules/
client/.dart_tool/
client/.flutter-plugins
client/.flutter-plugins-dependencies
+103 -5
View File
@@ -5,13 +5,111 @@ 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.2] - 2026-05-25
### 改进
- 添加 /release 本地发版 slash command,一键完成构建、测试、打 tag 和推送
## [1.0.20] - 2026-06-07
### 修复
- Web 版打印弹窗被浏览器拦截时,由静默失败改为给出明确提示,并自动上报错误
- 修复 Android 客户端登录页提示「服务不可达」、无法连接服务器的问题(正式版安装包此前缺少联网权限)
## [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
### 新功能
- 意见反馈(Bug 反馈 / 功能建议)改为应用内直接提交,支持文字 + 图片(最多 9 张),不再跳转邮件
### 改进
- 「关于」更名为「关于我们」并移至左侧菜单,成为独立页面
- 打印时默认文件名带类型与时间戳(如 标签_20260605_103000、入库单_…、出库单_…),方便「打印到 PDF」保存归档
## [1.0.16] - 2026-06-05
### 新功能
- 应用内更新:Windows/macOS 点「立即更新」直接在应用内下载并自动安装/重启(带进度条),规避浏览器 SmartScreen 下载拦截;Web 为刷新页面
- Windows 安装程序改为中文向导
### 改进
- 「检查更新」按钮增加反馈提示(正在检查 / 已是最新 / 发现新版本 / 检查失败)
- 更新提示文案优化:横幅显示「建议立即更新以获得最新功能与修复」,不再展示冗长更新内容
## [1.0.15] - 2026-06-05
### 修复
- 修复 Windows 桌面端打印卡死:打印字体由 OpenType/CFF 改为 TrueType(glyf)dart_pdf 渲染中文不再抛错(此前点「打印」后卡死)
### 改进
- 「关于我们」内容改为真实信息(服务商:岩美技术有限公司;网站:jiu.yanmei.com;邮箱:yammy2023@163.com),且统一从配置文件读取,不再硬编码
- Windows 桌面端窗口标题由 jiu_client 改为「岩美酒库」
## [1.0.13] - 2026-06-04
### 新功能
- Windows 客户端改为提供安装程序(Inno Setup 打包的 `jiu-windows-x64-setup.exe`,含安装向导、桌面/开始菜单快捷方式、卸载项),不再是解压版 zip
### 修复
- 修复客户端「无法安全下载」:下载地址从内网 HTTP Forgejo`http://192.168.3.200:3000`,公网不可达且被浏览器拦截)改为同源 HTTPS `https://jiu.51yanmei.com/downloads/...`Windows、macOS 均生效
- 部署时将最新安装包发布到 web 服务(`/opt/jiu/downloads/`nginx `/downloads/` 提供),仅保留最新版本(清旧放新)
## [1.0.9] - 2026-06-04
### 修复
- 适配 nginx 容器化部署:nginx 已迁入 `pangolin-edge` 容器(host 网络),deploy 改为将 jiu.conf 写入 pangolin 挂载目录并 `docker exec pangolin-edge nginx -s reload`,不再操作已废弃的主机 nginx
## [1.0.8] - 2026-06-03
### 修复
- 修复 EC2 部署最后一步 nginx reload 失败(`invalid PID number`):改用 `systemctl reload-or-restart nginx`(经 systemd MAINPID 发 HUP,不依赖空的 /run/nginx.pid),并 `enable nginx` 确保重启后自启
## [1.0.7] - 2026-06-03
### 修复
- 修复 Windows 构建 `PathExistsException`errno 183):复制 client/ 到 C:\jiu-build 后,先清除 cp 复制成真实目录的插件软链(windows/flutter/ephemeral)及 .dart_tool/build,再交给 flutter 干净重建
## [1.0.6] - 2026-06-03
### 改进
- CI 运行环境自愈预置:mac/windows runner 首次构建自动 `flutter precache` 并预热 pub/go/npm 缓存(stamp 文件按 Flutter 版本短路),后续构建不再运行时下载 engine
- 发版流水线同一 mac runner 上的任务改为串行(build-macos 依赖 build-linux-web),避免排队等待超时失败
- 暂停定时数据库备份(保留手动触发)
## [1.0.5] - 2026-06-03
### 修复
- 修复 Windows CI 构建失败:放弃脆弱的 `subst W:` 路径映射,改为将 client/ 复制到 System32 之外的干净短路径(C:\jiu-build)构建,规避 WOW64 文件系统重定向
## [1.0.4] - 2026-05-30
### 新功能
- 支持 macOS 桌面客户端下载,下载页 macOS 卡片已激活,下载 zip 解压即用
## [1.0.3] - 2026-05-28
### 新功能
- 新增用户自助注册功能,在官网 /register/ 填写门店信息即可完成注册,系统自动生成门店编码(S000001 格式)
- 注册账号默认设为超级管理员权限
- 支持 Windows 桌面客户端构建与发布
### 修复
- 修复新门店首次登录时库存、仓库、用户等列表加载崩溃的问题
- 修复库存管理页在无数据时页面崩溃(RangeError)的问题
- 商品标签打印改为横向 38mm×20mm 布局,修复标签内容错位问题
- 修复数据导入中移除不必要的"商品编码"导入项
## [1.0.2] - 2026-05-25
### 修复
- 打印入库单/出库单/商品标签时,若浏览器拦截了弹窗,现在会给出明确提示,不再静默失败
## [1.0.1] - 2026-05-25
+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
+4 -4
View File
@@ -1,10 +1,10 @@
version: "1.0.0"
build_number: 1
version: "1.0.3"
build_number: 4
force_update: false
release_notes: "初始版本发布,完成核心库存管理功能。"
release_notes: "新增用户自助注册功能;商品标签打印修复竖向 20mm×38mm 布局"
download_urls:
macos: ""
windows: ""
ios: ""
android: ""
android: "https://jiu.51yanmei.com/downloads/jiu-android.apk"
web: "https://jiu.51yanmei.com/app"
+17
View File
@@ -49,6 +49,23 @@ func (h *AuthHandler) Login(c *gin.Context) {
})
}
// Register POST /api/v1/public/register
func (h *AuthHandler) Register(c *gin.Context) {
var req service.RegisterInput
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.svc.Register(req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// Refresh POST /api/v1/auth/refresh
func (h *AuthHandler) Refresh(c *gin.Context) {
var req struct {
+198
View File
@@ -0,0 +1,198 @@
package handler
import (
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
)
type FeedbackHandler struct{ db *gorm.DB }
func NewFeedbackHandler(db *gorm.DB) *FeedbackHandler {
return &FeedbackHandler{db: db}
}
type submitFeedbackRequest struct {
Type string `json:"type" binding:"required,oneof=bug suggestion"`
Content string `json:"content"`
Images []string `json:"images"`
AppVersion string `json:"app_version"`
Platform string `json:"platform"`
}
// Submit POST /api/v1/feedback (需登录)
func (h *FeedbackHandler) Submit(c *gin.Context) {
shopID := middleware.GetShopID(c)
userID := middleware.GetUserID(c)
role := c.GetString(middleware.CtxRole)
var req submitFeedbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if strings.TrimSpace(req.Content) == "" && len(req.Images) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写反馈内容或添加图片"})
return
}
if len(req.Images) > 9 {
req.Images = req.Images[:9]
}
// 去规范化:从 JWT 身份补全 username / shop_code,便于后台查看
var username string
var u model.User
if err := h.db.Select("username").First(&u, userID).Error; err == nil {
username = u.Username
}
var shopCode string
var shop model.Shop
if err := h.db.Select("code").First(&shop, shopID).Error; err == nil {
shopCode = shop.Code
}
fb := model.Feedback{
ShopID: shopID,
ShopCode: shopCode,
UserID: userID,
Username: username,
Role: role,
Type: req.Type,
Content: strings.TrimSpace(req.Content),
Images: req.Images,
AppVersion: req.AppVersion,
Platform: req.Platform,
ClientIP: c.ClientIP(),
Status: "new",
}
if err := h.db.Create(&fb).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "submit failed"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": fb.ID})
}
// UploadImage POST /api/v1/feedback/images (需登录)
func (h *FeedbackHandler) UploadImage(c *gin.Context) {
shopID := middleware.GetShopID(c)
// 限制 5MB
if err := c.Request.ParseMultipartForm(5 << 20); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件超过 5MB 限制"})
return
}
file, _, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请上传文件(field: file"})
return
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "仅支持 JPEG/PNG 图片"})
return
}
resized := imaging.Fit(img, 1600, 1600, imaging.Lanczos)
filename := uuid.New().String() + ".jpg"
subdir := fmt.Sprintf("%s/feedback/%d", config.C.Storage.UploadDir, shopID)
if err := os.MkdirAll(subdir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储目录创建失败"})
return
}
fullPath := filepath.Join(subdir, filename)
if err := imaging.Save(resized, fullPath, imaging.JPEGQuality(85)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "图片保存失败"})
return
}
relURL := fmt.Sprintf("/images/feedback/%d/%s", shopID, filename)
c.JSON(http.StatusCreated, gin.H{"url": relURL})
}
// List GET /api/v1/admin/feedback (仅超级管理员)
func (h *FeedbackHandler) List(c *gin.Context) {
var q struct {
Type string `form:"type"`
Status string `form:"status"`
ShopID uint64 `form:"shop_id"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=50"`
}
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if q.PageSize <= 0 || q.PageSize > 200 {
q.PageSize = 50
}
if q.Page <= 0 {
q.Page = 1
}
db := h.db.Model(&model.Feedback{})
if q.Type != "" {
db = db.Where("type = ?", q.Type)
}
if q.Status != "" {
db = db.Where("status = ?", q.Status)
}
if q.ShopID != 0 {
db = db.Where("shop_id = ?", q.ShopID)
}
var total int64
db.Count(&total)
var list []model.Feedback
db.Order("id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Find(&list)
c.JSON(http.StatusOK, gin.H{
"data": list,
"total": total,
"page": q.Page,
"page_size": q.PageSize,
})
}
// UpdateStatus PATCH /api/v1/admin/feedback/:id (仅超级管理员)
func (h *FeedbackHandler) UpdateStatus(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req struct {
Status string `json:"status" binding:"required,oneof=new handled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
res := h.db.Model(&model.Feedback{}).Where("id = ?", id).Update("status", req.Status)
if res.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})
return
}
if res.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "updated"})
}
+72
View File
@@ -0,0 +1,72 @@
package handler
import (
"net/http"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/testutil"
)
func TestFeedbackHandler_SubmitAndList(t *testing.T) {
db := testutil.SetupTestDB()
require.NoError(t, db.AutoMigrate(&model.Feedback{}))
shop := testutil.CreateTestShop(db, "FB001")
user := testutil.CreateTestUser(db, shop.ID, "fbuser", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
fh := NewFeedbackHandler(db)
r := gin.New()
r.Use(gin.Recovery())
api := r.Group("/api/v1")
api.Use(middleware.JWT())
api.POST("/feedback", fh.Submit)
adminG := api.Group("/admin")
adminG.Use(middleware.SuperAdminOnly())
adminG.GET("/feedback", fh.List)
// 1. 正常提交(文字 + 图片)
w := makeRequest(r, "POST", "/api/v1/feedback", token, map[string]interface{}{
"type": "bug",
"content": "打印时卡住了",
"images": []string{"/images/feedback/1/a.jpg"},
})
require.Equal(t, http.StatusCreated, w.Code)
assert.NotZero(t, parseResponse(w)["id"])
// 去规范化字段已写入
var fb model.Feedback
require.NoError(t, db.Order("id DESC").First(&fb).Error)
assert.Equal(t, "fbuser", fb.Username)
assert.Equal(t, "FB001", fb.ShopCode)
assert.Equal(t, "new", fb.Status)
assert.Equal(t, 1, len(fb.Images))
// 2. 内容与图片都为空 → 400
w = makeRequest(r, "POST", "/api/v1/feedback", token, map[string]interface{}{
"type": "suggestion",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
// 3. 非法 type → 400
w = makeRequest(r, "POST", "/api/v1/feedback", token, map[string]interface{}{
"type": "other",
"content": "x",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
// 4. 普通管理员访问 admin 列表 → 403
w = makeRequest(r, "GET", "/api/v1/admin/feedback", token, nil)
assert.Equal(t, http.StatusForbidden, w.Code)
// 5. 超级管理员可列出
superToken := getAuthToken(user.ID, shop.ID, "superadmin")
w = makeRequest(r, "GET", "/api/v1/admin/feedback", superToken, nil)
require.Equal(t, http.StatusOK, w.Code)
assert.GreaterOrEqual(t, parseResponse(w)["total"].(float64), float64(1))
}
+1 -1
View File
@@ -51,7 +51,7 @@ func (h *FinanceHandler) ListRecords(c *gin.Context) {
var total int64
base.Count(&total)
var records []model.FinanceRecord
records := make([]model.FinanceRecord, 0)
offset := (q.Page - 1) * q.PageSize
base.Preload("Partner").Order("record_date DESC, id DESC").Offset(offset).Limit(q.PageSize).Find(&records)
+1 -1
View File
@@ -138,7 +138,7 @@ func (h *InventoryHandler) Logs(c *gin.Context) {
var total int64
query.Count(&total)
var logs []model.InventoryLog
logs := make([]model.InventoryLog, 0)
query.Preload("Product").Preload("Warehouse").Offset((page-1)*pageSize).Limit(pageSize).Order("id DESC").Find(&logs)
c.JSON(http.StatusOK, gin.H{"data": logs, "total": total, "page": page, "page_size": pageSize})
+1 -1
View File
@@ -32,7 +32,7 @@ var defaultRules = []struct {
// List GET /api/v1/number-rules
func (h *NumberRuleHandler) List(c *gin.Context) {
shopID := middleware.GetShopID(c)
var rules []model.NumberRule
rules := make([]model.NumberRule, 0)
h.db.Where("shop_id = ?", shopID).Order("id").Find(&rules)
if len(rules) == 0 {
+1 -1
View File
@@ -38,7 +38,7 @@ func (h *PartnerHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var partners []model.Partner
partners := make([]model.Partner, 0)
query.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&partners)
c.JSON(http.StatusOK, gin.H{"data": partners, "total": total, "page": page, "page_size": pageSize})
+1 -1
View File
@@ -48,7 +48,7 @@ func (h *ProductHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var products []model.Product
products := make([]model.Product, 0)
offset := (page - 1) * pageSize
query.Preload("Category").Offset(offset).Limit(pageSize).
Order("id DESC").Find(&products)
+3 -3
View File
@@ -22,7 +22,7 @@ func NewProductOptionHandler(db *gorm.DB) *ProductOptionHandler {
func (h *ProductOptionHandler) ListNames(c *gin.Context) {
shopID := middleware.GetShopID(c)
var items []model.ProductNameOption
items := make([]model.ProductNameOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
@@ -84,7 +84,7 @@ func (h *ProductOptionHandler) DeleteName(c *gin.Context) {
func (h *ProductOptionHandler) ListSeries(c *gin.Context) {
shopID := middleware.GetShopID(c)
var items []model.ProductSeriesOption
items := make([]model.ProductSeriesOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
@@ -146,7 +146,7 @@ func (h *ProductOptionHandler) DeleteSeries(c *gin.Context) {
func (h *ProductOptionHandler) ListSpecs(c *gin.Context) {
shopID := middleware.GetShopID(c)
var items []model.ProductSpecOption
items := make([]model.ProductSpecOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
+1 -1
View File
@@ -50,7 +50,7 @@ func (h *StockInHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var orders []model.StockInOrder
orders := make([]model.StockInOrder, 0)
query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
Offset((page - 1) * pageSize).Limit(pageSize).
Order("id DESC").Find(&orders)
+1 -1
View File
@@ -44,7 +44,7 @@ func (h *StockOutHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var orders []model.StockOutOrder
orders := make([]model.StockOutOrder, 0)
query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
Offset((page - 1) * pageSize).Limit(pageSize).
Order("id DESC").Find(&orders)
+1 -1
View File
@@ -22,7 +22,7 @@ func NewUserHandler(db *gorm.DB) *UserHandler {
// List GET /api/v1/users
func (h *UserHandler) List(c *gin.Context) {
shopID := middleware.GetShopID(c)
var users []model.User
users := make([]model.User, 0)
h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).
Order("id ASC").Find(&users)
c.JSON(http.StatusOK, gin.H{"data": users})
+1 -1
View File
@@ -20,7 +20,7 @@ func NewWarehouseHandler(db *gorm.DB) *WarehouseHandler {
func (h *WarehouseHandler) List(c *gin.Context) {
shopID := middleware.GetShopID(c)
var warehouses []model.Warehouse
warehouses := make([]model.Warehouse, 0)
h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).Find(&warehouses)
c.JSON(http.StatusOK, gin.H{"data": warehouses})
}
+1 -1
View File
@@ -51,7 +51,7 @@ func JWT() gin.HandlerFunc {
func AdminOnly() gin.HandlerFunc {
return func(c *gin.Context) {
role, _ := c.Get(CtxRole)
if role != "admin" {
if role != "admin" && role != "superadmin" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
+58
View File
@@ -0,0 +1,58 @@
package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
// StringSlice 以 JSON 数组形式存储的字符串切片(用于图片 URL 列表)
type StringSlice []string
func (s StringSlice) Value() (driver.Value, error) {
if s == nil {
return "[]", nil
}
b, err := json.Marshal(s)
return string(b), err
}
func (s *StringSlice) Scan(value interface{}) error {
if value == nil {
*s = nil
return nil
}
var b []byte
switch v := value.(type) {
case string:
b = []byte(v)
case []byte:
b = v
default:
return fmt.Errorf("cannot scan %T into StringSlice", value)
}
if len(b) == 0 {
*s = nil
return nil
}
return json.Unmarshal(b, s)
}
// Feedback 用户意见反馈(bug / 功能建议):文字 + 附图。多租户按 ShopID 隔离。
type Feedback struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
CreatedAt time.Time `json:"created_at"`
ShopID uint64 `gorm:"index;not null" json:"shop_id"`
ShopCode string `gorm:"size:50" json:"shop_code"`
UserID uint64 `gorm:"index" json:"user_id"`
Username string `gorm:"size:50" json:"username"`
Role string `gorm:"size:20" json:"role"`
Type string `gorm:"size:20;not null;index" json:"type"` // bug / suggestion
Content string `gorm:"type:text" json:"content"`
Images StringSlice `gorm:"type:json" json:"images"`
AppVersion string `gorm:"size:30" json:"app_version"`
Platform string `gorm:"size:20" json:"platform"`
ClientIP string `gorm:"size:60" json:"client_ip"`
Status string `gorm:"size:20;not null;default:new;index" json:"status"` // new / handled
}
+1
View File
@@ -4,6 +4,7 @@ type Shop struct {
Base
Name string `gorm:"size:100;not null" json:"name"`
Code string `gorm:"size:50;uniqueIndex" json:"code"`
Description string `gorm:"type:text" json:"description"`
Address string `gorm:"size:255" json:"address"`
Phone string `gorm:"size:30" json:"phone"`
BusinessHours string `gorm:"size:100" json:"business_hours"`
+11
View File
@@ -34,6 +34,7 @@ func Setup(r *gin.Engine, db *gorm.DB) {
adminH := handler.NewAdminHandler(db)
shopH := handler.NewShopHandler(db)
errorReportH := handler.NewErrorReportHandler(db)
feedbackH := handler.NewFeedbackHandler(db)
// 健康检查(无需认证,用于前端连通性探测)
r.GET("/health", func(c *gin.Context) {
@@ -58,6 +59,7 @@ func Setup(r *gin.Engine, db *gorm.DB) {
public.GET("/products/:public_id", publicH.GetProduct)
public.GET("/release", publicH.GetRelease)
public.POST("/errors", errorReportH.Submit)
public.POST("/register", authH.Register)
}
// 需要 JWT 的路由(ReadOnly 中间件:只读用户不可执行写操作)
@@ -170,6 +172,13 @@ func Setup(r *gin.Engine, db *gorm.DB) {
shop.POST("/logo", middleware.AdminOnly(), shopH.UploadLogo)
}
// 意见反馈(文字 + 附图,直接提交后台)
feedback := api.Group("/feedback")
{
feedback.POST("", feedbackH.Submit)
feedback.POST("/images", feedbackH.UploadImage)
}
// 编号规则
numberRules := api.Group("/number-rules")
{
@@ -216,6 +225,8 @@ func Setup(r *gin.Engine, db *gorm.DB) {
{
superAdmin.POST("/clear-data", adminH.ClearData)
superAdmin.GET("/errors", errorReportH.List)
superAdmin.GET("/feedback", feedbackH.List)
superAdmin.PATCH("/feedback/:id", feedbackH.UpdateStatus)
}
}
}
+72
View File
@@ -2,9 +2,11 @@ package service
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
@@ -61,6 +63,76 @@ func (s *AuthService) Login(shopCode, username, password string) (*TokenPair, *m
return pair, &user, nil
}
// RegisterInput 注册新门店所需参数
type RegisterInput struct {
ShopName string `json:"shop_name" binding:"required"`
Address string `json:"address" binding:"required"`
ManagerName string `json:"manager_name" binding:"required"`
Phone string `json:"phone" binding:"required"`
Description string `json:"description"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
// RegisterResult 注册成功后返回的数据
type RegisterResult struct {
ShopCode string `json:"shop_code"`
ShopName string `json:"shop_name"`
Username string `json:"username"`
}
// Register 自助注册新门店(公开接口,无需认证)
func (s *AuthService) Register(in RegisterInput) (*RegisterResult, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
var result RegisterResult
err = s.db.Transaction(func(tx *gorm.DB) error {
// 先用 UUID 占位创建门店,得到真实 ID
shop := model.Shop{
Name: in.ShopName,
Code: uuid.New().String(),
Address: in.Address,
Phone: in.Phone,
ManagerName: in.ManagerName,
Description: in.Description,
}
if err := tx.Create(&shop).Error; err != nil {
return err
}
// 用自增 ID 生成正式门店编码
shop.Code = fmt.Sprintf("S%06d", shop.ID)
if err := tx.Model(&shop).Update("code", shop.Code).Error; err != nil {
return err
}
// 创建管理员用户
user := model.User{
ShopID: shop.ID,
Username: in.Username,
PasswordHash: string(hash),
RealName: in.ManagerName,
Phone: in.Phone,
Role: "superadmin",
IsActive: true,
}
if err := tx.Create(&user).Error; err != nil {
return err
}
result = RegisterResult{
ShopCode: shop.Code,
ShopName: shop.Name,
Username: user.Username,
}
return nil
})
return &result, err
}
// HashPassword 生成 bcrypt 哈希
func HashPassword(plain string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
+1
View File
@@ -109,6 +109,7 @@ func autoMigrate(db *gorm.DB) {
&model.ProductSpecOption{},
&model.ProductImage{},
&model.ErrorReport{},
&model.Feedback{},
)
if err != nil {
log.Fatalf("auto migrate failed: %v", err)
+23
View File
@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS `shops` (
`address` VARCHAR(255) DEFAULT NULL,
`phone` VARCHAR(30) DEFAULT NULL,
`manager_name` VARCHAR(50) DEFAULT NULL COMMENT '负责人',
`description` TEXT DEFAULT NULL COMMENT '店铺简介',
`logo_url` VARCHAR(500) DEFAULT '' COMMENT '门店 logo URL',
`business_license` VARCHAR(500) DEFAULT NULL COMMENT '营业执照照片URL',
`shop_photos` JSON DEFAULT NULL COMMENT '门店照片URL数组',
@@ -445,4 +446,26 @@ CREATE TABLE IF NOT EXISTS `product_spec_options` (
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品规格选项';
CREATE TABLE IF NOT EXISTS `feedbacks` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`shop_id` BIGINT UNSIGNED NOT NULL,
`shop_code` VARCHAR(50) DEFAULT NULL,
`user_id` BIGINT UNSIGNED DEFAULT NULL,
`username` VARCHAR(50) DEFAULT NULL,
`role` VARCHAR(20) DEFAULT NULL,
`type` VARCHAR(20) NOT NULL COMMENT 'bug / suggestion',
`content` TEXT,
`images` JSON,
`app_version` VARCHAR(30) DEFAULT NULL,
`platform` VARCHAR(20) DEFAULT NULL,
`client_ip` VARCHAR(60) DEFAULT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'new',
PRIMARY KEY (`id`),
KEY `idx_shop_id` (`shop_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_type` (`type`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户意见反馈';
SET FOREIGN_KEY_CHECKS = 1;
+1
View File
@@ -53,6 +53,7 @@ func SetupTestDB() *gorm.DB {
deleted_at DATETIME,
name TEXT NOT NULL,
code TEXT UNIQUE,
description TEXT,
address TEXT,
phone TEXT,
manager_name TEXT,
+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,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- release 包走此主清单,必须显式声明联网权限;debug/profile 清单由 Flutter 模板自动添加,
release 不带会导致所有网络请求失败(登录页显示「服务不可达」)。 -->
<uses-permission android:name="android.permission.INTERNET"/>
<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
+5
View File
@@ -0,0 +1,5 @@
{
"provider": "岩美技术有限公司",
"website": "https://jiu.yanmei.com",
"email": "yammy2023@163.com"
}
Binary file not shown.
+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.
}
}
+28
View File
@@ -0,0 +1,28 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// 应用「关于我们」等品牌信息,从配置文件 assets/config/app_info.json 读取,
/// 不硬编码、不走环境变量。在 main() 启动时调用 [load] 加载一次。
class AppInfo {
const AppInfo._();
static const _assetPath = 'assets/config/app_info.json';
static String provider = '';
static String website = '';
static String email = '';
/// 加载配置文件。失败时保留为空(不影响应用启动)。
static Future<void> load() async {
try {
final raw = await rootBundle.loadString(_assetPath);
final map = jsonDecode(raw) as Map<String, dynamic>;
provider = (map['provider'] as String?) ?? '';
website = (map['website'] as String?) ?? '';
email = (map['email'] as String?) ?? '';
} catch (e) {
debugPrint('AppInfo.load failed: $e');
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ class PageResult<T> {
T Function(Map<String, dynamic>) fromJson,
) {
return PageResult(
data: (json['data'] as List)
data: ((json['data'] as List?) ?? [])
.map((e) => fromJson(e as Map<String, dynamic>))
.toList(),
total: json['total'] as int,
@@ -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;
}
}
+4
View File
@@ -15,6 +15,7 @@ import '../../screens/products/products_screen.dart';
import '../../screens/products/product_detail_screen.dart';
import '../../screens/public/public_product_screen.dart';
import '../../screens/settings/settings_screen.dart';
import '../../screens/about/about_screen.dart';
import '../auth/auth_state.dart';
Page<void> _noTransition(Widget child) =>
@@ -131,6 +132,9 @@ final appRouterProvider = Provider<GoRouter>((ref) {
GoRoute(
path: '/settings',
pageBuilder: (_, __) => _noTransition(const SettingsScreen())),
GoRoute(
path: '/about',
pageBuilder: (_, __) => _noTransition(const AboutScreen())),
],
),
],
+85
View File
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import '../../providers/update_provider.dart';
import 'app_updater_io.dart'
if (dart.library.js_interop) 'app_updater_web.dart' as impl;
/// 应用内更新统一入口。
/// - Web:刷新页面。
/// - Windows/macOS:应用内下载安装包并自动安装/重启(带进度对话框)。
/// - 其它平台 / 无对应下载地址:回退浏览器下载(launchUpdateUrl)。
Future<void> startInAppUpdate(BuildContext context, AppUpdateInfo info) async {
if (impl.isWeb) {
impl.reloadForUpdate();
return;
}
final url = impl.platformDownloadUrl(info.downloadUrls);
if (!impl.isInAppSupported || url == null || url.isEmpty) {
await launchUpdateUrl(info.downloadUrls); // 兜底:开浏览器/商店
return;
}
await _runWithProgress(context, info, url);
}
Future<void> _runWithProgress(
BuildContext context, AppUpdateInfo info, String url) async {
final progress = ValueNotifier<double>(0);
// 不可关闭的进度对话框;下载完成后 impl 会让进程退出。
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => PopScope(
canPop: false,
child: AlertDialog(
title: Text('正在更新到 v${info.latestVersion}'),
content: ValueListenableBuilder<double>(
valueListenable: progress,
builder: (_, p, __) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LinearProgressIndicator(value: p > 0 ? p : null),
const SizedBox(height: 12),
Text(p >= 1
? '下载完成,正在安装,应用即将重启…'
: '正在下载… ${(p * 100).toStringAsFixed(0)}%'),
],
),
),
),
),
);
try {
await impl.downloadAndInstall(url, (p) => progress.value = p);
// 桌面端到此通常已 exit(0),不会继续执行。
} catch (e) {
if (!context.mounted) return;
Navigator.of(context, rootNavigator: true).pop(); // 关进度对话框
if (!context.mounted) return;
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('更新失败'),
content: Text('$e\n\n可改用浏览器下载更新。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
launchUpdateUrl(info.downloadUrls);
},
child: const Text('浏览器下载'),
),
],
),
);
} finally {
progress.dispose();
}
}
@@ -0,0 +1,83 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
// 桌面/原生平台的应用内更新实现。Web 版见 app_updater_web.dart。
bool get isWeb => false;
/// 仅 Windows / macOS 支持「下载并自动安装」。其它平台回退到浏览器下载。
bool get isInAppSupported => Platform.isWindows || Platform.isMacOS;
String? platformDownloadUrl(Map<String, String> urls) {
if (Platform.isWindows) return urls['windows'];
if (Platform.isMacOS) return urls['macos'];
if (Platform.isAndroid) return urls['android'];
if (Platform.isIOS) return urls['ios'];
return urls['web'];
}
void reloadForUpdate() {/* 桌面无意义 */}
/// 用 Dio 下载安装包(不经浏览器,无 MOTW / quarantine),再启动安装。
/// 成功后进程会自行退出,调用方的后续代码可能不会执行。
Future<void> downloadAndInstall(
String url, void Function(double) onProgress) async {
final tmp = await getTemporaryDirectory();
final dio = Dio();
void prog(int r, int t) {
if (t > 0) onProgress(r / t);
}
if (Platform.isWindows) {
final exe = '${tmp.path}\\jiu-update-setup.exe';
await dio.download(url, exe, onReceiveProgress: prog);
// 启动 Inno Setup 安装包后退出本应用,让其覆盖安装。
await Process.start(exe, const [], mode: ProcessStartMode.detached);
await Future<void>.delayed(const Duration(milliseconds: 400));
exit(0);
}
if (Platform.isMacOS) {
final zip = '${tmp.path}/jiu-update.zip';
await dio.download(url, zip, onReceiveProgress: prog);
final extractDir = '${tmp.path}/jiu-update';
await Process.run('/bin/rm', ['-rf', extractDir]);
await Directory(extractDir).create(recursive: true);
// ditto 正确处理 .app 包(保留权限/符号链接)。
final unzip = await Process.run('/usr/bin/ditto', ['-x', '-k', zip, extractDir]);
if (unzip.exitCode != 0) {
throw Exception('解压失败:${unzip.stderr}');
}
final newApp = Directory(extractDir)
.listSync()
.map((e) => e.path)
.firstWhere((p) => p.endsWith('.app'),
orElse: () => throw Exception('更新包中未找到 .app'));
// 当前运行的 app bundle.../Xxx.app/Contents/MacOS/exe
final resolved = Platform.resolvedExecutable;
final idx = resolved.indexOf('.app');
if (idx < 0) {
throw Exception('无法定位当前 app 路径(非 .app 运行)');
}
final appPath = resolved.substring(0, idx + 4);
// 等本进程退出后替换并重启;替换失败(权限)则在访达中高亮让用户手动拖入。
final script = '${tmp.path}/jiu-update-swap.sh';
await File(script).writeAsString('''#!/bin/sh
while kill -0 $pid 2>/dev/null; do sleep 0.5; done
if rm -rf "$appPath" && ditto "$newApp" "$appPath"; then
open "$appPath"
else
open -R "$newApp"
fi
''');
await Process.start('/bin/sh', [script], mode: ProcessStartMode.detached);
await Future<void>.delayed(const Duration(milliseconds: 400));
exit(0);
}
throw Exception('当前平台不支持应用内更新');
}
@@ -0,0 +1,15 @@
import 'package:web/web.dart' as web;
// Web 平台:没有"安装",更新即刷新页面拿最新资源。
bool get isWeb => true;
bool get isInAppSupported => true;
String? platformDownloadUrl(Map<String, String> urls) => urls['web'];
void reloadForUpdate() => web.window.location.reload();
Future<void> downloadAndInstall(
String url, void Function(double) onProgress) async {
reloadForUpdate();
}
+17
View File
@@ -1,6 +1,8 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../../models/stock_in.dart';
import '../../models/stock_out.dart';
import '../errors/error_reporter.dart';
import 'print_util_stub.dart'
if (dart.library.js_interop) 'print_util_web.dart';
@@ -36,3 +38,18 @@ Future<void> printStockInOrder(StockInOrder order) =>
Future<void> printStockOutOrder(StockOutOrder order) =>
printStockOutOrderImpl(order);
/// 统一打印入口:catch 任意异常,上报并弹 SnackBar 给用户。
/// 用法:await safePrint(context, () => printStockInOrder(order));
Future<void> safePrint(BuildContext context, Future<void> Function() fn) async {
try {
await fn();
} catch (e, st) {
reportError(e, st);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('打印失败:$e'), backgroundColor: Colors.red),
);
}
}
}
+15 -5
View File
@@ -17,6 +17,13 @@ Future<pw.Font> _loadBoldFont() async {
return pw.Font.ttf(data);
}
/// 打印任务/另存默认文件名的时间戳:yyyyMMdd_HHmmss
String _fileTs() {
final n = DateTime.now();
String p(int v) => v.toString().padLeft(2, '0');
return '${n.year}${p(n.month)}${p(n.day)}_${p(n.hour)}${p(n.minute)}${p(n.second)}';
}
// ── 设计色彩 token ──────────────────────────────────────────────────────────
const _navy = PdfColor(0.122, 0.165, 0.227); // #1F2A3A header/chip
const _cream = PdfColor(0.957, 0.925, 0.847); // #F4ECD8 footer/reversed text
@@ -54,6 +61,7 @@ Future<void> printProductLabelImpl({
String shopAddress = '',
String shopPhone = '',
}) async {
try {
final font = await _loadFont();
final doc = pw.Document();
final qrImage = pw.MemoryImage(qrBytes);
@@ -190,8 +198,8 @@ Future<void> printProductLabelImpl({
),
));
try {
await Printing.layoutPdf(onLayout: (_) async => doc.save());
await Printing.layoutPdf(
name: '标签_${_fileTs()}', onLayout: (_) async => doc.save());
} catch (e, st) {
reportError(e, st);
throw Exception('打印失败,请检查打印机连接和驱动是否正常。\n详情:$e');
@@ -383,6 +391,7 @@ pw.Widget _sig(pw.Font font, pw.Font bold, String label) {
}
Future<void> printStockInOrderImpl(StockInOrder order) async {
try {
final font = await _loadFont();
final bold = await _loadBoldFont();
@@ -430,8 +439,8 @@ Future<void> printStockInOrderImpl(StockInOrder order) async {
),
],
));
try {
await Printing.layoutPdf(onLayout: (_) async => doc.save());
await Printing.layoutPdf(
name: '入库单_${_fileTs()}', onLayout: (_) async => doc.save());
} catch (e, st) {
reportError(e, st);
throw Exception('打印失败,请检查打印机连接和驱动是否正常。\n详情:$e');
@@ -486,7 +495,8 @@ Future<void> printStockOutOrderImpl(StockOutOrder order) async {
],
));
try {
await Printing.layoutPdf(onLayout: (_) async => doc.save());
await Printing.layoutPdf(
name: '出库单_${_fileTs()}', onLayout: (_) async => doc.save());
} catch (e, st) {
reportError(e, st);
throw Exception('打印失败,请检查打印机连接和驱动是否正常。\n详情:$e');
+58 -92
View File
@@ -30,111 +30,93 @@ Future<void> printProductLabelImpl({
}) async {
final base64Img = base64Encode(qrBytes);
final specVal = (spec ?? '').isNotEmpty ? spec! : '';
final seriesVal = (series ?? '').isNotEmpty ? series! : '';
final batchVal = (batchNo ?? '').isNotEmpty ? batchNo! : '';
final dateVal = (productionDate ?? '').isNotEmpty
final specVal = (spec ?? '').isNotEmpty ? spec! : '';
final batchVal = (batchNo ?? '').isNotEmpty ? batchNo! : '';
final dateVal = (productionDate ?? '').isNotEmpty
? (productionDate!.length > 10 ? productionDate.substring(0, 10) : productionDate)
: '';
final footerContact = [
if (shopAddress.isNotEmpty) shopAddress,
final contactLine = [
if (shopPhone.isNotEmpty) shopPhone,
].join(' · ');
// 标签生成时间
final now = DateTime.now();
final genTime =
'${now.year}-${now.month.toString().padLeft(2,'0')}-${now.day.toString().padLeft(2,'0')}'
' ${now.hour.toString().padLeft(2,'0')}:${now.minute.toString().padLeft(2,'0')}';
final remarkRow = (remark ?? '').isNotEmpty
? '''<div class="spec-row">
<span class="lbl">备 注</span>
<span class="val-full">$remark</span>
</div>'''
: '';
if (shopAddress.isNotEmpty) shopAddress,
].join(' ');
final html = '''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&family=Noto+Serif+SC:wght@700&family=IBM+Plex+Mono:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&family=IBM+Plex+Mono:wght@400&display=swap" rel="stylesheet">
<style>
@page { size: 4in 2in; margin: 0; }
/* 标签尺寸:38mm × 20mm,横向 */
@page { size: 38mm 20mm; margin: 0; }
* { box-sizing: border-box; margin: 0; padding: 0;
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
body { width: 4in; height: 2in; overflow: hidden; background: #fff; }
body { width: 38mm; height: 20mm; overflow: hidden; background: #fff; }
.label {
width: 4in; height: 2in;
display: grid; grid-template-rows: 42px 1fr 18px;
width: 38mm; height: 20mm;
display: flex; flex-direction: column;
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #111;
}
/* ── Header:酒行名称 ── */
/* ── Header:酒行名称 3.5mm ── */
.hdr {
background: #1f2a3a; color: #f4ecd8;
display: flex; align-items: center; justify-content: space-between;
padding: 0 15px; overflow: hidden;
height: 3.5mm; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
padding: 0 1.5mm; overflow: hidden;
}
.hdr-name {
font-family: "Noto Serif SC", serif;
font-weight: 700; font-size: 17px; letter-spacing: 0.08em;
font-size: 5.5pt; font-weight: 700; letter-spacing: 0.08em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hdr-tag {
font-size: 6.5px; letter-spacing: 0.2em; opacity: 0.8;
font-style: italic; white-space: nowrap; flex-shrink: 0; padding-left: 10px;
/* ── 中部:左侧商品信息 + 右侧二维码 ── */
.middle {
flex: 1; display: flex; flex-direction: row; overflow: hidden;
}
/* ── Body ── */
.body {
background: #fff; padding: 9px 15px 8px;
display: grid; grid-template-columns: 1fr 76px; gap: 10px;
align-items: center;
/* 左:商品名 + 规格批次 */
.info {
flex: 1; display: flex; flex-direction: column;
justify-content: center; padding: 0.8mm 1mm 0.5mm 1.5mm;
overflow: hidden;
}
/* 商品名 */
.product-name {
font-family: "Noto Serif SC", serif;
font-weight: 700; font-size: 13px; letter-spacing: 0.04em;
margin-bottom: 7px;
font-size: 6.5pt; font-weight: 700; line-height: 1.2;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-bottom: 0.8mm;
}
.specs {
display: flex; flex-direction: column; gap: 0.5mm;
}
/* 规格行 */
.spec-row {
display: flex; gap: 6px; align-items: baseline;
font-size: 6.5px; margin-bottom: 3.5px; flex-wrap: nowrap;
display: flex; gap: 0.8mm; align-items: baseline;
font-size: 4pt; white-space: nowrap; overflow: hidden;
}
.lbl { color: #888; letter-spacing: 0.1em; white-space: nowrap; flex-shrink: 0; }
.lbl { color: #999; flex-shrink: 0; }
.val { font-family: "IBM Plex Mono", monospace;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.val-full { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
color: #444; font-size: 6px; }
overflow: hidden; text-overflow: ellipsis; }
/* 同行双字段 */
.spec-row-2 {
display: grid; grid-template-columns: 28px 1fr 28px 1fr;
gap: 0 7px; font-size: 6.5px; margin-bottom: 3.5px;
align-items: baseline;
/* 右:二维码 */
.qr-wrap {
width: 14mm; flex-shrink: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 0.3mm;
padding: 0.5mm 0.5mm 0.5mm 0;
}
.qr-wrap img { width: 12mm; height: 12mm; object-fit: contain; }
.qr-cap { font-size: 3pt; color: #aaa; letter-spacing: 0.1em; }
/* QR */
.qr-col { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.qr-col img { width: 68px; height: 68px; object-fit: contain; display: block; }
.qr-cap { font-size: 5px; letter-spacing: 0.2em; color: #666; text-align: center; }
/* ── Footer:地址 + 生成时间 ── */
/* ── Footer:联系方式 3mm ── */
.ftr {
background: #f4ecd8;
display: flex; align-items: center; justify-content: space-between;
padding: 0 15px;
font-family: "IBM Plex Mono", monospace;
font-size: 5px; color: #5a4e35; letter-spacing: 0.04em; overflow: hidden;
background: #f4ecd8; height: 3mm; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
padding: 0 1.5mm;
font-size: 3.5pt; color: #5a4e35; overflow: hidden; white-space: nowrap;
text-overflow: ellipsis;
}
</style>
</head>
@@ -143,40 +125,24 @@ body { width: 4in; height: 2in; overflow: hidden; background: #fff; }
<div class="hdr">
<span class="hdr-name">$shopName</span>
<span class="hdr-tag">Certificate of Authenticity</span>
</div>
<div class="body">
<div>
<div class="middle">
<div class="info">
<div class="product-name">$name</div>
<div class="spec-row-2">
<span class="lbl">规 格</span>
<span class="val">$specVal</span>
<span class="lbl">系 列</span>
<span class="val">$seriesVal</span>
<div class="specs">
<div class="spec-row"><span class="lbl">规</span><span class="val">$specVal</span></div>
<div class="spec-row"><span class="lbl">批</span><span class="val">$batchVal</span></div>
<div class="spec-row"><span class="lbl">产</span><span class="val">$dateVal</span></div>
</div>
<div class="spec-row-2">
<span class="lbl">批 号</span>
<span class="val">$batchVal</span>
<span class="lbl">生产日期</span>
<span class="val">$dateVal</span>
</div>
$remarkRow
</div>
<div class="qr-col">
<div class="qr-wrap">
<img src="data:image/png;base64,$base64Img" />
<div class="qr-cap">扫码溯源 · TRACE</div>
<div class="qr-cap">扫码溯源</div>
</div>
</div>
<div class="ftr">
<span>${footerContact.isNotEmpty ? footerContact : shopName}</span>
<span>$genTime</span>
</div>
<div class="ftr">${contactLine.isNotEmpty ? contactLine : shopName}</div>
</div>
<script>
+6 -1
View File
@@ -4,6 +4,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'core/auth/auth_state.dart';
import 'core/config/app_info.dart';
import 'core/errors/error_reporter.dart';
import 'core/router/app_router.dart';
import 'core/theme/app_theme.dart';
@@ -24,7 +25,11 @@ void main() {
};
runZonedGuarded(
() => runApp(const ProviderScope(child: JiuApp())),
() async {
WidgetsFlutterBinding.ensureInitialized();
await AppInfo.load(); // 从配置文件加载「关于我们」品牌信息
runApp(const ProviderScope(child: JiuApp()));
},
(error, stack) {
debugPrint('═══ Zone Error ═══════════════════════════════');
debugPrint(error.toString());
@@ -0,0 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/api_client.dart';
import '../repositories/feedback_repository.dart';
final feedbackRepositoryProvider = Provider<FeedbackRepository>((ref) {
return FeedbackRepository(ref.watch(apiClientProvider));
});
@@ -0,0 +1,78 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../core/api/api_client.dart';
import '../core/exceptions.dart';
class FeedbackRepository {
final ApiClient _client;
const FeedbackRepository(this._client);
/// 上传单张图片,返回服务器相对 URL/images/feedback/...)。
Future<String> uploadImage({
Uint8List? bytes,
String? filePath,
String filename = 'image.jpg',
}) async {
try {
final MultipartFile file;
if (bytes != null) {
file = MultipartFile.fromBytes(bytes, filename: filename);
} else {
file = await MultipartFile.fromFile(filePath!);
}
final formData = FormData.fromMap({'file': file});
final resp = await _client.post('/feedback/images', data: formData);
return (resp.data as Map<String, dynamic>)['url'] as String;
} on DioException catch (e) {
throw AppException(
e.response?.data?['error'] as String? ?? '图片上传失败',
statusCode: e.response?.statusCode,
);
}
}
/// 提交反馈(bug / suggestion),文字 + 图片 URL 列表。
Future<void> submit({
required String type,
required String content,
required List<String> images,
}) async {
String appVersion = '';
try {
appVersion = (await PackageInfo.fromPlatform()).version;
} catch (_) {}
try {
await _client.post('/feedback', data: {
'type': type,
'content': content,
'images': images,
'app_version': appVersion,
'platform': _platform(),
});
} on DioException catch (e) {
throw AppException(
e.response?.data?['error'] as String? ?? '提交失败',
statusCode: e.response?.statusCode,
);
}
}
String _platform() {
if (kIsWeb) return 'web';
switch (defaultTargetPlatform) {
case TargetPlatform.windows:
return 'windows';
case TargetPlatform.macOS:
return 'macos';
case TargetPlatform.android:
return 'android';
case TargetPlatform.iOS:
return 'ios';
case TargetPlatform.linux:
return 'linux';
default:
return 'unknown';
}
}
}
@@ -10,7 +10,7 @@ class NumberRuleRepository {
Future<List<NumberRule>> list() async {
try {
final resp = await _client.get('/number-rules');
final data = resp.data['data'] as List;
final data = (resp.data['data'] as List?) ?? [];
return data
.map((e) => NumberRule.fromJson(e as Map<String, dynamic>))
.toList();
+1 -1
View File
@@ -7,7 +7,7 @@ class UserRepository {
Future<List<AppUser>> list() async {
final resp = await _client.get('/users');
final data = resp.data['data'] as List;
final data = (resp.data['data'] as List?) ?? [];
return data.map((e) => AppUser.fromJson(e as Map<String, dynamic>)).toList();
}
@@ -12,7 +12,7 @@ class WarehouseRepository {
try {
final resp = await _client.get('/warehouses');
final body = resp.data as Map<String, dynamic>;
final raw = body['data'] as List;
final raw = (body['data'] as List?) ?? [];
return raw
.map((e) => Warehouse.fromJson(e as Map<String, dynamic>))
.toList();
+512
View File
@@ -0,0 +1,512 @@
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';
import 'package:url_launcher/url_launcher.dart';
import '../../core/config/app_config.dart';
import '../../core/config/app_info.dart';
import '../../core/theme/app_theme.dart';
import '../../core/update/app_updater.dart';
import '../../core/utils/dialog_util.dart';
import '../../providers/feedback_provider.dart';
import '../../providers/license_provider.dart';
import '../../providers/update_provider.dart';
/// 「关于我们」独立页面(左侧菜单项)。原为系统设置里的「关于」Tab。
class AboutScreen extends ConsumerStatefulWidget {
const AboutScreen({super.key});
@override
ConsumerState<AboutScreen> createState() => _AboutScreenState();
}
class _AboutScreenState extends ConsumerState<AboutScreen> {
@override
Widget build(BuildContext context) {
final appVersion = ref.watch(appVersionProvider).valueOrNull ?? 'v1.0.0';
final updateInfo = ref.watch(updateProvider).valueOrNull;
final licenseAsync = ref.watch(licenseProvider);
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── 版本信息 ──
_AboutSection(
title: '版本信息',
children: [
_AboutRow(label: '当前版本', value: appVersion),
if (updateInfo != null && updateInfo.hasUpdate)
_AboutRow(
label: '最新版本',
value: 'v${updateInfo.latestVersion}',
valueColor: AppTheme.success,
trailing: TextButton(
onPressed: () => startInAppUpdate(context, updateInfo),
child: const Text('立即更新'),
),
)
else
_AboutRow(
label: '最新版本',
value: updateInfo != null ? '已是最新' : '检查中…',
valueColor: AppTheme.textSecondary,
trailing: TextButton(
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
const SnackBar(content: Text('正在检查更新…')));
await ref.read(updateProvider.notifier).forceCheck();
if (!context.mounted) return;
final r = ref.read(updateProvider).valueOrNull;
messenger.hideCurrentSnackBar();
final String msg;
if (r == null) {
msg = '检查更新失败,请检查网络';
} else if (r.hasUpdate) {
msg = '发现新版本 v${r.latestVersion}';
} else {
msg = '已是最新版本(v${r.latestVersion}';
}
messenger.showSnackBar(SnackBar(content: Text(msg)));
},
child: const Text('检查更新'),
),
),
],
),
const SizedBox(height: 20),
// ── 授权信息 ──
_AboutSection(
title: '授权信息',
children: [
licenseAsync.when(
loading: () => const _AboutRow(label: '授权状态', value: '加载中…'),
error: (_, __) =>
const _AboutRow(label: '授权状态', value: '暂无授权信息'),
data: (lic) {
if (lic == null) {
return const _AboutRow(label: '授权状态', value: '未激活');
}
return Column(
children: [
_AboutRow(label: '授权类型', value: lic.typeLabel),
_AboutRow(
label: '授权状态',
value: lic.isExpired
? '已过期'
: lic.isActive
? '正常'
: '已停用',
valueColor: lic.isExpired
? AppTheme.danger
: lic.isActive
? AppTheme.success
: AppTheme.textSecondary,
),
if (lic.expiresAt != null)
_AboutRow(
label: '到期时间',
value: DateFormat('yyyy-MM-dd').format(lic.expiresAt!),
trailing: lic.daysRemaining != null &&
lic.daysRemaining! <= 30
? Chip(
label: Text(
lic.isExpired
? '已过期'
: '剩余 ${lic.daysRemaining}',
style: const TextStyle(
fontSize: 11, color: Colors.white),
),
backgroundColor: lic.isExpired
? AppTheme.danger
: Colors.orange,
padding: EdgeInsets.zero,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
)
: null,
)
else
const _AboutRow(label: '到期时间', value: '永久有效'),
if (lic.activatedAt != null)
_AboutRow(
label: '激活时间',
value: DateFormat('yyyy-MM-dd')
.format(lic.activatedAt!),
),
],
);
},
),
const SizedBox(height: 8),
Row(
children: [
OutlinedButton.icon(
onPressed: () => _showRenewDialog(),
icon: const Icon(Icons.card_membership, size: 16),
label: const Text('续费 / 升级授权'),
),
],
),
],
),
const SizedBox(height: 20),
// ── 关于我们 ──
_AboutSection(
title: '关于我们',
children: [
_AboutRow(label: '服务商', value: AppInfo.provider),
_AboutRow(label: '官方网站', value: AppInfo.website),
_AboutRow(label: '联系邮箱', value: AppInfo.email),
const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
OutlinedButton.icon(
onPressed: () async {
final uri = Uri.parse(
'mailto:${AppInfo.email}?subject=酒库管理系统咨询');
if (await canLaunchUrl(uri)) launchUrl(uri);
},
icon: const Icon(Icons.email_outlined, size: 16),
label: const Text('发送邮件'),
),
],
),
],
),
const SizedBox(height: 20),
// ── 意见反馈 ──
_AboutSection(
title: '意见反馈',
children: [
const _AboutRow(
label: '问题反馈',
value: '遇到 Bug 或有功能建议,欢迎告知我们',
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton.icon(
onPressed: () => _showFeedbackDialog(isBug: true),
icon: const Icon(Icons.bug_report_outlined, size: 16),
label: const Text('反馈 Bug'),
),
OutlinedButton.icon(
onPressed: () => _showFeedbackDialog(isBug: false),
icon: const Icon(Icons.lightbulb_outline, size: 16),
label: const Text('功能建议'),
),
],
),
],
),
],
),
);
}
void _showRenewDialog() {
showAppDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('续费 / 升级授权'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('请联系我们获取续费报价:'),
const SizedBox(height: 12),
SelectableText('📧 ${AppInfo.email}',
style: const TextStyle(fontSize: 13)),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('关闭'),
),
ElevatedButton(
onPressed: () async {
await Clipboard.setData(ClipboardData(text: AppInfo.email));
if (ctx.mounted) {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('邮箱已复制到剪贴板')),
);
}
},
child: const Text('复制邮箱'),
),
],
),
);
}
void _showFeedbackDialog({required bool isBug}) {
showAppDialog(
context: context,
builder: (_) => _FeedbackDialog(type: isBug ? 'bug' : 'suggestion'),
);
}
}
/// 反馈表单弹窗:文字 + 附图,直接提交后台。
class _FeedbackDialog extends ConsumerStatefulWidget {
final String type; // bug / suggestion
const _FeedbackDialog({required this.type});
@override
ConsumerState<_FeedbackDialog> createState() => _FeedbackDialogState();
}
class _FeedbackDialogState extends ConsumerState<_FeedbackDialog> {
final _ctrl = TextEditingController();
final List<String> _images = []; // 已上传的图片 URL
bool _uploading = false;
bool _submitting = false;
static const _maxImages = 9;
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
Future<void> _addImages() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
withData: true,
);
if (result == null) return;
setState(() => _uploading = true);
try {
final repo = ref.read(feedbackRepositoryProvider);
for (final f in result.files) {
if (_images.length >= _maxImages) break;
if (f.bytes == null) continue;
final url = await repo.uploadImage(bytes: f.bytes!, filename: f.name);
if (!mounted) return;
setState(() => _images.add(url));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('图片上传失败:$e')));
}
} finally {
if (mounted) setState(() => _uploading = false);
}
}
Future<void> _submit() async {
if (_ctrl.text.trim().isEmpty && _images.isEmpty) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('请填写反馈内容或添加图片')));
return;
}
setState(() => _submitting = true);
try {
await ref.read(feedbackRepositoryProvider).submit(
type: widget.type,
content: _ctrl.text.trim(),
images: _images,
);
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('反馈已提交,感谢您的反馈!')));
} catch (e) {
if (!mounted) return;
setState(() => _submitting = false);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('提交失败:$e')));
}
}
@override
Widget build(BuildContext context) {
final isBug = widget.type == 'bug';
return AlertDialog(
title: Text(isBug ? '反馈 Bug' : '功能建议'),
content: SizedBox(
width: context.dialogWidth(440),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _ctrl,
maxLines: 6,
decoration: InputDecoration(
hintText: isBug ? '请描述问题的复现步骤和预期行为…' : '请描述您希望增加的功能…',
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (int i = 0; i < _images.length; i++) _thumb(i),
if (_images.length < _maxImages) _addButton(),
],
),
if (_uploading)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Row(children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2)),
SizedBox(width: 8),
Text('图片上传中…', style: TextStyle(fontSize: 12)),
]),
),
],
),
),
actions: [
TextButton(
onPressed: _submitting ? null : () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: (_submitting || _uploading) ? null : _submit,
child: Text(_submitting ? '提交中…' : '提交'),
),
],
);
}
Widget _thumb(int i) {
return Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
'${AppConfig.baseUrl}${_images[i]}',
width: 64,
height: 64,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 64,
height: 64,
color: Colors.grey.shade200,
child: const Icon(Icons.broken_image, size: 20),
),
),
),
Positioned(
right: -8,
top: -8,
child: GestureDetector(
onTap: () => setState(() => _images.removeAt(i)),
child: Container(
decoration: const BoxDecoration(
color: Colors.black54, shape: BoxShape.circle),
padding: const EdgeInsets.all(2),
child: const Icon(Icons.close, size: 14, color: Colors.white),
),
),
),
],
);
}
Widget _addButton() {
return InkWell(
onTap: _uploading ? null : _addImages,
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(Icons.add_a_photo_outlined, color: Colors.grey),
),
);
}
}
// ── 关于页辅助 widgets ──────────────────────────────────────
class _AboutSection extends StatelessWidget {
final String title;
final List<Widget> children;
const _AboutSection({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
const Divider(height: 24),
...children,
],
),
),
);
}
}
class _AboutRow extends StatelessWidget {
final String label;
final String value;
final Color? valueColor;
final Widget? trailing;
const _AboutRow({
required this.label,
required this.value,
this.valueColor,
this.trailing,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
SizedBox(
width: 88,
child: Text(label,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
),
Expanded(
child: Text(value,
style: TextStyle(
fontSize: 13,
color: valueColor ?? AppTheme.textPrimary,
fontWeight: FontWeight.w500)),
),
if (trailing != null) trailing!,
],
),
);
}
}
+19 -2
View File
@@ -1,9 +1,12 @@
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';
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/theme/app_theme.dart';
import '../../core/storage/login_history.dart';
import '../../providers/connectivity_provider.dart';
@@ -123,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,
@@ -261,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,
@@ -484,6 +487,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
letterSpacing: 2)),
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () => launchUrl(
Uri.parse('${AppConfig.publicBaseUrl}/register/'),
mode: LaunchMode.externalApplication,
),
child: const Text(
'还没有门店账号?前往官网注册',
style: TextStyle(
fontSize: 15,
color: AppTheme.textSecondary,
),
),
),
],
),
),
+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('商品名称')),
@@ -386,7 +527,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
])
]
: filteredItems
@@ -485,12 +625,12 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
item.productId != null
? TextButton(
onPressed: () async {
try {
final qrBytes = await ref
.read(productRepositoryProvider)
.getQRCodeBytes(item.productId!);
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
await printProductLabel(
final qrBytes = await ref
.read(productRepositoryProvider)
.getQRCodeBytes(item.productId!);
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
if (context.mounted) {
await safePrint(context, () => printProductLabel(
qrBytes: qrBytes,
name: item.productName,
code: item.productCode,
@@ -502,14 +642,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
shopName: shopInfo?.name ?? '',
shopAddress: shopInfo?.address ?? '',
shopPhone: shopInfo?.phone ?? '',
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('打印失败:$e'),
backgroundColor: AppTheme.danger),
);
}
));
}
},
child: const Text('打标签',
@@ -588,6 +721,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards: warnings.map(_warningCard).toList(),
columns: const [
DataColumn(label: Text('商品编码')),
DataColumn(label: Text('商品名称')),
@@ -725,6 +859,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards: logs.map(_logCard).toList(),
columns: const [
DataColumn(label: Text('商品名称')),
DataColumn(label: Text('仓库')),
@@ -796,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(
@@ -857,8 +995,10 @@ class _SummaryCard extends StatelessWidget {
),
],
),
),
);
);
return width != null
? SizedBox(width: width, child: card)
: Expanded(child: card);
}
}
@@ -958,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,

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