diff --git a/.claude/skills/phone-adapt/SKILL.md b/.claude/skills/phone-adapt/SKILL.md new file mode 100644 index 0000000..f2f520a --- /dev/null +++ b/.claude/skills/phone-adapt/SKILL.md @@ -0,0 +1,307 @@ +--- +name: phone-adapt +description: 参照 documents/5.5寸手持端设计文件/ 的 HTML 设计稿,把 AirLogistics 指定页面适配成 5.5 寸手机竖屏版本——浏览器渲染设计稿逐态截图分析、复用 Phone* 公共组件写手机布局、复用现有 ViewModel 与接口、保证 Pad 端零改动,最后走完整登录流程实机联调并与设计稿逐项核对 UI。当用户说"适配手机端/做手机版/手持端适配"、点名某个设计稿入口(如"02 出库交接"、"做一下出港计重")、要求"按设计稿还原页面"、或提到 5.5 寸/手机版/双形态时,都要使用本 skill,即使没明说"适配"二字。 +--- + +# 5.5 寸手持端页面适配 + +把一个已有的平板页面,按设计稿适配出手机竖屏版本。核心约束是**只重写布局,业务逻辑与接口全部复用**, +Pad 端行为必须零变化。 + +先读 `CLAUDE.md` 的「5.5 寸手持端(手机版)双形态适配规范」章节——那里是权威规范,本 skill 是执行流程。 + +## 前置确认 + +需要用户给出**目标页面**(设计稿目录名或业务名,如「02 出库交接」/「出港计重」)。 +其余自己查:`documents/5.5寸手持端设计文件/00_全部入口总览.html` 列出全部 11 个入口, +每个子目录下可能有多个 HTML(主页 + 详情 + 子页面),都要处理。 + +对应的平板页面靠业务名在 `module_gjc/gjj/gnc/gnj` 里搜 Activity。**注意认准首页菜单实际跳转的那个 +Activity**,同名旧版文件在这个仓库里很常见。 + +## 阶段 1:渲染设计稿,看图不看代码 + +设计稿是带 Tailwind + JS 的交互原型,一个 HTML 里常塞了列表页、Tab、详情页、筛选弹层,靠 JS 切换。 + +**只读 HTML 源码会出错**,必须渲染成图: + +```bash +S=.claude/skills/phone-adapt/scripts +# 1) 先看有哪些状态可切 +python3 $S/design_shots.py --html "documents/5.5寸手持端设计文件/02_出库交接/出库交接.html" --list + +# 2) 按 --list 结果逐态渲染(照抄 onclick 里的真实参数最省事) +python3 $S/design_shots.py --html "…/出库交接.html" --out /tmp/design \ + --state "01_列表:" \ + --state "02_已交接Tab:switchTab('shipped');" \ + --state "03_详情:showDetail({id:'PMC90901CZ',status:'已交接',rackNo:'076'});" \ + --state "04_筛选弹层:toggleFilterDrawer(true);" +``` + +然后用 Read 工具**逐张看图**,抽取:配色、字号、圆角、间距、图标、控件顺序、空态、 +各状态下哪些元素显示/隐藏(例如「已交接」Tab 下底部操作条整条消失)。 + +`--list` 输出里如果出现 `window.location.href='xxx.html'`,说明是多文件原型,那些文件同样要渲染。 + +**设计稿的 JS 可能有 bug,别照抄字段映射。** 出库交接的详情页就把「航班日期」绑成了重量、 +「备注」绑成了航班号(HTML 里 id 重复)。判断依据是**静态 label 文案**:label 写「航班日期」 +就绑 `fdate`,写「板型」就绑 `boardType`。label 是设计意图,JS 只是 demo 接线。 + +标签文案同理:卡片上写死的「IMP代码」是占位符,实际要绑 `bean.dgrCode` 的值。 + +## 阶段 2:对齐接口能力,不要臆想 + +把设计稿的每个筛选项/Tab/字段,对照平板 ViewModel 已有的字段: + +- **已有字段** → 直接复用(如出港移库的 Tab 直接绑既有 `moveState`) +- **平板没有的新维度**(新 Tab 状态、新下拉、新排序)→ **先查 api-doc MCP 拿权威参数名**, + 流程见 `references/api-doc.md`(连接方式 + 5 个工具 + 逐接口核对清单) + +**最高决策规则(用户 2026-07-29 拍板,此后不再逐次询问):业务改动完全以设计稿为准,不做妥协。** +接口不支持设计稿的某个维度时:UI 照设计稿完整实现(绑预留字段/占位,传参后端静默忽略无副作用), +缺口直接登记 `documents/后端接口对接问题清单.xlsx` 提修改需求,后端补齐后前端自动生效。 +仅当设计稿本身歧义(label 无法推断字段语义)才用 `AskUserQuestion` 问用户。 + +这一步不能猜,也**不要拿「实测不生效」当结论**。出库交接第一版猜了 `hoState`,实测两个 Tab +返回同一批数据,一度误判为「后端不支持」;后查 api-doc 才发现正确参数名是 `handoverState`—— +后端对不认识的参数是**静默忽略**,不报错,实测只能证明「这个名字不对」,证不出「后端没这能力」。 + +「参数是否真的生效」的 A/B 验证优先走 Proxyman MCP(`create_compose_http_from_flow` 复制 +真实请求改参重发,token 自动带上),分工与流程见 `references/proxyman.md`: +**api-doc 管契约(该有什么),Proxyman 管事实(实际发/回了什么)**。 + +核对完把结论落两处(模板与格式见 `references/api-doc.md`): + +- 后端真实缺口(缺入参/缺出参/数据不回填)→ 记入 `documents/后端接口对接问题清单.xlsx`, + 这是与后端对接的正式文档,按「模块 → 页面 → 接口定义 → 接口地址」索引,**每个页面适配完都要维护** +- 参数名/字段绑定错误这类前端问题 → 直接改代码 + +阶段 5 仍要用真实请求体验证新参数确实生效;没生效就在清单文档与交付说明里如实标注, +不要让它看起来是通的。 + +### 开工前对齐方案 + +分析完先把结论摊给用户,避免闷头做错方向。一次说清,别挤牙膏: + +``` +📱 页面适配方案:<页面名> + +设计稿:documents/5.5寸手持端设计文件/<目录>/(N 个 HTML:主页 / 详情 / …) +平板页面:XxxActivity + XxxViewModel(module_xxx) + +手机版结构: + 搜索行:<字段>(含扫码/筛选按钮) + 状态 Tab: / → 绑定 <字段名> + 列表卡片:<字段…>,两种形态(<形态A> / <形态B>) + 底部操作条:全选 + 已选统计 + [<按钮>] + 筛选弹层:<字段1> / <字段2> / <字段3> + 二级页:<详情页名>(新增 Activity / 复用已有) + +复用组件:PhoneSearchBar、PhoneStatusTab、PhoneStatBox…(无需新增 / 需新增 XXX) + +接口:列表 <接口> 复用;新增参数 <名>(待你确认);新增字典 +Pad 端影响:无(新字段默认空、额外请求由手机开关控制) + +设计稿与平板的差异:<如手机版无「配运」按钮> + +确认后开始编码。 +``` + +## 阶段 3:写布局,复用公共组件 + +`module_base/.../ui/weight/phone/` 已有 7 个组件,**优先复用,不要重写 XML**。 +组件清单、属性表与用法见 `references/components.md`(写布局前读一遍)。 + +改造步骤: + +1. 平板布局 `git mv` 到 `res/layout-sw600dp/`(item 布局同理) +2. 在 `res/layout/` 下建**同名**文件写手机布局 +3. Kotlin 侧 `layoutId()` / `itemLayoutId` **一律不动**,不做设备分支 + +三条硬约束(违反会编译失败或运行时崩溃,细节见 `references/pad-safety.md`): + +| 约束 | 说明 | +|------|------| +| 两变体 `` 完全一致 | 手机变体新增 `activity` 变量时,平板变体也要补上(写注释说明平板不用它) | +| 共用 id 双方都要有 | ViewHolder/Activity 引用的 id(`srl`/`rv`/`iv_icon` 等)两边都保留 | +| 单侧独有 id 自动 `@Nullable` | 如手机独有的 `ll_detail`,ViewHolder 里必须判空调用 | + +**一套 item 布局覆盖多种卡片形态**,不要为「待交接/已交接」建两个布局。按 bean 状态切 +visibility 即可(Bean 上加个只读计算属性如 `isHandovered` 让 XML 更干净;计算属性不参与 Gson +序列化,安全)。 + +## 阶段 4:接业务,且不碰 Pad + +手机端新增的查询维度用这个模式,能做到平板端请求次数与行为零变化: + +```kotlin +// ViewModel:新字段默认空值,getData() 里仅非空才进请求 +val handoverState = MutableLiveData("") // 平板布局无对应控件 → 恒为空 → 不进请求 +val filterParams = mapOf( + /* 原有字段… */ + "handoverState" to handoverState.value?.ifEmpty { null }, // 参数名以 api-doc 为准 +) + +// 手机专属的额外请求(Tab 角标计数、新下拉字典)用内部开关控制 +private var phoneExtrasEnabled = false +fun initPhoneExtras() { phoneExtrasEnabled = true; handoverState.value = "0"; /* 加载字典 */ } +``` + +```kotlin +// Activity:只有手机形态才开启,平板不调用 +if (DeviceUtil.isPhone()) viewModel.initPhoneExtras() +``` + +设备判断只用于**数据默认值与额外请求**,**绝不用于选布局**(选布局靠资源限定符)。 + +纯 UI 交互(弹层显隐、Tab 切换)写在 Activity,用 `ObservableBoolean` 暴露给 XML,不下沉到 ViewModel。 + +设计稿若少了平板上的某个按钮(如出库交接手机版只有「交接」没有「配运」),按设计稿做, +但要在交付说明里点出来这个功能在手机端不可达,让用户决定。 + +新增手机专属页面(详情页等)记得:`app/src/main/AndroidManifest.xml` 注册 + +`app/src/debug/AndroidManifest.xml` 追加 `exported="true"` 便于直启调试。 + +注册时 `screenOrientation` **必须写 `unspecified`**: + +```xml + +``` + +- 写死 `userLandscape` → 手机端「先横屏、约 1 秒后转竖屏」闪一下(方向在建窗口时就定了,代码再纠正已晚) +- 用 `@integer/xxx` 资源引用 → **更糟**:系统解析 Manifest 不套用设备限定符,平板会取到默认(竖屏)值, + 先竖后横,且 AutoSize 随之选错基准导致 UI 放大约 1.6 倍 + +`unspecified` 让系统按设备当前物理方向建窗口,两端起始方向本就正确, +`BaseActivity` 的形态锁定不会产生二次纠正。 + +### ⚠️ 别忘了加手机端首页入口 + +手机端首页是 `module_p` 的 `PDAEnterActivity`,「国际」Tab 的菜单在 +`module_p/.../ui/enter/gj/GjViewModel.kt`(国内在 `gn/GnViewModel.kt`)。 +**菜单是手写清单,不是接口下发**;权限串只做过滤(`authList.contains(bean.key)`)。 + +适配完页面不回来加一行,手机上就**根本没有入口**——出库交接第一版就漏了这步, +只能靠 adb 直启才能进去。加法: + +```kotlin +ActionBean( + "出港移库", + R.drawable.gjc_yi_ku_icon, // 图标复用 Pad 同款,复制 PNG 到 module_p/res/drawable-xxhdpi/ + Constant.AuthName.GjcYiKuListActivity, // 权限串与 Pad 端菜单一致 + ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE // 填了 route 就走通用跳转,无需 when 分支 +), +``` + +权限串和路由**都以 Pad 端 `app/.../HomeFragment.kt` 里该权限对应的 `ARouter.build(...)` 为准** +(同一业务常有新旧两个 Activity,Pad 菜单实际跳的才是要适配的那个)。 +`module_p` 只依赖 `module_base`,看不到业务模块,所以跨模块必须走 ARouter、图标必须复制一份。 + +菜单没出现时先分清是「没权限」还是「没加清单」: + +```bash +adb -s shell "run-as com.lukouguoji.aerologic \ + cat /data/data/com.lukouguoji.aerologic/shared_prefs/data.xml" | tr ',' '\n' | grep -oE "App[A-Za-z]+" | sort -u +``` + +## 阶段 5:端到端实机验证 + +先构建,再双端跑。命令与排障细节见 `references/verification.md`。 + +```bash +./gradlew assembleDebug 2>&1 | grep -E "^(BUILD|FAILURE)|error:" +``` + +构建过后核对 DataBinding 变体是否如预期(单侧 id 应为 `@Nullable`): + +```bash +grep -B3 "llDetail" module_gjc/build/generated/data_binding_base_class_source_out/debug/out/\ +com/lukouguoji/gjc/databinding/ItemXxxBinding.java +``` + +**手机端**(真实数据链路,从登录开始): + +```bash +P=.claude/skills/phone-adapt/scripts/ui_probe.sh +adb -s install -r -d app/build/outputs/apk/debug/app-debug.apk +# 走登录 → 底部「国际」Tab → 点新加的菜单项进入(顺便验证入口加对了) +# 同名文字多处时 tap-text 会提示命中数,用第 4 个参数指定序号(如底部 Tab 的「国际」) +# 若账号确实没有该权限,改用冷启动直启(见 verification.md) +$P form # 应打印 PHONE(sw=411dp…) +$P wait-text "待交接" # 点击前先等页面就绪:加载弹窗会吞掉点击 +$P tap-text "已交接" # 按文字点击,比手算坐标可靠 +$P texts # 用文字变化断言点击确实生效(如底部条应消失) +$P shot /tmp/p1.png +$P crash # 必须为 0 +``` + +核对请求/响应体优先用 Proxyman MCP(`filter_flows` + `get_flow_detail`,比 `req` 的 +logcat 抓包完整可靠;模拟器代理一行命令接入,见 `references/proxyman.md`); +Proxyman 不可用时兜底 `$P req pageQuery`。 + +逐项走查:列表/卡片两种形态、详情页、筛选弹层(重置+确认)、Tab 切换、单选与全选联动、 +下拉真实取数、扫码入口。每步 `crash` 都要为 0。 + +点完**一定要断言效果**(`texts` 或截图),不要假设点中了——加载弹窗盖屏时 `input tap` 会被静默吞掉。 + +手机端还要确认方向没闪屏(冷启动采样 `mRotation` 应恒为 0),命令见 `references/verification.md`。 + +**Pad 端回归**(必做):同样安装到平板模拟器打开同一页面截图,与改造前逐项对比—— +搜索条数量、默认值、按钮、底部统计都应一致。改过 `module_base` 公共组件时这步尤其不能省。 + +## 阶段 6:与设计稿二次核对 + +把阶段 1 的设计稿截图和阶段 5 的实机截图放在一起,用 Read 工具对照看,逐项核对: + +配色 / 字号 / 字重 / 圆角 / 内外间距 / 图标形状与大小 / 控件顺序 / 选中态 / 状态标签配色 / +**空值兜底**(真实数据空字段很常见——出库交接就出现过「库位」为空时橙色高亮标签只剩一个孤零零的 +小色块,需要空值时不套标签底)。 + +服务端当天没数据时(列表空)先放宽条件重查;仍为空、或要覆盖真实数据凑不出的形态 +(空字段、超长文本、罕见状态标签),**首选 Proxyman Map Local 造 mock**——零代码改动、 +零重构建,见 `references/proxyman.md` 场景 3(2026-07-31 出港查询用它一次验完 +已离港/已入库/未入库三种卡片形态)。验证完 `delete_rule` + 清模拟器代理。 + +Proxyman 不可用时才退回老办法:Activity 里临时注入样例 Bean 截图比对: + +```kotlin +// import com.lukouguoji.module_base.ktx.commonAdapter +binding.rv.postDelayed({ + binding.rv.commonAdapter()?.refresh(listOf(/* 覆盖每种卡片形态各一条 */)) +}, 2500) // 等首次请求结束,否则会被真实结果覆盖 +``` + +**核对完必须删掉**,删完重新构建并 grep 确认无残留。 + +## 收尾:文档与交付说明 + +代码之外还要落四处文档,否则下一个人会重复踩坑: + +- 新增了公共组件/资源 → 更新 `CLAUDE.md`「手机版公共组件库」表 + `references/components.md` +- 踩到新坑(崩溃、静默失效、环境问题)→ 写进 `CLAUDE.md`「常见编译错误速查」和本 skill 的 + 对应 reference,把**定位手法**也写上,不只写结论 +- 接口有后端缺口(阶段 2 / 阶段 5 发现的)→ 维护 `documents/后端接口对接问题清单.xlsx`; + 已确认的字段语义追加到 `references/api-doc.md` 的「已核对过的语义」,别让下个页面再查一遍 +- `CHANGELOG.md` 追加条目(新增 / 改进 / 修复 / 验证 四段),记忆文件 + `phone-5.5inch-adaptation.md` 同步 + +向用户汇报时明确写出这四块: + +1. 新增/修改的文件清单(布局、组件、Activity/ViewModel/Bean) +2. Pad 端零破坏的具体做法 + 回归结论 +3. 复用与新增的公共组件 +4. **待确认事项**:后端未生效的参数、自己推断的业务规则(如 IMP 代码红/绿的判定)、 + 设计稿与平板功能的差异。如实说,别含糊。 + +## 参考文件 + +- `references/api-doc.md` — api-doc MCP 连接与逐接口核对流程、后端缺口记录规范、已核对字段语义(阶段 2 前读) +- `references/proxyman.md` — Proxyman MCP 抓包/直调 A/B/Map Local mock 的接入与四个场景(阶段 2、5、6 用;与 api-doc 的分工:契约 vs 事实) +- `references/components.md` — 7 个手机版公共组件的属性表、用法与封装新组件的规矩(写布局前读) +- `references/pad-safety.md` — 布局变体约束、Pad 零破坏模式、易崩点(写代码前读) +- `references/verification.md` — 双端 adb 命令、登录与直启、已知环境坑(验证前读) +- `scripts/design_shots.py` — 设计稿逐态渲染截图 +- `scripts/ui_probe.sh` — 实机取文字/按文字点击/等待/截图/抓请求体/查崩溃 diff --git a/.claude/skills/phone-adapt/references/api-doc.md b/.claude/skills/phone-adapt/references/api-doc.md new file mode 100644 index 0000000..187b94c --- /dev/null +++ b/.claude/skills/phone-adapt/references/api-doc.md @@ -0,0 +1,187 @@ +# api-doc MCP:接口定义核对流程(阶段 2 必读) + +适配页面前,**所有用到的接口都要在 api-doc(AgentFox)里核对一遍入参/出参**, +不要靠平板代码或猜测推断参数名——出库交接就因为猜了 `hoState`(实际是 `handoverState`) +白白折腾了一轮实机排查。 + +## 连接方式 + +项目 `.mcp.json` 已配置 `api-doc` 远程 HTTP MCP。若会话内 MCP 工具可直接调用 +(`mcp__api-doc__*`),直接用;**若工具没有注入会话**(ToolSearch 搜不到),用 curl 走 +MCP 协议手工调用,三步: + +```bash +URL=$(python3 -c "import json;print(json.load(open('.mcp.json'))['mcpServers']['api-doc']['url'])") +AUTH="Authorization: $(python3 -c "import json;print(json.load(open('.mcp.json'))['mcpServers']['api-doc']['headers']['Authorization'])")" + +# 1) initialize:从响应头 mcp-session-id 取会话 id +curl -s -D /tmp/mcp_headers.txt -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-code","version":"1.0"}}}' > /dev/null +SID=$(grep -i 'mcp-session-id' /tmp/mcp_headers.txt | tr -d '\r' | awk '{print $2}') + +# 2) initialized 通知(必须发,否则 tools/* 报 session 未初始化) +curl -s -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" -H "mcp-session-id: $SID" \ + -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null + +# 3) tools/call(响应是 SSE,`sed 's/^data: //'` 后按 JSON 解析) +curl -s -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" -H "mcp-session-id: $SID" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_endpoints","arguments":{"keyword":"IntExpMove"}}}' +``` + +## 可用工具(5 个) + +| 工具 | 用途 | +|------|------| +| `get_project_overview` | 项目概览 + 模块清单(首次调用) | +| `list_modules` | 全部模块含描述 | +| `list_endpoints` | 单模块内接口列表(分页,`moduleId` + `cursor`) | +| `search_endpoints` | 按关键字搜接口(`keyword` 用控制器前缀最准,如 `IntExpOutHandover`) | +| `get_endpoint_detail` | **核心**:单接口完整契约,schema 已拍平成字段表(`endpointId`) | + +## 核对清单(每个页面做一遍) + +对 ViewModel 里每个 `NetApply.api.xxx` 调用: + +1. 从 `module_base/.../http/net/Api.kt` 拿到 `@POST` 路径 → `search_endpoints` 找到接口 → `get_endpoint_detail` +2. **入参**:前端传的每个 key 必须能在文档字段表里找到**同名字段**;找不到 = 传了也白传(后端静默忽略,不报错) +3. **类型**:状态类字段(`moveState`/`handoverState`/`outState`)文档均为 integer,前端传 `"0"` 字符串靠 + Jackson 隐式转换(实测可用);新代码尽量传数字 +4. **出参**:设计稿卡片/详情页的每个展示字段,都要能对上出参字段;对不上就是后端缺口 +5. **结论落两处**: + - 后端缺口(缺入参/缺出参/数据不回填)→ 记入 `documents/后端接口对接问题清单.xlsx` + (按「模块 → 页面 → 接口定义 → 接口地址」格式,含前端现状与期望,状态标 ⏳/🔍) + - 前端可自行解决的(参数名写错、字段绑错)→ 直接改代码,别记成后端问题 + +## 已核对过的语义(别再猜) + +- `IntExpOutHandover`:交接状态入参 = `handoverState`(不是 `hoState`),**2026-07-29 内网实测过滤生效** + (传 0/1 分别返回纯净的待/已交接集合,字符串 `"0"`/`"1"` 同样被接受);`ldId` 是**组装人**不是交接人; + 交接人姓名出参 `hoUserName`,编码 `hoId`;查询入参**没有**交接人筛选(后端缺口 #4) +- `IntExpOutHandover/pageQueryTotal`:**出参恒 0**(任何条件下 totalPc/totalWeight/wbNumber 均为 0, + 与同条件 pageQuery 不一致,后端缺口 #6)——依赖它的 Tab 角标/底部统计显示 0 不是前端问题 +- `IntExpMove`:出参无航班字段(缺口 #1)、无操作人姓名(缺口 #2);`opId`/`opdate`/`businessType` + 文档有定义但实测恒 null(缺口 #3);`pageQueryTotal` 正常(与 pageQuery 口径一致,`moveState` 过滤生效) +- `typeCode/getSpecialCodeByParam`:`flag=1` 国际,`ieFlag` 必须传空串(传 `"E"` 返回空数组) +- `eqm/uld`(ULD信息,6 个端点):查询入参仅 `uld`/`status`/`uldSuffix`(uldSuffix 名义是 + "uld后两位",实际就是**按所属航司筛选**,配 `DictUtils.getIntCarrierList`);`status` 仅两态 + 0 正常/1 故障(三态需求=缺口 #7);无「来源」字段(#8)、无 `checkInDate`/`ifNo` 筛选入参(#9)、 + 无批量删除(#10,`deleteUld` 是 `@Query` 单条);出参 `ifNo`/`efNo`/`checkInDate`/`checkOutDate` + 文档无描述,按命名推断进/出港航班号与出入库时间(#11 待确认) +- `IntExpArrive`(出港运抵,2026-07-30 内网实测):`declareStatus`(申报状态)只在出参, + `pageQuery`/`pageQueryTotal` 入参均无此字段(缺口 #20,待运抵/已运抵 Tab 靠客户端拆分); + `declareStatus`/`GjcHaWb.arrivalStatus` 三色编码:`01`=正常(绿)、`W`=靛蓝态、其余非空=琥珀异常态; + `haWbList[].response`/`arrivalOpDate` 是回执文本与对应时间,实测确认真实存在非空数据; + 分单补充收发货人信息**后端完全没有对应接口**(`search_endpoints` 对 `IntExpArrive`/`HaWb` + 关键字均搜不到分单更新接口,只有国际进港的 `IntImpAirManifest/complete`), + 实机调用拟定路径 `IntExpArrive/haWb/supplement` 确认 404(缺口 #19) +- `IntExpSearch`(出港查询,2026-07-31 内网实测):查询入参 `outState`/`fno`/`dest`/`spCode`/`awbType`/ + `businessType`/`goodsCn`/`beginDate`/`endDate`/`agentCode`/`wbNo` 全部真实生效(平板既有口径); + 文档定义的 `isFclose`(筛选航班是否关闭)**pageQuery 与 pageQueryTotal 均静默忽略**(A/B 对照 + 全量 72 票与单日两组,加参前后结果完全一致,缺口 #21);`pageQueryTotal` 出参无已入库/已离港 + 分项计数(#21);`detail` 出参 Map 无 `lockStatus`(锁定状态)与计费重量字段(#22); + 详情「入库件数/入库重量」= `warehouseList` 各批次 pc/weight 求和(mock 与实测均吻合); + 卡片状态推断口径:`fclose` 非空=已离港、否则 `opDate` 非空=已入库; + 特码字典沿用 `DictUtils.getSpecialCodeList(flag=1, ieFlag="")`(ieFlag 必须空串) +- `IntExpStorageUse`(出港仓库,2026-07-31 文档核对,内网 A/B 未测——当日开发机不在内网): + `pageQuery`/`pageQueryTotal` 同一套入参 schema:`fdate`/`fno`/`wbNo`/`likeNo`/`spCode`/`dest`/ + `agentCode`/`location`/`clearNormal`/`checkIn`/`reviewStatus`/`dep`/`hno`/`prefix`;设计稿筛选的 + **运单类型/业务类型/品名(中) 均无入参**(`awbType`/`businessType`/`goodsCn`,缺口 #23); + 手机三格统计按 `clearNormal=0/1` 各调一次 `pageQueryTotal` 取 `wbNumber`,该过滤**是否真实生效 + 待内网 A/B**(缺口 #24,参照 isFclose 有定义但被忽略的先例);出参 `clearNormal` 0=未清仓/1=已清仓 + (卡片标签与「清仓」按钮置灰按 ==\"1\" 判定);详情复用 `IntExpSearch/detail`(maWbId Query 参数) +- `IntImpStorage`(进港仓库,2026-07-31 文档核对;**注意前端 Api.kt 路径前缀是 `IntImpStorage` + 不是 `IntImpStorageUse`**,api-doc 按后者搜不到):`pageQuery`/`pageQueryTotal` 同一套入参: + `fdate`/`fno`/`wbNo`/`hno`/`fdep`(始发站)/`location`/`clearNormal`/`fid`;比出港仓库还少 + `agentCode`/`spCode`/`dest`——设计稿筛选的代理/特码/运单类型/业务类型/品名(中) **5 项均无入参** + (缺口 #25);分项统计与 clearNormal 过滤待实测同 #24(记为 #26);详情复用 + `IntImpSearch/detail`(Body 传 `prefix`+`no`,出参 `data.maWb` 无 schema,字段 key 以平板绑定为准: + `awbPc`/`awbWeight`/`cashWeight`/`inPc`/`inWeight`/`lockState`(0/1 需转中文)/`mftStatus` 原始舱单/ + `tallyStatus` 理货报告/`command` 海关放行/`by1` 承运人/`dlvTime` 出库时间;`dep`/`dest1`/`dest`/ + `unNumber`/`opDate` 为按出港推断,待实测 #27) +- `IntImpPickUpDlv`(提取出库,2026-07-31 文档核对):`pageQuery` 入参含 **`outState` + (0 未出库/1 已出库)**——待出库/已出库 Tab 直接真实过滤(抓包证实携带,效果待内网 A/B #29); + 另有 `fno`/`dest`/`origin`/`carrier`/`awbType`/`businessType`/`goods`/`likeNo`/`pkId`/ + `beginDate`/`endDate`+`dateType`(0 入库/1 离港)/`chargeFlag`(0 未提取/1 已提取); + **无 `fdate`(航班日期)入参**(缺口 #28);出参无车牌/目的港/业务类型名称(#28); + 运单号筛选参数名是 **`no`**(平板既有口径);`chargeName`/`chargeTime` = 柜台办理人/缴费(提取)时间, + `dlvTime` = 出库时间,`dlvName` 按提货人语义推断作「出库人」展示(待确认 #29); + 确认出库接口 `pickUpOut` 文档注明**需将全字段传至后端**(平板 `selectedItems.toRequestBody()` 已满足) +- `IntImpSearch`(进港查询,2026-07-31 文档核对):`pageQuery`/`pageQueryTotal` 分页参数名是 + **`page`/`limit`**(不是 pageNum/pageSize);手机筛选 9 项入参全部真实存在:`beginDate`/`endDate`/ + `fno`/`agentCode`/`spCode`/`origin`(始发站)/`awbType`/`businessType`/`goods`(品名,**不是 goodsCn**); + `outState`(0 未出库/1 已出库)文档为 integer,前端传字符串靠 Jackson 隐式转换;`pageQueryTotal` + 出参无已入库/已出库分项计数(缺口 #30,手机三格统计为客户端计数口径:dlvTime 非空=已出库、 + inDate 非空=已入库);出参含 `agentName`(中文名)+ `agentCode`,agentName 可为 null +- 「进港移库」(设计稿 11)**归属更正(2026-08-03 用户确认):属国内进港移库 `DomImpMove`**, + 不是国际进港移库——「后端模块缺失」的旧结论(原缺口 #31)作废,原按 IntExpMove 约定预接的 + IntImpMove 前端实现已整体删除。溯源:api-doc 53 模块中移库仅 `DomImpMove`(国内进港,8 接口)与 + `IntExpMove`(国际出港,4 接口),移库按转关业务前缀划分(DomImpMove 处理 `CI**`、IntExpMove + 处理 `IO**`),设计稿详情卡的「国际进港」红标签实为 `awbType` 值(如「国际进港(国内中转)」), + 不代表页面归属 +- `DomImpMove`(国内进港移库,2026-08-03 内网实测):手机 Tab 双端点——待移库 `search` / + 已移库 `searchMoved`(同一套入参 schema,分页参数名 **`page`/`limit`**);入参 A/B 实测: + **`wbNo`、`awbType`(值为 CICO/CIII/CIIO 转关码)过滤生效;`fdate`/`fno`/`spCode` 文档有定义但 + 被静默忽略**(缺口 #32);`searchMoved`/`detail` 出参 **`opId`/`opDate` 恒 null** 且无操作人姓名 + 字段(缺口 #33,移库人/移库时间显示"-"),`moveId` 形如 `CICO240617155016` 疑似内嵌移库时间戳但无 + 文档语义;`searchMoved` 出参**无 pic 三字段**(search 有)——拍照上传页统一先调 `GET detail` + 回填已有照片(#34 前端已规避);照片提交走 `modify` 最小体 + `{mawbId, remark, picNumber, pic, originalPic}`(平板移交编辑页同链路,removeEmptyOrNull); + 批量移库 `move` 入参 `{fid:"0", ids:[mawbId]}`;国内进港特码字典 + `DictUtils.getSpecialCodeList(flag=0, ieFlag="I")` 实测取数正常 +- `IntExpCheckIn`(出港计重,2026-07-29 内网实测):`likeNo` **并非任意模糊**——按 8 位 `no` + 匹配(11 位 wbNo 或部分号均 0 条),11 位全号必须走 `wbNo`;`checkIn`/`checkInList` + 文档有定义但 `pageQuery`/`pageQueryTotal` **均静默忽略**(缺口 #12,待计重/计重中计数依赖); + 查询入参无通道号/承运人/运单类型(#13)、`checked/*` 无收运时间范围(#14);`checked/pageQuery` + 出参无通道号(#15)但有 `userName`(计重人中文名);`listRecordByWh` 出参计重人仅 `opId`(#16); + `preArrive` 成功后 `pageQuery` 的 `arriveFlag` 不回填(#17);`splitCheckIn`/`completeCheckIn` + 无 `palletNumber`/`carWeight` 入参(#18,手机版托盘数量/自重按设计稿传预留参数); + 运单类型字典:`DictUtils.getWaybillTypeList2(ieFlag="E", type="IO")` + +## 内网直调验证技巧(比驱动 UI 快得多) + +登录态 token 存在 SharedPreference 里,取出后可以直接 curl 后端做行为验证 +(**注意:不要把 token 打印到输出里**,落临时文件引用): + +```bash +adb -s shell "run-as com.lukouguoji.aerologic cat \ + /data/data/com.lukouguoji.aerologic/shared_prefs/data.xml" \ + | grep -o '[^<]*' | sed 's/.*>//' > /tmp/air_token + +curl -s -X POST "http://192.168.1.250:8093/IntExpOutHandover/pageQuery" \ + -H "Authorization: $(cat /tmp/air_token)" -H "Content-Type: application/json" \ + -d '{"pageNum":1,"pageSize":50,"handoverState":0}' +``` + +用它做「参数是否生效」的 A/B 对比(同条件只改一个参数看 total 变化), +再回 App UI 走一遍确认端到端链路。UI 里的滚轮日期选择器可用 +`input swipe`(慢速 500ms、每次 3 格 ≈ 396px)驱动,滑完截图校准。 + +## 2026-10-09 后端反馈联调后的最新口径(与上文冲突时以本节为准) + +后端在问题清单补「解决方案」后逐项内网实测的结论。**通用规则:后端对手机端新增筛选维度一律答复 +「与平板端查询参数保持一致」——手机筛选只放平板已用的入参;文档里定义了但平板没用的入参, +多数实测也被忽略(agentCode/spCode/dest/opId 等),别再按文档加筛选。** + +- **统一统计 DTO**:各 `*/pageQueryTotal` 出参现含 `uldNumber`/`storedNumber`/`departedNumber`/ + `notClearNumber`/`clearNumber`/`waitCheckInNumber`/`checkingNumber`/`flightNumber`(`ManifestTotalDto` + 已补齐)。分项统计一律取出参,**不要再按状态入参多调几次 total** +- `IntExpOutHandover/pageQueryTotal` 已修复(与列表一致);Tab 角标取 `uldNumber`(板箱数);无交接人入参 +- `IntExpMove/pageQuery` 出参新增 `flight`("20260924/MU2222")/`businessName`/`opUserName`;已移库 `opId`/`opdate` 已回填 +- `eqm/uld`:状态维持两态;`source`(空运/陆运) 出参与保存入参支持、**查询入参不支持**;入库时间 = `inStoreTime` + (date-time,**只传日期 400**)、所在港 = `nowPort`;批删 `deleteUldBatch`(Body 为字符串数组); + ⚠️ `pageQuery` 带 `uld` 即 500(#35);`saveUld` 缺 `maxVolume`/`maxWeight` 即 500(#36) +- `IntExpCheckIn`:`listRecordByWh` 出参 `opUserName`/`trayNumber`/`trayTotalWeight`; + `checked/pageQuery` 出参 `passageWay`;`splitCheckIn`/`completeCheckIn` 托盘入参 = `trayNumber` + + `trayTotalWeight`(数量×自重);`arriveFlag` 已回填("0" 正常/"1" 提前运抵); + ⚠️ `pageQuery` 的 `checkIn` 过滤仍被忽略(#40,统计用分项出参);`checked/pageQuery` 的 `dest`/`opId` 被忽略 +- `IntExpArrive`:入参 `arrivalStatus`("0" 未运抵/"1" 已运抵)服务端过滤生效;`agentCode` 被忽略;补充信息页已删除 +- `IntExpSearch`:**`beginDate`/`endDate` 只有携带 `dateState`(0 入库/1 离港)才生效**;`outState` 生效 +- `IntExpStorageUse`:仅 `fdate`/`fno`/`wbNo`/`location`/`clearNormal` 有意义;⚠️ 列表 total 与统计 wbNumber 不一致(#38) +- `IntImpStorage`:⚠️ `pageQuery` 当前**所有**过滤条件均失效(#37),`pageQueryTotal` 正常 +- `IntImpSearch/detail`:按平板展示 航程 `range`(无经停站)/`unNumber`/`lockState`/出库时间 `dlvTime` +- `IntImpPickUpDlv`:运单号入参用 `wbNo`;出参新增 `refUserName`(出库人姓名,`dlvName` 是提货人);测试环境暂无数据 +- `DomImpMove`:`fdate`/`fno`/`spCode` 已生效;`searchMoved` 已回填 `opId`/`opDate`/`opUserName`; + 新增 `GET movedDetail?moveId=`(含 pic 三字段);⚠️ 已移库运单无 `mawbId`,`modify` 无法补传照片(#39) diff --git a/.claude/skills/phone-adapt/references/components.md b/.claude/skills/phone-adapt/references/components.md new file mode 100644 index 0000000..75a4710 --- /dev/null +++ b/.claude/skills/phone-adapt/references/components.md @@ -0,0 +1,140 @@ +# 手机版公共组件(`module_base/.../ui/weight/phone/`) + +7 个组件覆盖设计稿反复出现的部件。**先复用,实在没有再新增**——每个新组件都要维护、都要踩一遍 +状态恢复的坑,能少一个是一个。 + +所有属性都是**无命名空间写法**(与平板端 `PadSearchLayout` 一致),DataBinding 适配器集中在 +`PhoneWidgetKtx.kt`。同名属性(`hint`/`value`/`title`/`list`)按控件类型解析,与 Pad 系组件不冲突。 + +## 组件与属性 + +| 组件 | 用途 | 属性 | +|------|------|------| +| `PhoneSearchBar` | 顶部搜索行:搜索框 + 扫码 + 圆形筛选按钮 | `hint`、`value`(双向)、`showScan`、`showFilter`、`upperCase`、`setOnScanClickListener`、`setOnFilterClickListener`、`setRefreshCallBack` | +| `PhoneStatusTab` | 状态 Tab 条(文字 + 角标 + 选中下划线) | `tabs`(List\)、`counts`(List\)、`value`(双向)、`setOnTabChanged` | +| `PhoneDataLayout` | 表单/筛选项,三形态 | `title`、`hint`、`type`(`PhoneDataLayoutType.INPUT`/`DATE`/`SPINNER`)、`value`(双向)、`list`、`required`、`enable`、`setRefreshCallBack` | +| `PhoneFilterPanel` | 底部筛选弹层(遮罩 + 面板 + 重置/确认) | `title`、`panelVisible`、`setOnResetClick`、`setOnConfirmClick`、`setOnDismissClick` | +| `PhoneBottomBar` | 底部操作条(全选 + 已选统计 + 主按钮) | `allChecked`、`countText`、`actionText`、`setOnAllCheckClick`、`setOnActionClick` | +| `PhoneStatBox` | 卡片内 2~4 列统计框,未传 label 的列自动隐藏 | `label1..4`、`value1..4` | +| `PhoneKvItem` | 详情页字段项(灰标签 + 数值) | `title`、`value`、`tag`(true → 高亮标签)、`tagColor`("orange" 默认 / "green" 放行正常态);占位符 "-"/"--" 不套标签底 | +| `PhoneFormRow` | 行式表单项(左标签 minWidth 110dp + 右值 + 1px 分隔线),覆盖新增/修改/详情三态表单 | `title`、`hint`、`type`(`PhoneFormRowType.INPUT`/`SELECT`/`DATE`/`TEXT`)、`value`(双向)、`list`(SELECT 选项)、`required`、`enable`、`numeric`、`setRefreshCallBack` | + +`PhoneFormRow` 要点:SELECT 内置 XPopup 底部选择面板(点击整行弹出,无需页面代码);TEXT 为只读右对齐、 +空值自动占位 "--"(详情态);type 用三元按 `pageType` 切换即可一套布局覆盖三态页面(参考 +`app/res/layout/activity_uld_edit.xml`)。`PhoneBottomBar` 的 `actionDanger="@{true}"` 切浅红删除按钮。 + +`PhoneFilterPanel` 内容插槽已内置 ScrollView(2026-07-31 加):筛选项少时行为不变; +筛选项多到超屏(出港查询 9 项)时内容区收缩滚动,底部「重置/确认」按钮始终可见—— +此前无滚动时按钮会被挤出屏幕外且毫无报错,只表现为"点不到确认"。 + +⚠️ 两个已踩坑: +- **FAB 盖住弹层**:浮动按钮带 elevation 时会浮在 `PhoneFilterPanel` 之上,弹层实例要加 + `android:elevation="8dp"`(> FAB 的 6dp) +- **Spinner 选择回调是异步的**:`onItemSelected` 由系统 post,自动化测试里 tap 选项后立即 tap「确认」 + 可能抢在回值之前发出请求(表现为 UI 已显示选中但请求参数为 null)——脚本里选完要 `sleep 1` 以上; + 真人操作间隔足够,不受影响 + +`KeyValue` 是 `dev.utils.app.info.KeyValue`,字段是 **`key`(显示文案)+ `value`(业务值)**, +不是 `name`。`PhoneStatusTab` 的 `tabs` 就按 `KeyValue("待交接","0")` 传。 + +## PhoneFilterPanel 的内容插槽 + +筛选项直接写在标签内,组件 `onFinishInflate` 会自动把它们移进面板中部并施加 20dp 项间距, +使用方不用关心内部层级、也不用逐个写 `marginTop`: + +```xml + + + + + +``` + +记得在 `` 里 ``。 + +## ⚠️ 新增复合组件必做:屏蔽子控件状态恢复 + +同一页面放多个同类组件时(比如筛选弹层里 3 个 `PhoneDataLayout`),它们的内部子控件 +(`et`/`tv`/`spinner`…)**共用同一套 id**。系统按 id 保存/恢复视图状态会跨实例串档。 + +实际后果(出库交接踩过):DATE 项的默认日期被邻近输入框恢复的空文本覆盖, +`EditText.setText("")` 触发 `doOnTextChanged`,再经双向绑定把 ViewModel 的航班日期也写成空—— +**筛选条件静默丢失,界面上只表现为"日期没了",极难联想到状态恢复**。 + +所以每个含内部 id 的组件都要写: + +```kotlin +override fun dispatchSaveInstanceState(container: SparseArray) = + dispatchFreezeSelfOnly(container) + +override fun dispatchRestoreInstanceState(container: SparseArray) = + dispatchThawSelfOnly(container) +``` + +取值本来就由 ViewModel + DataBinding 持有,不需要视图级恢复,屏蔽掉没有副作用。 + +配套的第二道防线:多形态组件里,文本回写要限定形态,别让隐藏控件影响取值: + +```kotlin +et.doOnTextChanged { text, _, _, _ -> + if (type == PhoneDataLayoutType.INPUT) value = text.toString() +} +``` + +### 遇到"值被神秘清空"怎么定位 + +在 setter 里打一条带堆栈的日志,一次就能看到真凶: + +```kotlin +android.util.Log.d("DBG", "value '$field' -> '$v'", Throwable("trace")) +``` + +出库交接那次的栈是 `TextView.onRestoreInstanceState → EditText.setText → doOnTextChanged`, +指向非常明确。**排完记得删日志。** + +## 新增组件的写法约定 + +照现有 7 个组件的模式来,保持一致: + +1. 继承 `LinearLayout`/`FrameLayout`,`init` 里 `inflate(context, R.layout.layout_phone_xxx, this)` +2. 内部布局用 ``,避免多一层嵌套 +3. 属性做成 Kotlin `var` + setter 里立即生效;双向绑定值用 + `onChangeListener: InverseBindingListener?` +4. BindingAdapter 统一加到 `PhoneWidgetKtx.kt`,函数名带组件名前缀避免顶层函数重名 +5. 有互斥形态时,**`type` 要先于 `value` 生效**(把 type 放进同一个多属性 adapter 的第一个 + `?.let`),否则 value 会写进错误的子控件 +6. 加上上面那两个 `dispatch*SelfOnly` 覆写 +7. 完成后同步更新 `CLAUDE.md` 的组件表和记忆文件 `phone-5.5inch-adaptation.md` + +## 资源 + +配色一律用 `phone_*`,背景用 `bg_phone_*`,图标用 `ic_phone_*`,禁止写死色值。 +已有的(不够再加,加完更新 CLAUDE.md): + +- 色:`phone_primary` `phone_page_bg` `phone_bg` `phone_stat_bg` `phone_text_title/body/hint/label/strong/weak_btn` + `phone_divider` `phone_badge_bg` `phone_green` `phone_check_border` `phone_tag_gray_*` `phone_tag_green_*` + `phone_tag_red_*` `phone_orange_bg/text` +- 背景:`bg_phone_card` `bg_phone_search` `bg_phone_input` `bg_phone_stat_box` `bg_phone_panel_top` + `bg_phone_icon_round` `bg_phone_btn_primary` `bg_phone_btn_outline` `bg_phone_btn_weak`(浅蓝弱按钮) + `bg_phone_badge` `bg_phone_tag_green/gray/red/orange` `bg_phone_card_header_blue/green` + `bg_phone_card_top_accent`(卡片顶部主色描边条,出港计重头部卡片) +- 图标:`ic_phone_airplane` `ic_phone_location` `ic_phone_person` `ic_phone_check_circle` + `ic_phone_chevron_right/down` `ic_phone_calendar` `ic_phone_close` + `ic_phone_check_round_checked/unchecked`(圆形复选框) + `ic_phone_clipboard`(剪贴板,白色 FAB 用)`ic_phone_weight`(砝码,可 tint) + `ic_phone_package`(件数)`ic_phone_tag`(特码/标签) +- 圆点:`bg_phone_dot_green/blue/gray`(状态指示点;蓝=出港查询卡片头,灰=时间线未达节点) +- 复用 Pad 的矢量图:`img_search` `img_scan` `img_filter`(可 `app:tint` 染色) + +设计稿里的 MDI 图标若缺失,直接写 24×24 的 vector(用 MDI 官方 path),比找位图靠谱且能染色。 diff --git a/.claude/skills/phone-adapt/references/pad-safety.md b/.claude/skills/phone-adapt/references/pad-safety.md new file mode 100644 index 0000000..69b8c26 --- /dev/null +++ b/.claude/skills/phone-adapt/references/pad-safety.md @@ -0,0 +1,267 @@ +# Pad 端零破坏 & 易崩点 + +平板端是在跑的生产功能,适配手机不能改变它的任何行为。以下是必须遵守的约束和踩过的坑。 + +## 布局变体的三条硬约束 + +同名布局 + 资源限定符(`layout/` 手机 / `layout-sw600dp/` 平板)会让 DataBinding 生成 +**一个 Binding 基类 + 两个变体实现**,因此 Kotlin 侧零改动。代价是两个变体必须兼容: + +### 1. `` 必须完全一致 + +手机变体需要 `activity` 变量(暴露纯 UI 方法给 XML)时,**平板变体也要补上**,否则编译报 +`variable not found`。平板不用它,加个注释说明即可: + +```xml + + +``` + +### 2. 共用 id 双方都要保留 + +Activity / ViewHolder 引用到的 id(`srl`、`rv`、`iv_icon`、`checkIcon`…)两个变体都要有。 +可以换位置换样式,但 id 得在。 + +### 3. 单侧独有的 id 会变 `@Nullable` + +只在手机变体存在的 id(如「查看详情」的 `ll_detail`),Binding 基类里是可空字段, +Kotlin 侧必须判空: + +```kotlin +// 平板变体没有这个控件,为 null 时直接跳过 +binding.llDetail?.setOnClickListener { XxxDetailActivity.start(itemView.context, bean) } +``` + +编译器会强制你处理,不用担心漏掉。改完可以核对生成代码确认: + +```bash +grep -B3 "llDetail" module_gjc/build/generated/data_binding_base_class_source_out/debug/out/\ +com/lukouguoji/gjc/databinding/ItemXxxBinding.java +``` + +### ⛔ 绝对不要用「不同文件名 + 代码判断」切布局 + +`activity_xxx_phone.xml` 会生成 `ActivityXxxPhoneBinding`,而字段声明的类型是 +`ActivityXxxBinding`。泛型擦除让**编译期查不出来**,一进页面就 `ClassCastException`。 + +### 弹框(BaseDialogModel)也要双变体,最容易漏 + +页面布局适配完,长按/批量操作弹出的 DialogModel 仍会以 Pad 居中样式出现在手机上 +(出港运抵的「状态重置」就漏过)。做法与页面完全同构: + +1. dialog 布局 `git mv` 到 `layout-sw600dp/`,`layout/` 下写同名手机版(设计稿通常是底部弹层: + `bg_phone_panel_top` + 居中标题 + `ic_phone_close` + 全宽 `bg_phone_btn_primary` 确定键) +2. 弹出位置在构造参数按形态切换(XPopup 位置与布局是两回事,都要改): + +```kotlin +class XxxDialogModel(...) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) +``` + +3. 两变体 `` 一致(imports 不要求一致) + +参考:`IntExpArriveResetDialogModel` + `dialog_int_exp_arrive_reset.xml` 双变体。 +**适配收尾时逐个 grep 该页 ViewModel 里 `DialogModel(` 的调用点**,每个弹框都过一遍。 + +## 业务层零破坏的三个模式 + +### 新增查询维度:默认空值 + 仅非空进请求 + +平板布局没有对应控件 → 字段恒为空 → 不进请求体 → 平板查询与改造前逐字节一致。 + +```kotlin +val handoverState = MutableLiveData("") // 手机 Tab 绑这个,平板无控件 +val hoId = MutableLiveData("") + +val filterParams = mapOf( + "fdate" to flightDate.value?.ifEmpty { null }, + /* 原有字段… */ + "hoState" to handoverState.value?.ifEmpty { null }, // 空 → null → 不影响平板 + "hoId" to hoId.value?.ifEmpty { null }, +) +``` + +### 手机专属的额外请求:ViewModel 内部开关 + +Tab 角标计数、新增下拉字典这类请求,平板不需要,多发就是改变了平板行为(请求次数)。 +用一个内部布尔控制,由 Activity 在手机形态下开启: + +```kotlin +// ViewModel +private var phoneExtrasEnabled = false + +fun initPhoneExtras() { + phoneExtrasEnabled = true + handoverState.value = "0" // 手机默认停在第一个 Tab + DictUtils.getWHSUserList(addAll = false) { hoUserList.postValue(it) } +} + +override fun getData() { + /* 原有列表 + 统计请求… */ + if (phoneExtrasEnabled) loadTabCounts(filterParams) // 平板不触发 +} +``` + +```kotlin +// Activity +if (DeviceUtil.isPhone()) viewModel.initPhoneExtras() +``` + +`DeviceUtil.isPhone()` 只用来决定**数据默认值和额外请求**,**永远不用来选布局**。 + +### 纯 UI 状态放 Activity + +弹层显隐、Tab 高亮这类与业务无关的状态,用 `ObservableBoolean` 放在 Activity 上暴露给 XML, +不要污染 ViewModel: + +```kotlin +val filterPanelVisible = ObservableBoolean(false) +fun toggleFilterPanel() = filterPanelVisible.set(!filterPanelVisible.get()) +``` + +### 同名 id 在两个变体里必须是同一种控件 + +平板变体的 `btnMove` 是 `TextView`,手机版底部换成了 `PhoneBottomBar`——**不要为了复用 +Activity 里的 `binding.btnMove` 而把这个 id 安到 PhoneBottomBar 上**。同名不同类型会让 +DataBinding 把字段类型退化成公共父类,编译期未必报错,运行时行为难料。 + +正确做法:手机端的按钮点击走组件自身的 `setOnActionClick`,`btnMove` 就只留在平板变体里, +Activity 侧判空调用: + +```kotlin +binding.btnMove?.setOnClickListener { showMoveConfirmDialog() } +``` + +被 XML 调用的 Activity 方法记得改成 public。 + +### 调字典要对齐既有页面的参数口径 + +`DictUtils` 的方法参数常有"看起来合理但后端不认"的取值。出港移库的特码下拉一开始传 +`ieFlag="E"`(出港),接口返回空数组;而同模块既有页面(`GjcWeighingStartViewModel` 等) +一律传 `ieFlag=""`,能正常返回。 + +所以调字典前先 grep 一下同模块其他页面怎么调的,照抄它们的参数;改完必须在实机上 +**把下拉点开确认真的有数据**,空列表在 UI 上看不出报错。 + +### 共享方法别为手机改语义 + +`checkAllClick()` 这类平板也在用的方法,不要为了手机端的语义去改它。 +例:手机「待交接」Tab 下理论上只有未交接记录(服务端按状态过滤),所以全选照原样全量勾选就是对的; +若为此加上「跳过已交接」的过滤,反而改变了平板端(平板列表混排两种状态)的行为。 + +## 屏幕方向:Manifest 一律写 `unspecified` + +新增 Activity 注册时: + +```xml + +``` + +**两种错误方案都实测踩过,别再试:** +- 写死 `userLandscape` → 手机「先横屏、约 1 秒后转竖屏」(方向在建窗口时就定了, + `BaseActivity` 在 `super.onCreate` 之前纠正也来不及) +- 用 `@integer/screen_orientation` 资源引用(曾经以为是优雅方案)→ **更糟**:系统解析 + Manifest 属性不套用设备限定符,所有 Activity 实际都拿到 `values/` 默认值(竖屏), + 平板因此先竖后横,且 AutoSize 在竖屏配置下选中平板竖屏 720dp 基准 + (正常应为 1152×720 横屏基准),交互几次后整个 UI 放大约 1.6 倍 + +`unspecified` 让系统按设备当前物理方向建窗口(平板横屏摆放→横屏起,手机→竖屏起, +两端起始方向本就正确),`BaseActivity.applyDeviceOrientation()` 再按形态锁定, +不产生二次纠正。全项目 192 处已统一为 `unspecified`。 + +## 改 module_base 公共代码时 + +`module_base` 被所有业务模块依赖,改动会影响全部页面。原则: + +- **只做增量**:加新组件、新颜色、新 drawable,别改既有组件的行为 +- 确实要改公共基类(`BaseActivity` / `BaseBindingActivity` / Pad 系组件)时, + **必须在平板上回归**至少 2~3 个不同类型的既有页面 +- Bean 上加只读计算属性是安全的(Gson 只序列化字段,不动 getter); + 加**字段**要谨慎,会进请求体 + +## RecyclerView 里不要用 item 级 visibility 做 Tab/分类过滤 + +普通 ViewGroup(LinearLayout 等)里 `View.GONE` 的子 View 会自动塌陷为 0 高度、不占布局空间—— +但 **RecyclerView + LinearLayoutManager 不是这样**:它按子项的测量高度参与滚动区域计算, +不会因为子项是 GONE 就跳过。给 item 根节点加 `visibility="@{tab条件 ? VISIBLE : GONE}"` +切 Tab,会在列表里留下一段空白(隐藏项的高度仍被保留),角标数字和实际卡片数量对不上。 + +出港运抵的「待运抵/已运抵」Tab 就踩了这个坑(实机截图验证:切到「已运抵」后顶部有一大块 +空白,隐藏的待运抵卡片高度并未塌陷)。正确做法是在数据层过滤,而不是视图层: + +```kotlin +// ViewModel:自己维护完整列表,不依赖 adapter.items 的历史状态 +// (见下一节:adapter.items 在 loadMore 场景可能已经是上次过滤后的子集,不能当作真源) +private var allLoadedList: List = emptyList() + +override fun getData() { + launchLoadingCollect({ ... }) { + onSuccess = { result -> + val newPageItems = result.list ?: emptyList() + allLoadedList = if (pageModel.page <= 1) newPageItems else allLoadedList + newPageItems + + pageModel.handleListBean(result) { + // 无论 handleListBean 内部把 adapter.items 搞成什么样,这里用 allLoadedList + // 强制覆盖为「当前 Tab 的正确子集」,兜底纠正 + val filtered = allLoadedList.filter { it.isPending == (currentTab.value == "pending") } + pageModel.rv?.commonAdapter()?.refresh(filtered) + } + } + } +} + +fun onTabChangedPhone() { + val filtered = allLoadedList.filter { it.isPending == (currentTab.value == "pending") } + pageModel.rv?.commonAdapter()?.refresh(filtered) // 只是 items.clear()+addAll()+notifyDataSetChanged,无副作用 +} +``` + +`CommonAdapter.refresh()` 只是替换内部列表并 `notifyDataSetChanged()`,不影响 +SmartRefreshLayout 的分页/完成状态,可以放心在 `handleListBean` 回调里再调用一次。 + +## Intent 传递 Serializable Bean:`@Transient` 字段反序列化后是 null + +`GjcMaWb`/`GjcHaWb` 的 `checked`/`showMore`/`actionMenuVisible` 三个 UI 状态字段都标了 +`@Transient`(本意是避免 Gson 把 `ObservableBoolean` 的内部结构序列化进网络请求体)。 +但 Kotlin 的 `@Transient` 注解生成的就是 JVM 的 `transient` 修饰符,这个修饰符对 **Java +对象序列化**(`Intent.putExtra(String, Serializable)` 走的正是这条路径)有实际含义: +反序列化时会跳过 `transient` 字段的恢复,对象引用类型的字段结果就是 **null**。 + +新增手机专属页面时,如果要把 `List` 通过 `ArrayList` 塞进 `Intent` +传给下一个页面,接收方拿到的对象「看起来」字段都对(wbNo、pc、weight 这些正常字段没问题, +因为它们不是 `@Transient`),但只要调用 `bean.checked.set(...)` 就会立刻崩溃: + +``` +NullPointerException: Attempt to invoke virtual method +'void androidx.databinding.ObservableBoolean.set(boolean)' on a null object reference +``` + +出港运抵的「分单明细」页第一版全选按钮就是这样崩的。**修复只需一行**,在接收方 +`initOnCreated` 里对拿到的列表整体 `.copy()` 一遍——`.copy()` 会重新触发一次真正的对象构造, +类体里 `val checked = ObservableBoolean(false)` 这类初始化逻辑跟着重新跑一遍, +把 null 补成全新实例: + +```kotlin +@Suppress("UNCHECKED_CAST") +fun initOnCreated(intent: Intent) { + val list = intent.getSerializableExtra(Constant.Key.DATA) as? ArrayList + // .copy() 补上被 Java 序列化跳过的 @Transient 字段 + fullList = (list ?: emptyList()).map { it.copy() } +} +``` + +这个坑只在「Intent 传递 Serializable Bean + 接收方要切换 checked/showMore 等状态」时才会 +触发;纯只读展示(如回执页)不调用这些字段的 setter,不会崩,但补一道 `.copy()` 无害, +建议只要通过 Intent 传了 haWbList/maWbList 就统一加上,别等下次踩到才想起来。 + +## 已知的既有问题(不是你改坏的) + +用 `am start` 直启页面时,若**先经过首页**再启目标页,会崩在 +`LoadingModel.showLoading` → XPopup `popupInfo is null`。在**未改动**的页面上同样复现, +属于既有缺陷。**冷启动直启不受影响**(`force-stop` 后直接 `am start`)。 + +遇到崩溃先做这个对照实验再下结论:拿一个你没碰过的页面用同样方式打开,看是否同样崩。 diff --git a/.claude/skills/phone-adapt/references/proxyman.md b/.claude/skills/phone-adapt/references/proxyman.md new file mode 100644 index 0000000..d0a2aa4 --- /dev/null +++ b/.claude/skills/phone-adapt/references/proxyman.md @@ -0,0 +1,80 @@ +# Proxyman MCP:抓包 / 直调 / Mock(阶段 2、5、6 的调试利器) + +Proxyman 桌面端 + `mcp__proxyman__*` 工具(会话内直接可用,缺 schema 时用 ToolSearch +`select:mcp__proxyman__get_flows,...` 加载)。**2026-07-31 已在出港查询页全链路实测**: +抓包核对请求体、Map Local mock 三种卡片形态(含真实数据不存在的「未入库」兜底态)一次跑通。 + +## 与 api-doc 的分工(先契约后事实) + +| | api-doc MCP | Proxyman MCP | +|---|---|---| +| 回答什么 | **契约**:接口该有什么入参/出参、参数名叫什么 | **事实**:App 实际发了什么、后端实际回了什么 | +| 用在何时 | 阶段 2 开工前逐接口核对 | 阶段 5/6 实机验证与 corner case 补测 | + +标准配合流程: +api-doc 拿权威参数名 → 前端实现 → Proxyman flow 核对请求体确实带上了 → +compose 改参 A/B 验证后端认不认 → 不认 → 记问题清单(附 `export_flow_curl` 证据)→ +map local 造 mock 补 UI corner case 验证。 + +## 接入(一次性,实测步骤) + +```bash +# 1) 确认 Proxyman 在跑(返回 Recording: Active + 端口,默认 9090) +# mcp__proxyman__get_proxy_status +# 2) 模拟器全局代理指向宿主机(模拟器视角宿主机 = 10.0.2.2) +adb -s shell settings put global http_proxy 10.0.2.2:9090 +# 3) 验证完【必须】清掉,否则 Proxyman 一关模拟器就断网,下次验证莫名全挂 +adb -s shell settings put global http_proxy :0 +``` + +本项目内网服务是**纯 HTTP**(192.168.1.250:8093)→ 不需要装证书,明文直接可见。 +(若未来切 HTTPS,才需要 `install_certificate` + `enable_ssl_proxying`。) + +## 场景 1:核对请求/响应(替代 `ui_probe req`) + +logcat 抓 OkHttp 多行 pretty JSON 脆弱且要人肉拼接;Proxyman 一步到位: + +- `filter_flows`(key=url, value="IntExpSearch")→ 拿 flow ID 列表 +- `get_flow_detail`(flow_id)→ 完整请求体 + 响应体 + headers(Authorization 自动脱敏) + +**出参无文档定义时(如 `IntExpSearch/detail` 返回裸 Map),用真实 flow 的 responseBody +直接盘点字段清单**——判定「锁定状态/计费重量是否存在」这类问题的最快路径。 + +## 场景 2:直调后端 A/B 验证(替代 run-as 提 token + 手写 curl) + +`create_compose_http_from_flow`(从真实 flow 复制请求,**token 自动带上,不用碰 +shared_prefs**)→ `update_compose_http` 改一个参数 → `send_compose_http` 看结果对比。 +isFclose 这类「参数是否生效」的 A/B 实验用这条链。给后端提缺口时用 +`export_flow_curl` 导出可复现的 curl 附进问题清单。 + +## 场景 3:Map Local 造 mock 验证 corner case(替代代码注入样例 Bean) + +老流程「Activity 临时注入 Bean → 截图 → 删码 → 重构建」彻底淘汰:**零代码改动、零重构建**。 + +- `create_map_local`:url 精确到接口路径,method=POST,response_body 直接写目标 JSON + (构造空字段/超长文本/罕见状态,比如出港查询的 fclose+opDate 双空「未入库」兜底态) +- 改真实响应个别字段更省事时用 `create_map_local_from_flow` +- 验证完 `delete_rule`(rule_type=maplocal)+ 清代理,**规则残留会污染后续真实数据验证** + +响应结构照抄真实 flow 的顶层结构(如 pageQuery 是裸 PageInfo:`{total, list, pages, ...}`, +不要包 `{status, data}`)。 + +⚠️ 实测坑(2026-07-31 出港仓库):mock `BaseResultBean` 包装的接口(pageQueryTotal/detail 等)时, +**`status` 必须写 `"1"`**——`BaseResultBean.verifySuccess()` 判 `status == "1"`,写 "200" 会被当失败 +静默丢弃(UI 保持默认值,无任何报错,排查靠 flow 已命中但 UI 不变这一矛盾现象)。 + +⚠️ 实测坑:mock 响应到达后 UI 渲染可能滞后(冷启动首帧 Davey 卡顿 2s+), +**截图断言前先 `ui_probe texts` 确认数据已上屏**——这次就因截图太早误判过一轮「mock 没生效」, +其实 `get_flow_detail` 里 `matchedTools: ["Map Local"]` 早已证明规则命中,UI 只是还没画完。 + +## 场景 4:环境模拟(按需) + +- `create_network_condition`:弱网/高延迟,验证 loading 弹窗与超时提示 +- `create_breakpoint`:单次拦截改包(比 map local 更临时的一次性实验) +- `create_map_remote`:把请求转发到另一环境(如后端修复验证用的测试实例) + +## 验证收尾清单 + +- [ ] `list_rules` 确认无本次遗留规则(maplocal/breakpoint/network_condition) +- [ ] 模拟器代理已清(`settings get global http_proxy` 应为 `:0` 或空) +- [ ] 用真实数据再过一遍被 mock 过的页面,确认无假数据残影 diff --git a/.claude/skills/phone-adapt/scripts/design_shots.py b/.claude/skills/phone-adapt/scripts/design_shots.py new file mode 100755 index 0000000..ed1c754 --- /dev/null +++ b/.claude/skills/phone-adapt/scripts/design_shots.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +把 5.5 寸手持端设计稿(HTML 原型)渲染成截图,供逐像素比对。 + +设计稿是带 Tailwind + JS 的交互原型:列表页、Tab、详情页、筛选弹层往往挤在同一个 +HTML 里,靠 JS 函数切换。只读源码容易漏掉视觉细节(间距、圆角、真实配色、 +以及 JS 里 id 映射写错导致的字段错位),所以务必渲染成图来看。 + +用法: + # 1) 先列出 HTML 里可用的状态切换函数与元素 id,据此决定要截哪些状态 + python3 design_shots.py --html 出库交接.html --list + + # 2) 渲染:默认状态 + 注入 JS 触发的各个状态 + python3 design_shots.py --html 出库交接.html --out /tmp/shots \ + --state "01_列表:" \ + --state "02_已交接Tab:switchTab('shipped');" \ + --state "03_详情:showDetail({id:'PMC1',status:'已交接'});" \ + --state "04_筛选弹层:toggleFilterDrawer(true);" + +每个 --state 形如 "名称:JS",JS 可为空表示页面初始态。 +输出 /<名称>.png,同时保留注入后的 <名称>.html 便于复查。 +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys +import urllib.parse + +CHROME_CANDIDATES = [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + shutil.which("google-chrome") or "", + shutil.which("chromium") or "", + shutil.which("chromium-browser") or "", +] + + +def find_chrome() -> str: + for path in CHROME_CANDIDATES: + if path and os.path.exists(path): + return path + sys.exit( + "找不到 Chrome/Chromium。请安装,或用 --chrome 指定可执行文件路径。" + ) + + +def list_hooks(html: str) -> None: + """列出可用于构造状态的 JS 函数和元素 id。""" + funcs = sorted(set(re.findall(r"function\s+([A-Za-z_$][\w$]*)\s*\(", html))) + onclicks = sorted(set(re.findall(r'onclick="([^"]+)"', html))) + ids = sorted(set(re.findall(r'id="([^"]+)"', html))) + + print("== JS 函数(可直接注入调用)==") + for f in funcs: + print(f" {f}()") + print("\n== onclick 表达式(含真实参数,照抄最省事)==") + for o in onclicks[:40]: + print(f" {o}") + if len(onclicks) > 40: + print(f" ... 其余 {len(onclicks) - 40} 条略") + print("\n== 元素 id(判断有哪些页面/弹层)==") + print(" " + ", ".join(ids)) + + +def render(chrome: str, html: str, name: str, js: str, out_dir: str, + width: int, height: int, scale: float, budget: int) -> str: + # 等 load 之后再触发,确保 CDN 的 Tailwind/Iconify 已生效 + inject = ( + "" + ) + page_path = os.path.join(out_dir, f"{name}.html") + with open(page_path, "w", encoding="utf-8") as fp: + fp.write(html.replace("", inject + "")) + + png_path = os.path.join(out_dir, f"{name}.png") + subprocess.run( + [ + chrome, + "--headless=new", + "--disable-gpu", + "--hide-scrollbars", + f"--force-device-scale-factor={scale}", + f"--window-size={width},{height}", + f"--virtual-time-budget={budget}", + f"--screenshot={png_path}", + "file://" + urllib.parse.quote(os.path.abspath(page_path)), + ], + capture_output=True, + ) + return png_path + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--html", required=True, help="设计稿 HTML 路径") + ap.add_argument("--out", default="/tmp/design_shots", help="输出目录") + ap.add_argument("--state", action="append", default=[], + help='形如 "名称:JS",可重复;JS 为空表示初始态') + ap.add_argument("--list", action="store_true", + help="只列出可用的 JS 函数 / onclick / id,不截图") + ap.add_argument("--chrome", default="", help="Chrome 可执行文件路径") + # 原型容器一般是 375×812 的手机壳,留出边距后 460×900 刚好完整收进去 + ap.add_argument("--width", type=int, default=460) + ap.add_argument("--height", type=int, default=900) + ap.add_argument("--scale", type=float, default=2.0, help="缩放倍数,2 便于看清细节") + ap.add_argument("--budget", type=int, default=9000, + help="虚拟时间预算(ms),需覆盖 CDN 脚本加载") + args = ap.parse_args() + + with open(args.html, encoding="utf-8") as fp: + html = fp.read() + + if args.list: + list_hooks(html) + return + + chrome = args.chrome or find_chrome() + os.makedirs(args.out, exist_ok=True) + + states = args.state or ["01_default:"] + for spec in states: + name, _, js = spec.partition(":") + png = render(chrome, html, name.strip(), js, args.out, + args.width, args.height, args.scale, args.budget) + ok = os.path.exists(png) and os.path.getsize(png) > 0 + size = os.path.getsize(png) if ok else 0 + print(f"{'✓' if ok else '✗'} {name.strip():<24} {png} ({size} bytes)") + + print(f"\n用 Read 工具逐张查看 {args.out}/*.png,再开始写布局。") + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/phone-adapt/scripts/ui_probe.sh b/.claude/skills/phone-adapt/scripts/ui_probe.sh new file mode 100755 index 0000000..5f75115 --- /dev/null +++ b/.claude/skills/phone-adapt/scripts/ui_probe.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# 实机验证辅助:读取界面文字 / 按文字点击 / 截图 / 抓请求体。 +# +# 为什么需要它:手机端验证要反复点 Tab、弹层按钮、卡片链接。靠肉眼估坐标点击 +# 极易点空(本次适配就因此白跑了几轮),而 uiautomator 能拿到元素真实 bounds。 +# tap-text 直接按文字定位并点中心,比手算坐标可靠得多。 +# +# 用法: +# ui_probe.sh texts 列出当前界面所有可见文字 +# ui_probe.sh bounds "已交接" 打印该文字元素的 bounds 与中心点 +# ui_probe.sh tap-text "确认" [序号] 按文字点击(取中心点);同名多处时默认第 1 处 +# ui_probe.sh wait-text "待交接" [秒] 等文字出现(默认 20 秒),点击前先等页面就绪 +# ui_probe.sh wait-gone "请稍候……" [秒] 等文字消失(如等加载弹窗关闭) +# ui_probe.sh shot /tmp/a.png 截图到本地 +# ui_probe.sh req <关键字> 从 logcat 抓含关键字的请求/响应体 +# ui_probe.sh crash 统计并打印最近一次崩溃栈 +# ui_probe.sh form 打印形态识别结果 PHONE/TABLET + +set -uo pipefail + +SERIAL="${1:?用法: ui_probe.sh <命令> [参数]}" +CMD="${2:?缺少命令}" +ARG="${3:-}" +PKG="com.lukouguoji.aerologic" +A="adb -s $SERIAL" + +dump() { + $A shell uiautomator dump /sdcard/_probe.xml >/dev/null 2>&1 + # 注意:dump 出来的 XML 是一整行,必须按 '<' 拆成一节点一行才能按节点过滤。 + # (`tr '>' '>\n'` 是字符映射不是字符串替换,等于空操作,别踩这个坑。) + $A shell cat /sdcard/_probe.xml | tr '<' '\n' +} + +# 打印所有 text 完全等于 $1 的节点的 bounds 与中心点 +centers_of() { + dump | grep -F "text=\"$1\"" \ + | grep -oE 'bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' \ + | sed -E 's/bounds="\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\]"/\1 \2 \3 \4/' \ + | awk '{printf "bounds=[%d,%d][%d,%d] center=%d,%d\n", $1,$2,$3,$4, ($1+$3)/2, ($2+$4)/2}' +} + +case "$CMD" in + texts) + dump | tr '<' '\n' | grep -oE 'text="[^"]+"' | sed 's/text=//;s/"//g' | awk '!seen[$0]++' + ;; + + bounds) + [ -z "$ARG" ] && { echo "需要文字参数"; exit 1; } + centers_of "$ARG" + ;; + + tap-text) + [ -z "$ARG" ] && { echo "需要文字参数"; exit 1; } + idx="${4:-1}" + all=$(centers_of "$ARG") + [ -z "$all" ] && { echo "界面上找不到文字:$ARG"; exit 1; } + total=$(echo "$all" | wc -l | tr -d ' ') + # 同一文字可能在多处出现(如底部 Tab「国际」与列表里的「国际」), + # 默认点第 1 个;命中多个时提示,必要时用第 4 个参数指定序号。 + if [ "$total" -gt 1 ]; then + echo "注意:${ARG} 命中 $total 处,正在点第 $idx 处(可用第 4 个参数指定序号)" + echo "$all" | nl -w2 -s' ' + fi + line=$(echo "$all" | sed -n "${idx}p") + [ -z "$line" ] && { echo "序号 $idx 超出范围(共 $total 处)"; exit 1; } + cx=${line#*center=}; cx=${cx%%,*} + cy=${line##*,} + # 变量必须写成 ${ARG}:紧跟中文标点时 $ARG 会被当成变量名的一部分 + echo "tap ${ARG} @ $cx,$cy" + $A shell input tap "$cx" "$cy" + ;; + + # 等某段文字出现 / 消失。点击前先等页面就绪很重要: + # 加载弹窗("请稍候……")会盖住整屏并吞掉 input tap,导致点击"静默失效"。 + wait-text) + [ -z "$ARG" ] && { echo "需要文字参数"; exit 1; } + for _ in $(seq 1 "${4:-20}"); do + if dump | grep -qF "text=\"$ARG\""; then echo "出现: ${ARG}"; exit 0; fi + sleep 1 + done + echo "超时未出现: ${ARG}"; exit 1 + ;; + + wait-gone) + [ -z "$ARG" ] && { echo "需要文字参数"; exit 1; } + for _ in $(seq 1 "${4:-20}"); do + if ! dump | grep -qF "text=\"$ARG\""; then echo "已消失: ${ARG}"; exit 0; fi + sleep 1 + done + echo "超时仍存在: ${ARG}"; exit 1 + ;; + + shot) + out="${ARG:-/tmp/shot.png}" + $A exec-out screencap -p > "$out" + echo "$out ($(wc -c < "$out") bytes)" + ;; + + req) + # OkHttp 日志是多行 pretty JSON,用 -v raw 拿干净文本再按关键字取上下文 + $A logcat -d -v raw | grep -B24 -A24 -- "${ARG:-POST}" | grep -E '^\s*"|POST|<-- [0-9]{3}' | tail -60 + ;; + + crash) + n=$($A logcat -d | grep -c "FATAL EXCEPTION" || true) + echo "FATAL EXCEPTION 次数: $n" + if [ "$n" -gt 0 ]; then + $A logcat -d | grep -A25 "FATAL EXCEPTION" | tail -30 + fi + ;; + + form) + $A logcat -d -s BaseActivity:D | grep -oE "(PHONE|TABLET)\(sw=[0-9]+dp[^)]*\)" | tail -3 + ;; + + *) + echo "未知命令: $CMD"; exit 1 + ;; +esac diff --git a/CHANGELOG.md b/CHANGELOG.md index c20f070..10ee09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,101 +1,420 @@ -# 版本变更日志 (Changelog) - -本文档记录 AirLogistics 项目的所有重要变更。 - -格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), -版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 - -## [1.8.4] - 2024-12-02 - -### 新增 (Added) -- 完善 Claude Code 配置,添加 Android 开发权限 -- 创建 8 个快捷命令 (/build-debug, /install, /logs 等) -- 定制 CLAUDE.md 为 Android 项目专用开发指南 -- 添加 CONTRIBUTING.md 贡献指南文档 -- 添加 CHANGELOG.md 版本变更日志 - -### 改进 (Changed) -- 优化首页菜单交互体验 -- 改进 UI 界面显示效果 - -### 技术栈 -- Kotlin 1.6.21 -- Android Gradle Plugin 7.2.0 -- Gradle 7.3.3 -- minSdk 24, targetSdk 30, compileSdk 31 - ---- - -## [1.8.x] - 之前版本 - -### 主要功能 -- 国内出港业务模块 (货物收运、复磅称重、转运管理、出库装机) -- 国内进港业务模块 (舱单管理、卸机入库、出库提货、移库管理) -- 国际出港业务模块 (国际货物收运、板箱组装、ULD容器管理) -- 国际进港业务模块 (国际舱单管理、报文解析、理货管理) -- 航班管理模块 (航班查询、航班统计) -- 货物追踪模块 (货物状态追踪、运输日志) -- 监装监卸模块 (监装监卸管理) -- PDA功能模块 (PDA专用功能) -- 蓝牙打印模块 (佳博打印机集成) - -### 核心架构 -- MVVM 架构模式 -- 组件化模块设计 (11个业务模块 + 1个基础库) -- DataBinding 双向绑定 -- Kotlin Coroutines + Flow 异步处理 -- ARouter 模块间通信 -- Retrofit + OkHttp 网络请求 -- Glide 图片加载 -- MPAndroidChart 图表展示 - -### 开发基础设施 -- BaseActivity/BaseBindingActivity 基类 -- BaseViewModel/BasePageViewModel 基类 -- CommonAdapter + BaseViewHolder 列表适配 -- PadSearchLayout/PadDataLayout 自定义组件 -- 完整的 Kotlin 扩展函数库 -- DataBinding 适配器集合 - ---- - -## 版本说明 - -### 版本号规则 -- **Major.Minor.Patch** (例: 1.8.4) -- **Major**: 重大架构变更或不兼容的 API 修改 -- **Minor**: 新功能添加,向下兼容 -- **Patch**: Bug 修复和小的改进 - -### 变更类型 -- **Added**: 新增功能 -- **Changed**: 功能改进或变更 -- **Deprecated**: 即将废弃的功能 -- **Removed**: 已移除的功能 -- **Fixed**: Bug 修复 -- **Security**: 安全性修复 - ---- - -## 未来计划 - -### 待开发功能 -- [ ] 优化数据同步机制 -- [ ] 增强离线模式支持 -- [ ] 改进用户权限管理 -- [ ] 优化蓝牙打印稳定性 -- [ ] 添加数据导出功能 -- [ ] 性能监控和分析 - -### 技术改进 -- [ ] 升级到 Kotlin 2.0 -- [ ] 迁移到 Jetpack Compose -- [ ] 优化构建速度 -- [ ] 完善单元测试覆盖 -- [ ] 添加 UI 自动化测试 - ---- - -**维护**: 本文档应在每次版本发布时更新。 -**负责人**: 项目维护团队 -**最后更新**: 2024-12-02 +# 版本变更日志 (Changelog) + +本文档记录 AirLogistics 项目的所有重要变更。 + +格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), +版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 + +## [Unreleased] + +### 变更 (Changed) +- 【手机适配分支合入主干 · 2026-10-11】`feature/phone-ui-adaptation` 合并入 `main`,此后平板与 5.5 寸手机只在 + `main` 上同时开发。合并以平板业务为准: + ① 主干 58 个平板提交(运抵海关放行/运抵状态筛选/海关回执、出库交接装机状态/允许装机、出港移库详情与侧滑编辑、 + 开始计重托盘信息与运抵重量自动计算、计重明细重量/托盘自重联动、出入港仓库清仓备注与入/出库件数重量、 + 进港查询出库状态筛选、ULD 来源/入库时间/所在港/位置等)全部同步到手机布局;冲突的平板布局三方合并进 + `layout-sw600dp/`,ViewModel/Bean 冲突取平板业务逻辑。 + ② 新增手机变体:出库/入库/清仓/配运/删除申报弹框、出港移库详情编辑页(`IntExpMoveEditActivity`,替代手机专属 + `IntExpMoveDetailActivity`)、共用海关回执页 `CustomsResponseActivity`;`PhoneBottomBar` 增次操作按钮。 + ③ 手机端对齐平板:移除平板没有的查询入参(`likeNo`、进港移库 fdate/fno/spCode、出港移库 spCode)与改写的默认值 + (计重/计重记录/提取出库默认日期),补齐平板表单与筛选项(运抵分单号、进港查询出库状态、计重记录航班号、 + 进港移库承运人、开始计重运单号/地磅重量/平板车/航班可编辑)与操作(运抵删除申报、计重记录运抵申报、计重新增); + ULD 表单移除平板没有的进港航班号与额外必填校验。 +- 【后端反馈联调 · 2026-10-09】后端在《问题清单》补「解决方案/备注」后,逐项(#1~#34)api-doc 核对 + + 内网直调 A/B + AeroLogic_Phone 模拟器端到端验证,并按后端意见调整前端: + ① 后端已补字段/接口 → 前端对接:出港移库 flight/businessName/opUserName;出库交接 uldNumber 角标; + ULD source/inStoreTime/nowPort + `deleteUldBatch` 一次批删;出港计重 waitCheckInNumber/checkingNumber + 统计、passageWay、opUserName、trayNumber/trayTotalWeight;运抵 arrivalStatus 服务端 Tab; + 出港/进港查询 storedNumber/departedNumber;出港/进港仓库 notClearNumber/clearNumber(统计请求 3→1); + 提取出库 refUserName;国内进港移库 opUserName + `movedDetail` 照片回填。 + `ManifestTotalDto` 补齐后端统一统计 DTO 分项字段。 + ② 后端「按平板端调整/设计如此」→ 前端对齐:出库交接/ULD/计重/记录/出港仓库/进港仓库/提取出库/运抵 + 的手机筛选全部收敛为平板同款查询参数;ULD 状态回归两态;删除运抵「补充信息」页;出港详情移除 + 锁定状态/计费重量;进港详情按平板改 航程/出库时间(无经停站);提取出库移除无出参占位。 + ③ 联调新发现记入问题清单 #35~#41(ULD uld 搜索 500、saveUld 缺 maxVolume/maxWeight 500、 + IntImpStorage/pageQuery 过滤全失效、出港仓库列表与统计口径不一致、已移库无 mawbId 无法补传照片等)。 +- 【进港移库归属更正 · 2026-08-03】设计稿 11「进港移库」经确认归属**国内进港移库 + (DomImpMove,后端 8 接口齐全,Pad 首页「国内进港→进港移库」既有页面)**,此前误判为 + 国际进港并按 IntExpMove 约定预接不存在的 IntImpMove 模块(原问题清单 #31 作废)。处置: + ① 删除 module_gjj 的 IntImpMove 全套前端实现(3 Activity + 3 ViewModel + Holder + 4 布局 + + `IntImpMoveBean` + Api 4 方法 + 国际 Tab 菜单项 + 权限串/路由/双 Manifest 注册); + ② 按 phone-adapt 流程重新适配到 `GnjMoveStashListActivity`(app 模块):主列表双形态 + (Pad 布局移 layout-sw600dp 零改动;手机版 = PhoneSearchBar + 待移库/已移库 Tab + (`DomImpMove/search`/`searchMoved` 双端点,`searchMoved` 无移库标识按端点打 `isMoved`)+ + 角标(双端点 limit=1 取 total,仅手机发起)+ 筛选弹层 4 项 + 全选/已选计数 + 批量移库 + (复用平板 `move` 链路)+ 卡片长按「拍照」浮层);新增手机专属页 + `GnjMoveStashDetailActivity`(Intent 传 bean 零请求,三卡片)与 + `GnjMoveStashPhotoActivity`(先调 `detail` 回填已有照片,提交走 `modify` 最小体 + 三字段规范,与平板移交编辑页同链路);`MoveStashBean` 增 `agentName`/`isMoved`/ + `actionMenuVisible`/`agentDisplay`/`orDash`;手机首页入口改挂**国内 Tab**(`GnViewModel` + 补通用 route 跳转,权限串 `GnjYiKu=AppDomImpMove` 与 Pad 一致,新路由 + `/app/GnjMoveStashListActivity`)。 + 2026-08-03 内网真实数据双端验证通过(登录→国内 Tab→列表/Tab/详情/筛选/全选/长按拍照全链路 + crash=0;平板回归原行为零变化);Proxyman A/B:`wbNo`/`awbType` 过滤生效, + `fdate`/`fno`/`spCode` 被后端静默忽略(新缺口 #32)、`searchMoved`/`detail` 出参 + `opId`/`opDate` 恒 null 且无姓名字段(#33)、`searchMoved` 无 pic 三字段(#34,前端已规避) + +### 修复 (Fixed) +- 出港计重「修改记录」托盘自重输入框:Double 回写导致光标跳回开头(输入 18 存成 81),改为保留输入原文 +- ULD 新增:入库时间日期行仅产出 yyyy-MM-dd(后端 400)→ 保存时补 00:00:00;补充最大装载容积/最大载重 + 可编辑必填(缺失时 saveUld 500) +- 提取出库运单号入参 no → wbNo(与主干平板端修复一致) + +### 新增 (Added) +- 「进港查询」5.5 寸手机端适配(设计稿 10_进港查询,06 出港查询的进港镜像;详情页与运单追踪 + 复用 08/06 已适配页零新增):列表 `IntImpQueryActivity`(module_gjj,首页菜单实际跳转页; + 旧版 `GjjQueryListActivity` 路由已注释弃用)手机布局 = PhoneSearchBar(运单号+扫码+筛选)+ + 三格统计(总票数/已入库/已出库,已入库/已出库为已加载数据客户端计数,`pageQueryTotal` 无分项 + 出参,缺口 #30)+ 状态标签卡片(前端推断:`dlvTime` 非空=已出库绿 / `inDate` 非空=已入库橙 / + 兜底未入库灰)+ 卡片字段件数(inPc)/重量(inWeight)/代理人/特码;筛选弹层 9 项 + (航班日期起止/航班号/代理/特码/始发站/运单类型/业务类型/品名中,**全部经 api-doc 核对为 + pageQuery 真实入参**,无缺口);平板侧滑筛选面板代码判空守卫,Pad 端行为零变化 +- 「进港移库」5.5 寸手机端全新页面(设计稿 11_进港移库;**首个后端整模块缺失的适配页**: + api-doc 确认无 IntImpMove 模块、权限体系无对应 key,缺口 #31): + `IntImpMoveActivity`(待移库/已移库 Tab + 角标、搜索/扫码、筛选弹层 4 项(航班日期/航班号/ + 特码/运单类型)、多选 + 全选 + 批量移库(ConfirmDialogModel 确认)、卡片长按浮层「拍照」)+ + `IntImpMoveDetailActivity`(运单详情三卡片:蓝头基本信息/绿头移库信息(仅已移库)/橙头异常照片, + Intent 传 bean 零请求)+ `IntImpMovePhotoActivity`(拍照上传:图片网格复用 module_base + `ImageSelectNewViewHolder`,提交走 pic/originalPic/picNumber 三字段规范); + 接口按 IntExpMove 同名约定预接四个(pageQuery/pageQueryTotal/move/uploadPic),权限串 + `AppIntImpMove` 预定义,后端交付后前端零改动生效(权限串下发前手机首页无入口,debug 直启验证) +- 新增 `IntImpMoveBean`(镜像 GjcMove + 航班/照片字段 + isMoved/flightInfo/orDash 计算属性); + `IntImpQueryBean` 新增 isQueryOutbound/isQueryStored/orDash/agentDisplay 计算属性; + 手机首页「国际」Tab 增加「进港查询」(图标 gjj_query_icon)与「进港移库」(图标复用 + gjc_yi_ku_icon)入口 +- 新增手机版资源:`bg_phone_btn_green`(绿色主按钮)、`bg_phone_card_header_orange`(橙色详情卡 + 表头)、`bg_phone_photo_add`(虚线加号槽位)、色值 `phone_amber` + +### 修复 (Fixed) +- 【11 模块全量设计稿走查】修正 4 类差异(2026-07-31,3 个并行审计 + 逐态渲染比对): + ① 运单追踪手机竖排时间线补齐已发生节点的状态 pill(绿底标签展示该环节日志 content, + 如「放行状态:01」,超长省略,设计稿 06/08 运单追踪均有此元素); + ② 11 个页面手机端标题对齐设计稿短标题(出库交接/出港待计重/计重记录/计重明细/开始计重/ + 出港运抵/出港移库/出港查询/出港货物/进港查询/进港货物),平板保持原长标题零变化; + ③ ULD 编辑表单「来源」字段移至「所在港」之后(设计稿顺序); + ④ 出港计重列表卡片(日期/航程/代理/特码)与计重记录卡片(代理)补齐字段小图标 + (复用 ic_phone_airplane/location/person/tag,与其余模块卡片风格统一) +- 出港查询/进港查询卡片「代理人」空值兜底失效:转换器把 JSON null 归一成空串,DataBinding + `??` 只判 null 不判空串导致 agentCode 回退永不生效;改用 Bean 计算属性 `agentDisplay` + (isNullOrEmpty 双兜底),已同步修正 06 出港查询(`item_gjc_query.xml`) + +### 验证 (Verified) +- 进港查询手机端:Map Local mock 验证三格统计计数、三种状态标签、空值 "--" 兜底、筛选弹层 + 9 项收缩滚动;平板端回归横排搜索条/侧滑筛选面板/底部统计栏零变化;全程崩溃数 0 +- 进港移库手机端:Map Local mock 验证双 Tab 角标、双形态卡片、单选/全选联动计数、空选拦截、 + 移库确认弹框、长按拍照浮层、运单详情、拍照上传页;全程崩溃数 0(后端模块缺失,接口联调待 + 后端交付,见问题清单 #31) +- 「提取出库」5.5 寸手机端适配(设计稿 09_提取出库):列表 `IntImpPickUpDLVActivity`(module_gjj) + 手机布局 = PhoneSearchBar(运单号+扫码+筛选)+ **PhoneStatusTab 待出库/已出库**(`outState` + 为 pageQuery 文档定义入参 0/1,Tab 真实过滤 + 角标双 pageQueryTotal,抓包证实携带)+ + 双形态卡片(待出库:圆形勾选框+橙标签+库位行;已出库:绿 √ 标签+出库人·出库时间行, + `isPickedUp` = dlvTime 非空)+ 卡片内 PhoneStatBox 三列(代理人/件数/重量)+「查看详情」链接 + + PhoneBottomBar(全选+已选 N 项+出库,仅待出库 Tab 显示,复用既有 confirmOutbound); + 筛选弹层 4 项(航班日期为 pageQuery 未定义入参预留;航班号 fno;代理人/特码下拉); + 手机端进入时清空平板默认「当天缴费日期」查全量(initPhoneExtras,平板默认值不变) +- 手机专属新页「运单详情」`IntImpPickUpDetailActivity`:蓝头运单卡(红标签国际进港+品名大标题+ + 两列 KV+库位橙标签)+ 绿头提货信息卡(柜台办理人/时间,已出库追加出库人/出库时间行), + 数据直接 Intent 传列表 bean(Serializable),零新增请求 +- `IntImpPickUpDLVBean` 新增 `isPickedUp` 计算属性;手机首页「国际」Tab 增加「提取出库」入口 + (权限 AppIntImpPickUpDLV 对应权限串,图标复用 Pad 端 gjj_chu_ku_icon) +- 后端缺口 2 项登记问题清单 #28~#29(pageQuery 无 fdate 入参、出参无车牌/目的港/业务类型; + outState 过滤与 dlvName=出库人语义待内网实测) + +- 「进港仓库」5.5 寸手机端适配(设计稿 08_进港仓库,07 出港仓库的进港镜像 + 进港详情页首次适配): + 列表 `IntImpStorageUseActivity`(module_gjj,首页菜单实际跳转页;旧版 `GjjWareHouseActivity` + 路由已注释弃用)手机布局 = PhoneSearchBar(placeholder「搜索运单号/航班号」,`phoneSearchClick` + 分流:含字母→fno、其余→wbNo,实机抓包证实)+ 三格统计(总票数/未清仓/已清仓,clearNormal=0/1 + 双 total)+ 卡片四操作按钮(清仓/修改库位/出库/入库,已清仓置灰)+ 卡片字段件数/重量/航班号/ + **始发站**(bean.dep,出港为目的站);筛选弹层 8 项(始发站走 fdep 入参;代理/特码/运单类型/ + 业务类型/品名中 5 项为后端未定义入参,预留传参) +- 进港详情页 `IntImpQueryDetailsActivity` + 3 Fragment(运单/仓库/库位信息)首次手机适配: + 移植 06 出港详情手机布局,报文区按设计稿 3 项(原始舱单 mftStatus/理货报告 tallyStatus/ + 海关放行 command),件重区直取 detail 出参 awbPc/awbWeight/cashWeight/inPc/inWeight + (进港自带无需求和),库位卡片优先显示中文姓名(inOpName/outOpName); + ViewModel 增加 str/strOr/dec 取值帮助(与出港同口径) +- 进港版 3 个操作弹框双变体化(清仓/入库/修改库位,手机底部弹层);修改库位手机版增加 + 「原库位」单选且**只列未出库库位**(沿用平板「已出库不允许修改」校验,卡片级操作时全部已出库 + toast 拦截),单库位默认选中 +- 手机首页「国际」Tab 增加「进港仓库」入口(权限 AppGjjWareHouse=Constant.AuthName.GjjWareHouseActivity 对应权限串,图标复用出港仓库同款) +- 后端缺口 3 项登记问题清单 #25~#27(pageQuery 无代理/特码/运单类型/业务类型/品名中 5 项筛选 + 入参;clearNormal 分项统计待实测;detail 出参 dep/dest1/dest/unNumber/opDate 字段待实测确认) +- 「出港仓库」5.5 寸手机端适配(设计稿 07_出港仓库,详情/运单追踪复用已适配页零新增): + 列表 `IntExpStorageUseActivity` 手机布局 = PhoneSearchBar(运单号 + 扫码 + 筛选)+ 三格统计 + (总票数/未清仓/已清仓,彩色左边条卡片;分项计数按同条件 + `clearNormal=0/1` 各调一次 + `pageQueryTotal`,仅手机形态发起)+ 状态标签卡片(已清仓绿/未清仓橙,`clearNormal=="1"` 判定); + **卡片级四操作按钮**(清仓/修改库位/出库/入库,已清仓时清仓置灰拦截)取代平板的勾选+底部批量条, + 语义等价于「勾选该主单+全部库位」复用既有 `performClear/performModifyStorage/performOutStorage/ + performInStorage` 请求链路;卡片点击进详情(复用出港查询详情 `GjcQueryDetailsActivity` 3 Tab + 运单/仓库/库位信息,运单追踪同样复用 `LogDetailActivity`);筛选弹层 8 项按设计稿完整实现 + (航班日期/航班号/代理/特码/目的站/运单类型/业务类型/品名中,后三项为后端未定义入参,预留传参) +- 清仓/入库/修改库位三个操作弹框(`IntExpMoveClearDialogModel`/`IntExpInStorageDialogModel`/ + `IntExpModifyStorageDialogModel`)双变体化:手机底部弹层(PhoneDataLayout)+ 平板居中弹框不变, + 弹出位置按 `DeviceUtil.isPhone()` 切换;修改库位弹框手机版新增「原库位」单选 + (卡片级操作时运单可能有多个库位,构造参数 `sourceStorageList` 传入,单库位默认选中,平板路径不受影响) +- 出库二次确认由系统 `AlertDialog` 改为项目规范的 `ConfirmDialogModel`(平板批量出库与手机卡片出库共用) +- 手机首页「国际」Tab 增加「出港仓库」入口(权限 AppIntExpStorageUse,图标复用 Pad 端 gjc_cang_ku_icon) +- `GjcMaWb` 新增 `isCleared` 手机版计算属性;新增手机版资源 `bg_phone_btn_disabled`(灰底禁用按钮) +- 后端缺口 2 项登记问题清单 #23~#24(pageQuery/pageQueryTotal 无 awbType/businessType/goodsCn + 筛选入参;pageQueryTotal 的 clearNormal 分项统计过滤待内网 A/B 实测确认) +- 「出港查询」5.5 寸手机端适配(设计稿 06_出港查询,含列表/详情/运单追踪三页): + 列表 `GjcQueryActivity` 手机布局 = PhoneSearchBar(运单号 + 扫码 + 筛选)+ 三格统计 + (总票数/已入库/已离港,彩色左边条卡片)+ 状态标签卡片(已离港绿/已入库橙/未入库灰, + 前端推断口径:fclose 非空=已离港,否则 opDate 非空=已入库);筛选弹层 9 项(平板搜索行的 + 日期起止/代理并入弹层,特码按设计稿改下拉,字典 `getSpecialCodeList(flag=1, ieFlag="")`, + 仅手机形态经 `initPhoneExtras()` 加载);详情 `GjcQueryDetailsActivity` 3 Tab 全部复用 + ViewPager2 + Fragment,手机变体以 PhoneKvItem 网格呈现「基本信息/件重信息/回执信息」, + 比平板多展示承运人/始发站/中转站/目的站/UN编号,入库件数/入库重量 = warehouseList 各批次求和; + 仓库/库位 Tab 改批次/库位卡片;「运单追踪」为公共页 `LogDetailActivity` 双变体首例—— + 手机竖排流转时间线(Activity `buildVerticalSteps` 动态构建,节点绿=已达/灰=未达 + 环节时间), + 平板横向步骤条零改动,手机标题按形态显示「运单追踪」 +- 手机首页「国际」Tab 增加「出港查询」入口(权限 AppIntExpSearch,图标复用 Pad 端 gjc_query_icon) +- `PhoneKvItem` 新增 `tagColor` 属性("green" 绿色高亮,海关放行正常态;默认 "orange"), + 且占位符 "-"/"--" 不再套高亮标签底 +- `GjcMaWb` 新增 `isQueryDeparted`/`isQueryStored`/`orDash` 手机版计算属性 +- 新增手机版资源:`ic_phone_package`(件数)、`ic_phone_tag`(特码)、`bg_phone_dot_blue/gray` +- 后端缺口 2 项登记问题清单 #21~#22(isFclose 入参实测被 pageQuery/pageQueryTotal 静默忽略且 + 无已入库/已离港分项统计出参;detail 出参无锁定状态/计费重量字段) + +### 修复 (Fixed) +- 提取出库手机版底部条「已选 N 项」在点全选后不更新:`checkAllClick` 只改勾选状态未同步 + `selectedCountText`(卡片单选走 FlowBus→updateCheckAllStatus 正常),补同步后实测「已选 3 项」正确 +- **`PhoneFilterPanel` 底部按钮被挤出屏幕**:面板高度 wrap_content 且内容插槽无滚动,筛选项多 + (出港查询 9 项)时「重置/确认」按钮直接超出屏幕外且无任何报错。修复:内容插槽外包 + ScrollView(weight=1),筛选项少时行为不变,超屏时内容区收缩滚动、按钮始终可见 + (已回归出港计重 4 项弹层,行为不变) + +### 验证 (Verified) +- 提取出库双端验证通过(2026-07-31,Proxyman Map Local mock 口径):手机端菜单入口、 + Tab 角标(双 total)、双形态卡片(含库位空值兜底)、单选/全选与「已选 N 项」联动、 + 出库 ConfirmDialogModel、待出库/已出库两种详情页形态(出库人行按需显隐)、筛选弹层 4 项、 + Tab 切换请求体抓包证实 outState=1 正确携带、冷启动方向恒竖屏、崩溃 0; + 平板端回归:缴费日期默认当天/5 搜索条/全选/底部统计/确认出库与改造前一致,崩溃 0。 + **待内网补测**:outState 过滤 A/B、dlvName=出库人语义、出库真实提交链路 +- 进港仓库双端验证通过(2026-07-31,同出港仓库为 Proxyman Map Local mock 口径): + 手机端登录→国际 Tab→进港仓库菜单入口、三格统计、三种卡片形态(含空值 "--" 兜底)、 + 详情 3 Tab 全字段(锁定状态转中文、库位中文姓名、时间截断、空值兜底)、四操作弹框、 + 已出库库位过滤(双库位卡片原库位仅列未出库项并默认选中)、无库位/已清仓拦截、 + 筛选弹层 8 项、搜索分流实机抓包证实(输入 MU1111 → 请求体 fno="MU1111"、wbNo=null)、 + 冷启动方向恒竖屏、崩溃 0;平板端回归:5 搜索条/全选/底部统计/4 批量按钮与改造前一致,崩溃 0。 + **待内网补测**:clearNormal 分项统计 A/B、detail 推断字段(dep/dest1/dest/unNumber/opDate)、 + 四操作真实提交链路 +- 出港仓库双端验证通过(2026-07-31,开发机不在内网 → 全程 Proxyman Map Local mock 口径): + 手机端登录→国际 Tab→出港仓库菜单入口、三格统计渲染、三种卡片形态(已清仓置灰清仓/未清仓/ + 全空字段 "--" 兜底)、四操作按钮(清仓/入库底部弹层、修改库位弹层含原库位默认选中、 + 出库 ConfirmDialogModel 文案带运单号与库位数、无库位卡片 toast 拦截、已清仓清仓点击拦截)、 + 筛选弹层 8 项滚动 + 底部按钮可见、卡片点击进详情(Proxyman flow 证实 maWbId 正确传参且 + detail mock 命中)、详情右上运单追踪跳转、冷启动方向恒竖屏(ROTATION_0×3 采样)、崩溃 0; + 平板端同页面回归:5 搜索条/全选/底部统计/4 批量按钮与改造前一致,崩溃 0。 + **待内网补测**:clearNormal 分项统计 A/B、agentCode/spCode/dest 筛选实测、四操作真实提交链路 +- 出港查询双端实机验证通过(2026-07-31,内网真实数据 2026-06-09 共 14 票): + 手机端登录→国际 Tab→出港查询菜单入口、列表加载/状态标签/三格统计(10+4=14 与卡片一致)、 + 筛选弹层(日期滚轮、特码/代理下拉真实取数、重置、确认后重新查询)、运单号搜索、扫码拉起、 + 详情 3 Tab(含仓库批次卡与库位空态)、运单追踪时间线、冷启动方向恒竖屏(mRotation=0×8 采样)、 + 全程 FATAL 0;平板端回归:列表 5 搜索条 + 底部统计 + 侧滑筛选面板 + 详情表格样式均与改造前一致 +- 「出港运抵」5.5 寸手机端适配(主列表 + 3 个手机专属新页,设计稿 04_出港运抵): + 主列表 `IntExpArriveActivity` 新增「待运抵/已运抵」Tab(客户端按 declareStatus 是否为空拆分数据集, + 接口无此查询参数)+ 卡片长按浮层(待运抵→补充、已运抵→重置+回执,纯 UI 长按)+ 底部全选/已选/申报 + (仅待运抵 Tab 显示,选中范围独立于 Pad 全量选择逻辑);新增「分单明细」`IntExpArriveSubOrderActivity` + (数据直接复用主列表已加载的 haWbList,零新增请求,长按单条重置 + 底部批量申报);新增「回执内容」 + `IntExpArriveReceiptActivity`(只读页,复用 haWbList[].response + arrivalOpDate);新增「补充信息」 + `IntExpArriveSupplementActivity`(参照 Pad 端国际进港「收发货人信息」批量应用模式,对整个运单的 + haWbList 批量应用同一份表单,后端接口不存在,已实机确认 404,见问题清单 #19) +- 手机首页「国际」Tab 增加「出港运抵」入口 +- `GjcMaWb`/`GjcHaWb` 新增 `isPending`/`declareTagColor`/`haWbTagColor`/`arrivalOpDateText` 等 + 手机版专属计算属性;`GjcHaWb` 补充 `Serializable` 实现(供 Intent 跨页传递分单列表) +- 新增手机版资源:`bg_phone_tag_indigo`/`bg_phone_tag_amber`(申报状态三色标签)、 + `bg_phone_fab_purple`/`bg_phone_fab_amber`/`bg_phone_fab_green`(长按浮层圆形操作按钮) +- 后端缺口 2 项登记问题清单 #19~#20(分单补充信息保存接口不存在、Tab 无申报状态查询参数) +- 「出港运抵」对照设计稿 review 修正:状态重置弹框手机端改为设计稿底部弹层 + (**弹框适配首个先例**:`dialog_int_exp_arrive_reset.xml` 同名布局双变体,Pad 版移至 + `layout-sw600dp/`,`IntExpArriveResetDialogModel` 构造参数按 `DeviceUtil.isPhone()` 切 + `DIALOG_TYPE_BOTTOM`/`CENTER`);主列表补空态(图标 + 文案按 Tab 切换「暂无待/已运抵的运单」); + 回执页补空态兜底「暂无回执内容」(单分单入口常无回执,避免纯白屏);分单明细长按浮层按设计稿 + 补「回执」按钮(单分单跳回执页);分单页底部统计加分母「已选 N / M 项」;Tab 角标初始显示 0 + +### 修复 (Fixed) +- **RecyclerView + Tab 过滤新坑**:给 item 根节点加 `visibility="@{...}"` 切 Tab 会在列表里 + 留下空白——RecyclerView/LinearLayoutManager 不像普通 ViewGroup 那样让 GONE 子项自动塌陷为 + 0 高度。改为在 ViewModel 数据层维护完整列表,Tab 切换时把过滤后的子集整体 `refresh()` 给 + adapter(已实机截图验证空白消失、卡片数与角标一致) +- **Serializable Intent + @Transient 崩溃新坑**:`GjcMaWb`/`GjcHaWb` 的 `checked` 等 UI 状态 + 字段标了 `@Transient`(避免 Gson 序列化 ObservableBoolean),但这同时是 JVM `transient` + 修饰符,Java 序列化(Intent 传递机制)会跳过其恢复,反序列化后为 null,切换选中状态时 + NullPointerException 崩溃。修复:接收方 `initOnCreated` 对列表 `.map { it.copy() }` 重新 + 触发类体初始化补上(已实机崩溃复现并验证修复) +- `PhoneStatusTab`/`PhoneFilterPanel` 补上遗漏的 `dispatchSaveInstanceState`/ + `dispatchRestoreInstanceState` 屏蔽覆写(其余 5 个手机版复合组件均已重写,这两个是历史遗漏; + 同页多实例时子控件状态可能按 id 跨实例串档) + +### 验证 (Verified) +- 出港运抵双端实机验证通过:手机端 Tab 切换(角标/卡片双向正确)/全选/单选/长按菜单/查看分单 + /分单明细全选与长按重置/补充信息(真实数据回填+下拉字典+提交请求体完整+后端404如实呈现)/ + 回执页 全链路 crash=0、方向无闪屏;平板端布局与交互(4 搜索条+分单内嵌展开+底部 5 元素操作栏) + 与改造前逐项一致 +- 出港运抵 review 修正双端实机验证通过:手机端重置底部弹层(长按→重置→下拉展开选项)/主列表空态 + /回执页空态/分单浮层重置+回执双按钮/「已选 0 / 2 项」分母 全部与设计稿一致且 crash=0; + 平板端回归:列表、批量「状态重置」弹框仍为居中 Pad 样式,行为与改造前一致 +- 「出港计重」5.5 寸手机端适配(首个全链路 4+1 页模块,设计稿 03_出港计重): + 待计重列表(三格统计 总票数/待计重/计重中 + 卡片状态标签 未计重/计重中 + 卡片内单票「提前运抵」+ + FAB 进计重记录 + 筛选弹层 通道号/承运人/运单类型/代理人)、开始计重(头部预配统计卡 + PhoneFormRow 表单, + 托盘数量/托盘自重按设计稿手输)、计重记录(三格统计 + 卡片地磅图标进明细/「查看详情」进运单详情 + + 筛选弹层 入库日期起止/代理人/特码/目的站/计重人)、计重明细(汇总卡含托盘总重 + 记录卡「修改」→ + 全屏「修改记录」覆层,复用平板 isEditMode 编辑链路,保存后留在本页)、手机专属新页「运单详情」 + `GjcWeighingWbDetailActivity`(12 字段只读网格);手机首页「国际」Tab 增「出港计重」入口 +- 新增手机版公共资源:`bg_phone_btn_weak`(浅蓝弱按钮)、`bg_phone_card_top_accent`(卡片顶部主色描边条)、 + `ic_phone_clipboard`、`ic_phone_weight` +- `GjcMaWb` 预留 `palletNumber`/`carWeight` 字段、`GjcCheckInRecord` 增 `carWeightStr` 双向绑定属性 + (均为手机版专属,平板请求体零变化);后端缺口 7 项登记问题清单 #12~#18 + (checkIn 过滤不生效、主列表筛选三入参、入库日期范围、通道号出参、计重人姓名、arriveFlag 不回填、 + 托盘数量/自重入参) + +### 修复 (Fixed) +- 出港计重手机版搜索分流按实测修正:`likeNo` 并非任意模糊(按 8 位 no 匹配), + 11 位全号改走 `wbNo` 精确查询,含字母走 `fno` + +### 验证 (Verified) +- 出港计重双端实机验证通过:手机端 待计重列表/筛选/开始计重(必填校验)/计重记录/记录筛选/运单详情/ + 计重明细/修改覆层保存/单票提前运抵 全链路 crash=0、方向无闪屏、请求体新参数全部正确携带; + 平板端 待计重/计重记录/开始计重 三页回归无变化(横屏、5 搜索条、日期默认今天、地磅区/托盘车号/浮动按钮组均在) +- 「ULD管理」5.5 寸手机端适配(app 模块首个适配页):列表卡片(三色状态标签)+ 筛选弹层(入库时间/进港航班号/所属航司/来源)+ + 右下角浮动新增 + 底部全选/批量删除(循环调用单条删除接口)+ 一套 `activity_uld_edit` 手机布局覆盖 新增/修改/详情 三态 + (详情态右上铅笔进入修改、状态底部选择面板、所在港默认 HFE、必填校验仅手机生效) +- 新增手机版公共组件 `PhoneFormRow`(行式表单项,INPUT/SELECT/DATE/TEXT 四形态,SELECT 内置底部选择面板); + `PhoneBottomBar` 增加 `actionDanger` 危险按钮样式;新增 `bg_phone_fab`、`bg_phone_btn_danger`、`bg_phone_tag_red_light`、 + `ic_phone_trash`、`ic_phone_edit` 等资源 +- `ULDBean` 补解析接口既有出参 `ifNo`/`efNo`/`checkInDate`/`checkOutDate` 并预留 `source`;后端缺口 5 项 + (状态三态、来源字段、筛选入参、批量删除接口、字段语义)登记问题清单 #7~#11 + +### 验证 (Verified) +- ULD管理双端实机验证通过:手机端列表/筛选/详情/修改/新增/单选全选/批删确认全链路 crash=0、方向无闪屏; + 平板端列表与详情回归无变化 + +### 修复 (Fixed) +- 出库交接手机版「待/已交接」Tab 与角标计数的过滤参数名修正:`hoState` → `handoverState` + (以 api-doc 接口文档为准;此前误传 `hoState` 被后端静默忽略,两个 Tab 返回同一批数据)。 + 2026-07-29 内网实机验证通过:fdate=2026-07-16 下待交接/已交接 Tab 分别精确返回对应板箱,与后端直调结果一致。 + 验证中发现 `IntExpOutHandover/pageQueryTotal` 出参恒 0(后端缺陷,致 Tab 角标与平板底部统计为 0,已记录问题清单 #6) + +### 新增 (Added) +- 新增《后端接口对接问题清单》(`documents/后端接口对接问题清单.xlsx`):手机端适配走查发现的接口缺口 + (移库列表缺航班字段、缺操作人姓名、`opId/opdate/businessType` 不回填、出库交接查询缺「交接人」入参), + 按模块/页面/接口定义/接口地址索引,作为与后端对接的持续维护文档 +- phone-adapt skill 新增 `references/api-doc.md`:api-doc MCP 连接与逐接口核对流程,接口对齐从「问用户」升级为「先查接口文档」 + +--- + +## [1.9.0] - 2026-07-29 + +### 新增 (Added) +- 5.5 寸手持端双形态适配框架:同名布局 + 资源限定符(`layout/` 手机、`layout-sw600dp/` 平板)自动切换,业务逻辑(ViewModel/ViewHolder)零改动 +- 新增 `DeviceUtil` 设备形态判定工具,及手机版公共组件库(`PhoneSearchBar`、`PhoneStatusTab`、`PhoneDataLayout`、`PhoneFilterPanel`、`PhoneBottomBar`、`PhoneStatBox`、`PhoneKvItem`) +- 完成「国际出港移库」「国际出港出库交接」两个页面的手机端适配,含各自的详情页(`IntExpMoveDetailActivity`、`IntExpOutHandoverDetailActivity`) +- 手机端首页菜单接入「出港移库」「出库交接」入口 + +### 修复 (Fixed) +- 修复手机端进入页面时「先横屏、约 1 秒后转竖屏」的闪屏问题:Manifest 统一改为 `android:screenOrientation="unspecified"`(全项目 192 处),由 `BaseActivity.applyDeviceOrientation()` 按设备形态在运行时锁定方向,避免系统建窗口时用错误方向 +- 修复平板端因中间方案(`@integer/screen_orientation` 资源限定符)引入的回归:先竖屏再转横屏,且交互后 UI 整体放大约 1.6 倍(AutoSize 误选平板竖屏基准所致);验证系统解析 Manifest 属性不套用设备限定符,恢复为 `unspecified` 方案后两端方向与缩放均恢复正常 +- AutoSize 增加手机竖屏专用设计基准 390×844dp,避免手机端沿用平板竖屏 720dp 基准导致 UI 压缩 + +### 技术栈 +- Kotlin 1.6.21 +- Android Gradle Plugin 7.2.0 +- Gradle 7.3.3 +- minSdk 24, targetSdk 30, compileSdk 31 + +--- + +## [1.8.4] - 2024-12-02 + +### 新增 (Added) +- 完善 Claude Code 配置,添加 Android 开发权限 +- 创建 8 个快捷命令 (/build-debug, /install, /logs 等) +- 定制 CLAUDE.md 为 Android 项目专用开发指南 +- 添加 CONTRIBUTING.md 贡献指南文档 +- 添加 CHANGELOG.md 版本变更日志 + +### 改进 (Changed) +- 优化首页菜单交互体验 +- 改进 UI 界面显示效果 + +### 技术栈 +- Kotlin 1.6.21 +- Android Gradle Plugin 7.2.0 +- Gradle 7.3.3 +- minSdk 24, targetSdk 30, compileSdk 31 + +--- + +## [1.8.x] - 之前版本 + +### 主要功能 +- 国内出港业务模块 (货物收运、复磅称重、转运管理、出库装机) +- 国内进港业务模块 (舱单管理、卸机入库、出库提货、移库管理) +- 国际出港业务模块 (国际货物收运、板箱组装、ULD容器管理) +- 国际进港业务模块 (国际舱单管理、报文解析、理货管理) +- 航班管理模块 (航班查询、航班统计) +- 货物追踪模块 (货物状态追踪、运输日志) +- 监装监卸模块 (监装监卸管理) +- PDA功能模块 (PDA专用功能) +- 蓝牙打印模块 (佳博打印机集成) + +### 核心架构 +- MVVM 架构模式 +- 组件化模块设计 (11个业务模块 + 1个基础库) +- DataBinding 双向绑定 +- Kotlin Coroutines + Flow 异步处理 +- ARouter 模块间通信 +- Retrofit + OkHttp 网络请求 +- Glide 图片加载 +- MPAndroidChart 图表展示 + +### 开发基础设施 +- BaseActivity/BaseBindingActivity 基类 +- BaseViewModel/BasePageViewModel 基类 +- CommonAdapter + BaseViewHolder 列表适配 +- PadSearchLayout/PadDataLayout 自定义组件 +- 完整的 Kotlin 扩展函数库 +- DataBinding 适配器集合 + +--- + +## 版本说明 + +### 版本号规则 +- **Major.Minor.Patch** (例: 1.8.4) +- **Major**: 重大架构变更或不兼容的 API 修改 +- **Minor**: 新功能添加,向下兼容 +- **Patch**: Bug 修复和小的改进 + +### 变更类型 +- **Added**: 新增功能 +- **Changed**: 功能改进或变更 +- **Deprecated**: 即将废弃的功能 +- **Removed**: 已移除的功能 +- **Fixed**: Bug 修复 +- **Security**: 安全性修复 + +--- + +## 未来计划 + +### 待开发功能 +- [ ] 优化数据同步机制 +- [ ] 增强离线模式支持 +- [ ] 改进用户权限管理 +- [ ] 优化蓝牙打印稳定性 +- [ ] 添加数据导出功能 +- [ ] 性能监控和分析 + +### 技术改进 +- [ ] 升级到 Kotlin 2.0 +- [ ] 迁移到 Jetpack Compose +- [ ] 优化构建速度 +- [ ] 完善单元测试覆盖 +- [ ] 添加 UI 自动化测试 + +--- + +**维护**: 本文档应在每次版本发布时更新。 +**负责人**: 项目维护团队 +**最后更新**: 2026-07-29 diff --git a/CLAUDE.md b/CLAUDE.md index 2ed8745..c687f81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -846,6 +846,396 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志 --- +## 5.5 寸手持端(手机版)双形态适配规范 + +项目需同时支持 **10 寸平板横屏** 与 **5.5 寸手机竖屏**。核心原则:**业务逻辑层完全复用,只重写布局**。 + +> **分支约定(2026-10-11 起)**:手机适配分支 `feature/phone-ui-adaptation` 已合并入 `main`,此后平板与手机**只在 `main` 上同时开发**,不再区分分支。 +> **业务以平板为准**:主流程交互、表单/筛选项、接口入参一律与平板一致,手机端只做 UI 适配——平板底部批量按钮在手机上可改为卡片级操作或 `PhoneBottomBar` 次按钮(`subAction1Text/subAction2Text`), +> 但不得新增平板没有的查询入参(如 `likeNo`)、不得改写平板的默认查询值(如默认当天日期)、不得少平板的表单项。 +> 平板新增/修改功能时,必须同步检查对应手机布局与该页全部弹框的手机变体。 + +### 核心机制:布局按资源限定符自动切换 + +``` +res/layout-sw600dp/activity_xxx.xml ← 平板(最小宽度 >= 600dp) +res/layout/activity_xxx.xml ← 手机(兜底,5.5 寸约 360~411dp) +``` + +两个变体**同名**,DataBinding 会生成同一个 Binding 基类 + 两个变体实现: + +``` +ActivityXxxBinding.java 公共基类(Activity 中引用的就是它) +ActivityXxxBindingImpl.java 手机变体 +ActivityXxxBindingSw600dpImpl.java 平板变体 +``` + +因此 **Activity / ViewModel / ViewHolder 的 `layoutId()`、`itemLayoutId` 全部保持原样,无需任何设备判断**。 + +### ⛔ 严禁:用「不同文件名 + 代码判断」切换布局 + +```kotlin +// ❌ 错误:会生成两个不同的 Binding 类 +override fun layoutId() = + if (DeviceUtil.isPhone()) R.layout.activity_xxx_phone else R.layout.activity_xxx +``` + +`activity_xxx_phone.xml` 生成的是 `ActivityXxxPhoneBinding`,而 `binding` 字段声明的类型是 `ActivityXxxBinding`。 +泛型擦除导致**编译期不报错**,但一进入该页面就 `ClassCastException` 崩溃。**必须用同名文件 + 资源限定符。** + +### 布局变体的强制约束 + +| 约束 | 说明 | +|------|------| +| **DataBinding 变量必须完全一致** | 两个变体 `` 中的 `` 名称与类型必须相同,否则编译失败 | +| **共用控件 id 必须双方都有** | Activity / ViewHolder 中引用的 id(如 `srl`/`rv`/`checkIcon`/`iv_icon`)两个变体都要保留 | +| **单侧独有的 id 自动变 `@Nullable`** | 只在一个变体存在的 id,Binding 基类中会是可空字段,Kotlin 侧强制 null 检查(编译器自动保障,不用担心漏改) | + +### 已改造的基础设施 + +| 文件 | 作用 | +|------|------| +| `module_base/.../util/DeviceUtil.kt` | 设备形态判定。按 `smallestScreenWidthDp >= 600` 区分平板/手机;读 `Resources.getSystem()` 以绕开 AutoSize 对 density 的改写,保证判定稳定 | +| `module_base/BaseActivity.kt` | `applyDeviceOrientation()` 按形态锁方向(手机竖屏 / 平板横屏)。Manifest 统一写 `unspecified` 配合它使用。子类覆写 `lockOrientationByDevice()` 返回 false 可跳过 | +| `module_base/base/BaseBindingActivity.kt` | 注释固化变体约定(`layoutId()` 不做设备分支) | +| `module_base/MyApplication.kt` | **AutoSize 增加手机竖屏基准 390×844**(见下节) | +| `module_base/res/values/colors.xml` | 手机端配色 `phone_*` 系列 | +| `module_base/res/drawable/bg_phone_*.xml` | 手机端卡片、搜索框、状态标签、主按钮背景 | +| `module_base/res/layout/title_tool_bar.xml` | 手机版公共标题栏(平板版已移至 `layout-sw600dp/`) | + +`DeviceUtil` 用法: + +```kotlin +DeviceUtil.isPhone() // 是否手机 +DeviceUtil.isTablet() // 是否平板 +DeviceUtil.describe() // "PHONE(sw=411dp, force=AUTO)",日志排查用 +DeviceUtil.forceMode = DeviceUtil.ForceMode.PHONE // 调试:在平板上强制走手机布局 +``` + +> `DeviceUtil.layout(padId, phoneId)` 仅用于**极少数**确实需要代码级判断的场景(如非 DataBinding 的布局)。 +> **常规页面一律用资源限定符,不要调用它。** + +### ⚠️ 屏幕方向:Manifest 一律写 `unspecified`,由 `BaseActivity` 按形态锁定 + +**症状(改造前)**:手机上打开页面(开屏页最明显)先显示横屏,约 1 秒后才转竖屏,伴随 Activity 重建。 + +**原因**:`android:screenOrientation` 由系统在**创建 Activity 窗口时**生效,此时业务代码还没执行。 +Manifest 写死 `userLandscape` 时,即使 `BaseActivity` 在 `super.onCreate` 之前 +`setRequestedOrientation(PORTRAIT)`,也只能在横屏窗口已建好之后再纠正 → 必然闪一下。 + +**做法**:Manifest 统一写 `unspecified`,让系统按设备**当前物理方向**建窗口 +(平板横屏摆放 → 横屏起,手机 → 竖屏起,两端起始方向本就正确), +`BaseActivity.applyDeviceOrientation()` 再按形态锁定,不产生二次纠正: + +```xml + +``` + +全项目 192 处已统一(`module_p` 等 24 处本就是 `portrait` 的 PDA 页面保持不变)。 + +#### ⛔ 别用 `@integer/screen_orientation` 这类资源引用(已验证行不通) + +一度改成 `android:screenOrientation="@integer/screen_orientation"` + `values/`=portrait、 +`values-sw600dp/`=userLandscape,**看似优雅,实测是错的**: + +系统解析 Manifest 属性时**用的是默认配置,不套用设备限定符**,所以 `values-sw600dp/` 永远不会被选中, +所有 Activity 实际都拿到了 `values/` 的竖屏值。后果是平板端: + +1. 先竖屏、随后被 `BaseActivity` 纠正成横屏 —— 反而把闪屏问题搬到了平板上 +2. **AutoSize 在竖屏配置下选了平板竖屏基准 720dp**(density=1280/720≈1.78,正常应为 1280/1152≈1.11) + → 交互几次后整个 UI 放大约 1.6 倍 + +> 验证方法(可复现):把 `values/` 与 `values-sw600dp/` 的值**互换**,并临时让 +> `lockOrientationByDevice()` 返回 false(排除运行时纠正),装到平板上看首屏方向。 +> 结果是平板取了 `values/` 的值 → 证明限定符被忽略。 + +### ⚠️ AutoSize 设计基准必须区分手机(易漏且后果严重) + +项目使用 AndroidAutoSize,`MyApplication.initAutoSizeConfig()` 按方向+设备切换设计尺寸: + +| 场景 | 设计基准 | +|------|----------| +| 横屏(平板) | 1152 × 720 dp | +| 竖屏 + 手机 | **390 × 844 dp**(对应 5.5 寸设计稿画布) | +| 竖屏 + 平板 | 720 × 1280 dp | +| PictureSelector | 360 × 480 dp | + +**若手机沿用平板竖屏的 720dp 基准,手机上所有 dp 会被压缩约一半,布局全部错位。** + +### 手机版 UI 规范(依据 `documents/5.5寸手持端设计文件/` 设计稿) + +| 部位 | 规范 | +|------|------| +| 画布基准 | 390 × 844 dp | +| 顶栏 | 高 48dp,`@color/phone_primary`(#2563EB),左侧「‹ 返回」,标题居中 18sp 加粗 | +| 页面背景 | `@color/phone_bg` (#F3F4F6) | +| 列表卡片 | `@drawable/bg_phone_card`,圆角 12dp,`marginHorizontal=16dp`,`marginTop=12dp`,`padding=14dp` | +| 卡片头行 | 圆形勾选框 20dp(`radiobtn_checked_style`/`radiobtn_unchecked_style`)+ 运单号 15sp 加粗 + 右侧状态标签 | +| 卡片字段 | 两列网格,13sp,`@color/phone_text_body`,行距 8dp | +| 状态标签 | `bg_phone_tag_green`(已完成)/ `bg_phone_tag_gray`(未完成),11sp | +| 搜索行 | 搜索框 40dp 高 + `bg_phone_search`,内含放大镜 + 输入 + 扫码;右侧筛选按钮 40dp | +| 状态 Tab | 高 42dp,选中文字 `phone_primary` + 底部 28×3dp 指示条 | +| 底部操作条 | 白底 + `elevation=8dp`,全选 + 统计(13sp/11sp 两行)+ 主按钮 `bg_phone_btn_primary` | +| 筛选弹层 | `FrameLayout` 遮罩 `#80000000` + 底部白色面板,重置 / 确定各占一半 | + +### 手机版页面改造步骤 + +1. **移动平板布局**:`git mv res/layout/activity_xxx.xml res/layout-sw600dp/activity_xxx.xml`(item 布局同理) +2. **新建手机布局**:在 `res/layout/` 下创建**同名**文件,保持相同 `` 与共用控件 id +3. **Kotlin 侧不动**:`layoutId()`、`itemLayoutId`、ViewHolder 全部保持原样 +4. **纯 UI 交互**(Tab 切换、弹层显隐)写在 Activity,不污染 ViewModel;状态过滤优先复用 ViewModel 既有字段 +5. 手机布局顶部 `` 不变(变体自动切换) +6. **弹框(BaseDialogModel)同样要适配**:dialog 布局照样走同名双变体(Pad 版移 `layout-sw600dp/`、 + 手机版在 `layout/` 写设计稿的底部弹层样式),弹出位置在构造参数按形态切换: + `BaseDialogModel(if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER)` + (参考 `IntExpArriveResetDialogModel` + `dialog_int_exp_arrive_reset.xml` 双变体)。 + 页面适配完要**逐个排查该页所有 DialogModel**,漏掉的会在手机上弹出 Pad 居中样式 +7. **回到手机端首页加菜单入口**(最易遗忘,见下节)——不加的话页面在手机上根本进不去 + +### 手机端首页菜单入口(适配完必须加,否则没入口) + +手机端首页是 `module_p` 的 `PDAEnterActivity`(手持机首页),底部四个 Tab 对应: + +| Tab | ViewModel | 位置 | +|-----|-----------|------| +| 国内 | `GnViewModel` | `module_p/.../ui/enter/gn/` | +| 国际 | `GjViewModel` | `module_p/.../ui/enter/gj/` | + +**菜单项是手写清单,不是接口下发**。权限机制只做过滤:登录返回的权限串存在 +`SharedPreferenceUtil.getString(Constant.Share.authList)`(一个逗号分隔的长字符串), +菜单项按 `authList.contains(bean.key)` 决定显不显示。 + +所以「适配完页面但手机首页看不到入口」的原因通常**不是没权限,而是清单里没加**。 +排查顺序:先确认权限串里有对应 key,再去看清单。 + +```bash +# 看当前登录账号实际拥有的权限串 +adb -s shell "run-as com.lukouguoji.aerologic \ + cat /data/data/com.lukouguoji.aerologic/shared_prefs/data.xml" | tr ',' '\n' | grep -oE "App[A-Za-z]+" | sort -u +``` + +加一项就是加一行 —— `ActionBean` 的第 4 个参数填 ARouter 路由即可,无需写 when 分支 +(`module_p` 只依赖 `module_base`,看不到 `module_gjc` 等业务模块,跨模块只能走路由): + +```kotlin +ActionBean( + "出港移库", + R.drawable.gjc_yi_ku_icon, // 图标复用 Pad 端同款 + Constant.AuthName.GjcYiKuListActivity, // 权限串,与 Pad 端菜单保持一致 + ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE // 有 route 就走通用跳转 +), +``` + +两点注意: + +- **权限串与路由都要和 Pad 端菜单对齐**:以 `app/.../HomeFragment.kt` 里该权限对应的 + `ARouter.build(...)` 为准。同一业务常有新旧两个 Activity(如移库的 `GjcYiKuListActivity` + 与 `IntExpMoveActivity`),Pad 菜单实际跳的才是要适配的那个 +- **图标**:`module_p` 看不到 `module_gjc` 的资源,把 Pad 图标 PNG 复制一份到 + `module_p/src/main/res/drawable-xxhdpi/`(该模块既有 4 个图标就是这么放的,同名不同密度桶可共存) + +**示例:状态 Tab 零 ViewModel 改动**(`IntExpMoveActivity` 已实现) + +```kotlin +// Activity 中新增纯 UI 方法,写入 ViewModel 既有的 moveState 字段("" 全部 / "0" 未移库 / "1" 已移库) +fun switchTab(state: String) { + if (viewModel.moveState.value == state) return + viewModel.moveState.value = state + viewModel.searchClick() // 复用原有查询链路 +} +``` + +```xml + +android:textColor="@{`0`.equals(viewModel.moveState) ? @color/phone_primary : @color/phone_text_body}" +``` + +### 手机版公共组件库(`module_base/.../ui/weight/phone/`) + +7 个组件覆盖设计稿中反复出现的部件,**新页面优先复用,不要重写 XML**。属性均为无命名空间写法, +DataBinding 适配器统一放在 `PhoneWidgetKtx.kt`(同名属性按控件类型解析,与平板端 Pad* 组件互不冲突)。 + +| 组件 | 用途 | 关键属性 | +|------|------|----------| +| `PhoneSearchBar` | 顶部搜索行:搜索框 + 扫码 + 圆形筛选按钮 | `hint`、`value`(双向)、`showScan`、`showFilter`、`upperCase`、`setOnScanClickListener`、`setOnFilterClickListener`、`setRefreshCallBack` | +| `PhoneStatusTab` | 状态 Tab 条(文字 + 角标,选中态下划线) | `tabs`(List\)、`counts`(List\)、`value`(双向)、`setOnTabChanged` | +| `PhoneDataLayout` | 表单/筛选项,`PhoneDataLayoutType` = `INPUT`/`DATE`/`SPINNER` | `title`、`hint`、`type`、`value`(双向)、`list`、`required`、`enable`、`setRefreshCallBack` | +| `PhoneFilterPanel` | 底部筛选弹层(遮罩 + 面板 + 重置/确认) | `title`、`panelVisible`、`setOnResetClick`、`setOnConfirmClick`、`setOnDismissClick` | +| `PhoneBottomBar` | 底部操作条:全选 + 已选统计 + 主按钮 | `allChecked`、`countText`、`actionText`、`setOnAllCheckClick`、`setOnActionClick` | +| `PhoneStatBox` | 卡片内 2~4 列统计框(未传 label 的列自动隐藏) | `label1..4`、`value1..4` | +| `PhoneKvItem` | 详情页字段项(灰标签 + 数值),`tag=true` 转高亮标签(占位符 "-"/"--" 不套底) | `title`、`value`、`tag`、`tagColor`("orange" 默认/"green") | +| `PhoneFormRow` | 行式表单项(左标签+右值+分隔线),`PhoneFormRowType` = `INPUT`/`SELECT`(内置底部选择面板)/`DATE`/`TEXT`(只读右对齐,空值 "--") | `title`、`hint`、`type`、`value`(双向)、`list`、`required`、`enable`、`numeric`、`setRefreshCallBack` | + +`PhoneBottomBar` 追加 `actionDanger` 属性:true 时主按钮转浅红底红字 + 垃圾桶图标(ULD 管理批量删除)。 +新增资源:`bg_phone_btn_danger`、`bg_phone_fab`(圆形浮动 + 按钮)、`bg_phone_tag_red_light`(浅红标签)、 +`ic_phone_trash`、`ic_phone_edit`、色值 `phone_danger_bg/text`。 +出港计重追加:`bg_phone_btn_weak`(浅蓝弱按钮,卡片内提前运抵)、`bg_phone_card_top_accent` +(卡片顶部主色描边条)、`ic_phone_clipboard`(FAB 剪贴板)、`ic_phone_weight`(砝码图标,可 tint)。 +出港查询追加:`ic_phone_package`(件数)、`ic_phone_tag`(特码)、`bg_phone_dot_blue/gray`(状态圆点); +`PhoneFilterPanel` 内容插槽已内置 ScrollView(筛选项超屏时收缩滚动,底部按钮始终可见—— +9 个筛选项曾把「重置/确认」挤出屏幕)。 +出港仓库追加:`bg_phone_btn_disabled`(灰底禁用按钮,与 `bg_phone_btn_weak` 同 10dp 圆角, +卡片操作按钮禁用态配 `phone_tag_gray_text` 文字)。 +进港移库追加:`bg_phone_btn_green`(绿色主按钮,拍照上传「确认提交」)、`bg_phone_card_header_orange` +(橙色详情卡表头,异常照片)、`bg_phone_photo_add`(虚线加号槽位)、色值 `phone_amber`。 + +⚠️ **弹层与浮动按钮的层级**:`PhoneFilterPanel` 默认无 elevation,页面里有带 elevation 的浮动按钮(FAB)时 +会穿透到弹层之上——给弹层实例加 `android:elevation="8dp"`(大于 FAB 的 6dp)。 + +**`PhoneFilterPanel` 自带内容插槽**:把 `PhoneDataLayout` 直接写在其标签内即可,组件会自动移入面板中部 +并施加 20dp 项间距,无需关心内部层级: + +```xml + + + + + +``` + +#### ⚠️ 复合自定义 View 必须屏蔽子控件状态恢复(已踩坑) + +同一页面放多个 `PhoneDataLayout`/`PhoneStatBox` 时,它们的内部子控件(`et`/`tv`/`spinner`…)**共用同一套 id**。 +系统按 id 保存/恢复视图状态会跨实例串档 —— 实测表现为:DATE 项的默认日期被邻近输入框恢复的空文本清空, +并通过双向绑定把 ViewModel 字段也写成了空值(筛选条件静默丢失)。 + +因此每个含内部 id 的手机版组件都重写了: + +```kotlin +override fun dispatchSaveInstanceState(container: SparseArray) = dispatchFreezeSelfOnly(container) +override fun dispatchRestoreInstanceState(container: SparseArray) = dispatchThawSelfOnly(container) +``` + +**新增同类组件时必须照做**(取值由 ViewModel + DataBinding 持有,本就不需要视图级恢复)。 + +### 已完成 / 待办 + +- ✅ **已完成**:适配框架 + 手机端公共资源 + 7 个手机版公共组件 +- ✅ **已完成页面**: + - 「国际出港移库」`IntExpMoveActivity`(框架参考页);详情/编辑与平板共用 `IntExpMoveEditActivity` + (手机布局为设计稿「运单详情」样式,卡片「查看详情」/「编辑」分别对应平板点击卡片/侧滑编辑) + - 「国际出港出库交接」`IntExpOutHandoverActivity` + 「板箱详情」`IntExpOutHandoverDetailActivity` + (公共组件首个完整落地页,双端实机验证通过) + - 「ULD管理」`UldListActivity` + `UldEditActivity`(app 模块首个适配页;一套编辑布局覆盖 + 新增/修改/详情三态;批量删除为前端循环调用单条接口;状态三态/来源等 5 项后端缺口见问题清单 #7~#11) + - 「出港计重」全链路 4+1 页:`GjcWeighingListActivity`(待计重列表,三格统计 + 卡片单票提前运抵 + + FAB 进记录)、`GjcWeighingStartActivity`(开始计重,托盘数量/自重按设计稿手输)、 + `GjcWeighingRecordListActivity`(计重记录)、`GjcWeighingRecordDetailsActivity`(计重明细, + isEditMode 复用为手机全屏「修改记录」覆层)+ 手机专属新页 `GjcWeighingWbDetailActivity`(运单详情)。 + 合并搜索框只分流到平板同名入参:含字母→fno、其余→wbNo; + checkIn 过滤等 7 项后端缺口见问题清单 #12~#18 + - 「出港运抵」主列表 + 3 个手机专属新页:`IntExpArriveActivity`(待运抵/已运抵 Tab,客户端按 + `declareStatus` 是否为空拆分数据集,卡片长按浮层:待运抵→补充、已运抵→重置+回执)、 + `IntExpArriveSubOrderActivity`(分单明细,数据直接复用主列表已加载的 `haWbList`,零新增请求)、 + `IntExpArriveReceiptActivity`(分单回执只读页,复用 `haWbList[].response`;主单「回执」与平板侧滑同走 + `getResponse` → 共用 `CustomsResponseActivity`)、 + `IntExpArriveSupplementActivity`(补充信息,参照 Pad 端国际进港「收发货人信息」批量应用模式, + 后端接口不存在,实机 404 已确认,见问题清单 #19)。 + Tab 过滤两个坑:① **RecyclerView 里 `View.GONE` 不会让 item 塌陷为 0 高度**(不同于普通 + ViewGroup),必须在数据层整体 `refresh()` 过滤后的子集给 adapter,不能给 item 根节点加 + visibility 绑定(实机截图验证过,见「常见编译错误速查」);② 通过 `Intent.putExtra(Serializable)` + 传递的 `GjcHaWb`/`GjcMaWb`,其 `checked`/`showMore`/`actionMenuVisible` 字段标了 + `@Transient`(= JVM `transient`,Java 序列化会跳过),反序列化后为 null,直接 + `.checked.set(...)` 会崩溃——接收方 `initOnCreated` 必须对列表 `.map { it.copy() }` 补上。 + 2026-07-30 review 修正:状态重置弹框改手机底部弹层(弹框双变体首例,见改造步骤第 6 条)、 + 主列表/回执页补空态、分单浮层补「回执」、分单统计加分母、Tab 角标初始 0 + - 「提取出库」`IntImpPickUpDLVActivity`(module_gjj,类型2 多选批量页 + 手机专属详情新页 + `IntImpPickUpDetailActivity`(Intent 传 Serializable bean 零请求)。待出库/已出库 Tab 绑 + **文档定义入参 `outState`**(0/1,抓包证实携带,效果待内网 #29)+ 角标双 pageQueryTotal; + 双形态卡片按 `isPickedUp`(dlvTime 非空)切换;手机进入时清空平板默认「当天缴费日期」查全量; + 缺口 #28:无 fdate 入参、出参无车牌(提货车辆)/目的港/业务类型。 + ⚠️ 踩坑:`checkAllClick` 全选后底部「已选 N 项」不更新——批量勾选不走 FlowBus 单卡事件, + 必须在 checkAllClick 里同步 selectedCountText) + - 「进港查询」`IntImpQueryActivity`(module_gjj;06 出港查询的进港镜像:三格统计 + 总票数/已入库/已出库为已加载数据客户端计数(`pageQueryTotal` 无分项出参,缺口 #30), + 卡片状态前端推断:`dlvTime` 非空=已出库(绿)/`inDate` 非空=已入库(橙)/兜底未入库(灰); + 筛选弹层 9 项**入参全部经 api-doc 核对真实存在**(分页参数名 `page`/`limit`,品名参数名 + `goods`);详情页与运单追踪复用 08/06 已适配页零新增;平板侧滑筛选面板代码已判空守卫。 + ⚠️ 踩坑:代理人绑定不能写 `bean.agentName ?? bean.agentCode`——转换器把 JSON null 归一成 + 空串,`??` 只判 null 不判空串,须用 Bean 计算属性 `agentDisplay`(06 出港查询同步修正)) + - 「进港移库」`GnjMoveStashListActivity`(app 模块,**2026-08-03 归属更正后重做**:设计稿 11 + 实际归属**国内进港移库 DomImpMove**(后端 8 接口齐全、Pad 首页「国内进港→进港移库」既有页面), + 最初误判为国际进港并按 IntExpMove 约定预接了不存在的 IntImpMove 模块(原缺口 #31),该套 + module_gjj 前端实现已整体删除。现行实现:主列表双形态(Pad 布局移 layout-sw600dp 零改动), + 待移库/已移库 Tab 走 **`search`/`searchMoved` 双端点**(`searchMoved` 出参无移库标识,前端按 + 端点给 bean 打 `isMoved`),Tab 角标 = 两端点各调一次 limit=1 取分页 total(`initPhoneExtras` + 开关,仅手机发起);手机专属新页 `GnjMoveStashDetailActivity`(Intent 传 bean 零请求,三卡片: + 基本信息/移库信息(仅已移库)/异常照片)+ `GnjMoveStashPhotoActivity`(进入先调 `detail` 回填 + 已有照片——`searchMoved` 出参无 pic 三字段 #34,提交走 **`modify` 最小体** + `{mawbId,remark,picNumber,pic,originalPic}`,与平板移交编辑页同链路);批量移库复用平板 + `moveStashClick`(`move`:fid+ids)。手机首页入口在**国内 Tab**(`GnViewModel`,权限串 + `GnjYiKu=AppDomImpMove`,需补通用 route 跳转逻辑,图标从 module_gnj 复制); + GnjMoveStashListActivity 补 @Route `/app/GnjMoveStashListActivity`。 + 2026-08-03 内网真实数据双端验证通过;筛选 A/B:`wbNo`/`awbType` 生效, + `fdate`/`fno`/`spCode` 被后端静默忽略(#32)、`opId`/`opDate` 恒 null(#33)) + - 「进港仓库」`IntImpStorageUseActivity`(module_gjj,07 出港仓库的进港镜像;**首页菜单实际跳转页, + 旧版 `GjjWareHouseActivity` 路由已注释弃用**。差异:卡片/筛选用始发站(fdep 入参)替代目的站; + 搜索框「运单号/航班号」分流(含字母→fno、其余→wbNo,抓包证实);筛选 5 项无入参(缺口 #25, + 比出港多 2 项);连带首次适配进港详情 `IntImpQueryDetailsActivity` + 3 Fragment(报文区 3 项: + 原始舱单/理货报告/海关放行,件重区直取 inPc/inWeight 无需求和,detail keys 以平板绑定为准 + + dep/dest1/dest/unNumber/opDate 按出港推断待实测 #27);修改库位手机弹层「原库位」只列未出库 + 库位(沿用平板校验);**前端接口前缀是 `IntImpStorage` 不是 `IntImpStorageUse`**,api-doc 按 + 后者搜不到) + - 「出港仓库」`IntExpStorageUseActivity`(嵌套多选列表页型的首个适配:平板「勾选主/子列表 + + 底部批量操作」在手机上改为**卡片级四按钮**(清仓/修改库位/出库/入库,已清仓置灰),语义等价 + 「勾选该主单+全部库位」直接复用既有 performXxx 链路,ViewHolder 对双侧独有 id 全判空; + 三格统计 总票数/未清仓/已清仓(后两格 = 同条件 + `clearNormal=0/1` 各调一次 pageQueryTotal, + 过滤是否生效待内网 A/B,缺口 #24);筛选弹层 8 项中运单类型/业务类型/品名(中) 为后端未定义 + 入参(缺口 #23,预留传参);三个操作弹框双变体化 + 手机版修改库位弹层加「原库位」单选; + 出库确认 AlertDialog → ConfirmDialogModel;详情与运单追踪完全复用出港查询已适配页,零新增页面) + - 「出港查询」`GjcQueryActivity`(列表:三格统计 总票数/已入库/已离港 + 卡片状态标签, + 状态为前端推断:`fclose` 非空=已离港/`opDate` 非空=已入库;筛选弹层 9 项,日期起止与代理 + 从平板搜索行并入,特码改下拉 `getSpecialCodeList(flag=1, ieFlag="")`;分项统计为已加载 + 数据客户端计数,`isFclose` 实测被后端忽略,缺口 #21)+ `GjcQueryDetailsActivity`(3 Tab + 复用 ViewPager2+Fragment,手机变体 PhoneKvItem 网格;入库件数/重量= warehouseList 求和; + 锁定状态/计费重量后端无字段,缺口 #22)+ 「运单追踪」`LogDetailActivity`(app 模块公共页 + 双变体首例:手机竖排时间线由 Activity `buildVerticalSteps` 动态构建,平板横向步骤条不动; + 手机标题按形态切「运单追踪」)。平板侧滑筛选面板代码已判空守卫(手机变体无 filter_panel + include,Binding 字段为 @Nullable) +- ✅ **设计稿 11 个入口(01~11)已于 2026-07-31 全部适配完成** +- ⏳ **待办**: + - `PhoneSearchBar` 接入 AutoQuery 联想(`AutoQueryManager` 目前只对接了 Pad 系组件) + - 联调新发现的后端缺口 #35~#41(ULD uld 搜索 500、IntImpStorage 列表过滤全失效、已移库无 mawbId + 无法补传照片等)待后端处理,详见问题清单 +- ✅ **2026-10-09 后端反馈联调完成**:问题清单 #1~#34 逐项内网直调 + 模拟器端到端验证,结论写入清单 P 列。 + **后端口径:手机筛选一律与平板端查询参数保持一致**(不为手机新增入参);分项统计统一取 + `pageQueryTotal` 出参(`ManifestTotalDto` 已补齐分项字段),不要再按状态多调 total。 + 最新字段语义见 `.claude/skills/phone-adapt/references/api-doc.md` 末节 +- 📋 **接口对接**:适配中发现的后端接口缺口统一记录在 `documents/后端接口对接问题清单.xlsx` + (按模块/页面/接口定义/接口地址索引,与后端对接的正式文档,每页适配完必须维护); + 接口入参/出参一律以 api-doc MCP 为准核对,流程见 + `.claude/skills/phone-adapt/references/api-doc.md`——**后端对不认识的参数静默忽略**, + 猜参数名会得出「后端不支持」的误判(出库交接 `hoState`→`handoverState` 已踩过) + +### 调试便利:直接拉起页面 + +`app/src/debug/AndroidManifest.xml`(**仅 Debug 生效,不进 Release 包**)通过 `tools:replace` 将适配调试页临时置为 `exported="true"`,免去每次登录再逐级点入: + +```bash +adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.page.move.IntExpMoveActivity +adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.activity.IntExpOutHandoverActivity +``` + +新增调试页时在该文件追加对应 `` 即可。 + +> 模拟器通常连不上内网服务(提示「接口访问失败」),列表为空。若需验证卡片/详情的还原度, +> 可在 Activity 里临时注入几条样例 Bean(`binding.rv.commonAdapter()?.refresh(list)`)截图比对,**验证后务必删除**。 + +**参考文件**:`module_base/.../util/DeviceUtil.kt`、`module_gjc/res/layout/activity_int_exp_move.xml`(手机)、`module_gjc/res/layout-sw600dp/activity_int_exp_move.xml`(平板) + +--- + ## Import 路径速查 ### 基类与常用类 @@ -877,6 +1267,7 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志 | 类 | 正确路径 | |----|----------| | `DictUtils` | `com.lukouguoji.module_base.util.DictUtils` | +| `DeviceUtil` | `com.lukouguoji.module_base.util.DeviceUtil` | | `MediaUtil` | `com.lukouguoji.module_base.util.MediaUtil` | | `UploadUtil` | `com.lukouguoji.module_base.util.UploadUtil` | | `KeyValue` | `dev.utils.app.info.KeyValue` | @@ -903,6 +1294,18 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志 | 资源引用不存在 | drawable/color/string 缺失 | 先检查资源是否存在,不存在则创建或用已有资源 | | `View.VISIBLE` 报错 | 未导入 | XML `` 中加 `` | | `textStyle` DataBinding 报错 | 不支持 | 用固定值 `android:textStyle="bold"` | +| 手机版页面一进入就 `ClassCastException` | 用了「不同文件名 + 代码判断」切换布局,生成了两个 Binding 类 | 改用同名布局 + 资源限定符(`layout/` 与 `layout-sw600dp/`),见「5.5 寸手持端双形态适配规范」 | +| 布局变体编译报 `variable not found` | 两个变体的 `` 中 `` 不一致 | 手机与平板变体的变量名/类型必须完全相同 | +| 手机上元素小一半 / 布局错位 | AutoSize 仍按平板 720dp 基准换算 | 确认 `MyApplication.initAutoSizeConfig()` 中手机竖屏走 390×844 分支 | +| 手机版筛选项默认值(如日期)自动被清空 | 多个复合自定义 View 内部子控件 id 相同,系统按 id 恢复状态时跨实例串档 | 组件重写 `dispatchSaveInstanceState`/`dispatchRestoreInstanceState` 为 `dispatchFreezeSelfOnly`/`dispatchThawSelfOnly`(见「手机版公共组件库」) | +| 手机上页面先横屏再转竖屏(闪一下) | Manifest 写死 `screenOrientation="userLandscape"`,系统建窗口时已按横屏,代码纠正为时已晚 | 改成 `android:screenOrientation="unspecified"`,由 `BaseActivity` 按形态锁定(见「屏幕方向」小节) | +| 平板先竖屏再转横屏 + 交互后 UI 放大约 1.6 倍 | Manifest 用了 `@integer/screen_orientation` 资源引用;系统解析 Manifest 不套用限定符,全部取到竖屏值,AutoSize 随之选了平板竖屏 720dp 基准 | Manifest 改回 `unspecified`(见「屏幕方向」小节的 ⛔ 说明) | +| Tab/分类切换后列表顶部留一大块空白,卡片数量却不对 | 给 item 根节点加了 `visibility="@{tab条件 ? VISIBLE : GONE}"`——RecyclerView/LinearLayoutManager 不会像普通 ViewGroup 那样让 GONE 的子项自动塌陷为 0 高度,仍按测量高度参与滚动区域计算 | 不要用 item 级 visibility 做过滤;在 ViewModel 里自行维护完整列表,Tab 切换时把过滤后的子集整体 `commonAdapter()?.refresh(filteredList)` 给 adapter | +| 通过 `Intent.putExtra(Serializable)` 传递的 Bean,切换其 `checked`/`showMore` 等状态时崩溃 `NullPointerException: ... on a null object reference` | 这些字段标了 `@Transient`(= JVM `transient`,本意是避免 Gson 网络请求序列化 `ObservableBoolean`),但 Java 对象序列化(Intent 传递走的机制)会跳过 `transient` 字段的恢复,反序列化后是 null | 接收方 `initOnCreated` 里对列表做 `.map { it.copy() }`——`.copy()` 会重新走一遍类体初始化逻辑,用全新 `ObservableBoolean(false)` 补上 | +| 布局构建报 `Error: The string "--" is not permitted within comments` | XML 注释中出现连续双横线(如写"兜底 \"--\""),XML 规范禁止 | 注释里改用文字描述(如「兜底双横线」),代码绑定表达式中的 `"--"` 不受影响 | +| 多选页手机版点「全选」后底部「已选 N 项」不更新(单卡勾选正常) | 批量勾选在 `checkAllClick` 里直接 set,不走 FlowBus 的单卡 EVENT_CHECK_CHANGED 事件,`selectedCountText` 只在事件回调里更新 | `checkAllClick` 末尾自行同步 `selectedCountText`(平板布局不绑定该字段,无影响) | +| 手机版数字输入框(`PhoneFormRow` 双向绑 Double 的 `xxxStr`)输入 "18" 变成 "81" | getter 由 Double 反推字符串("1" → "1.0"),与输入框文本不等触发 `setText`,光标回到开头 | Bean 里保留输入原文(`@Transient` 私有 String),getter 优先返回原文(参考 `GjcCheckInRecord.trayTotalWeightStr`) | +| XML 里 `bean.a ?? bean.b` 兜底不生效,字段明明有值却显示占位符 | 网络层转换器把 JSON null 归一成空串 `""`,DataBinding 的 `??` 只判 null 不判空串,永远取到左侧的空串 | 在 Bean 上加计算属性做 `isNullOrEmpty` 双兜底(如 `agentDisplay`),XML 绑计算属性(进港查询/出港查询代理人已踩过) | --- @@ -968,10 +1371,16 @@ private fun loadDictLists() { ```xml + android:exported="false" + android:screenOrientation="unspecified" /> ``` +> `screenOrientation` **必须写 `unspecified`**,不要写死 `userLandscape`、也不要用资源引用。 +> 写死横屏会让手机端「先横屏、约 1 秒后转竖屏」闪一下;资源引用则会让平板端出问题 +> (详见下方「屏幕方向」小节)。 + 3. 在 `ARouterConstants` 注册路由(如需) 4. 标题栏统一用 ``,Activity 中 `setBackArrow("标题")` +5. **双形态布局**:需同时支持手机的页面,平板布局放 `res/layout-sw600dp/`、手机布局放 `res/layout/`,两者**同名**且变量与共用 id 一致。详见「5.5 寸手持端双形态适配规范」 ### 常见业务操作 @@ -1119,6 +1528,9 @@ Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail) - `ic_new_expand`(全部展开/收起)36dp + padding 4dp - **图标之间间距统一 `marginStart=16dp`**(禁止使用 8dp,会导致视觉过近) - 常用资源: `bg_white_radius_8`、`colorPrimary`、`text_normal`、`text_gray`、`color_bottom_layout` +- **双形态适配:逻辑与 UI 严格分离** — 手机版只重写布局,业务逻辑一律复用现有 ViewModel / ViewHolder;纯 UI 交互(Tab 切换、弹层显隐)写在 Activity,不下沉到 ViewModel +- **禁止用不同文件名切换布局** — 手机/平板布局必须同名 + 资源限定符,否则 `ClassCastException`(详见「5.5 寸手持端双形态适配规范」) +- 手机版资源统一使用 `phone_*` 颜色与 `bg_phone_*` drawable,禁止直接写死色值 ### 环境配置 @@ -1132,4 +1544,20 @@ Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail) 2. 资源引用错误 → 检查 drawable/color/string 是否存在 3. DataBinding 错误 → 检查 import、枚举值、View 类导入 4. suspend function 错误 → 在 `viewModelScope.launch` 中调用 -5. 仍有问题 → `./gradlew clean` 后重新构建 +5. 双形态布局问题 → 查「常见编译错误速查」中 3 条布局变体相关条目;可用 `DeviceUtil.describe()` 打日志确认当前形态 +6. 仍有问题 → `./gradlew clean` 后重新构建 + +### 双端验证命令 + +```bash +~/Library/Android/sdk/emulator/emulator -list-avds # 可用:Aerologic_Phone_5.5_API31(手机,首选)、Aerologic_Tablet(平板)、Medium_Phone_API_36.1 +adb -s <设备> shell wm size && adb -s <设备> shell wm density # 确认屏幕参数 +adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果 PHONE(sw=384dp) / TABLET(sw=800dp) +adb -s <设备> exec-out screencap -p > /tmp/shot.png # 截图比对 +``` + +> 手机端验证**首选 `Aerologic_Phone_5.5_API31`**:按真实交付手持机 1:1 定制——720×1440、5.51 寸(300dpi,sw=384dp)、 +> Android 12 / API 31(与 targetSdk 一致)、arm64-v8a,与实机的差异最小。`Medium_Phone_API_36.1`(1080×2400、API 36) +> 仅作兼容性参考,分辨率与系统版本都和交付机不符。 + +> 改动 `BaseActivity` / `BaseBindingActivity` / 公共组件后,**必须在平板上回归**,确认现有平板版行为不变。 diff --git a/README.md b/README.md index 462aa44..6e21cb0 100644 --- a/README.md +++ b/README.md @@ -1,301 +1,326 @@ -# AirLogistics - 航空物流信息管理系统 - -一款专业的航空物流管理Android应用,为机场货运站提供全流程数字化解决方案,涵盖国内外货物进出港、仓储管理、车辆调度等核心业务。 - -## 📋 项目信息 - -- **版本**: 1.7.9 (versionCode 79) -- **包名**: com.lukouguoji.aerologic -- **最低支持**: Android 7.0 (API 24) -- **目标版本**: Android 10 (API 30) -- **开发语言**: Kotlin + Java -- **架构模式**: MVVM + 组件化 - -## 🏗️ 架构设计 - -### 核心架构 - -``` -┌─────────────────────────────────────────────────────┐ -│ 应用层 (App) │ -├─────────────────────────────────────────────────────┤ -│ 国内出港 │ 国内进港 │ 国际出港 │ 国际进港 │ -│ (GNC) │ (GNJ) │ (GJC) │ (GJJ) │ -├─────────────────────────────────────────────────────┤ -│ 基础服务层 (module_base) │ -│ • MVVM基类 • 网络框架 • UI组件 • 工具类 │ -├─────────────────────────────────────────────────────┤ -│ 蓝牙打印 │ 图表库 │ 航班管理 │ 货物追踪 │ -│ (Printer) │ (Chart) │ (Hangban) │ (Cargo) │ -└─────────────────────────────────────────────────────┘ -``` - -### MVVM模式 - -- **View层**: Activity/Fragment + DataBinding -- **ViewModel层**: BaseViewModel / BasePageViewModel -- **Model层**: Repository + Retrofit API -- **数据流**: 协程(Coroutines) + Flow - -### 组件化架构 - -- **模块独立**: 通过`isBuildModule`参数控制模块独立运行或集成 -- **路由通信**: ARouter实现模块间页面跳转和通信 -- **事件总线**: FlowBus(基于Flow) + EventBus -- **依赖注入**: 基于ServiceLoader的服务发现机制 - -### 基础设施 - -#### 核心基类 -- `BaseActivity`: 协程支持、Loading管理、扫码功能、键盘控制 -- `BaseBindingActivity`: DataBinding自动绑定、ViewModel生命周期管理 -- `BaseViewModel`: Loading管理、Activity结果处理 -- `BasePageViewModel`: 分页列表专用,集成PageModel自动处理分页 -- `CommonAdapter + BaseViewHolder`: 统一列表适配器封装 - -#### 网络请求层 -- **ServiceCreator**: Retrofit实例创建,动态IP配置 -- **拦截器**: 自动添加Token、时间戳,统一错误处理 -- **扩展函数**: - - `launchCollect`: 无Loading的后台请求 - - `launchLoadingCollect`: 带Loading的关键操作 - - `toRequestBody`: Map/Bean自动转JSON - -#### 统一UI组件 -- **PadSearchLayout**: 搜索输入框组合控件(支持文本/数字/下拉/日期) -- **PadDataLayout**: 数据展示/编辑组合控件(标题对齐) -- **StatusView**: 状态栏占位View -- **统一样式**: 颜色、字体、圆角、间距的全局规范 - -## 🚀 技术栈 - -### 核心框架 -- **Kotlin**: 1.6.21 + Coroutines 1.6.0 -- **Jetpack**: ViewModel、LiveData、DataBinding -- **网络**: Retrofit 2.6.1 + OkHttp 3.12.12 -- **JSON**: FastJSON 1.2.73 + Gson 2.10.1 - -### UI相关 -- **Material Design**: Material Components -- **屏幕适配**: AutoSize 1.2.+ (横屏1152dp × 720dp) -- **下拉刷新**: SmartRefreshLayout 2.0.3 -- **图表**: MPAndroidChart (定制版) -- **弹窗**: XPopup 2.9.19 -- **图片**: Glide 4.15.1 + PictureSelector v3.11.2 - -### 功能组件 -- **路由**: ARouter 1.5.2 -- **扫码**: ZXing 2.2.9 -- **权限**: AndPermission 2.0.2 -- **打印**: 佳博SDK 2.0.4 -- **日志**: Timber 5.0.1 - -## 💼 核心业务模块 - -### 国内出港 (module_gnc) -货物收运、复磅称重、转运管理、出库装机、仓库管理、数据统计 - -### 国内进港 (module_gnj) -舱单管理、卸机入库、出库提货、移库管理、货物交接、实时查询 - -### 国际出港 (module_gjc) -国际货物收运、板箱组装、ULD容器管理、复磅称重、仓储移库 - -### 国际进港 (module_gjj) -国际舱单管理、报文解析、理货管理、出库操作、货物交接 - -### 支持模块 -- **航班管理** (module_hangban): 航班查询、航班统计 -- **货物追踪** (module_cargo): 货物状态追踪、运输日志 -- **监装监卸** (module_mit): 监装监卸管理 -- **PDA功能** (module_p): PDA专用功能 -- **蓝牙打印** (Printer): 佳博打印机SDK集成 - -## 📦 项目结构 - -``` -aerologic-app/ -├── app/ # 应用壳层,整合所有业务模块 -├── module_base/ # 核心基础库,提供所有通用能力 -│ ├── base/ # MVVM基类 -│ ├── http/ # 网络请求框架 -│ ├── ui/weight/ # 统一UI组件 -│ ├── ktx/ # Kotlin扩展函数 -│ ├── bean/ # 数据模型 -│ └── router/ # ARouter路由常量 -├── module_gnc/ # 国内出港业务 -├── module_gnj/ # 国内进港业务 -├── module_gjc/ # 国际出港业务 -├── module_gjj/ # 国际进港业务 -├── module_hangban/ # 航班查询管理 -├── module_cargo/ # 货物追踪查询 -├── module_mit/ # 监装监卸管理 -├── module_p/ # PDA专用功能 -├── Printer/ # 蓝牙打印模块 -├── MPChartLib/ # 定制图表库 -├── CLAUDE.md # 详细开发指南 -└── README.md # 项目说明文档 -``` - -## 🚀 快速开始 - -### 环境要求 - -- **IDE**: Android Studio Arctic Fox (2020.3.1) 或更高版本 -- **JDK**: 1.8 -- **Gradle**: 7.3.3 -- **Kotlin**: 1.6.21 - -### 依赖配置 - -如遇依赖下载问题: - -1. 下载 gradle-7.3.3-bin.zip - - 百度网盘: https://pan.baidu.com/s/18wsuGRlNxjMYbxLhBH9yeg - - 提取码: 1029 - -2. 配置Gradle: - - Settings → Build, Execution, Deployment → Build Tools → Gradle - - 解压并替换到 "Gradle user home" 目录 - -### 构建项目 - -```bash -# 克隆项目 -git clone [repository-url] -cd aerologic-app - -# 清理构建 -./gradlew clean - -# 构建Debug版本 -./gradlew assembleDebug - -# 构建Release版本(已签名) -./gradlew assembleRelease - -# 安装到设备 -./gradlew installDebug -``` - -### 组件化开发模式 - -支持模块独立运行调试: - -1. 编辑 `gradle.properties` -2. 设置 `isBuildModule=true` (独立模块) 或 `false` (集成模式) -3. Sync项目并运行对应模块 - -## ⚙️ 配置说明 - -### 网络配置 - -服务器地址配置位置: -- **文件**: `module_base/src/main/res/values/strings.xml` -- **主服务器**: `system_url_inner` -- **地磅服务器**: `weight_url` -- **运行时**: 可通过SharedPreferences动态修改IP地址 - -### 签名配置 - -Release版本签名信息: -- **KeyStore**: `key.jks` (项目根目录) -- **密码**: storePassword/keyPassword均为 `123321` -- **别名**: `key` - -### 网络安全 - -- **超时时间**: 30秒(连接/读取/写入) -- **认证方式**: Bearer Token(自动添加) -- **HTTP支持**: 配置在 `res/xml/network_security_config.xml` - -## 📚 开发文档 - -详细的开发指南请参考 **[CLAUDE.md](./CLAUDE.md)**,包含: - -- ✅ 完整的架构详解(基类、网络、UI组件) -- ✅ 标准开发模板(Activity、ViewModel、ViewHolder) -- ✅ 实际业务示例(列表页、详情页、编辑页) -- ✅ 开发检查清单(7步列表页、4步详情页、5步编辑页) -- ✅ DataBinding适配器(10+种) -- ✅ Kotlin扩展函数(20+种) -- ✅ 常见业务场景实现 - -## 🔄 Git工作流 - -### 分支策略 - -- **develop**: 主开发分支 -- **feature/xxx**: 功能开发分支 -- **release/x.x.x**: 发布分支 -- **hotfix/xxx**: 紧急修复分支 - -### 当前分支 - -- **开发分支**: feature/hefei -- **主分支**: develop(用于PR) - -### 提交规范 - -提交前确保: -- ✅ 代码编译通过 -- ✅ 无明显错误和警告 -- ✅ 遵循项目代码规范 - -## 📋 开发原则 - -1. ✅ **优先使用现有基类和封装**,避免重复造轮子 -2. ✅ **充分利用PadDataLayout和PadSearchLayout组件**,保证UI一致性 -3. ✅ **遵循统一的命名和目录组织规范** -4. ✅ **使用DataBinding简化代码**,减少findViewById -5. ✅ **利用扩展函数处理通用逻辑**(Toast、Dialog、权限等) -6. ✅ **保持架构一致性**,新增业务参考现有模块 - -## 🔐 特性亮点 - -### 1. 高度组件化 -- 业务模块完全解耦,支持独立开发、测试、发布 -- 通过ARouter实现模块间通信,无需直接依赖 - -### 2. 统一UI规范 -- 自定义PadSearchLayout、PadDataLayout组合控件 -- 全局统一的颜色、字体、圆角、间距规范 -- 丰富的DataBinding适配器,简化UI开发 - -### 3. 高效网络框架 -- 协程+Flow的现代化异步方案 -- 统一的请求封装(launchCollect/launchLoadingCollect) -- 自动的Loading管理和错误处理 - -### 4. 完善的基础设施 -- BaseActivity提供扫码、Loading、键盘控制等通用能力 -- BasePageViewModel自动处理分页逻辑 -- CommonAdapter统一列表适配器管理 - -### 5. 丰富的工具扩展 -- 20+ Kotlin扩展函数(Toast、Dialog、JSON、日期等) -- 10+ DataBinding适配器(图片加载、可见性、Shape等) -- 完整的业务开发模板和检查清单 - -## 🛠️ 技术特点 - -- **响应式编程**: Kotlin Coroutines + Flow -- **声明式UI**: DataBinding双向绑定 -- **函数式编程**: Kotlin扩展函数和高阶函数 -- **依赖注入**: 基于ServiceLoader的轻量级方案 -- **模块化路由**: ARouter组件化通信 -- **横屏适配**: AutoSize自动适配1152dp × 720dp -- **蓝牙打印**: 佳博打印机SDK深度集成 -- **二维码扫描**: ZXing高性能扫码 - -## 📄 许可证 - -本项目为企业内部系统,版权归所属公司所有。 - -## ⚠️ 注意事项 - -**重要**: 本项目包含敏感业务信息和配置,仅供授权人员使用,严禁外传。 - ---- - -**文档维护**: 本README提供项目概览和架构信息,详细开发指南请参考 [CLAUDE.md](./CLAUDE.md) +# AirLogistics - 航空物流信息管理系统 + +一款专业的航空物流管理Android应用,为机场货运站提供全流程数字化解决方案,涵盖国内外货物进出港、仓储管理、车辆调度等核心业务。 + +## 📋 项目信息 + +- **版本**: 1.7.9 (versionCode 79) +- **包名**: com.lukouguoji.aerologic +- **最低支持**: Android 7.0 (API 24) +- **目标版本**: Android 10 (API 30) +- **开发语言**: Kotlin + Java +- **架构模式**: MVVM + 组件化 + +## 🏗️ 架构设计 + +### 核心架构 + +``` +┌─────────────────────────────────────────────────────┐ +│ 应用层 (App) │ +├─────────────────────────────────────────────────────┤ +│ 国内出港 │ 国内进港 │ 国际出港 │ 国际进港 │ +│ (GNC) │ (GNJ) │ (GJC) │ (GJJ) │ +├─────────────────────────────────────────────────────┤ +│ 基础服务层 (module_base) │ +│ • MVVM基类 • 网络框架 • UI组件 • 工具类 │ +├─────────────────────────────────────────────────────┤ +│ 蓝牙打印 │ 图表库 │ 航班管理 │ 货物追踪 │ +│ (Printer) │ (Chart) │ (Hangban) │ (Cargo) │ +└─────────────────────────────────────────────────────┘ +``` + +### MVVM模式 + +- **View层**: Activity/Fragment + DataBinding +- **ViewModel层**: BaseViewModel / BasePageViewModel +- **Model层**: Repository + Retrofit API +- **数据流**: 协程(Coroutines) + Flow + +### 组件化架构 + +- **模块独立**: 通过`isBuildModule`参数控制模块独立运行或集成 +- **路由通信**: ARouter实现模块间页面跳转和通信 +- **事件总线**: FlowBus(基于Flow) + EventBus +- **依赖注入**: 基于ServiceLoader的服务发现机制 + +### 基础设施 + +#### 核心基类 +- `BaseActivity`: 协程支持、Loading管理、扫码功能、键盘控制 +- `BaseBindingActivity`: DataBinding自动绑定、ViewModel生命周期管理 +- `BaseViewModel`: Loading管理、Activity结果处理 +- `BasePageViewModel`: 分页列表专用,集成PageModel自动处理分页 +- `CommonAdapter + BaseViewHolder`: 统一列表适配器封装 + +#### 网络请求层 +- **ServiceCreator**: Retrofit实例创建,动态IP配置 +- **拦截器**: 自动添加Token、时间戳,统一错误处理 +- **扩展函数**: + - `launchCollect`: 无Loading的后台请求 + - `launchLoadingCollect`: 带Loading的关键操作 + - `toRequestBody`: Map/Bean自动转JSON + +#### 统一UI组件 +- **PadSearchLayout**: 搜索输入框组合控件(支持文本/数字/下拉/日期) +- **PadDataLayout**: 数据展示/编辑组合控件(标题对齐) +- **StatusView**: 状态栏占位View +- **统一样式**: 颜色、字体、圆角、间距的全局规范 + +#### 双形态适配(平板 / 手机) +同时支持 **10 寸平板横屏** 与 **5.5 寸手机竖屏**,业务逻辑层完全复用,只重写布局。 + +- **DeviceUtil**: 设备形态判定(按 `smallestScreenWidthDp >= 600` 区分),提供调试用强制形态开关 +- **布局自动切换**: 通过 Android 资源限定符实现,`res/layout-sw600dp/`(平板)与 `res/layout/`(手机)下放**同名**布局,DataBinding 生成同一个 Binding 类,Activity / ViewModel / ViewHolder 无需任何设备判断 +- **屏幕方向**: Manifest 统一写 `android:screenOrientation="unspecified"`,系统按设备当前物理方向建窗口,`BaseActivity.applyDeviceOrientation()` 再按形态锁定(手机竖屏 / 平板横屏),两端均无「先横屏再转竖屏」闪屏。**不要改用资源引用**——系统解析 Manifest 不套用限定符,会导致平板取到竖屏值并触发 AutoSize 放大 +- **AutoSize 基准**: 横屏 1152×720 / 手机竖屏 390×844 / 平板竖屏 720×1280 +- **手机端资源**: `phone_*` 配色 + `bg_phone_*` drawable + 手机版公共标题栏 + +> ⚠️ 禁止用「不同文件名 + 代码判断」切换布局,会生成两个 Binding 类导致运行时 `ClassCastException`。 +> 详见 [`documents/5.5寸手持端适配技术方案.md`](documents/5.5寸手持端适配技术方案.md) 与 `CLAUDE.md` 对应章节。 + +## 🚀 技术栈 + +### 核心框架 +- **Kotlin**: 1.6.21 + Coroutines 1.6.0 +- **Jetpack**: ViewModel、LiveData、DataBinding +- **网络**: Retrofit 2.6.1 + OkHttp 3.12.12 +- **JSON**: FastJSON 1.2.73 + Gson 2.10.1 + +### UI相关 +- **Material Design**: Material Components +- **屏幕适配**: AutoSize 1.2.+ (横屏1152dp × 720dp) +- **下拉刷新**: SmartRefreshLayout 2.0.3 +- **图表**: MPAndroidChart (定制版) +- **弹窗**: XPopup 2.9.19 +- **图片**: Glide 4.15.1 + PictureSelector v3.11.2 + +### 功能组件 +- **路由**: ARouter 1.5.2 +- **扫码**: ZXing 2.2.9 +- **权限**: AndPermission 2.0.2 +- **打印**: 佳博SDK 2.0.4 +- **日志**: Timber 5.0.1 + +## 💼 核心业务模块 + +### 国内出港 (module_gnc) +货物收运、复磅称重、转运管理、出库装机、仓库管理、数据统计 + +### 国内进港 (module_gnj) +舱单管理、卸机入库、出库提货、移库管理、货物交接、实时查询 + +### 国际出港 (module_gjc) +国际货物收运、板箱组装、ULD容器管理、复磅称重、仓储移库 + +### 国际进港 (module_gjj) +国际舱单管理、报文解析、理货管理、出库操作、货物交接 + +### 支持模块 +- **航班管理** (module_hangban): 航班查询、航班统计 +- **货物追踪** (module_cargo): 货物状态追踪、运输日志 +- **监装监卸** (module_mit): 监装监卸管理 +- **PDA功能** (module_p): PDA专用功能 +- **蓝牙打印** (Printer): 佳博打印机SDK集成 + +## 📦 项目结构 + +``` +aerologic-app/ +├── app/ # 应用壳层,整合所有业务模块 +├── module_base/ # 核心基础库,提供所有通用能力 +│ ├── base/ # MVVM基类 +│ ├── http/ # 网络请求框架 +│ ├── ui/weight/ # 统一UI组件 +│ ├── ktx/ # Kotlin扩展函数 +│ ├── bean/ # 数据模型 +│ ├── util/ # 工具类(含 DeviceUtil 设备形态判定) +│ └── router/ # ARouter路由常量 +├── module_gnc/ # 国内出港业务 +├── module_gnj/ # 国内进港业务 +├── module_gjc/ # 国际出港业务 +├── module_gjj/ # 国际进港业务 +├── module_hangban/ # 航班查询管理 +├── module_cargo/ # 货物追踪查询 +├── module_mit/ # 监装监卸管理 +├── module_p/ # PDA专用功能 +├── Printer/ # 蓝牙打印模块 +├── MPChartLib/ # 定制图表库 +├── documents/ # 设计稿、报价单、技术方案文档 +├── CLAUDE.md # 详细开发指南 +└── README.md # 项目说明文档 +``` + +各业务模块内的布局按设备形态分目录存放: + +``` +module_gjc/src/main/res/ +├── layout/ # 手机布局(5.5 寸竖屏,兜底) +└── layout-sw600dp/ # 平板布局(10 寸横屏) +``` + +> 两目录下**同名**的布局是同一布局的资源变体,系统按屏幕最小宽度自动选择。 +> 尚未适配手机的页面,布局保留在 `layout/` 即可(两端共用),支持渐进式适配。 + +## 🚀 快速开始 + +### 环境要求 + +- **IDE**: Android Studio Arctic Fox (2020.3.1) 或更高版本 +- **JDK**: 1.8 +- **Gradle**: 7.3.3 +- **Kotlin**: 1.6.21 + +### 依赖配置 + +如遇依赖下载问题: + +1. 下载 gradle-7.3.3-bin.zip + - 百度网盘: https://pan.baidu.com/s/18wsuGRlNxjMYbxLhBH9yeg + - 提取码: 1029 + +2. 配置Gradle: + - Settings → Build, Execution, Deployment → Build Tools → Gradle + - 解压并替换到 "Gradle user home" 目录 + +### 构建项目 + +```bash +# 克隆项目 +git clone [repository-url] +cd aerologic-app + +# 清理构建 +./gradlew clean + +# 构建Debug版本 +./gradlew assembleDebug + +# 构建Release版本(已签名) +./gradlew assembleRelease + +# 安装到设备 +./gradlew installDebug +``` + +### 组件化开发模式 + +支持模块独立运行调试: + +1. 编辑 `gradle.properties` +2. 设置 `isBuildModule=true` (独立模块) 或 `false` (集成模式) +3. Sync项目并运行对应模块 + +## ⚙️ 配置说明 + +### 网络配置 + +服务器地址配置位置: +- **文件**: `module_base/src/main/res/values/strings.xml` +- **主服务器**: `system_url_inner` +- **地磅服务器**: `weight_url` +- **运行时**: 可通过SharedPreferences动态修改IP地址 + +### 签名配置 + +Release版本签名信息: +- **KeyStore**: `key.jks` (项目根目录) +- **密码**: storePassword/keyPassword均为 `123321` +- **别名**: `key` + +### 网络安全 + +- **超时时间**: 30秒(连接/读取/写入) +- **认证方式**: Bearer Token(自动添加) +- **HTTP支持**: 配置在 `res/xml/network_security_config.xml` + +## 📚 开发文档 + +详细的开发指南请参考 **[CLAUDE.md](./CLAUDE.md)**,包含: + +- ✅ 完整的架构详解(基类、网络、UI组件) +- ✅ 标准开发模板(Activity、ViewModel、ViewHolder) +- ✅ 实际业务示例(列表页、详情页、编辑页) +- ✅ 开发检查清单(7步列表页、4步详情页、5步编辑页) +- ✅ DataBinding适配器(10+种) +- ✅ Kotlin扩展函数(20+种) +- ✅ 常见业务场景实现 + +## 🔄 Git工作流 + +### 分支策略 + +- **develop**: 主开发分支 +- **feature/xxx**: 功能开发分支 +- **release/x.x.x**: 发布分支 +- **hotfix/xxx**: 紧急修复分支 + +### 当前分支 + +- **开发分支**: feature/hefei +- **主分支**: develop(用于PR) + +### 提交规范 + +提交前确保: +- ✅ 代码编译通过 +- ✅ 无明显错误和警告 +- ✅ 遵循项目代码规范 + +## 📋 开发原则 + +1. ✅ **优先使用现有基类和封装**,避免重复造轮子 +2. ✅ **充分利用PadDataLayout和PadSearchLayout组件**,保证UI一致性 +3. ✅ **遵循统一的命名和目录组织规范** +4. ✅ **使用DataBinding简化代码**,减少findViewById +5. ✅ **利用扩展函数处理通用逻辑**(Toast、Dialog、权限等) +6. ✅ **保持架构一致性**,新增业务参考现有模块 + +## 🔐 特性亮点 + +### 1. 高度组件化 +- 业务模块完全解耦,支持独立开发、测试、发布 +- 通过ARouter实现模块间通信,无需直接依赖 + +### 2. 统一UI规范 +- 自定义PadSearchLayout、PadDataLayout组合控件 +- 全局统一的颜色、字体、圆角、间距规范 +- 丰富的DataBinding适配器,简化UI开发 + +### 3. 高效网络框架 +- 协程+Flow的现代化异步方案 +- 统一的请求封装(launchCollect/launchLoadingCollect) +- 自动的Loading管理和错误处理 + +### 4. 完善的基础设施 +- BaseActivity提供扫码、Loading、键盘控制等通用能力 +- BasePageViewModel自动处理分页逻辑 +- CommonAdapter统一列表适配器管理 + +### 5. 丰富的工具扩展 +- 20+ Kotlin扩展函数(Toast、Dialog、JSON、日期等) +- 10+ DataBinding适配器(图片加载、可见性、Shape等) +- 完整的业务开发模板和检查清单 + +## 🛠️ 技术特点 + +- **响应式编程**: Kotlin Coroutines + Flow +- **声明式UI**: DataBinding双向绑定 +- **函数式编程**: Kotlin扩展函数和高阶函数 +- **依赖注入**: 基于ServiceLoader的轻量级方案 +- **模块化路由**: ARouter组件化通信 +- **横屏适配**: AutoSize自动适配1152dp × 720dp +- **蓝牙打印**: 佳博打印机SDK深度集成 +- **二维码扫描**: ZXing高性能扫码 + +## 📄 许可证 + +本项目为企业内部系统,版权归所属公司所有。 + +## ⚠️ 注意事项 + +**重要**: 本项目包含敏感业务信息和配置,仅供授权人员使用,严禁外传。 + +--- + +**文档维护**: 本README提供项目概览和架构信息,详细开发指南请参考 [CLAUDE.md](./CLAUDE.md) diff --git a/app/build.gradle b/app/build.gradle index c9dfb46..794d215 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,8 +26,8 @@ android { applicationId "com.lukouguoji.aerologic" minSdkVersion 24 targetSdkVersion 30 - versionCode 88 - versionName "1.8.8" + versionCode 87 + versionName "1.8.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..a2f6d7e --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9a61963..a980489 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,578 +1,623 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/detail/GnjMoveStashDetailActivity.kt b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/detail/GnjMoveStashDetailActivity.kt new file mode 100644 index 0000000..efb74e3 --- /dev/null +++ b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/detail/GnjMoveStashDetailActivity.kt @@ -0,0 +1,49 @@ +package com.lukouguoji.aerologic.page.gnj.move.stash.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.google.gson.Gson +import com.lukouguoji.aerologic.R +import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashDetailBinding +import com.lukouguoji.aerologic.page.gnj.move.stash.photo.GnjMoveStashPhotoActivity +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.MoveStashBean +import com.lukouguoji.module_base.common.Constant + +/** + * 国内进港移库「运单详情」(5.5 寸手持端专属页面,无平板变体) + * + * 由列表卡片的「查看详情」进入;「异常照片」卡片的加号槽位跳拍照上传页。 + * 展示字段全部由列表项带入(Gson JSON),零新增请求。 + */ +class GnjMoveStashDetailActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_gnj_move_stash_detail + override fun viewModelClass() = GnjMoveStashDetailViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("运单详情") + binding.viewModel = viewModel + binding.activity = this + viewModel.initOnCreated(intent) + } + + /** 「异常照片」加号槽位 → 拍照上传页 */ + fun openPhoto() { + viewModel.dataBean.value?.let { + GnjMoveStashPhotoActivity.start(this, it.mawbId, it.wbNo, it.moveId) + } + } + + companion object { + @JvmStatic + fun start(context: Context, bean: MoveStashBean) { + context.startActivity( + Intent(context, GnjMoveStashDetailActivity::class.java) + .putExtra(Constant.Key.DATA, Gson().toJson(bean)) + ) + } + } +} diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/detail/GnjMoveStashDetailViewModel.kt b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/detail/GnjMoveStashDetailViewModel.kt new file mode 100644 index 0000000..4cca875 --- /dev/null +++ b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/detail/GnjMoveStashDetailViewModel.kt @@ -0,0 +1,25 @@ +package com.lukouguoji.aerologic.page.gnj.move.stash.detail + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import com.google.gson.Gson +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.MoveStashBean +import com.lukouguoji.module_base.common.Constant + +/** + * 国内进港移库「运单详情」ViewModel(5.5 寸手持端专属页面) + * + * 列表项数据已包含详情页所需全部字段(含 isMoved 端点打标),直接由列表带入,不额外请求。 + */ +class GnjMoveStashDetailViewModel : BaseViewModel() { + + val dataBean = MutableLiveData(MoveStashBean()) + + fun initOnCreated(intent: Intent) { + val json = intent.getStringExtra(Constant.Key.DATA).orEmpty() + if (json.isNotEmpty()) { + dataBean.value = Gson().fromJson(json, MoveStashBean::class.java) + } + } +} diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListActivity.kt b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListActivity.kt index 86a24b3..ca8cfef 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListActivity.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListActivity.kt @@ -1,36 +1,102 @@ -package com.lukouguoji.aerologic.page.gnj.move.stash.list - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import com.lukouguoji.aerologic.R -import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashListBinding -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.ktx.getLifecycleOwner - -class GnjMoveStashListActivity : - BaseBindingActivity() { - override fun layoutId() = R.layout.activity_gnj_move_stash_list - - override fun viewModelClass() = GnjMoveStashListViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国内进港移库") - - binding.viewModel = viewModel - - viewModel.pageModel - .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner()) - - viewModel.refresh() - } - - companion object { - @JvmStatic - fun start(context: Context) { - val starter = Intent(context, GnjMoveStashListActivity::class.java) - context.startActivity(starter) - } - } - -} \ No newline at end of file +package com.lukouguoji.aerologic.page.gnj.move.stash.list + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.alibaba.android.arouter.facade.annotation.Route +import com.lukouguoji.aerologic.R +import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashListBinding +import com.lukouguoji.aerologic.page.gnj.move.stash.photo.GnjMoveStashPhotoActivity +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.MoveStashBean +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.observe +import com.lukouguoji.module_base.ktx.getLifecycleOwner +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +@Route(path = ARouterConstants.ACTIVITY_URL_GNJ_MOVE_STASH_LIST) +class GnjMoveStashListActivity : + BaseBindingActivity() { + override fun layoutId() = R.layout.activity_gnj_move_stash_list + + override fun viewModelClass() = GnjMoveStashListViewModel::class.java + + /** 筛选弹层显隐(仅手机 UI 状态,平板布局不绑定) */ + val filterPanelVisible = ObservableBoolean(false) + + override fun initOnCreate(savedInstanceState: Bundle?) { + // 设计稿手机端标题为「进港移库」,平板保持原标题 + setBackArrow(if (DeviceUtil.isPhone()) "进港移库" else "国内进港移库") + + binding.viewModel = viewModel + binding.activity = this + + viewModel.pageModel + .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner()) + + if (DeviceUtil.isPhone()) { + // 手机专属:Tab 角标 + 特码字典等额外请求 + viewModel.initPhoneExtras() + + // 卡片勾选变化 → 更新全选状态与已选计数 + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { + viewModel.updateCheckAllStatus() + } + } + + // 拍照上传等子页面完成后刷新列表(平板无发射方,观察无副作用) + FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { + viewModel.refresh() + } + + viewModel.refresh() + } + + // ==================== 筛选弹层(仅手机) ==================== + + fun toggleFilterPanel() { + filterPanelVisible.set(!filterPanelVisible.get()) + } + + /** 筛选:重置(只清空弹层内条件,不影响 Tab 与顶部搜索框) */ + fun resetFilter() { + viewModel.carrier.value = "" + viewModel.businessType.value = "" + } + + fun confirmFilter() { + filterPanelVisible.set(false) + viewModel.refresh() + } + + // ==================== 卡片长按「拍照」浮层(仅手机) ==================== + + /** 卡片长按:展开操作浮层(View.OnLongClickListener 要求返回 Boolean) */ + fun showCardActionPhone(bean: MoveStashBean): Boolean { + bean.actionMenuVisible.set(true) + return true + } + + /** 点浮层空白处关闭 */ + fun hideCardActionPhone(bean: MoveStashBean) { + bean.actionMenuVisible.set(false) + } + + /** 浮层「拍照」→ 拍照上传页 */ + fun openPhoto(bean: MoveStashBean) { + bean.actionMenuVisible.set(false) + GnjMoveStashPhotoActivity.start(this, bean.mawbId, bean.wbNo, bean.moveId) + } + + companion object { + @JvmStatic + fun start(context: Context) { + val starter = Intent(context, GnjMoveStashListActivity::class.java) + context.startActivity(starter) + } + } + +} diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewHolder.kt b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewHolder.kt index 0304c4d..f337635 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewHolder.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewHolder.kt @@ -3,34 +3,64 @@ package com.lukouguoji.aerologic.page.gnj.move.stash.list import android.view.View import android.widget.Toast import com.lukouguoji.aerologic.databinding.ItemGnjMoveStashListBinding +import com.lukouguoji.aerologic.page.gnj.move.stash.detail.GnjMoveStashDetailActivity import com.lukouguoji.gnj.page.yiku.handover.GnjYiKuHandoverActivity import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.bean.MoveStashBean +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +/** + * 国内进港移库列表项 ViewHolder(双形态共用) + * + * 平板/手机布局差异较大,单侧独有的控件 id 在 Binding 基类中为 @Nullable, + * 全部走判空调用(平板:swipe_menu/btn_edit/ll;手机:ll_detail)。 + */ class GnjMoveStashListViewHolder(view: View) : BaseViewHolder(view) { override fun onBind(item: Any?, position: Int) { val bean = getItemBean(item)!! binding.bean = bean + binding.position = position + binding.activity = itemView.context as? GnjMoveStashListActivity + // 勾选框(双形态共有):切换选中;手机端另行广播刷新全选/已选计数 binding.ivIcon.setOnClickListener { bean.checked.set(!bean.checked.get()) + binding.executePendingBindings() + + GlobalScope.launch { + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).emit("check_changed") + } } + // ===== 平板独有交互 ===== + // 点击列表项进入移交详情 - binding.ll.setOnClickListener { + binding.ll?.setOnClickListener { GnjYiKuHandoverActivity.startForDetails(itemView.context, bean.mawbId) } // 侧滑菜单 - 编辑按钮 - binding.btnEdit.setOnClickListener { - binding.swipeMenu.quickClose() + binding.btnEdit?.setOnClickListener { + binding.swipeMenu?.quickClose() if (bean.pickFlag == "1") { Toast.makeText(itemView.context, "已出库,不可编辑", Toast.LENGTH_SHORT).show() return@setOnClickListener } GnjYiKuHandoverActivity.startForEdit(itemView.context, bean.mawbId) } + + // ===== 手机独有交互 ===== + + // 「查看详情」→ 手机版运单详情页 + binding.llDetail?.setOnClickListener { + GnjMoveStashDetailActivity.start(itemView.context, bean) + } + + binding.executePendingBindings() } } diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewModel.kt b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewModel.kt index 9295d2c..18e65fd 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewModel.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/list/GnjMoveStashListViewModel.kt @@ -49,6 +49,31 @@ class GnjMoveStashListViewModel : BasePageViewModel() { val number = MutableLiveData("0") val weight = MutableLiveData("0") + // ========== 5.5 寸手持端专属字段(平板布局不绑定,默认值保证平板行为零变化) ========== + + /** 移库状态 Tab:"0" 待移库(DomImpMove/search)/ "1" 已移库(DomImpMove/searchMoved) */ + val moveState = MutableLiveData("0") + + val moveStateTabs = MutableLiveData( + listOf(KeyValue("待移库", "0"), KeyValue("已移库", "1")) + ) + + /** 状态 Tab 角标(与 [moveStateTabs] 等长,取两端点分页 total) */ + val moveStateCounts = MutableLiveData(listOf("", "")) + + /** 底部操作条「已选 N 项」 */ + val selectedCount = MutableLiveData("0") + + /** 全选状态(手机 PhoneBottomBar 勾选图标) */ + val isAllChecked = MutableLiveData(false) + + /** 手机专属的额外请求(Tab 角标)开关,仅手机形态在 Activity 中开启 */ + private var phoneExtrasEnabled = false + + fun initPhoneExtras() { + phoneExtrasEnabled = true + } + /////////////////////////////////////////////////////////////////////////// // 方法区 /////////////////////////////////////////////////////////////////////////// @@ -95,25 +120,69 @@ class GnjMoveStashListViewModel : BasePageViewModel() { } + /** 查询条件(分页参数除外),search / searchMoved / searchTotal 三处共用同一套入参 */ + private fun buildFilterParams(): Map = mapOf( + "awbType" to businessType.value!!.ifEmpty { null }, + "wbNo" to waybill.value!!.ifEmpty { null }, + "carrier" to carrier.value!!.ifEmpty { null }, + ) + override fun getData() { - val body = mapOf( + val body = (buildFilterParams() + mapOf( "page" to pageModel.page, "limit" to pageModel.limit, - "awbType" to businessType.value!!.ifEmpty { null }, - "wbNo" to waybill.value!!.ifEmpty { null }, - "carrier" to carrier.value!!.ifEmpty { null }, - ).toRequestBody() + )).toRequestBody() + val moved = moveState.value == "1" launchLoadingCollect({ - NetApply.api.getGnjMoveStashList( - body - ) + // 平板 moveState 恒为 "0" → 恒走 search(原行为);已移库 Tab 走 searchMoved + if (moved) NetApply.api.getGnjMoveStashMovedList(body) + else NetApply.api.getGnjMoveStashList(body) }) { onSuccess = { - pageModel.handleListBean(it) + // searchMoved 出参无移库标识,按端点打标(卡片形态/详情移库信息卡依赖) + it.list?.forEach { bean -> bean.isMoved = moved } + pageModel.handleListBean(it) { updateCheckAllStatus() } count.value = it.total } } getCountData(body) + if (phoneExtrasEnabled) loadTabCounts() + } + + /** Tab 角标:search / searchMoved 各以 limit=1 查一次取分页 total(仅手机形态发起) */ + private fun loadTabCounts() { + val body = (buildFilterParams() + mapOf("page" to 1, "limit" to 1)).toRequestBody() + launchCollect({ NetApply.api.getGnjMoveStashList(body) }) { + onSuccess = { updateTabCount(0, it.total) } + } + launchCollect({ NetApply.api.getGnjMoveStashMovedList(body) }) { + onSuccess = { updateTabCount(1, it.total) } + } + } + + private fun updateTabCount(index: Int, total: Int?) { + val current = moveStateCounts.value?.toMutableList() ?: mutableListOf("", "") + if (index < current.size) { + current[index] = (total ?: 0).toString() + moveStateCounts.value = current + } + } + + // ========== 手机端全选 / 已选计数 ========== + + fun toggleSelectAll() { + val list = pageModel.rv?.commonAdapter()?.items + ?.filterIsInstance() ?: return + CheckUtil.handleAllCheck(list) + updateCheckAllStatus() + pageModel.rv?.commonAdapter()?.notifyDataSetChanged() + } + + fun updateCheckAllStatus() { + val list = pageModel.rv?.commonAdapter()?.items + ?.filterIsInstance() ?: return + isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } + selectedCount.value = list.count { it.checked.get() }.toString() } private fun getCountData(body: RequestBody) { diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/photo/GnjMoveStashPhotoActivity.kt b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/photo/GnjMoveStashPhotoActivity.kt new file mode 100644 index 0000000..0382a3f --- /dev/null +++ b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/photo/GnjMoveStashPhotoActivity.kt @@ -0,0 +1,49 @@ +package com.lukouguoji.aerologic.page.gnj.move.stash.photo + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.lukouguoji.aerologic.R +import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashPhotoBinding +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.ktx.addOnItemClickListener + +/** + * 国内进港移库「拍照上传」(5.5 寸手持端专属页面,无平板变体) + * + * 由列表卡片长按浮层「拍照」或运单详情「异常照片」加号槽位进入。 + * 已有照片经 DomImpMove/detail 回填,提交走 DomImpMove/modify + * (mawbId + pic/originalPic/picNumber 三字段规范,与平板移交编辑页同一链路)。 + */ +class GnjMoveStashPhotoActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_gnj_move_stash_photo + override fun viewModelClass() = GnjMoveStashPhotoViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("拍照上传") + binding.viewModel = viewModel + + viewModel.rv = binding.rv + binding.rv.addOnItemClickListener(viewModel) + + viewModel.initOnCreated(intent) + } + + companion object { + /** 已移库运单的 moveId(movedDetail 回填照片用) */ + const val EXTRA_MOVE_ID = "moveId" + + @JvmStatic + fun start(context: Context, mawbId: String, wbNo: String, moveId: String = "") { + context.startActivity( + Intent(context, GnjMoveStashPhotoActivity::class.java) + .putExtra(Constant.Key.ID, mawbId) + .putExtra(Constant.Key.CODE, wbNo) + .putExtra(EXTRA_MOVE_ID, moveId) + ) + } + } +} diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/photo/GnjMoveStashPhotoViewModel.kt b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/photo/GnjMoveStashPhotoViewModel.kt new file mode 100644 index 0000000..8f78ac6 --- /dev/null +++ b/app/src/main/java/com/lukouguoji/aerologic/page/gnj/move/stash/photo/GnjMoveStashPhotoViewModel.kt @@ -0,0 +1,188 @@ +package com.lukouguoji.aerologic.page.gnj.move.stash.photo + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.RecyclerView +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.FileBean +import com.lukouguoji.module_base.bean.GnjYiKuBean +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.http.net.NetApply +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.ImageSelectNewViewHolder +import com.lukouguoji.module_base.interfaces.IOnItemClickListener +import com.lukouguoji.module_base.ktx.commonAdapter +import com.lukouguoji.module_base.ktx.launchCollect +import com.lukouguoji.module_base.ktx.launchLoadingCollect +import com.lukouguoji.module_base.ktx.noNull +import com.lukouguoji.module_base.ktx.showToast +import com.lukouguoji.module_base.ktx.toRequestBody +import com.lukouguoji.module_base.util.MediaUtil +import com.lukouguoji.module_base.util.UploadUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch + +/** + * 国内进港移库「拍照上传」ViewModel(5.5 寸手持端专属页面) + * + * 图片选择/预览/删除复用 module_base 的 ImageSelectNewViewHolder(末位加号槽位)。 + * 已有照片回填:待移库经 DomImpMove/detail(mawbId),已移库经 DomImpMove/movedDetail(moveId, + * searchMoved 出参不含 mawbId)。提交走 DomImpMove/modify 最小体 + * (mawbId/remark/picNumber/pic/originalPic),与平板移交编辑页同一链路。 + */ +class GnjMoveStashPhotoViewModel : BaseViewModel(), IOnItemClickListener { + + var mawbId = "" + + /** 已移库运单的移库记录 ID(searchMoved 出参),用于 movedDetail 回填 */ + var moveId = "" + + /** 运单号(副标题展示,由列表带入) */ + val wbNo = MutableLiveData("") + + /** detail 回填的完整 bean(提交时保留 remark 等原值) */ + private var detailBean = GnjYiKuBean() + + // 图片网格适配器配置(复用公共图片选择组件) + val itemViewHolder = ImageSelectNewViewHolder::class.java + val itemLayoutId = com.lukouguoji.module_base.R.layout.item_image_select_new + + var rv: RecyclerView? = null + + fun initOnCreated(intent: Intent) { + mawbId = intent.getStringExtra(Constant.Key.ID).orEmpty() + wbNo.value = intent.getStringExtra(Constant.Key.CODE).orEmpty() + moveId = intent.getStringExtra(GnjMoveStashPhotoActivity.EXTRA_MOVE_ID).orEmpty() + + // 已有照片回填(pic 缩略图 + originalPic 原图),末位补加号槽位; + // 已移库运单无 mawbId,改按 moveId 走 movedDetail + launchCollect({ + if (mawbId.isEmpty() && moveId.isNotEmpty()) NetApply.api.getGnjMoveStashMovedDetail(moveId) + else NetApply.api.getGnjMoveStashDetail(mawbId) + }) { + onSuccess = { + detailBean = it.data ?: GnjYiKuBean() + val picList = detailBean.pic.split(",").filter { p -> p.isNotEmpty() } + val originalList = detailBean.originalPic.split(",").filter { p -> p.isNotEmpty() } + val images = picList.mapIndexed { index, picUrl -> + FileBean( + path = MediaUtil.fillUrl(picUrl), + url = picUrl, + originalPic = MediaUtil.fillUrl(originalList.getOrElse(index) { picUrl }) + ) + } + rv?.post { + rv?.commonAdapter()?.refresh(images + FileBean()) + } + } + onFailed = { _, _ -> + // 详情取不到也允许直接新增照片 + rv?.post { rv?.commonAdapter()?.refresh(listOf(FileBean())) } + } + } + } + + /** + * 确认提交:先补传未上传的本地图片,再按三字段规范提交 modify + */ + fun submitClick() { + // modify 以 mawbId 定位运单;已移库运单 searchMoved/movedDetail 均不返回 mawbId(后端缺口),无法提交 + if (mawbId.isEmpty()) { + showToast("已移库运单缺少运单ID,暂不支持提交照片") + return + } + (rv?.commonAdapter()?.items ?: emptyList()) + .asFlow() + .map { it as FileBean } + .filter { it.path.isNotEmpty() && it.url.isEmpty() } + .onEach { + val data = UploadUtil.upload(it.path).data + // UploadUtil 返回:newName=原图(较大),zipFileName=缩略图(较小) + it.url = data?.zipFileName ?: "" + it.originalPic = data?.newName ?: "" + } + .flowOn(Dispatchers.IO) + .onStart { showLoading() } + .catch { + showToast(it.message.noNull("上传图片失败")) + dismissLoading() + } + .onCompletion { + launchLoadingCollect({ + val list = (rv?.commonAdapter()?.items ?: emptyList()) + .filterIsInstance() + .filter { it.path.isNotEmpty() } + + // 最小体提交:只更新图片三字段(remark 保留 detail 原值),与平板移交编辑页一致 + val params = mapOf( + "mawbId" to mawbId, + "remark" to detailBean.remark, + "picNumber" to list.size.toString(), + "pic" to list.joinToString(",") { MediaUtil.removeUrl(it.url) }, + "originalPic" to list.joinToString(",") { MediaUtil.removeUrl(it.originalPic) }, + ).toRequestBody(removeEmptyOrNull = true) + + NetApply.api.modifyGnjMoveStash(params) + }) { + onSuccess = { + showToast("提交成功") + viewModelScope.launch { + FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh") + } + getTopActivity().finish() + } + onFailed = { _, msg -> + showToast(msg.noNull("提交失败")) + } + } + } + .launchIn(viewModelScope) + } + + /** + * 「拍照/上传」按钮:直接唤起图片选择(含拍照入口) + */ + fun pickClick() { + MediaUtil.pickImage(getTopActivity(), maxNum = 9) { results -> + val adapter = rv?.commonAdapter() ?: return@pickImage + results.forEach { result -> + // 在加号槽位(最后一项)之前插入 + val insertPos = (adapter.items.size - 1).coerceAtLeast(0) + adapter.items.add(insertPos, FileBean(path = result.realPath)) + adapter.notifyItemInserted(insertPos) + } + } + } + + /** + * 图片网格点击回调(长按出删除角标 / 点删除角标移除) + */ + override fun onItemClick(position: Int, type: Int) { + val adapter = rv?.commonAdapter() ?: return + val bean = adapter.getItem(position) as? FileBean ?: return + when (type) { + com.lukouguoji.module_base.R.id.rl -> { + if (bean.path.isNotEmpty()) { + bean.canDelete.set(!bean.canDelete.get()) + } + } + + com.lukouguoji.module_base.R.id.iv_delete -> { + adapter.removeItem(position) + } + + else -> {} + } + } +} diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/log/detail/LogDetailActivity.kt b/app/src/main/java/com/lukouguoji/aerologic/page/log/detail/LogDetailActivity.kt index 5c254a0..0a80fc9 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/log/detail/LogDetailActivity.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/log/detail/LogDetailActivity.kt @@ -19,6 +19,7 @@ import com.lukouguoji.module_base.base.BaseBindingActivity import com.lukouguoji.module_base.bean.LogBean import com.lukouguoji.module_base.common.Constant import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil @Route(path = ARouterConstants.ACTIVITY_URL_LOG_DETAIL) class LogDetailActivity : BaseBindingActivity() { @@ -34,7 +35,8 @@ class LogDetailActivity : BaseBindingActivity, + activeCodes: Set, + latestCode: String + ) { + val container = binding.llSteps + container.removeAllViews() + container.orientation = LinearLayout.VERTICAL + + val logs = viewModel.statusLogList.value ?: emptyList() + val colorGreen = 0xFF16A34A.toInt() + val colorGray = 0xFF9CA3AF.toInt() + val colorLine = 0xFFE5E7EB.toInt() + + for (i in steps.indices) { + val step = steps[i] + val isActive = step.code in activeCodes + val isLast = i == steps.size - 1 + val stepLog = logs.lastOrNull { it.status == step.code } + val time = stepLog?.opDate ?: "" + val statusContent = stepLog?.content ?: "" + + val row = LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + + // 左列:圆点 + 连接竖线 + val dotColumn = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER_HORIZONTAL + layoutParams = LinearLayout.LayoutParams(dp(20), LinearLayout.LayoutParams.MATCH_PARENT) + } + val dot = View(this).apply { + setBackgroundResource( + if (isActive) com.lukouguoji.module_base.R.drawable.bg_phone_dot_green + else com.lukouguoji.module_base.R.drawable.bg_phone_dot_gray + ) + layoutParams = LinearLayout.LayoutParams( + dp(if (isActive) 12 else 8), dp(if (isActive) 12 else 8) + ).apply { topMargin = dp(if (isActive) 2 else 4) } + } + dotColumn.addView(dot) + if (!isLast) { + val line = View(this).apply { + setBackgroundColor(colorLine) + layoutParams = LinearLayout.LayoutParams(dp(2), 0).apply { + weight = 1f + topMargin = dp(2) + bottomMargin = dp(2) + } + } + dotColumn.addView(line) + } + row.addView(dotColumn) + + // 右列:步骤名 + 时间 + val content = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + layoutParams = LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + weight = 1f + leftMargin = dp(10) + } + setPadding(0, 0, 0, if (isLast) 0 else dp(24)) + } + // 名称行:步骤名(左)+ 已发生节点的状态 pill(右,设计稿如「放行状态:01」) + val nameRow = LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + nameRow.addView(TextView(this).apply { + text = step.name + setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f) + setTextColor(if (isActive) colorGreen else colorGray) + if (isActive) setTypeface(typeface, android.graphics.Typeface.BOLD) + layoutParams = LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { weight = 1f } + }) + if (isActive && statusContent.isNotEmpty()) { + nameRow.addView(TextView(this).apply { + text = statusContent + setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) + setTextColor(colorGreen) + setBackgroundResource(com.lukouguoji.module_base.R.drawable.bg_phone_tag_green) + setPadding(dp(8), dp(3), dp(8), dp(3)) + maxLines = 1 + ellipsize = android.text.TextUtils.TruncateAt.END + maxWidth = dp(180) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { leftMargin = dp(8) } + }) + } + content.addView(nameRow) + if (time.isNotEmpty()) { + content.addView(TextView(this).apply { + text = time + setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) + setTextColor(colorGray) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { topMargin = dp(4) } + }) + } + row.addView(content) + + container.addView(row) + } } private fun buildStepProgressBar( diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditActivity.kt b/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditActivity.kt index 9d2559e..b4410e0 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditActivity.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditActivity.kt @@ -7,7 +7,15 @@ import com.lukouguoji.aerologic.R import com.lukouguoji.aerologic.databinding.ActivityUldEditBinding import com.lukouguoji.module_base.base.BaseBindingActivity import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.common.DetailsPageType +import com.lxj.xpopup.XPopup +/** + * ULD 新增 / 修改 / 详情(一页三态,标题按 pageType 切换)。 + * + * 布局 id 不区分设备,平板/手机由资源限定符自动选择: + * layout-sw600dp/activity_uld_edit.xml(平板) / layout/activity_uld_edit.xml(手机) + */ class UldEditActivity : BaseBindingActivity() { override fun layoutId() = R.layout.activity_uld_edit @@ -18,6 +26,26 @@ class UldEditActivity : BaseBindingActivity + options.getOrNull(position)?.let { viewModel.onStatusSelected(it.value) } + } + .show() } companion object { diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditViewModel.kt b/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditViewModel.kt index e3f38a9..af58a56 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditViewModel.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/uld/edit/UldEditViewModel.kt @@ -11,6 +11,7 @@ import com.lukouguoji.module_base.ktx.launchLoadingCollect import com.lukouguoji.module_base.ktx.noNull import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.ktx.toRequestBody +import com.lukouguoji.module_base.util.DeviceUtil import com.lukouguoji.module_base.util.DictUtils import dev.DevUtils import dev.utils.app.info.KeyValue @@ -41,6 +42,17 @@ class UldEditViewModel : BaseViewModel() { // 所属航司下拉列表(数据源:POST /typeCode/intCarrier) val carrierList = MutableLiveData>(emptyList()) + // ========== 手机版(5.5 寸手持端)专用 ========== + + /** + * 手机版状态选项。后端枚举维持两态(0 正常 / 1 故障,不新增「占用」), + * 按设计稿文案映射 可用=0 / 维修=1。 + */ + val statusPhoneList = listOf( + KeyValue("可用", "0"), + KeyValue("维修", "1"), + ) + fun initOnCreate(intent: Intent) { id = intent.getStringExtra(Constant.Key.ID) ?: "" val modify = intent.getBooleanExtra(Constant.Key.DATA, false) @@ -56,6 +68,11 @@ class UldEditViewModel : BaseViewModel() { } } + /** 手机版状态行选中回调(copy 重新赋值以触发标签样式刷新) */ + fun onStatusSelected(statusValue: String) { + uldBean.value = uldBean.value?.copy(status = statusValue) + } + private fun getData() { if (id.isEmpty()) return launchLoadingCollect({ @@ -81,6 +98,10 @@ class UldEditViewModel : BaseViewModel() { showToast("ULD编号不能为空") return } + // 手机版日期行只产出 yyyy-MM-dd,而 inStoreTime 为 date-time(仅日期后端返回 400),补齐时分秒 + if (DeviceUtil.isPhone() && bean.inStoreTime.length == 10) { + bean.inStoreTime = "${bean.inStoreTime} 00:00:00" + } val isAdd = pageType.value == DetailsPageType.Add launchLoadingCollect({ if (isAdd) NetApply.api.saveUld(bean.toRequestBody()) diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListActivity.kt b/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListActivity.kt index 6bd4c30..a45b1d8 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListActivity.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListActivity.kt @@ -3,12 +3,27 @@ package com.lukouguoji.aerologic.page.uld.list import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.alibaba.android.arouter.facade.annotation.Route import com.lukouguoji.aerologic.R import com.lukouguoji.aerologic.databinding.ActivityUldListBinding import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.observe import com.lukouguoji.module_base.ktx.addOnItemClickListener import com.lukouguoji.module_base.ktx.getLifecycleOwner +import com.lukouguoji.module_base.ktx.showToast +import com.lukouguoji.module_base.model.ConfirmDialogModel +import com.lukouguoji.module_base.router.ARouterConstants +/** + * ULD管理列表页。 + * + * 布局 id 不区分设备,平板/手机由资源限定符自动选择: + * layout-sw600dp/activity_uld_list.xml(平板) / layout/activity_uld_list.xml(手机) + */ +@Route(path = ARouterConstants.ACTIVITY_URL_ULD_LIST) class UldListActivity : BaseBindingActivity() { override fun layoutId(): Int { @@ -19,9 +34,13 @@ class UldListActivity : BaseBindingActivity(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { + viewModel.onItemCheckChanged() + } } override fun onResume() { @@ -36,6 +60,44 @@ class UldListActivity : BaseBindingActivity(view) { override fun onBind(item: Any?, position: Int) { val itemBean = getItemBean(item)!! binding.bean = itemBean + + // 点卡片进详情(两端共用,走 ViewModel.onItemClick 的 R.id.ll_content 分支) notifyItemClick(position, binding.llContent) - notifyItemClick(position, binding.tvModify) - notifyItemClick(position, binding.tvDelete) + + // 平板侧滑按钮(手机变体无此控件) + binding.tvModify?.let { notifyItemClick(position, it) } + binding.tvDelete?.let { notifyItemClick(position, it) } + + // 手机版勾选框(平板变体无此控件):切换选中态并通知页面刷新全选/已选统计 + binding.ivCheck?.setOnClickListener { + itemBean.checked.set(!itemBean.checked.get()) + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") + binding.executePendingBindings() + } + + binding.executePendingBindings() } } diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListViewModel.kt b/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListViewModel.kt index 72f577f..03976c3 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListViewModel.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/uld/list/UldListViewModel.kt @@ -27,6 +27,22 @@ class UldListViewModel : BasePageViewModel() { val uldSuffix = MutableLiveData("") val uldNo = MutableLiveData("") + // ========== 手机版(5.5 寸手持端)筛选 ========== + // 后端确认查询入参与平板端保持一致(uld / status / uldSuffix),手机筛选弹层只放 状态 + 所属航司。 + + /** 手机版状态下拉:文案与卡片标签一致(可用=0 / 维修=1) */ + val statusPhoneFilterList = MutableLiveData>( + listOf( + KeyValue("全部", ""), + KeyValue("可用", "0"), + KeyValue("维修", "1"), + ) + ) + + // ========== 手机版批量选择状态 ========== + val isAllChecked = MutableLiveData(false) + val selectedCount = MutableLiveData("0") + // 状态下拉列表 val statusList = MutableLiveData>( listOf( @@ -65,12 +81,54 @@ class UldListViewModel : BasePageViewModel() { ) }) { onSuccess = { - pageModel.handleListBean(it.toBaseListBean()) + pageModel.handleListBean(it.toBaseListBean()) { updateCheckAllStatus() } count.value = it.total } } } + // ========== 手机版:多选与批量删除 ========== + + fun onItemCheckChanged() { + updateCheckAllStatus() + } + + fun updateCheckAllStatus() { + val list = pageModel.rv?.commonAdapter()?.items?.filterIsInstance() ?: return + isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } + selectedCount.value = list.count { it.checked.get() }.toString() + } + + fun toggleSelectAll() { + val list = pageModel.rv?.commonAdapter()?.items?.filterIsInstance() ?: return + val shouldCheckAll = !(isAllChecked.value ?: false) + list.forEach { it.checked.set(shouldCheckAll) } + isAllChecked.value = shouldCheckAll + selectedCount.value = (if (shouldCheckAll) list.size else 0).toString() + pageModel.rv?.commonAdapter()?.notifyDataSetChanged() + } + + fun getSelectedItems(): List = + pageModel.rv?.commonAdapter()?.items?.filterIsInstance() + ?.filter { it.checked.get() } ?: emptyList() + + /** + * 批量删除选中的 ULD(/eqm/uld/deleteUldBatch,请求体为 ULD 编号数组,一次调用)。 + */ + fun deleteSelected() { + val ulds = getSelectedItems().map { it.uld } + if (ulds.isEmpty()) { + showToast("请选择要删除的ULD") + return + } + launchLoadingCollect({ NetApply.api.deleteUldBatch(ulds.toRequestBody()) }) { + onSuccess = { + showToast("已删除 ${ulds.size} 条") + refresh() + } + } + } + fun onUldScanClick() { ScanModel.startScan(DevUtils.getTopActivity(), Constant.RequestCode.ULD) } diff --git a/app/src/main/res/layout-sw600dp/activity_gnj_move_stash_list.xml b/app/src/main/res/layout-sw600dp/activity_gnj_move_stash_list.xml new file mode 100644 index 0000000..7582268 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/activity_gnj_move_stash_list.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/activity_log_detail.xml b/app/src/main/res/layout-sw600dp/activity_log_detail.xml new file mode 100644 index 0000000..2f55b92 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/activity_log_detail.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw600dp/activity_uld_edit.xml b/app/src/main/res/layout-sw600dp/activity_uld_edit.xml new file mode 100644 index 0000000..177dd16 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/activity_uld_edit.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw600dp/activity_uld_list.xml b/app/src/main/res/layout-sw600dp/activity_uld_list.xml new file mode 100644 index 0000000..e06e2c6 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/activity_uld_list.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw600dp/item_gnj_move_stash_list.xml b/app/src/main/res/layout-sw600dp/item_gnj_move_stash_list.xml new file mode 100644 index 0000000..3f1f658 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/item_gnj_move_stash_list.xml @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-sw600dp/item_uld_list.xml b/app/src/main/res/layout-sw600dp/item_uld_list.xml new file mode 100644 index 0000000..d00a66d --- /dev/null +++ b/app/src/main/res/layout-sw600dp/item_uld_list.xml @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_gnj_move_stash_detail.xml b/app/src/main/res/layout/activity_gnj_move_stash_detail.xml new file mode 100644 index 0000000..fd40e5c --- /dev/null +++ b/app/src/main/res/layout/activity_gnj_move_stash_detail.xml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_gnj_move_stash_list.xml b/app/src/main/res/layout/activity_gnj_move_stash_list.xml index 1e068ef..d82f1d6 100644 --- a/app/src/main/res/layout/activity_gnj_move_stash_list.xml +++ b/app/src/main/res/layout/activity_gnj_move_stash_list.xml @@ -1,148 +1,147 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_gnj_move_stash_photo.xml b/app/src/main/res/layout/activity_gnj_move_stash_photo.xml new file mode 100644 index 0000000..67215c8 --- /dev/null +++ b/app/src/main/res/layout/activity_gnj_move_stash_photo.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_log_detail.xml b/app/src/main/res/layout/activity_log_detail.xml index 2f55b92..577c215 100644 --- a/app/src/main/res/layout/activity_log_detail.xml +++ b/app/src/main/res/layout/activity_log_detail.xml @@ -1,6 +1,22 @@ + + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -12,7 +28,7 @@ @@ -20,208 +36,99 @@ + android:fillViewport="true" + android:overScrollMode="never"> + android:padding="12dp"> - + + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:padding="16dp"> - - - - - - - - - - + + android:gravity="center_vertical" + android:orientation="horizontal"> - + android:layout_weight="1" + android:text="@{viewModel.waybillNo}" + android:textColor="@color/phone_text_title" + android:textSize="20sp" + android:textStyle="bold" + tools:text="78133358743" /> - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_gravity="center_horizontal" + android:layout_marginTop="24dp" + android:layout_marginBottom="16dp" + android:letterSpacing="0.3" + android:text="查询信息已触底" + android:textColor="@color/phone_text_hint" + android:textSize="12sp" /> diff --git a/app/src/main/res/layout/activity_uld_edit.xml b/app/src/main/res/layout/activity_uld_edit.xml index 910db0b..54eb992 100644 --- a/app/src/main/res/layout/activity_uld_edit.xml +++ b/app/src/main/res/layout/activity_uld_edit.xml @@ -1,210 +1,260 @@ - + + - + + + + + + + + - + + + + + + + + + android:fillViewport="true" + android:overScrollMode="never"> + android:padding="16dp"> + + android:paddingHorizontal="16dp"> - + + + + - + android:text="状态" + android:textColor="@color/phone_text_label" + android:textSize="15sp" /> - + + android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.VISIBLE : View.GONE}" /> - + + + + android:gravity="end|center_vertical" + android:orientation="horizontal" + android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.GONE : View.VISIBLE}"> + + + + - - + + + + enable="@{viewModel.pageType != DetailsPageType.Details}" + hint='@{"请选择所属航司"}' + list="@{viewModel.carrierList}" + title='@{"所属航司"}' + type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.SELECT}" + value="@={viewModel.uldBean.carrier}" /> - - - - - - - - - - + + enable="@{viewModel.pageType != DetailsPageType.Details}" + hint='@{"请输入自重"}' + numeric="@{true}" + title='@{"自重(kg)"}' + type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}" + value="@={viewModel.uldBean.uldWeight}" /> - - - - - - - - - - + + + + + + + + + + + + + + + + + + type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}" + value="@={viewModel.uldBean.remark}" /> - - + - - - - - - + android:onClick="@{()-> viewModel.onSaveClick()}" + android:text="保存" + android:textColor="@color/white" + android:textSize="16sp" + android:textStyle="bold" + android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.GONE : View.VISIBLE}" /> + diff --git a/app/src/main/res/layout/activity_uld_list.xml b/app/src/main/res/layout/activity_uld_list.xml index be9908d..8b760b1 100644 --- a/app/src/main/res/layout/activity_uld_list.xml +++ b/app/src/main/res/layout/activity_uld_list.xml @@ -1,118 +1,157 @@ + - + + + + + - - - + android:background="@color/phone_page_bg"> + android:layout_height="match_parent" + android:orientation="vertical"> - - - - - + + + android:background="@color/white" + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="16dp" + android:paddingBottom="12dp"> - - - + - - - - - + + android:layout_height="0dp" + android:layout_weight="1"> - + - + - + + - + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_gnj_move_stash_list.xml b/app/src/main/res/layout/item_gnj_move_stash_list.xml index b4268bd..6b2f863 100644 --- a/app/src/main/res/layout/item_gnj_move_stash_list.xml +++ b/app/src/main/res/layout/item_gnj_move_stash_list.xml @@ -1,287 +1,227 @@ + + + + + + + - - + android:layout_marginHorizontal="12dp" + android:layout_marginTop="12dp"> - - + android:layout_height="wrap_content" + android:background="@drawable/bg_phone_card" + android:onLongClick="@{()-> activity.showCardActionPhone(bean)}" + android:orientation="vertical" + android:padding="16dp"> - - + + android:gravity="center_vertical" + android:orientation="horizontal"> + + android:layout_width="20dp" + android:layout_height="20dp" + android:layout_marginEnd="8dp" + android:src="@drawable/ic_phone_check_round_unchecked" + android:visibility="@{bean.isMoved ? View.GONE : View.VISIBLE}" + loadImage="@{bean.checked.get() ? @drawable/ic_phone_check_round_checked : @drawable/ic_phone_check_round_unchecked}" /> - + + android:text="@{bean.wbNo}" + android:textColor="@{bean.isMoved ? @color/phone_text_title : @color/phone_primary}" + android:textSize="15sp" + android:textStyle="bold" /> - + + - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginStart="4dp" + android:text="已移库" + android:textColor="@color/phone_text_title" + android:textSize="13sp" /> - - + - - - - + - + + + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:layout_weight="1" + android:text='@{"航班信息:" + bean.orDash(bean.flight)}' + android:textColor="@color/phone_text_body" + android:textSize="13sp" + tools:text="航班信息:20260709/MU2311" /> - + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_uld_list.xml b/app/src/main/res/layout/item_uld_list.xml index d00a66d..c53aee6 100644 --- a/app/src/main/res/layout/item_uld_list.xml +++ b/app/src/main/res/layout/item_uld_list.xml @@ -1,320 +1,175 @@ + + + + + android:layout_marginHorizontal="12dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:padding="16dp"> - + + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal"> - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + android:layout_height="20dp" + android:layout_marginStart="8dp" + android:background="@{bean.isStatusAvailable ? @drawable/bg_phone_tag_green : @drawable/bg_phone_tag_red_light}" + android:gravity="center" + android:paddingHorizontal="8dp" + android:text="@{bean.statusPhoneName}" + android:textColor="@{bean.isStatusAvailable ? @color/phone_tag_green_text : @color/phone_danger_text}" + android:textSize="11sp" /> - + - + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documents/5.5寸手持端-后端字段补充清单.md b/documents/5.5寸手持端-后端字段补充清单.md new file mode 100644 index 0000000..f7328eb --- /dev/null +++ b/documents/5.5寸手持端-后端字段补充清单.md @@ -0,0 +1,55 @@ +# 5.5 寸手持端适配 —— 后端接口补充清单 + +适配手机端时,设计稿要求展示/筛选的部分内容,现有接口没有提供或未生效。 +以下为**实机联调(内网真实数据)验证过**的结论,客户端侧已按设计稿把 UI 与参数都做好, +后端补齐后即可直接生效,客户端基本无需再改。 + +整理时间:2026-07-29  验证环境:内网 `192.168.1.250:8093`,账号 ADMIN + +--- + +## 一、参数已发送但后端未生效(客户端已就绪,等后端支持) + +| 接口 | 参数 | 用途 | 实测现象 | +|------|------|------|----------| +| `IntExpOutHandover/pageQuery`
`IntExpOutHandover/pageQueryTotal` | `hoState`
("0" 待交接 / "1" 已交接) | 出库交接页「待交接 / 已交接」Tab 过滤与角标计数 | 请求体确认携带 `"hoState":"0"` / `"1"`,但两个 Tab 返回**完全相同**的数据;`pageQueryTotal` 的 `wbNumber` 也不随之变化 | +| `IntExpMove/pageQuery`
`IntExpMove/pageQueryTotal` | `spCode`
(特码,如 "PER") | 出港移库页筛选弹层「特码」 | 请求体确认携带 `"spCode":"PER"`,但返回结果中仍含特码为 `PEF` 的记录,角标计数也无变化 | + +> 对照参考:同一接口的 `moveState`、`by1`(承运人)、`awbType`(运单类型)**均正常生效** +> (实测承运人填 MU 后待移库数量 67 → 60),因此可以排除客户端传参方式的问题。 +> +> 若贵方使用的参数名与上表不同,请告知正确名称,客户端改一处常量即可。 + +--- + +## 二、接口未返回、设计稿需要展示的字段 + +以下字段目前接口完全不返回,或恒为 `null`。客户端已按设计稿保留完整 UI 结构, +数据为空时统一占位 `-`,后端补齐后自动显示。 + +### 2.1 `IntExpMove/pageQuery`(出港移库) + +| 设计稿位置 | 需要的字段 | 当前状况 | +|------------|-----------|----------| +| 列表卡片第二行「航班信息:20260709/MU2311」 | 航班日期 + 航班号(如 `fdate` / `fno`) | **接口无任何航班相关字段** | +| 详情页「移库信息」卡片 —— 移库人 | 移库人**姓名**(现只有 `opId`) | `opId` 恒为 `null`,且即便有值也是 ID 而非姓名 | +| 详情页「移库信息」卡片 —— 移库时间 | `opdate` | 恒为 `null`,**已移库记录也是 null** | +| 详情页蓝色表头右侧时间戳 | 同上 `opdate` | 同上 | +| 详情页类型行右侧「普通货物运输」 | `businessType` 或 `cargoType` | 两者均恒为 `null` | + +### 2.2 说明 + +- 出库交接页的详情所需字段(板型、库位、复磅人/时间等)接口均已返回,无需补充。 +- 移库详情页红色标签绑的是 `awbTypeName`(真实值形如「国际出港(国际直航)」),已正常。 + 设计稿上写的「国际进港」经判断为设计稿笔误(该页属国际出港),未按字面实现。 + +--- + +## 三、附:字典接口调用口径(客户端已按此对齐,仅作备忘) + +`typeCode/getSpecialCodeByParam`(特码字典): + +- `flag=1`(国际)+ `ieFlag=""`(**留空**)→ 正常返回 RXE/PER/PEM/PEF/LDP/GJJ/DGR 等 +- `flag=1` + `ieFlag="E"` → 返回 `"data": []` 空数组 + +国际出港各页面统一使用 `ieFlag=""`。如该行为非预期,请后端确认 `ieFlag` 的取值约定。 diff --git a/documents/5.5寸手持端设计文件/00_全部入口总览.html b/documents/5.5寸手持端设计文件/00_全部入口总览.html new file mode 100644 index 0000000..2907b32 --- /dev/null +++ b/documents/5.5寸手持端设计文件/00_全部入口总览.html @@ -0,0 +1,195 @@ + + + + +5.5 寸手持端设计稿 · 全部入口总览 + + + + +
+

5.5 寸手持端设计稿 · 全部入口总览 + 共 11 个业务入口 · 单机画布 390 × 844 +

+
+ + + + + + + +
+
+ +
+ + + + diff --git a/documents/5.5寸手持端设计文件/01_ULD管理/ULD管理.html b/documents/5.5寸手持端设计文件/01_ULD管理/ULD管理.html new file mode 100644 index 0000000..e171678 --- /dev/null +++ b/documents/5.5寸手持端设计文件/01_ULD管理/ULD管理.html @@ -0,0 +1,650 @@ + + + + + +ULD管理 + + + + + +
+
+
+ 09:41 +
+ + + +
+
+
+
+
+ + 返回 +
+

ULD管理

+
+
+
+
+ + + +
+ +
+
+
+ +
+
+
+ + PMC78133363396 +
+
+ 占用 + +
+
+
+
+
+ + 所属航司:CA +
+ +
+ + 来源:空运 +
+
+
+ + 入库时间:2026-07-20 15:59:20 +
+
+
+ +
+
+
+ + PMC78133363397 +
+
+ 可用 + +
+
+
+
+
+ + 所属航司:CA +
+
+ + 来源:空运 +
+
+ +
+ + 入库时间:2026-07-20 15:59:20 +
+
+
+ +
+
+
+ + PMC78133363398 +
+
+ 维修 + +
+
+
+
+
+ + 所属航司:CA +
+
+ + 来源:陆运 +
+
+
+ + 入库时间:2026-07-20 15:59:20 +
+
+
+ +
+
+
+ + PMC78133363399 +
+
+ 占用 + +
+
+
+
+
+ + 所属航司:CA +
+
+ + 来源:空运 +
+
+
+ + 入库时间:2026-07-20 15:59:20 +
+
+
+
+
+
+ +
+
+
+ + +
+
+
+ 已选 0 / 4 项 + +
+
+
+
+ + + + + + + + + + + + + +
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/02_出库交接/出库交接.html b/documents/5.5寸手持端设计文件/02_出库交接/出库交接.html new file mode 100644 index 0000000..718e8cb --- /dev/null +++ b/documents/5.5寸手持端设计文件/02_出库交接/出库交接.html @@ -0,0 +1,715 @@ + + + + + +出库交接 - 移动端原型 + + + + + + + +
+ +
+09:41 +
+ + + +
+
+ + +
+ +
+
+ +返回 +
+

出库交接

+
+
+ + +
+ +
+
+
+ + + +
+ +
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
PMC90901CZ
+
+
IMP代码
+
+ +
+ +航班信息:20260706/MU1111 +
+ +
+
+架子车号 +076 +
+
+目的港 +LAX +
+
+装机重 +200kg +
+
+ +
+ +库位:大棚A3 +
+ +
+
+ 查看详情 +
+
+
+ + +
+ +
+
+ +
AKE87865MU
+
+
IMP代码
+
+ +
+ +航班信息:20260706/CA2222 +
+ +
+
+架子车号 +066 +
+
+目的港 +LAX +
+
+装机重 +150kg +
+
+ +
+ +库位:空侧-05 +
+ +
+
+ 查看详情 +
+
+
+ + +
+ +
+
+ +
PMC87678MU
+
+
IMP代码
+
+ +
+ +航班信息:20260707/3U3333 +
+ +
+
+架子车号 +055 +
+
+目的港 +LAX +
+
+装机重 +450kg +
+
+ +
+ +库位:C区-01货位 +
+ +
+
+ 查看详情 +
+
+
+
+ + + +
+ + +
+ +
+
+ + +
+
+
+已选 0 项 + +
+
+
+
+ + +
+ +
+
+ +返回 +
+

板箱详情

+
+
+ +
+ +
+ +
+781-22222620 +
+
+ +
076
+ +
+
+
装机重
+
55
+
+
+
航班日期
+
20260706
+
+
+
航班号
+
MU2231
+
+
+
目的港
+
LAX
+
+
+
IMP代码
+
X
+
+
+
板型
+
QP
+
+
+
库位
+
+1111111 +
+
+
+
备注
+
危险品
+
+
+
+
+ + + +
+
+ + + + + +
+ + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/03_出港计重/detail.html b/documents/5.5寸手持端设计文件/03_出港计重/detail.html new file mode 100644 index 0000000..af16584 --- /dev/null +++ b/documents/5.5寸手持端设计文件/03_出港计重/detail.html @@ -0,0 +1,151 @@ + + + + + +运单详情 + + + + + +
+
+ + +
+ +
+ 09:10 +
+ + + +
+
+ +
+ + + 返回 + +

运单详情

+
+
+ +
+ +
+ +
+
+ 基本信息 +
+ +
+
+ 运单号 + 78111112222 +
+
+ 件数 + 85 +
+
+ 重量 + 156 kg +
+
+ 特码 + DGR +
+
+ 代理 + ATR +
+
+ 航班日期 + 2026-07-03 +
+
+ 航班号 + MU2222 +
+
+ 航程 + PVG - CAN +
+
+ 业务类型 + 普通货物运输 +
+
+ 通道号 + 出港收运1 +
+
+ 品名 + 电子产品 +
+
+ 备注 + 测试 +
+
+
+
+
+ + +
+
+
+ +
+
+ + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/03_出港计重/main.html b/documents/5.5寸手持端设计文件/03_出港计重/main.html new file mode 100644 index 0000000..0e77c7b --- /dev/null +++ b/documents/5.5寸手持端设计文件/03_出港计重/main.html @@ -0,0 +1,419 @@ + + + + + +出港待计重 + + + + + +
+
+ +
+ +
+ 09:10 +
+ + + +
+
+ +
+ +

出港待计重

+
+
+
+ + +
+ +
+
+
+
+ + + +
+ +
+
+ + +
+
+
总票数
+
4
+
+
+
待计重
+
2
+
+
+
计重中
+
2
+
+
+ + +
+ +
+
+
+
+
+ 78133363398 +
+
+ 未计重 + +
+
+
+
+ + 日期:20260721 +
+
+ + 航程:HFE-LAX +
+
+ + 代理:中外运 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ 78133363399 +
+
+ 计重中 + +
+
+
+
+ + 日期:20260721 +
+
+ + 航程:HFE-LAX +
+
+ + 代理:中外运 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ 78133363398 +
+
+ 未计重 + +
+
+
+
+ + 日期:20260721 +
+
+ + 航程:HFE-LAX +
+
+ + 代理:中外运 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ 78133363399 +
+
+ 计重中 + +
+
+
+
+ + 日期:20260721 +
+
+ + 航程:HFE-LAX +
+
+ + 代理:中外运 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + + + + + + +
+
+
+

筛选条件

+ +
+
+ +
+ 请选择通道号 + +
+
+
+
+ +
+ MU +
+
+
+ +
+ 请选择运单类型 + +
+
+
+ +
+ 请选择代理人 + +
+
+
+ + +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/03_出港计重/records.html b/documents/5.5寸手持端设计文件/03_出港计重/records.html new file mode 100644 index 0000000..6e1c709 --- /dev/null +++ b/documents/5.5寸手持端设计文件/03_出港计重/records.html @@ -0,0 +1,282 @@ + + + + + +计重记录 - 国际货运管理系统 + + + + + + +
+ +
+09:10 +
+ + + +
+
+ +
+ +

计重记录

+ +
+ +
+ +
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+总票数 +3 +
+ +
+总件数 +1 +
+ +
+总重量 +2 +
+
+
+ +
+ +
+
+78133363396 + + +
+
+ +代理 : 马道 +
+
+
+运抵件数 +
+85件 +
+
+
+运抵重量 +
+1560kg +
+
+
+运抵体积 +
+12.5m³ +
+
+
+
+ +计重人:张三 · 计重时间:2026-07-05 11:45 +
+
+
+查看详情 +
+
+
+ +
+
+78133363398 + + +
+
+ +代理 : 徽远 +
+
+
+运抵件数 +
+60件 +
+
+
+运抵重量 +
+920kg +
+
+
+运抵体积 +
+8.1m³ +
+
+
+
+ +计重人:张三 · 计重时间:2026-07-05 11:45 +
+
+
+查看详情 +
+
+
+
+
+ +
+ + +
+ + +
+
+

筛选条件

+ +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ 全部 + +
+
+
+ +
+ 全部 + +
+
+
+ + +
+
+ +
+ 全部 + +
+
+
+ + +
+
+
+
+ +
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/03_出港计重/weighing.html b/documents/5.5寸手持端设计文件/03_出港计重/weighing.html new file mode 100644 index 0000000..eca89ab --- /dev/null +++ b/documents/5.5寸手持端设计文件/03_出港计重/weighing.html @@ -0,0 +1,256 @@ + + + + + +开始计重 - 货运管理系统 + + + + + +
+
+ + +
+ +
+ 09:10 +
+ + + +
+
+ +
+ +

开始计重

+
+
+
+ + +
+ +
+
+ +
+ +
+ +
+
+
+ 781333617 +
+ 国际出港 +
+ +
+ +
+ +
+
+

预配件数

+

85

+
+
+

预配重量

+

156 kg

+
+
+

预配体积

+

4.52 m³

+
+
+
+
+
+
+ + +
+ +
+ 通道号* + 请选择通道号 + +
+ +
+ 代理人 + 请选择代理人 + +
+ +
+ 特码 + 请选择特码 + +
+ +
+ 运抵件数 + +
+ +
+ 运抵重量 + +
+ +
+ 运抵体积 + +
+ +
+ 托盘数量 + +
+ +
+ 托盘自重 + +
+ + +
+ 航班日期 + 20260703 +
+
+ 航班号 + MU1111 +
+
+ 航程 + HFE-LAX +
+ + +
+ 业务类型 + 普通货物运输 + +
+ + +
+ 备注 + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/03_出港计重/weight-detail.html b/documents/5.5寸手持端设计文件/03_出港计重/weight-detail.html new file mode 100644 index 0000000..5d5a78a --- /dev/null +++ b/documents/5.5寸手持端设计文件/03_出港计重/weight-detail.html @@ -0,0 +1,306 @@ + + + + + +计重明细 + + + + + + +
+
+ + +
+ 09:41 +
+ + + +
+
+ + +
+
+
+ + 返回 +
+

计重明细

+
+ +
+ +
+ +
+ +
+ +
+
+
+ 78133363396 +
+ 国际出港 +
+ +
+ +
+
+
+

总件数

+

85

+
+
+

总重量(kg)

+

1560

+
+
+

托盘总重(kg)

+

54

+
+
+
+
+
+ + +
+
+
+ 运抵件数 + 20 +
+
+ 运抵重量(kg) + 320 +
+
+ 托盘自重(kg) + 18 +
+
+ 计重人 + 张三 +
+
+ 计重时间 + 2026-07-05 10:02:15 +
+
+
+ +
+
+ + +
+
+
+ 运抵件数 + 35 +
+
+ 运抵重量(kg) + 590 +
+
+ 托盘自重(kg) + 18 +
+
+ 计重人 + 张三 +
+
+ 计重时间 + 2026-07-05 10:25:30 +
+
+
+ +
+
+ + +
+
+
+ 运抵件数 + 30 +
+
+ 运抵重量(kg) + 650 +
+
+ 托盘自重(kg) + 18 +
+
+ 计重人 + 张三 +
+
+ 计重时间 + 2026-07-05 11:45:08 +
+
+
+ +
+
+
+
+ + + +
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/04_出港运抵/main.html b/documents/5.5寸手持端设计文件/04_出港运抵/main.html new file mode 100644 index 0000000..ac51214 --- /dev/null +++ b/documents/5.5寸手持端设计文件/04_出港运抵/main.html @@ -0,0 +1,535 @@ + + + + + +出港运抵 + + + + + + + +
+
+
+ 09:41 +
+ + + +
+
+
+
+ + 返回 +
+
+

出港运抵

+
+
+
+
+ +
+
+
+
+ + + +
+ +
+
+ +
+ + +
+ +
+ + + +
+
+
+ +
+ 78133363396 +
+
+
+ 提前运抵 +
+
+
+
代理:马道
+
件数:444
+
重量:150kg
+
航班日期:20260713
+
航班号:MU2033
+
航程:HFE-LAX
+
+
+ +
+ + +
+ + + + + + + + + + +
+
+ + +
+
+
+ + 全选 +
+
+ 已选 0 项 + +
+
+
+
+
+
+ + + +
+
+
+

筛选条件

+ +
+
+
+ +
+ + +
+
+
+ +
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + + +
+
+
+

重置状态

+ +
+
+ + +
+ +
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/04_出港运抵/receipt.html b/documents/5.5寸手持端设计文件/04_出港运抵/receipt.html new file mode 100644 index 0000000..73e8d44 --- /dev/null +++ b/documents/5.5寸手持端设计文件/04_出港运抵/receipt.html @@ -0,0 +1,73 @@ + + + + + + +回执内容 + + + + +
+ +
+09:41 +
+ + + +
+
+ +
+ + + 返回 + +回执内容 +
+
+ +
+ +
+

+ + 单一窗口回执内容:45201 空运出口运抵报告传输成功。比对结果为:运抵正常。 +

+

2026-07-10 11:03:38

+
+ +
+

+ + 单一窗口回执内容:45202 空运出口运抵报告传输成功。比对结果为:运抵正常。 +

+

2026-07-10 11:02:15

+
+ +
+

+ + 单一窗口回执内容:45203 空运出口运抵报告传输成功。比对结果为:运抵正常。 +

+

2026-07-10 10:58:42

+
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/04_出港运抵/suborder.html b/documents/5.5寸手持端设计文件/04_出港运抵/suborder.html new file mode 100644 index 0000000..9354985 --- /dev/null +++ b/documents/5.5寸手持端设计文件/04_出港运抵/suborder.html @@ -0,0 +1,413 @@ + + + + + +分单明细 - 出港运抵 + + + + + + + + +
+ +
+ +
+ 09:41 +
+ + + +
+
+ +
+ +

分单明细

+
+
+
+ +
+
+
+ + + +
+ +
+
+ +
+ +
+ +
+
+
+ + 10001 +
+
+ W + 11 +
+
+
+
+ 件数:150 +
+
+ 重量:50kg +
+
+ + +
+ +
+
+
+ + 10002 +
+
+ E + 11 +
+
+
+
+ 件数:120 +
+
+ 重量:40kg +
+
+ + +
+ +
+
+
+ + 10003 +
+
+ 01 + 11 +
+
+
+
+ 件数:174 +
+
+ 重量:60kg +
+
+ + +
+
+
+ +
+
+
+ + 全选 +
+
+ 已选 0 / 3 项 + +
+
+ + + + +
+
+ +
+

筛选条件

+ +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+
+
+
+ + +
+
+ +
+

重置状态

+ +
+ +
+ + +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/04_出港运抵/supplement.html b/documents/5.5寸手持端设计文件/04_出港运抵/supplement.html new file mode 100644 index 0000000..4349c2d --- /dev/null +++ b/documents/5.5寸手持端设计文件/04_出港运抵/supplement.html @@ -0,0 +1,175 @@ + + + + + + +补充信息 + + + + + + + + +
+ +
+ +
+09:41 +
+ + + +
+
+ +
+
+ +返回 +
+
+

补充信息

+
+
+
+
+ +
+ +
+
+
+

收货人信息

+
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+

发货人信息

+
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ + +
+
+
+
+
+ + diff --git a/documents/5.5寸手持端设计文件/05_出港移库/出港移库.html b/documents/5.5寸手持端设计文件/05_出港移库/出港移库.html new file mode 100644 index 0000000..d9a166d --- /dev/null +++ b/documents/5.5寸手持端设计文件/05_出港移库/出港移库.html @@ -0,0 +1,668 @@ + + + + + +出港移库 - 移动端原型 + + + + + + + +
+ +
+ 09:41 +
+ + + +
+
+ + +
+ +
+
+ + 返回 +
+

出港移库

+
+
+ + +
+ +
+
+
+ + + +
+ +
+
+ + +
+ + +
+ + +
+ +
+
+
+ +
78133363573
+
+
+ 待移库 +
+
+
+ + 航班信息:20260709/MU2311 +
+
+
+ 特码 + NOR +
+
+ 件数 + 100件 +
+
+ 重量 + 200kg +
+
+
+ + 出运路径:HFE-NKG-LAX +
+
+
+ 查看详情 +
+
+
+ +
+
+
+ +
78133363574
+
+
+ 待移库 +
+
+
+ + 航班信息:20260709/MU2311 +
+
+
+ 特码 + CTA +
+
+ 件数 + 85件 +
+
+ 重量 + 150kg +
+
+
+ + 出运路径:HFE-PEK-LAX +
+
+
+ 查看详情 +
+
+
+ +
+
+
+ +
78133363575
+
+
+ 待移库 +
+
+
+ + 航班信息:20260709/MU2311 +
+
+
+ 特码 + DHL +
+
+ 件数 + 200件 +
+
+ 重量 + 450kg +
+
+
+ + 出运路径:HFE-NKG-LAX +
+
+
+ 查看详情 +
+
+
+
+ + + +
+ + +
+
+
+ + +
+
+
+ 已选 0 项 + +
+
+
+
+ + +
+
+
+ + 返回 +
+

运单详情

+
+
+
+
+
+ 781-22222620 +
+ + 2026-05-10 08:30 +
+
+
+
+
+ 国际进港 + 普通货物运输 +
+
+
电子元器件
+
+
+
件数
+
55
+
+
+
重量
+
622.00 kg
+
+
+
特码
+
NOR
+
+
+
代理
+
NOR
+
+
+
出运路径
+
HFE-PEK-LAX
+
+
+
承运人
+
MU
+
+
+
+
+ + + +
+
+ + +
+ + +
+
+

筛选条件

+ +
+
+
+ +
+ MU +
+
+
+ +
+ 请选择运单类型 + +
+
+
+ +
+ 请选择特码 + +
+
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/06_出港查询/出港查询.html b/documents/5.5寸手持端设计文件/06_出港查询/出港查询.html new file mode 100644 index 0000000..73ed37e --- /dev/null +++ b/documents/5.5寸手持端设计文件/06_出港查询/出港查询.html @@ -0,0 +1,963 @@ + + + + + +出港查询 + + + + + + + +
+ +
+
+ 09:41 +
+ + + +
+
+
+ + +
+
+ +
+ +

筛选条件

+
+ + +
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ + +
+ + +
+
+ + +
+ +
+ +
+ +

出港查询

+
+ +
+
+ + + +
+ + +
+ +
+
+
总票数
+
4
+
+
+
已入库
+
2
+
+
+
已离港
+
2
+
+
+ +
+ +
+
+
+
+ 78133363396 +
+
+ 已离港 + +
+
+
+
+ + 件数:444 +
+
+ + 重量:555 +
+
+ + 代理人:中外运 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+ 78133363397 +
+
+ 已离港 + +
+
+
+
+ + 件数:120 +
+
+ + 重量:320 +
+
+ + 代理人:徽远 +
+
+ + 特码:PER +
+
+
+ +
+
+
+
+ 78133363398 +
+
+ 已入库 + +
+
+
+
+ + 件数:268 +
+
+ + 重量:780 +
+
+ + 代理人:中外运 +
+
+ + 特码:NOR +
+
+
+ +
+
+
+
+ 78133363399 +
+
+ 已离港 + +
+
+
+
+ + 件数:85 +
+
+ + 重量:210 +
+
+ + 代理人:徽远 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+ 78133363400 +
+
+ 已离港 + +
+
+
+
+ + 件数:350 +
+
+ + 重量:430 +
+
+ + 代理人:中外运 +
+
+ + 特码:PER +
+
+
+
+
+ + +
+ +
+ +

出港详情

+
+ +
+
+ +
+
+
运单信息
+
仓库信息
+
库位信息
+
+
+ +
+ +
+ +
+
+
+ 基本信息 +
+
+ +
+ 运单号 + +
+
+ 代理人 + +
+ +
+ 特码 + +
+
+ 承运人 + +
+ +
+ 始发站 + +
+
+ 中转站 + +
+
+ 目的站 + +
+
+ UN编号 + +
+ +
+ 锁定状态 +
+ +
+
+
+ 包装类型 + +
+ +
+ 业务类型 + +
+
+ 入库时间 + +
+ +
+ 运单类型 + +
+ +
+ 品名(中) + +
+ +
+ 品名(英) + +
+ +
+ 备注 + +
+
+
+ +
+
+
+ 件重信息 +
+
+
+
运单件数
+
+
+
+
运单重量
+
+
+
+
计费重量
+
+
+
+
入库件数
+
+
+
+
入库重量
+
+
+
+
—
+
—
+
+
+
+ +
+
+
+ 回执信息 +
+
+
+
预配舱单
+
+
+
+
运抵报告
+
+
+
+
海关放行
+
+
+
+
+
+
装载舱单
+
+
+
+
理货报告
+
+
+
+
—
+
—
+
+
+
+
+ + + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/06_出港查询/运单追踪.html b/documents/5.5寸手持端设计文件/06_出港查询/运单追踪.html new file mode 100644 index 0000000..7d9fe30 --- /dev/null +++ b/documents/5.5寸手持端设计文件/06_出港查询/运单追踪.html @@ -0,0 +1,139 @@ + + + + + + +国际进港查询 - 物流跟踪 + + + + + + +
+ +
+ +
+09:41 +
+ + + +
+
+ +
+ +

运单追踪

+
+
+
+ +
+ +
+ +
+
+ 78133358743 +
+国际进港 +
+ +
+

+ + + + + 流转状态 +

+
+
+
+ +
+
+
+
+
+
+原始舱单 +放行状态:01 +
+
+2026-07-10 11:03:38 +
+
+
+ +
+
+
+分拣理货 +
+
+ +
+
+
+理货申报 +
+
+ +
+
+
+海关放行 +
+
+ +
+
+
+柜台办结 +
+
+ +
+
+
+提取出库 +
+
+
+
+ + +
+

查询信息已触底

+
+
+ +
+
+
+
+ +ml> \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/07_出港仓库/出港仓库.html b/documents/5.5寸手持端设计文件/07_出港仓库/出港仓库.html new file mode 100644 index 0000000..7dd7e47 --- /dev/null +++ b/documents/5.5寸手持端设计文件/07_出港仓库/出港仓库.html @@ -0,0 +1,932 @@ + + + + + +出港仓库 + + + + + +
+ +
+09:41 +
+ + + +
+
+ +
+
+ +
+ +

筛选条件

+
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ + +
+
+ + +
+ +
+ +
+ +

出港仓库

+
+
+ +
+
+
+ + + +
+ +
+
+ +
+
+
总票数
+
8
+
+
+
未清仓
+
4
+
+
+
已清仓
+
4
+
+
+ +
+
+ + + + +
+ + +
+ +
+ +

出港货物

+
+ +
+
+ +
+
+
运单信息
+
仓库信息
+
库位信息
+
+
+ +
+ +
+ +
+
+
+基本信息 +
+
+ +
+运单号 + +
+
+代理人 + +
+ +
+特码 + +
+
+承运人 + +
+ +
+始发站 + +
+
+中转站 + +
+ +
+目的站 + +
+
+UN编号 + +
+ +
+锁定状态 +
+ +
+
+
+包装类型 + +
+ +
+业务类型 + +
+
+入库时间 + +
+ +
+运单类型 + +
+ +
+品名(中) + +
+ +
+品名(英) + +
+ +
+备注 + +
+
+
+ +
+
+
+件重信息 +
+
+
+
运单件数
+
+
+
+
运单重量
+
+
+
+
计费重量
+
+
+
+
入库件数
+
+
+
+
入库重量
+
+
+
+
—
+
—
+
+
+
+ +
+
+
+回执信息 +
+
+
+
预配舱单
+
+
+
+
运抵报告
+
+
+
+
海关放行
+
+
+
+
+
+
装载舱单
+
+
+
+
理货报告
+
+
+
+
+
+ + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/07_出港仓库/运单追踪.html b/documents/5.5寸手持端设计文件/07_出港仓库/运单追踪.html new file mode 100644 index 0000000..7d9fe30 --- /dev/null +++ b/documents/5.5寸手持端设计文件/07_出港仓库/运单追踪.html @@ -0,0 +1,139 @@ + + + + + + +国际进港查询 - 物流跟踪 + + + + + + +
+ +
+ +
+09:41 +
+ + + +
+
+ +
+ +

运单追踪

+
+
+
+ +
+ +
+ +
+
+ 78133358743 +
+国际进港 +
+ +
+

+ + + + + 流转状态 +

+
+
+
+ +
+
+
+
+
+
+原始舱单 +放行状态:01 +
+
+2026-07-10 11:03:38 +
+
+
+ +
+
+
+分拣理货 +
+
+ +
+
+
+理货申报 +
+
+ +
+
+
+海关放行 +
+
+ +
+
+
+柜台办结 +
+
+ +
+
+
+提取出库 +
+
+
+
+ + +
+

查询信息已触底

+
+
+ +
+
+
+
+ +ml> \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/08_进港仓库/运单追踪.html b/documents/5.5寸手持端设计文件/08_进港仓库/运单追踪.html new file mode 100644 index 0000000..7d9fe30 --- /dev/null +++ b/documents/5.5寸手持端设计文件/08_进港仓库/运单追踪.html @@ -0,0 +1,139 @@ + + + + + + +国际进港查询 - 物流跟踪 + + + + + + +
+ +
+ +
+09:41 +
+ + + +
+
+ +
+ +

运单追踪

+
+
+
+ +
+ +
+ +
+
+ 78133358743 +
+国际进港 +
+ +
+

+ + + + + 流转状态 +

+
+
+
+ +
+
+
+
+
+
+原始舱单 +放行状态:01 +
+
+2026-07-10 11:03:38 +
+
+
+ +
+
+
+分拣理货 +
+
+ +
+
+
+理货申报 +
+
+ +
+
+
+海关放行 +
+
+ +
+
+
+柜台办结 +
+
+ +
+
+
+提取出库 +
+
+
+
+ + +
+

查询信息已触底

+
+
+ +
+
+
+
+ +ml> \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/08_进港仓库/进港仓库.html b/documents/5.5寸手持端设计文件/08_进港仓库/进港仓库.html new file mode 100644 index 0000000..d7e80a7 --- /dev/null +++ b/documents/5.5寸手持端设计文件/08_进港仓库/进港仓库.html @@ -0,0 +1,924 @@ + + + + + +进港仓库 + + + + + +
+ +
+09:41 +
+ + + +
+
+ +
+
+ +
+ +

筛选条件

+
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ + +
+
+ + +
+ +
+ +
+ +

进港仓库

+
+
+ +
+
+
+ + + +
+ +
+
+ +
+
+
总票数
+
8
+
+
+
未清仓
+
4
+
+
+
已清仓
+
4
+
+
+ +
+
+ + + + +
+ + +
+ +
+ +

进港货物

+
+ +
+
+ +
+
+
运单信息
+
仓库信息
+
库位信息
+
+
+ +
+ +
+ +
+
+
+基本信息 +
+
+ +
+运单号 + +
+
+代理人 + +
+ +
+特码 + +
+
+承运人 + +
+ +
+始发站 + +
+
+中转站 + +
+ +
+目的站 + +
+
+UN编号 + +
+ +
+锁定状态 +
+ +
+
+
+包装类型 + +
+ +
+业务类型 + +
+
+入库时间 + +
+ +
+运单类型 + +
+ +
+品名(中) + +
+ +
+品名(英) + +
+ +
+备注 + +
+
+
+ +
+
+
+件重信息 +
+
+
+
运单件数
+
+
+
+
运单重量
+
+
+
+
计费重量
+
+
+
+
入库件数
+
+
+
+
入库重量
+
+
+
+
—
+
—
+
+
+
+ +
+
+
+回执信息 +
+
+
+
原始舱单
+
+
+
+
理货报告
+
+
+
+
海关放行
+
+
+
+
+
+
+
+ + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/09_提取出库/提取出库.html b/documents/5.5寸手持端设计文件/09_提取出库/提取出库.html new file mode 100644 index 0000000..cc8763c --- /dev/null +++ b/documents/5.5寸手持端设计文件/09_提取出库/提取出库.html @@ -0,0 +1,773 @@ + + + + + + +提取出库 - 移动端原型 + + + + + + + +
+ +
+09:41 +
+ + + +
+
+ +
+ +
+
+ +返回 +
+

提取出库

+
+
+ +
+ +
+
+
+ + + +
+ +
+
+ +
+ + +
+ +
+ +
+ +
+
+ +
78133363573
+
+
+ 待出库 +
+
+ +
+ +提货车辆:皖A88909 +
+ +
+
+代理人 +ATR +
+
+件数 +100件 +
+
+重量 +200kg +
+
+ +
+ +库位:A区-03货位 +
+ +
+
+ 查看详情 +
+
+
+ +
+ +
+
+ +
78133363574
+
+
+ 待出库 +
+
+ +
+ +提货车辆:皖A78223 +
+ +
+
+代理人 +CTA +
+
+件数 +85件 +
+
+重量 +150kg +
+
+ +
+ +库位:B区-05货位 +
+ +
+
+ 查看详情 +
+
+
+ +
+ +
+
+ +
78133363575
+
+
+ 待出库 +
+
+ +
+ +提货车辆:皖A67651 +
+ +
+
+代理人 +DHL +
+
+件数 +200件 +
+
+重量 +450kg +
+
+ +
+ +库位:C区-01货位 +
+ +
+
+ 查看详情 +
+
+
+
+ + +
+ +
+ +
+
+ + +
+
+
+已选 0 项 + +
+
+
+
+ +
+ +
+
+ +返回 +
+

运单详情

+
+
+
+ +
+ +
+781-22222620 +
+ +2026-05-10 08:30 +
+
+
+ +
+
+国际进港 +普通货物运输 +
+
+ +
电子元器件
+ +
+
+
件数
+
55
+
+
+
重量
+
622.00 kg
+
+
+
特码
+
NOR
+
+
+
代理
+
ATR
+
+
+
始发港
+
LAX
+
+
+
目的港
+
MU
+
+
+
航班日期
+
20260706
+
+
+
航班号
+
MU1111
+
+
+
库位
+
+1111111 +
+
+
+
提货单号
+
TH20260706001
+
+
+
+
+ + +
+
+ +
+ +
+ +
+

筛选条件

+ +
+ +
+ +
+ +
+ 请选择航班日期 + +
+
+ +
+ +
+ +
+
+ +
+ +
+ 请选择代理人 + +
+
+ +
+ +
+ 请选择特码 + +
+
+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/10_进港查询/运单追踪.html b/documents/5.5寸手持端设计文件/10_进港查询/运单追踪.html new file mode 100644 index 0000000..adbe4c0 --- /dev/null +++ b/documents/5.5寸手持端设计文件/10_进港查询/运单追踪.html @@ -0,0 +1,147 @@ + + + + + + +国际进港查询 - 物流跟踪 + + + + + + +
+ +
+ +
+09:41 +
+ + + +
+
+ +
+ +

运单追踪

+
+
+
+ +
+ +
+ +
+
+ 78133358743 +
+国际进港 +
+ +
+

+ + + + + 流转状态 +

+
+
+
+ + +
+
+
+
+
+
+原始舱单 +放行状态:01 +
+
+2026-07-10 11:03:38 +
+
+
+ +
+
+
+航班落地 +
+
+ +
+
+
+分拣理货 +
+
+ +
+
+
+理货申报 +
+
+ +
+
+
+海关放行 +
+
+ +
+
+
+柜台办结 +
+
+ +
+
+
+提取出库 +
+
+
+
+ + +
+

查询信息已触底

+
+
+ +
+
+
+
+ +ml> \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/10_进港查询/进港查询.html b/documents/5.5寸手持端设计文件/10_进港查询/进港查询.html new file mode 100644 index 0000000..f764f58 --- /dev/null +++ b/documents/5.5寸手持端设计文件/10_进港查询/进港查询.html @@ -0,0 +1,951 @@ + + + + + +进港查询 + + + + + + + +
+ +
+
+ 09:41 +
+ + + +
+
+
+ + +
+
+ +
+ +

筛选条件

+
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ + +
+
+ + +
+ +
+ +
+ +

进港查询

+
+ +
+
+ + + +
+ + +
+ +
+
+
总票数
+
4
+
+
+
已入库
+
2
+
+
+
已出库
+
2
+
+
+ +
+ +
+
+
+
+ 78133363396 +
+
+ 已出库 + +
+
+
+
+ + 件数:444 +
+
+ + 重量:555 +
+
+ + 代理人:中外运 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+ 78133363397 +
+
+ 已出库 + +
+
+
+
+ + 件数:120 +
+
+ + 重量:320 +
+
+ + 代理人:徽远 +
+
+ + 特码:PER +
+
+
+ +
+
+
+
+ 78133363398 +
+
+ 已入库 + +
+
+
+
+ + 件数:268 +
+
+ + 重量:780 +
+
+ + 代理人:中外运 +
+
+ + 特码:NOR +
+
+
+ +
+
+
+
+ 78133363399 +
+
+ 已出库 + +
+
+
+
+ + 件数:85 +
+
+ + 重量:210 +
+
+ + 代理人:徽远 +
+
+ + 特码:DGR +
+
+
+ +
+
+
+
+ 78133363400 +
+
+ 已出库 + +
+
+
+
+ + 件数:350 +
+
+ + 重量:430 +
+
+ + 代理人:中外运 +
+
+ + 特码:PER +
+
+
+
+
+ + +
+ +
+ +

进港货物

+
+ +
+
+ +
+
+
运单信息
+
仓库信息
+
库位信息
+
+
+ +
+ +
+ +
+
+
+ 基本信息 +
+
+ +
+ 运单号 + +
+
+ 代理人 + +
+ +
+ 特码 + +
+
+ 承运人 + +
+ +
+ 始发港 + +
+
+ 中转站 + +
+ +
+ 目的站 + +
+
+ UN编号 + +
+ +
+ 锁定状态 +
+ +
+
+
+ 包装类型 + +
+ +
+ 业务类型 + +
+
+ 出库时间 + +
+ +
+ 运单类型 + +
+ +
+ 品名(中) + +
+ +
+ 品名(英) + +
+ +
+ 备注 + +
+
+
+ +
+
+
+ 件重信息 +
+
+
+
运单件数
+
+
+
+
运单重量
+
+
+
+
计费重量
+
+
+
+
已出库件数
+
+
+
+
已出库重量
+
+
+
+
—
+
—
+
+
+
+ +
+
+
+ 回执信息 +
+
+
+
原始舱单
+
+
+
+
理货报告
+
+
+
+
海关放行
+
+
+
+
+
+
+
+ + + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/11_进港移库/photo.html b/documents/5.5寸手持端设计文件/11_进港移库/photo.html new file mode 100644 index 0000000..b41cd69 --- /dev/null +++ b/documents/5.5寸手持端设计文件/11_进港移库/photo.html @@ -0,0 +1,134 @@ + + + + + +拍照上传 + + + + + + +
+ +
+ 09:41 +
+ + + +
+
+ + +
+
+ + 返回 +
+

拍照上传

+
+
+ + +
+
+
+

运单照片上传

+
+ +
+ + + +
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/documents/5.5寸手持端设计文件/11_进港移库/进港移库.html b/documents/5.5寸手持端设计文件/11_进港移库/进港移库.html new file mode 100644 index 0000000..6daace1 --- /dev/null +++ b/documents/5.5寸手持端设计文件/11_进港移库/进港移库.html @@ -0,0 +1,831 @@ + + + + + +进港移库 - 移动端原型 + + + + + + + +
+ +
+ 09:41 +
+ + + +
+
+ + +
+ +
+
+ + 返回 +
+

进港移库

+
+
+ + +
+ +
+
+
+ + + +
+ +
+
+ + +
+ + +
+ + +
+ +
+
+
+ +
78133363573
+
+
+ 待移库 +
+
+
+ + 航班信息:20260709/MU2311 +
+
+
+ 特码 + NOR +
+
+ 件数 + 100件 +
+
+ 重量 + 200kg +
+
+
+ + 出运路径:HFE-NKG-LAX +
+
+
+ 查看详情 +
+
+ +
+ +
+
+ +
+
+
+ +
78133363574
+
+
+ 待移库 +
+
+
+ + 航班信息:20260709/MU2311 +
+
+
+ 特码 + PER +
+
+ 件数 + 85件 +
+
+ 重量 + 150kg +
+
+
+ + 出运路径:HFE-PEK-LAX +
+
+
+ 查看详情 +
+
+
+ +
+
+ +
+
+
+ +
78133363575
+
+
+ 待移库 +
+
+
+ + 航班信息:20260709/MU2311 +
+
+
+ 特码 + DHL +
+
+ 件数 + 200件 +
+
+ 重量 + 450kg +
+
+
+ + 出运路径:HFE-NKG-LAX +
+
+
+ 查看详情 +
+
+
+ +
+
+
+ + + +
+ + +
+
+
+ + +
+
+
+ 已选 0 项 + +
+
+
+
+ + +
+
+
+ + 返回 +
+

运单详情

+
+
+
+
+
+ 781-22222620 +
+ + 2026-05-10 08:30 +
+
+
+
+
+ 国际进港 + 普通货物运输 +
+
+
电子元器件
+
+
+
件数
+
55
+
+
+
重量
+
622.00 kg
+
+
+
特码
+
NOR
+
+
+
代理
+
NOR
+
+
+
出运路径
+
HFE-PEK-LAX
+
+
+
承运人
+
MU
+
+
+
+
+ + + + + +
+
+ 异常照片 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+

筛选条件

+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ 请选择特码 + +
+
+
+ +
+ 请选择特码 + +
+
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documents/5.5寸手持端适配技术方案.md b/documents/5.5寸手持端适配技术方案.md new file mode 100644 index 0000000..028362b --- /dev/null +++ b/documents/5.5寸手持端适配技术方案.md @@ -0,0 +1,312 @@ +# 5.5 寸手持端适配技术方案 + +> 适用范围:AirLogistics(航空物流信息管理系统)在现有 10 寸平板横屏基础上,增加 5.5 寸手机竖屏形态。 +> 状态:核心技术已打通并双端实机验证,参考页面「国际出港移库」已完成。 +> 最后更新:2026-07-28 + +--- + +## 一、背景与结论 + +设计稿位于 `documents/5.5寸手持端设计文件/`,共 11 个业务入口、约 66 个界面(37 主页面 + 17 弹层/筛选面板 + 12 详情页 Tab 子页)。手机版是全新 UI,但**功能与交互保持不变**。 + +前置调研结论:**本项目的逻辑与 UI 分离度很好,适配成本可集中在 XML 布局重写。** + +| 层 | 现状 | 手机版可复用性 | +|---|---|---| +| ViewModel(业务逻辑/网络/校验/统计) | 继承 `BaseViewModel`/`BasePageViewModel`,只持有 `MutableLiveData`,不引用 View | 完全复用 | +| Activity | 极薄。如 `IntExpStorageUseActivity` 195 行仅引用 `binding.srl`/`binding.rv`/`binding.viewModel` | 完全复用 | +| ViewHolder | 通过 id 访问控件(`binding.ivIcon` 等) | 保留同名 id 即复用 | +| Adapter | `CommonAdapter` 由 `itemLayoutId` 驱动 | 完全复用 | +| 布局 XML | Pad 横屏专用,三列表单、宽表格式 item | 需全部重写 | +| 公共 UI 组件 | `PadSearchLayout` / `PadDataLayoutNew` / `title_tool_bar` 均为 Pad 形态 | 需新增手机版 | + +两个必须先解决的工程前提: + +1. `app/src/main/AndroidManifest.xml` 中 **95 个 Activity 全部为 `screenOrientation="userLandscape"`**(强制横屏) + —— 现已统一改为 `unspecified`,见下文「屏幕方向」一节 +2. 项目**没有任何资源限定符目录**,只有 `res/layout/` + +--- + +## 二、核心机制:布局按资源限定符自动切换 + +``` +res/layout-sw600dp/activity_xxx.xml ← 平板(最小宽度 >= 600dp,10 寸约 800dp) +res/layout/activity_xxx.xml ← 手机(兜底,5.5 寸约 360~411dp) +``` + +两个变体**同名**,DataBinding 会生成同一个 Binding 基类 + 两个变体实现: + +``` +ActivityIntExpMoveBinding.java 公共基类(Activity 中引用的就是它) +ActivityIntExpMoveBindingImpl.java 手机变体 +ActivityIntExpMoveBindingSw600dpImpl.java 平板变体 +``` + +因此 **Activity / ViewModel / ViewHolder 完全不需要设备判断**,`layoutId()` 与 `itemLayoutId` 保持原样。 + +### 为什么不用「不同文件名 + 代码判断」 + +这是本次实施中被否掉的第一版方案,务必不要走回去: + +```kotlin +// ❌ 错误做法 +override fun layoutId() = + if (DeviceUtil.isPhone()) R.layout.activity_xxx_phone else R.layout.activity_xxx +``` + +`activity_xxx_phone.xml` 生成的是 `ActivityXxxPhoneBinding`,而 `binding` 字段声明的类型是 `ActivityXxxBinding`。 +泛型擦除让**编译期完全不报错**,但一进入该页面就 `ClassCastException` 崩溃。 + +### 变体的强制约束 + +| 约束 | 说明 | 保障方式 | +|---|---|---| +| DataBinding 变量必须完全一致 | 两变体 `` 中 `` 名称与类型相同 | 编译失败 | +| 共用控件 id 双方都要有 | Activity/ViewHolder 引用的 id(`srl`/`rv`/`checkIcon`/`iv_icon` 等) | 见下 | +| 单侧独有 id 自动 `@Nullable` | 只在一个变体存在的 id,基类中为可空字段 | Kotlin 强制 null 检查 | + +实测生成的基类字段可空性(出港移库): + +```java +@NonNull public final TextView btnMove; // 两变体都有 +@NonNull public final ImageView checkIcon; // 两变体都有 +@NonNull public final RecyclerView rv; // 两变体都有 +@NonNull public final SmartRefreshLayout srl; // 两变体都有 +@Nullable public final EditText etWaybillNo; // 仅手机变体 +@Nullable public final PadSearchLayout searchWaybillNo; // 仅平板变体 +``` + +即:**约束由编译器静态保障**,不依赖人工检查。 + +--- + +## 三、已实现的基础设施 + +### 1. `DeviceUtil`(新增) + +`module_base/src/main/java/com/lukouguoji/module_base/util/DeviceUtil.kt` + +按 `smallestScreenWidthDp >= 600` 区分平板/手机。关键点:读取 `Resources.getSystem()`(系统资源)而非应用 Resources,以绕开 AndroidAutoSize 对 density 的动态改写,保证判定稳定。 + +```kotlin +DeviceUtil.isPhone() // 是否手机 +DeviceUtil.isTablet() // 是否平板 +DeviceUtil.describe() // "PHONE(sw=411dp, force=AUTO)",日志排查用 +DeviceUtil.forceMode = DeviceUtil.ForceMode.PHONE // 调试:平板上强制走手机布局 +``` + +`DeviceUtil.layout(padId, phoneId)` 仅用于极少数确实需要代码级判断的场景(如非 DataBinding 布局),**常规页面一律用资源限定符**。 + +### 2. 屏幕方向:Manifest 写 `unspecified` + `BaseActivity` 按形态锁定 + +方案演进过了两轮,两次失败都有明确的实测证据,记录如下以免重蹈。 + +**方案 A(一期,已废弃)**:Manifest 保持 `userLandscape`,靠 `BaseActivity` 在 `onCreate` 里纠正。 +缺陷:手机上「先横屏、约 1 秒后转竖屏」+ Activity 重建。因为 `screenOrientation` 由系统在 +**创建 Activity 窗口时**生效,那时业务代码还没跑,代码只能事后纠正。 + +**方案 B(二期,已废弃)**:Manifest 改 `@integer/screen_orientation`, +`values/`=portrait、`values-sw600dp/`=userLandscape。**看似优雅,实测是错的**: + +> 系统解析 Manifest 属性时用的是**默认配置,不套用设备限定符**,`values-sw600dp/` 永远不会被选中。 +> 于是所有 Activity 都拿到了 `values/` 的竖屏值,平板端出现两个回归: +> 1. 先竖屏、再被 `BaseActivity` 纠正成横屏(把闪屏从手机搬到了平板) +> 2. **AutoSize 在竖屏配置下选了平板竖屏基准 720dp**(density = 1280/720 ≈ 1.78, +> 正常应为 1280/1152 ≈ 1.11)→ 交互几次后整个 UI 放大约 1.6 倍 +> +> 验证方法(可复现):把 `values/` 与 `values-sw600dp/` 的取值**互换**,并临时让 +> `lockOrientationByDevice()` 返回 false 以排除运行时纠正,装到平板上看首屏方向 —— +> 结果平板取的是 `values/` 的值,证明限定符被忽略。 + +**方案 C(现行)**:Manifest 一律写 `unspecified`,系统按设备**当前物理方向**建窗口 +(平板横屏摆放 → 横屏起;手机 → 竖屏起,两端起始方向本就正确), +`BaseActivity.applyDeviceOrientation()` 再按形态锁定,因方向一致故不产生二次纠正与重建。 + +```xml + +``` + +全项目 192 处已统一(`module_p` 等 24 处本就是 `portrait` 的 PDA 页面保持不变)。 + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + applyDeviceOrientation() // 必须在 super 之前 + super.onCreate(savedInstanceState) + ... +} + +protected open fun applyDeviceOrientation() { + if (!lockOrientationByDevice()) return + requestedOrientation = if (DeviceUtil.isPhone(this)) + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + else + ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE +} + +protected open fun lockOrientationByDevice(): Boolean = true // 子类可覆写跳过 +``` + +**已知边界**:若平板在启动瞬间被竖持且系统自动旋转开启,仍会先竖后横。 +现场平板为横屏固定安装,实测无此现象。 + +**验证**:手机冷启动连拍 6 帧全为竖屏;平板冷启动连拍 6 帧全为横屏(1280×800), +登录后多次切换菜单与进出页面,UI 尺寸正常无放大。 + +### 3. AutoSize 设计基准(易漏,后果严重) + +`MyApplication.initAutoSizeConfig()` 原先只按方向切换,现增加手机分支: + +| 场景 | 设计基准 | +|---|---| +| 横屏(平板) | 1152 × 720 dp | +| 竖屏 + 手机 | **390 × 844 dp** ← 本次新增 | +| 竖屏 + 平板 | 720 × 1280 dp | +| PictureSelector | 360 × 480 dp | + +**若手机沿用平板竖屏的 720dp 基准,手机上所有 dp 会被压缩约一半,布局全部错位。** + +### 4. 手机端公共资源 + +| 资源 | 内容 | +|---|---| +| `colors.xml` | `phone_primary`(#2563EB)、`phone_primary_light`、`phone_bg`、`phone_text_title/body/hint`、`phone_divider`、`phone_tag_*` | +| `bg_phone_card.xml` | 列表卡片:白底 + 12dp 圆角 + 0.5dp 边框 | +| `bg_phone_search.xml` | 搜索框:白底 + 12dp 圆角 + 1dp 边框 | +| `bg_phone_btn_primary.xml` | 主按钮:主色实底 + 10dp 圆角 + 按下态 | +| `bg_phone_tag_gray/green.xml` | 状态标签底色 | +| `layout/title_tool_bar.xml` | 手机版标题栏(平板版已移至 `layout-sw600dp/`),保留 `toolbar`/`tool_back`/`title_name` id,`setBackArrow()` 零改动复用 | + +--- + +## 四、手机版 UI 规范 + +| 部位 | 规范 | +|---|---| +| 画布基准 | 390 × 844 dp | +| 顶栏 | 高 48dp,`phone_primary`,左「‹ 返回」,标题居中 18sp 加粗 | +| 页面背景 | `phone_bg` (#F3F4F6) | +| 列表卡片 | `bg_phone_card`,`marginHorizontal=16dp`,`marginTop=12dp`,`padding=14dp` | +| 卡片头行 | 圆形勾选框 20dp(`radiobtn_checked_style`/`radiobtn_unchecked_style`)+ 主字段 15sp 加粗 + 状态标签 | +| 卡片字段 | 两列网格,13sp,`phone_text_body`,行距 8dp | +| 状态标签 | 绿底=已完成 / 灰底=未完成,11sp,高 20dp | +| 搜索行 | 搜索框高 40dp(放大镜 + 输入 + 扫码)+ 右侧筛选按钮 40dp | +| 状态 Tab | 高 42dp,选中文字 `phone_primary` + 底部 28×3dp 指示条 | +| 底部操作条 | 白底 + `elevation=8dp`,全选 + 统计(13sp/11sp 两行)+ 主按钮 | +| 筛选弹层 | 遮罩 `#80000000` + 底部白面板,重置 / 确定各占一半 | + +--- + +## 五、页面改造步骤 + +1. **移动平板布局** + ```bash + mkdir -p module_xxx/src/main/res/layout-sw600dp + git mv module_xxx/src/main/res/layout/activity_xxx.xml module_xxx/src/main/res/layout-sw600dp/activity_xxx.xml + git mv module_xxx/src/main/res/layout/item_xxx.xml module_xxx/src/main/res/layout-sw600dp/item_xxx.xml + ``` +2. **新建手机布局**:在 `res/layout/` 下创建**同名**文件,保持相同 `` 与共用控件 id +3. **Kotlin 侧不动**:`layoutId()`、`itemLayoutId`、ViewHolder 全部保持原样 +4. **纯 UI 交互写在 Activity**,不污染 ViewModel;状态过滤优先复用 ViewModel 既有字段 +5. 手机布局顶部 `` 不变(变体自动切换) + +### 示例:状态 Tab 零 ViewModel 改动 + +出港移库的 `moveState` 字段(`""` 全部 / `"0"` 未移库 / `"1"` 已移库)与查询参数本就存在,Tab 直接复用: + +```kotlin +// IntExpMoveActivity 中新增纯 UI 方法 +fun switchTab(state: String) { + if (viewModel.moveState.value == state) return + viewModel.moveState.value = state + viewModel.searchClick() // 复用原有查询链路 +} +``` + +```xml + +android:textColor="@{`0`.equals(viewModel.moveState) ? @color/phone_primary : @color/phone_text_body}" +``` + +--- + +## 六、参考页面实施结果:国际出港移库 + +| 文件 | 改动 | +|---|---| +| `IntExpMoveViewModel.kt` | **0 行业务改动**(仅补注释) | +| `IntExpMoveViewHolder.kt` | **完全未改** | +| `IntExpMoveActivity.kt` | 仅新增 4 个纯 UI 方法:`switchTab()` / `toggleFilterPanel()` / `resetFilter()` / `confirmFilter()`,以及 `filterPanelVisible` 状态 | +| `layout-sw600dp/activity_int_exp_move.xml` | 原平板布局,内容未变,仅移动目录 | +| `layout-sw600dp/item_int_exp_move.xml` | 同上 | +| `layout/activity_int_exp_move.xml` | 新增手机版 | +| `layout/item_int_exp_move.xml` | 新增手机版 | + +--- + +## 七、双端验证 + +| | 手机模拟器 | 平板模拟器 | +|---|---|---| +| AVD | `Medium_Phone_API_36.1` | `Aerologic_Tablet` | +| 屏幕 | 1080×2400 @420dpi | 1280×800 @160dpi | +| 形态识别 | `PHONE(sw=411dp, force=AUTO)` | `TABLET(sw=800dp, force=AUTO)` | +| 方向 | 竖屏(`unspecified` + BaseActivity 锁定,无闪屏) | 横屏 | +| 加载布局 | 手机版 | 原平板布局,**行为无变化** | +| 交互 | Tab 切换选中态迁移并触发查询;筛选弹层正常弹出/关闭 | — | + +APK 内两套变体均正确打包: + +``` +res/layout-sw600dp-v13/activity_int_exp_move.xml +res/layout-sw600dp-v13/item_int_exp_move.xml +res/layout-sw600dp-v13/title_tool_bar.xml +res/layout/activity_int_exp_move.xml +res/layout/item_int_exp_move.xml +res/layout/title_tool_bar.xml +``` + +### 验证命令 + +```bash +~/Library/Android/sdk/emulator/emulator -list-avds +~/Library/Android/sdk/emulator/emulator -avd Medium_Phone_API_36.1 -no-snapshot-load -no-boot-anim & + +adb -s <设备> install -r -t app/build/outputs/apk/debug/app-debug.apk +adb -s <设备> shell wm size && adb -s <设备> shell wm density +adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果 +adb -s <设备> exec-out screencap -p > /tmp/shot.png +``` + +### 调试便利 + +`app/src/debug/AndroidManifest.xml`(**仅 Debug 生效,不进 Release 包**)用 `tools:replace` 将适配调试页临时置为 `exported="true"`,免去每次登录再逐级点入: + +```bash +adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.page.move.IntExpMoveActivity +``` + +新增调试页时在该文件追加对应 `` 即可。 + +--- + +## 八、待办 + +| 项 | 说明 | +|---|---| +| `PhoneDataLayout` 手机版表单组件 | 对标 `PadDataLayoutNew`(319 行 kt)+ `DataLayoutKtx`(404 行)+ `AutoQueryManager`(179 行),支持 INPUT/SPINNER/DATE + required + AutoQuery | +| `PhoneSearchBar` 手机版搜索组件 | 当前手机版搜索框为原生 `EditText`,组件化后恢复运单号 AutoQuery 联想能力 | +| 筛选弹层内控件替换 | 当前仍复用 `PadSearchLayout`(功能完整、视觉待统一) | +| 首页 / 登录 / 我的 | **设计稿缺失**,是手机端使用闭环的前置条件 | + +--- + +## 九、注意事项 + +1. 改动 `BaseActivity` / `BaseBindingActivity` / 公共组件后,**必须在平板上回归**,确认现有平板版行为不变 +2. 未适配手机的页面,布局保留在 `res/layout/` 即可(两端共用),手机上视觉不佳但不会崩溃——支持渐进式适配 +3. 手机版资源统一使用 `phone_*` 颜色与 `bg_phone_*` drawable,禁止直接写死色值 +4. 两个 git 仓库(`aerologic-app` 与 `air-cargo`)为同一份源码的独立副本,适配改动需同步 diff --git a/documents/后端接口对接问题清单.xlsx b/documents/后端接口对接问题清单.xlsx new file mode 100644 index 0000000..547946a Binary files /dev/null and b/documents/后端接口对接问题清单.xlsx differ diff --git a/documents/航空货运APP-5.5寸手持端UI适配-工时报价明细.xlsx b/documents/航空货运APP-5.5寸手持端UI适配-工时报价明细.xlsx new file mode 100644 index 0000000..b04fdfb Binary files /dev/null and b/documents/航空货运APP-5.5寸手持端UI适配-工时报价明细.xlsx differ diff --git a/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx b/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx new file mode 100644 index 0000000..8fb6f7b Binary files /dev/null and b/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx differ diff --git a/documents/航空货运APP-5.5寸手持端适配报价单.xlsx b/documents/航空货运APP-5.5寸手持端适配报价单.xlsx new file mode 100644 index 0000000..0d78202 Binary files /dev/null and b/documents/航空货运APP-5.5寸手持端适配报价单.xlsx differ diff --git a/documents/长卿科技-航空货运作业管理系统报价.xlsx b/documents/长卿科技-航空货运作业管理系统报价.xlsx new file mode 100644 index 0000000..717ba27 Binary files /dev/null and b/documents/长卿科技-航空货运作业管理系统报价.xlsx differ diff --git a/module_base/src/main/debug/AndroidManifest.xml b/module_base/src/main/debug/AndroidManifest.xml index f6ebe9f..3bc8754 100644 --- a/module_base/src/main/debug/AndroidManifest.xml +++ b/module_base/src/main/debug/AndroidManifest.xml @@ -1,33 +1,33 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt b/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt index 193ceb2..3efee48 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt @@ -2,6 +2,7 @@ package com.lukouguoji.module_base import android.content.Context import android.content.Intent +import android.content.pm.ActivityInfo import android.content.res.Configuration import android.content.res.Resources import android.net.Uri @@ -26,6 +27,7 @@ import com.yanzhenjie.permission.runtime.Permission import com.yzq.zxinglibrary.android.CaptureActivity import com.yzq.zxinglibrary.bean.ZxingConfig import com.yzq.zxinglibrary.common.Constant +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.HandlerUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -49,15 +51,48 @@ open class BaseActivity : AppCompatActivity(), CoroutineScope { private var timerTask: TimerTask? = null override fun onCreate(savedInstanceState: Bundle?) { + // 必须在 super.onCreate 之前设置,避免进入页面后再旋转造成闪屏与重建 + applyDeviceOrientation() super.onCreate(savedInstanceState) -// requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE setContentView(R.layout.activity_base) - Log.d("BaseActivity", javaClass.simpleName) + Log.d("BaseActivity", "${javaClass.simpleName} ${DeviceUtil.describe()}") ActivityCollector.addActivity(this) ARouter.getInstance().inject(this); /////////必须要写这行,自动注入解析 loadingDialog = LoadingDialog(this) } + /** + * 按设备形态锁定屏幕方向 —— **兜底手段** + * + * 方向的主控权在 Manifest:各 Activity 统一写 + * `android:screenOrientation="@integer/screen_orientation"`, + * 由资源限定符取值(`values/` = portrait,`values-sw600dp/` = userLandscape)。 + * 这样方向在**窗口创建阶段**就已确定,不会出现"先横屏、约 1 秒后转竖屏"的闪屏。 + * + * 本方法只作为安全网:万一某个 Activity 的 Manifest 漏配(仍写死 userLandscape), + * 至少还能在手机上纠正为竖屏——但那种情况下闪屏依旧存在,应当去补 Manifest。 + * + * 子类如需特殊方向(如扫码页),覆写 [lockOrientationByDevice] 返回 false 即可跳过。 + */ + protected open fun applyDeviceOrientation() { + if (!lockOrientationByDevice()) return + try { + requestedOrientation = if (DeviceUtil.isPhone(this)) { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } else { + ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + } + } catch (e: Exception) { + // 半透明 / 悬浮类 Activity 不允许指定方向,忽略即可 + Log.w("BaseActivity", "setRequestedOrientation failed: ${e.message}") + } + } + + /** + * 是否由基类按设备形态自动锁定方向,默认开启 + */ + protected open fun lockOrientationByDevice(): Boolean = true + override fun onDestroy() { super.onDestroy() job.cancel() diff --git a/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt b/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt index 4bad6a1..8bdaf41 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt @@ -13,6 +13,7 @@ import com.cretin.www.cretinautoupdatelibrary.model.UpdateConfig import com.cretin.www.cretinautoupdatelibrary.utils.AppUpdateUtils import com.cretin.www.cretinautoupdatelibrary.utils.SSLUtils.TrustAllHostnameVerifier import com.lukouguoji.module_base.handler.MyCrashHandler +import com.lukouguoji.module_base.util.DeviceUtil import com.lukouguoji.module_base.util.OkHttp3Connection import com.scwang.smart.refresh.footer.ClassicsFooter import com.scwang.smart.refresh.header.ClassicsHeader @@ -79,20 +80,26 @@ class MyApplication : Application() { .setScreenHeight(activity.resources.displayMetrics.heightPixels) //根据屏幕方向,设置设计尺寸 if (activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - //设置横屏设计尺寸 + //设置横屏设计尺寸(平板) AutoSizeConfig.getInstance() - .setDesignWidthInDp(1152) - .setDesignHeightInDp(720) + .setDesignWidthInDp(DeviceUtil.TABLET_DESIGN_WIDTH_DP_LAND) + .setDesignHeightInDp(DeviceUtil.TABLET_DESIGN_HEIGHT_DP_LAND) } else { if(activity.javaClass.simpleName.startsWith("PictureSelectorSupporterActivity")){ AutoSizeConfig.getInstance() .setDesignWidthInDp(360) .setDesignHeightInDp(480) - }else{ - //设置竖屏设计尺寸 + } else if (DeviceUtil.isPhone(activity)) { + //手机竖屏:按 5.5 寸手持端设计稿画布 390 × 844 换算 + //若沿用平板竖屏的 720 × 1280 基准,手机上所有 dp 会被压缩约一半,布局全部错位 AutoSizeConfig.getInstance() - .setDesignWidthInDp(720) - .setDesignHeightInDp(1280) + .setDesignWidthInDp(DeviceUtil.PHONE_DESIGN_WIDTH_DP) + .setDesignHeightInDp(DeviceUtil.PHONE_DESIGN_HEIGHT_DP) + } else { + //设置竖屏设计尺寸(平板) + AutoSizeConfig.getInstance() + .setDesignWidthInDp(DeviceUtil.TABLET_DESIGN_WIDTH_DP_PORT) + .setDesignHeightInDp(DeviceUtil.TABLET_DESIGN_HEIGHT_DP_PORT) } } } diff --git a/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt b/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt index 29b8f95..b7eec00 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt @@ -21,7 +21,17 @@ abstract class BaseBindingActivity : BaseAc override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - //dataBinding初始化 + /* + * dataBinding初始化 + * + * 平板 / 手机双形态的布局切换由 Android 资源限定符自动完成,此处无需分支: + * res/layout-sw600dp/xxx.xml → 平板(最小宽度 >= 600dp) + * res/layout/xxx.xml → 手机(兜底) + * 两个变体同名,DataBinding 会生成同一个 Binding 类,因此 Activity 与 ViewModel 无需改动。 + * + * 注意:不要按「不同文件名 + 代码判断」的方式切换布局, + * 那样会生成两个不同的 Binding 类,泛型擦除下编译期无法发现,运行时必然 ClassCastException。 + */ binding = DataBindingUtil.setContentView(this, layoutId()) binding.lifecycleOwner = this @@ -37,6 +47,9 @@ abstract class BaseBindingActivity : BaseAc /** * 布局ID + * + * 双形态适配时保持同一个 id 即可,具体加载哪份布局由资源限定符决定: + * res/layout-sw600dp/ 平板 res/layout/ 手机 */ abstract fun layoutId(): Int diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt index fb29587..6d62372 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt @@ -1,8 +1,19 @@ -package com.lukouguoji.module_base.bean - -class ActionBean( - val title: String, - val icon: Int, - val key: String = "" -) { -} \ No newline at end of file +package com.lukouguoji.module_base.bean + +/** + * 手持端(手机版)首页菜单项 + * + * @param title 菜单显示名称 + * @param icon 菜单图标 + * @param key 权限串(取 [com.lukouguoji.module_base.common.Constant.AuthName] 中的常量)。 + * 菜单按登录返回的 authList 是否包含该串来决定显不显示 + * @param route ARouter 路由路径。填了就由通用逻辑直接跳转,无需再写 when 分支; + * 留空表示由所在 ViewModel 的 onItemClick 自行处理(module_p 内的 PDA 页面走这条) + */ +class ActionBean( + val title: String, + val icon: Int, + val key: String = "", + val route: String = "" +) { +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcCheckInRecord.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcCheckInRecord.kt index 7662905..bd6ed4f 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcCheckInRecord.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcCheckInRecord.kt @@ -25,9 +25,14 @@ data class GjcCheckInRecord( var whId: Long = 0, // GJC_WAREHOUSE.ID var carWeight: String = "", // 托盘车重 var trayNumber: Int = 0, // 托盘数量 - var trayTotalWeight: Double = 0.0 // 托盘重量(托盘自重) + var trayTotalWeight: Double = 0.0, // 托盘重量(托盘自重) + var opUserName: String = "" // 计重人姓名(opId 关联查询) ) : BaseObservable() { + /** 计重人:优先姓名,为空回退操作员编码 */ + val opUserDisplay: String + get() = opUserName.ifEmpty { opId } + // 数据变化回调 var onDataChanged: (() -> Unit)? = null diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMaWb.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMaWb.kt index 9e53dc2..598e616 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMaWb.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMaWb.kt @@ -2,6 +2,7 @@ package com.lukouguoji.module_base.bean import androidx.databinding.ObservableBoolean import com.lukouguoji.module_base.ktx.toPlainText +import java.io.Serializable import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -130,6 +131,8 @@ data class GjcMaWb( val checked: ObservableBoolean = ObservableBoolean(false) // 选中状态 @Transient val showMore: ObservableBoolean = ObservableBoolean(false) // 展开状态 + @Transient + val actionMenuVisible: ObservableBoolean = ObservableBoolean(false) // 5.5寸手机版:卡片长按操作浮层显隐 // 兼容现有API的isSelected属性 var isSelected: Boolean @@ -143,6 +146,44 @@ data class GjcMaWb( val canCheckAllHaWb: Boolean get() = allTally == "1" + /** + * 5.5寸手机版:申报状态三色标签的着色分类(01=绿/W=靛蓝/其余非空=琥珀)。 + */ + val declareTagColor: String + get() = when (declareStatus) { + "01" -> "green" + "W" -> "indigo" + else -> if (declareStatus.isNullOrEmpty()) "" else "amber" + } + + /** + * 5.5寸手机版-出港查询:卡片状态标签。 + * 已离港 = 航班关闭时间(fclose)非空;已入库 = 入库时间(opDate)非空;都为空 = 未入库(灰)。 + * 接口无独立状态出参,此判定为前端推断(与卡片「入库时间/离港时间」两列同源)。 + */ + val isQueryDeparted: Boolean + get() = !fclose.isNullOrEmpty() + + val isQueryStored: Boolean + get() = !opDate.isNullOrEmpty() + + /** 5.5寸手机版:空值统一占位 "--"(空串与 null 都兜底,布局绑定用) */ + fun orDash(value: String?): String = if (value.isNullOrEmpty()) "--" else value + + /** + * 5.5寸手机版:代理人展示值(名称优先,空则回退代码)。 + * 不能在 XML 里用 `??` 兜底——转换器会把 JSON null 归一成空串,`??` 只判 null 不判空串。 + */ + val agentDisplay: String? + get() = if (agentName.isNullOrEmpty()) agentCode else agentName + + /** + * 5.5寸手机版-出港仓库:卡片状态标签(已清仓=绿 / 未清仓=橙,clearNormal 为空视作未清仓)。 + * 已清仓时卡片「清仓」按钮置灰不可点。 + */ + val isCleared: Boolean + get() = clearNormal == "1" + // ==================== 状态转换扩展属性 ==================== /** * 计重状态中文 @@ -292,15 +333,36 @@ data class GjcHaWb( var tallyOpId: String? = null, // 理货操作人ID var declareCount: Long? = null, // 申报次数 var activeId: Long? = null // 活动ID -) { +) : Serializable { // ==================== UI扩展字段 ==================== @Transient val checked: ObservableBoolean = ObservableBoolean(false) // 选中状态 + @Transient + val actionMenuVisible: ObservableBoolean = ObservableBoolean(false) // 5.5寸手机版:卡片长按操作浮层显隐 // 兼容现有API的isSelected属性 var isSelected: Boolean get() = checked.get() set(value) = checked.set(value) + + /** + * 5.5寸手机版:申报状态三色标签的着色分类(01=绿/W=靛蓝/其余非空=琥珀)。 + * 与 GjcMaWb.declareTagColor 同源逻辑,作用于分单自身的 arrivalStatus。 + */ + val haWbTagColor: String + get() = when (arrivalStatus) { + "01" -> "green" + "W" -> "indigo" + else -> if (arrivalStatus.isNullOrEmpty()) "" else "amber" + } + + /** + * 5.5寸手机版「回执」页:运抵操作时间(格式化)。 + */ + val arrivalOpDateText: String + get() = arrivalOpDate?.let { + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(it) + } ?: "" } /** diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMove.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMove.kt index 36276c7..81374eb 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMove.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMove.kt @@ -44,6 +44,9 @@ data class GjcMove( var likeNo: String = "", // 部分运单号no(模糊查询) var range: String = "", + var flight: String = "", // 航班信息(航班日期/航班号,如 20260924/MU2222) + var businessName: String = "", // 业务类型名称(中) + var opUserName: String = "", // 操作人(移库人)姓名 // UI扩展字段 - 使用ObservableBoolean实现自动UI更新 val checked: ObservableBoolean = ObservableBoolean(false) @@ -56,4 +59,29 @@ data class GjcMove( var isSelected: Boolean get() = checked.get() set(value) = checked.set(value) + + // ========== 5.5 寸手持端展示辅助字段(仅用于手机版布局,不参与序列化) ========== + + /** 是否已移库(moveState:0-未移交,1-已移交) */ + val isMoved: Boolean + get() = moveState == 1 + + /** 品名:优先中文,为空回退英文 */ + val goodsName: String + get() = goodsCn.ifEmpty { goods } + + /** 业务类型:优先中文名称,为空回退编码 */ + val businessDisplay: String + get() = businessName.ifEmpty { businessType } + + /** 移库人:优先姓名,为空回退操作人ID */ + val opUserDisplay: String + get() = opUserName.ifEmpty { opId } + + /** + * 详情页字段统一空值占位。 + * 接口对 businessType / opdate / opId 等字段常返回 null, + * 直接绑会显示成 "null" 或空白,这里统一成 "-"。 + */ + fun orDash(value: String?): String = if (value.isNullOrEmpty()) "-" else value } diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcUldUseBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcUldUseBean.kt index 7184390..e010d8a 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcUldUseBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcUldUseBean.kt @@ -122,4 +122,19 @@ class GjcUldUseBean : Serializable { // 复磅状态文本 val wtStatusText: String get() = if (wtDate.isNotEmpty()) "已复磅" else "未复磅" + + // ========== 5.5 寸手持端展示辅助字段(仅用于手机版布局,不参与序列化) ========== + + /** 是否已交接:有交接时间即视为已交接 */ + val isHandovered: Boolean + get() = hoDate.isNotEmpty() + + /** + * IMP 代码是否属于危险品类 —— 决定手机版卡片上 IMP 代码标签是红底还是绿底。 + * + * 依据 IATA IMP 特货代码惯例:以 R 开头(RFL/RRY/REX 等)及 ICE 归为危险品。 + * 若后端另有判定口径,只需改这一处。 + */ + val isDgrDangerous: Boolean + get() = dgrCode.startsWith("R", ignoreCase = true) || dgrCode.equals("ICE", ignoreCase = true) } diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingRecordBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingRecordBean.kt index 87e8625..2c95b45 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingRecordBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingRecordBean.kt @@ -73,6 +73,7 @@ class GjcWeighingRecordBean : Serializable { var paperTime: String = "" // 单据时间 var userName: String = "" // 收运人 + var passageWay: String = "" // 通道号(计重时选定的通道号名称) var userId: String = "" // 收运人ID var remark: String = "" // 备注 diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingStatisticsBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingStatisticsBean.kt index 602f236..92ca382 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingStatisticsBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcWeighingStatisticsBean.kt @@ -10,4 +10,6 @@ class GjcWeighingStatisticsBean { var totalPc: Long = 0 // 总件数 var totalWeight: Double = 0.0 // 总重量 var cargoWeight: Double = 0.0 // 总货重 + var waitCheckInNumber: Int = 0 // 待计重票数(IntExpCheckIn/pageQueryTotal) + var checkingNumber: Int = 0 // 计重中票数(IntExpCheckIn/pageQueryTotal) } diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpPickUpDLVBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpPickUpDLVBean.kt index c51e8bc..ed780c3 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpPickUpDLVBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpPickUpDLVBean.kt @@ -46,6 +46,7 @@ class IntImpPickUpDLVBean : Serializable { var dlvId: String = "" var dlvIdType: String = "" var dlvName: String = "" + var refUserName: String = "" // 出库人姓名(refId 关联查询) var dlvPhone: String = "" var locFlag: String = "" var origin: String = "" @@ -74,4 +75,11 @@ class IntImpPickUpDLVBean : Serializable { var isSelected: Boolean get() = checked.get() set(value) = checked.set(value) + + /** + * 5.5寸手机版:是否已出库(出库时间非空)。 + * 卡片按此切两种形态:待出库(勾选框+橙标签+库位行)/已出库(绿标签+出库人行)。 + */ + val isPickedUp: Boolean + get() = dlvTime.isNotEmpty() } diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpQueryBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpQueryBean.kt index a32ea69..30823b9 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpQueryBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/IntImpQueryBean.kt @@ -54,4 +54,26 @@ data class IntImpQueryBean( // ==================== 其他 ==================== var remark: String? = null, // 备注 var activeId: Long? = null // 有效值 -) +) { + + /** + * 5.5寸手机版-进港查询:卡片状态标签。 + * 已出库 = 出库时间(dlvTime)非空;已入库 = 入库时间(inDate)非空;都为空 = 未入库(灰)。 + * 接口无独立状态出参,此判定为前端推断(与平板列表「入库时间/出库时间」两列同源)。 + */ + val isQueryOutbound: Boolean + get() = !dlvTime.isNullOrEmpty() + + val isQueryStored: Boolean + get() = !inDate.isNullOrEmpty() + + /** 5.5寸手机版:空值统一占位 "--"(空串与 null 都兜底,布局绑定用) */ + fun orDash(value: String?): String = if (value.isNullOrEmpty()) "--" else value + + /** + * 5.5寸手机版:代理人展示值(名称优先,空则回退代码)。 + * 不能在 XML 里用 `??` 兜底——转换器会把 JSON null 归一成空串,`??` 只判 null 不判空串。 + */ + val agentDisplay: String? + get() = if (agentName.isNullOrEmpty()) agentCode else agentName +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/ManifestTotalDto.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/ManifestTotalDto.kt index 2ed5a4a..749f69c 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/ManifestTotalDto.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/ManifestTotalDto.kt @@ -9,5 +9,14 @@ data class ManifestTotalDto( var totalPc: Long = 0, // 总件数 var totalWeight: Double = 0.0, // 总重量 var cargoWeight: Double = 0.0, // 总货重 - var flightNumber: Int = 0 // 航班航次 + + // ===== 后端统一统计 DTO 的分项计数(2026-09 后端补齐,按接口各取所需) ===== + var flightNumber: Int = 0, // 航班航次 + var uldNumber: Int = 0, // 板箱数(出库交接 Tab 角标) + var storedNumber: Int = 0, // 已入库票数(出港/进港查询) + var departedNumber: Int = 0, // 已离港(出港)/ 已出库(进港)票数 + var notClearNumber: Int = 0, // 未清仓票数(出港/进港仓库) + var clearNumber: Int = 0, // 已清仓票数(出港/进港仓库) + var waitCheckInNumber: Int = 0, // 待计重票数(出港计重) + var checkingNumber: Int = 0 // 计重中票数(出港计重) ) diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/MoveStashBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/MoveStashBean.kt index 8c151b9..e9fda60 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/MoveStashBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/MoveStashBean.kt @@ -6,6 +6,7 @@ import com.lukouguoji.module_base.interfaces.ICheck class MoveStashBean : ICheck { var activeId: String = "" // null var agentCode: String = "" // HFEXB + var agentName: String = "" // 代理人名称(search 出参有定义) var awbType: String = "" // 国际进港(国内中转) var businessType: String = "" // null var by1: String = "" // null @@ -26,7 +27,9 @@ class MoveStashBean : ICheck { var flight: String = "" // 20240711/MU7766 var fno: String = "" // MU7766 var goods: String = "" // 国际货 - var mawbId: String = "" // 996422 + var mawbId: String = "" // 996422(searchMoved 出参不含此字段) + var moveId: String = "" // CIII260930141150(已移库,movedDetail 入参) + var opUserName: String = "" // 移库人姓名(searchMoved / movedDetail 出参) var no: String = "" // 44445074 var opDate: String = "" // null var opId: String = "" // null @@ -49,4 +52,23 @@ class MoveStashBean : ICheck { override fun getCheckObservable(): ObservableBoolean { return checked } + + // ========== 5.5 寸手持端展示辅助字段(仅前端使用,不参与提交) ========== + + /** 是否已移库:由列表加载端点决定(search=待移库 / searchMoved=已移库),载入时打标 */ + var isMoved: Boolean = false + + /** 卡片长按「拍照」操作浮层显隐(仅 UI 状态) */ + val actionMenuVisible = ObservableBoolean(false) + + /** 代理显示:网络层会把 null 归一成空串,须 isNullOrEmpty 双兜底(沿用进港/出港查询口径) */ + val agentDisplay: String + get() = if (agentName.isNotEmpty()) agentName else agentCode + + /** 移库人:优先姓名,为空回退操作人 ID */ + val opUserDisplay: String + get() = opUserName.ifEmpty { opId } + + /** 详情页/卡片字段统一空值占位 */ + fun orDash(value: String?): String = if (value.isNullOrEmpty()) "-" else value } \ No newline at end of file diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/ULDBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/ULDBean.kt index bd66acf..1d122c2 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/ULDBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/ULDBean.kt @@ -19,9 +19,16 @@ data class ULDBean( var status: String = "", // 备注 var remark: String = "", + // ========== 以下字段接口出参已有(api-doc /eqm/uld/pageQuery) ========== + // 进港航班号 + var ifNo: String = "", + // 出港航班号 + var efNo: String = "", + var checkInDate: String = "", + var checkOutDate: String = "", // 来源(空运、陆运) var source: String = "", - // 入库时间 yyyy-MM-dd HH:mm:ss + // 入库时间 yyyy-MM-dd HH:mm:ss(后端确认「入库时间」取 inStoreTime,与平板端一致) var inStoreTime: String = "", // 所在港 var nowPort: String = "", @@ -35,4 +42,19 @@ data class ULDBean( "1" -> "故障" else -> status } + + // ========== 手机版(5.5 寸手持端)状态展示 ========== + // 后端确认状态枚举维持两态(0 正常 / 1 故障),不新增「占用」; + // 手机版按设计稿文案映射 0→可用(绿)、1→维修(红)。 + + /** 手机版状态文案 */ + val statusPhoneName: String + get() = when (status) { + "0" -> "可用" + "1" -> "维修" + else -> statusName() + } + + /** 可用(绿色标签) */ + val isStatusAvailable: Boolean get() = status == "0" } \ No newline at end of file diff --git a/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt b/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt index 5f8e47f..6f677c0 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt @@ -272,6 +272,8 @@ interface Constant { const val IntImpTally = "AppIntImpTally" //理货报告 const val IntImpPickUpRecord = "AppIntImpPickUpRecord" //提取记录 const val IntImpPickUpDLV = "AppIntImpPickUpDLV" //提取出库 + // 进港移库:后端暂无该模块与权限串(2026-07-31 api-doc 核对),按 AppIntImp* 命名约定预定义, + // 权限串下发前手机首页菜单不会显示该入口(已登记后端接口对接问题清单) const val IntImpAccidentVisa = "AppIntImpAccidentVisa" //事故签证 const val GjjManifestListActivity = "AppIntExpManifest" //舱单 const val GjjTallyListActivity = "AppIntExpTally" //理货 diff --git a/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt b/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt index d676315..7b3f5a1 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt @@ -899,6 +899,16 @@ interface Api { @POST("IntExpArrive/deleteDeclare") suspend fun deleteArriveDeclare(@Body data: RequestBody): BaseResultBean + /** + * 国际出港运抵-分单补充收发货人信息(5.5寸手机版新增) + * + * 后端暂无此接口(api-doc 搜索 IntExpArrive、HaWb 均无匹配),路径为参照 Pad 端国际进港 + * IntImpAirManifest/complete 命名习惯拟定的建议值,见 documents/后端接口对接问题清单.xlsx。 + * 请求体为分单数组,仅需 haWbId 与收发货人相关字段。 + */ + @POST("IntExpArrive/haWb/supplement") + suspend fun supplementArriveHaWb(@Body data: RequestBody): BaseResultBean + /** * 国际出港移库-分页查询 * 接口路径: /IntExpMove/pageQuery @@ -1836,6 +1846,12 @@ interface Api { @POST("DomImpMove/search") suspend fun getGnjMoveStashList(@Body data: RequestBody): BaseListBean + /** + * 获取-国内进港移库-已移库列表(移库完成分页搜索,5.5 寸手机端「已移库」Tab) + */ + @POST("DomImpMove/searchMoved") + suspend fun getGnjMoveStashMovedList(@Body data: RequestBody): BaseListBean + /** * 获取-国内进港移库-列表--运单列表 */ @@ -1860,6 +1876,13 @@ interface Api { @GET("DomImpMove/detail") suspend fun getGnjMoveStashDetail(@Query("mawbId") mawbId: String): BaseResultBean + /** + * 国内进港移库-移库完成详情(已移库运单无 mawbId,按 moveId 查,含 pic 三字段与移库人) + * 接口路径: /DomImpMove/movedDetail + */ + @GET("DomImpMove/movedDetail") + suspend fun getGnjMoveStashMovedDetail(@Query("moveId") moveId: String): BaseResultBean + /** * 编辑-国内进港移库(新接口) */ @@ -2173,6 +2196,13 @@ interface Api { @POST("eqm/uld/deleteUld") suspend fun deleteUld(@Query("uld") uld: String): BaseResultBean + /** + * 批量删除-ULD-信息(请求体为 ULD 编号字符串数组) + * 接口路径: /eqm/uld/deleteUldBatch + */ + @POST("eqm/uld/deleteUldBatch") + suspend fun deleteUldBatch(@Body data: RequestBody): BaseResultBean + /////////////////////////////////////////////////////////////////////////// // 冷库登记 /////////////////////////////////////////////////////////////////////////// diff --git a/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt b/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt index 8fe00d5..54ced74 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt @@ -104,6 +104,7 @@ object ARouterConstants { // 国内进港移库(新版本) const val ACTIVITY_URL_GNJ_YIKU_LIST = "/gnj/GnjYiKuListActivity" //国内进港 移库列表 + const val ACTIVITY_URL_GNJ_MOVE_STASH_LIST = "/app/GnjMoveStashListActivity" //国内进港 移库(首页菜单实际跳转页,双形态) const val ACTIVITY_URL_GNJ_YIKU_DETAILS = "/gnj/GnjYiKuDetailsActivity" //国内进港 移库详情 const val ACTIVITY_URL_GNJ_YIKU_EDIT = "/gnj/GnjYiKuEditActivity" //国内进港 移库编辑 const val ACTIVITY_URL_GNJ_YIKU_HANDOVER = "/gnj/GnjYiKuHandoverActivity" //国内进港 移交编辑/详情 @@ -145,6 +146,8 @@ object ARouterConstants { const val ACTIVITY_URL_INT_EXP_MOVE_EDIT = "/gjc/IntExpMoveEditActivity" //国际出港 出港移库编辑/详情 const val ACTIVITY_URL_GJC_ASSEMBLE_ALLOCATE = "/gjc/GjcAssembleAllocateActivity" //国际出港 组装分配 const val ACTIVITY_URL_INT_EXP_OUT_HANDOVER = "/gjc/IntExpOutHandoverActivity" //国际出港 出库交接 + + const val ACTIVITY_URL_ULD_LIST = "/app/UldListActivity" //综合管理 ULD管理(手机端首页入口用) const val ACTIVITY_URL_INT_EXP_LOAD = "/gjc/IntExpLoadActivity" //国际出港 出港装载 const val ACTIVITY_URL_INT_EXP_TALLY = "/gjc/IntExpTallyActivity" //国际出港 出港理货 const val ACTIVITY_URL_INT_EXP_ARRIVE = "/gjc/IntExpArriveActivity" //国际出港 出港运抵 diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneBottomBar.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneBottomBar.kt new file mode 100644 index 0000000..79851c9 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneBottomBar.kt @@ -0,0 +1,136 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.util.SparseArray +import android.view.Gravity +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.lukouguoji.module_base.R + +/** + * 手机版底部操作条(5.5 寸手持端公共组件) + * + * 结构:全选(圆形勾选框 + 文字)+ 已选统计 + 次操作按钮(可选,最多 2 个)+ 主操作按钮。 + * 平板底部有多个批量操作按钮的页面,用 subAction1Text/subAction2Text 承载其余操作(如出库交接「允许装机」「配运」)。 + * 对应设计稿各多选列表页底部的白色操作条。 + * + * XML 用法: + * ```xml + * + * ``` + */ +class PhoneBottomBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + val llCheckAll: LinearLayout + val ivCheckAll: ImageView + val tvCount: TextView + val tvAction: TextView + val tvSubAction1: TextView + val tvSubAction2: TextView + + /** 全选状态(驱动勾选框图标切换) */ + var allChecked: Boolean = false + set(v) { + field = v + ivCheckAll.setImageResource( + if (v) R.drawable.ic_phone_check_round_checked + else R.drawable.ic_phone_check_round_unchecked + ) + } + + /** 已选统计文案,如「已选 3 项」 */ + var countText: String = "" + set(v) { + field = v + tvCount.text = v + } + + /** 主按钮文字 */ + var actionText: String = "" + set(v) { + field = v + tvAction.text = v + } + + /** 次操作按钮 1 文字(空则隐藏) */ + var subAction1Text: String = "" + set(v) { + field = v + tvSubAction1.text = v + tvSubAction1.visibility = if (v.isEmpty()) GONE else VISIBLE + } + + /** 次操作按钮 2 文字(空则隐藏) */ + var subAction2Text: String = "" + set(v) { + field = v + tvSubAction2.text = v + tvSubAction2.visibility = if (v.isEmpty()) GONE else VISIBLE + } + + /** + * 危险操作样式:浅红底 + 红字 + 垃圾桶图标(如 ULD 管理的批量删除)。 + * 默认 false 为主色实底按钮,既有页面不受影响。 + */ + var actionDanger: Boolean = false + set(v) { + field = v + if (v) { + tvAction.setBackgroundResource(R.drawable.bg_phone_btn_danger) + tvAction.setTextColor(ContextCompat.getColor(context, R.color.phone_danger_text)) + val icon = ContextCompat.getDrawable(context, R.drawable.ic_phone_trash)?.apply { + val size = (16 * resources.displayMetrics.density).toInt() + setBounds(0, 0, size, size) + } + tvAction.setCompoundDrawables(icon, null, null, null) + tvAction.compoundDrawablePadding = (4 * resources.displayMetrics.density).toInt() + } else { + tvAction.setBackgroundResource(R.drawable.bg_phone_btn_primary) + tvAction.setTextColor(ContextCompat.getColor(context, R.color.white)) + tvAction.setCompoundDrawables(null, null, null, null) + } + } + + init { + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setBackgroundColor(ContextCompat.getColor(context, R.color.white)) + elevation = 8 * resources.displayMetrics.density + val padH = (16 * resources.displayMetrics.density).toInt() + val padV = (14 * resources.displayMetrics.density).toInt() + setPadding(padH, padV, padH, padV) + + inflate(context, R.layout.layout_phone_bottom_bar, this) + + llCheckAll = findViewById(R.id.ll_check_all) + ivCheckAll = findViewById(R.id.iv_check_all) + tvCount = findViewById(R.id.tv_count) + tvAction = findViewById(R.id.tv_action) + tvSubAction1 = findViewById(R.id.tv_sub_action1) + tvSubAction2 = findViewById(R.id.tv_sub_action2) + } + + /** 只保存/恢复自身状态,避免内部子控件 id 与其他复合组件实例串档 */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneDataLayout.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneDataLayout.kt new file mode 100644 index 0000000..411a105 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneDataLayout.kt @@ -0,0 +1,238 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.util.SparseArray +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.Spinner +import android.widget.TextView +import androidx.core.widget.doOnTextChanged +import androidx.databinding.InverseBindingListener +import com.lukouguoji.module_base.R +import com.lukouguoji.module_base.adapter.bindAdapter +import com.lukouguoji.module_base.adapter.bindOnSelected +import com.lukouguoji.module_base.interfaces.IOnSpinnerSelected +import com.lukouguoji.module_base.ktx.formatDate +import com.lukouguoji.module_base.ktx.getActivity +import com.lukouguoji.module_base.util.Common +import dev.utils.app.info.KeyValue +import java.util.Calendar + +/** + * 手机版表单 / 筛选项(5.5 寸手持端公共组件) + * + * 结构:上方标题(14sp #374151)+ 下方 40dp 高控件框(白底 + 1px 边框 + 8dp 圆角)。 + * 支持三种形态,由 [type] 决定: + * + * | type | 控件 | 右侧图标 | + * |-----------|-----------|----------| + * | `INPUT` | EditText | 无 | + * | `DATE` | TextView(点击弹日历)| 日历 | + * | `SPINNER` | Spinner | 下箭头 | + * + * XML 用法: + * ```xml + * + * ``` + * + * 手机端专用,与平板端 PadDataLayoutNew 并列存在,互不影响。 + */ +class PhoneDataLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + val tvTitle: TextView + val tvMust: TextView + val llContent: LinearLayout + val et: EditText + val tv: TextView + val spinner: Spinner + val iv: ImageView + + /** 双向绑定回调 */ + var onChangeListener: InverseBindingListener? = null + + /** 输入完成 / 选择完成回调 */ + var refreshCallBack: (() -> Unit)? = null + + var type: PhoneDataLayoutType = PhoneDataLayoutType.INPUT + set(v) { + field = v + applyType() + applyValue() + } + + var title: String = "" + set(v) { + field = v + tvTitle.text = v + } + + var hint: String = "" + set(v) { + field = v + et.hint = v + tv.hint = v + bindAdapter(spinner, list, v, R.layout.item_spinner_list_14sp) + } + + var value: String = "" + set(v) { + if (field == v) return + field = v + applyValue() + onChangeListener?.onChange() + } + + var list: List = emptyList() + set(v) { + field = v + bindAdapter(spinner, v, hint, R.layout.item_spinner_list_14sp) + applyValue() + } + + var required: Boolean = false + set(v) { + field = v + tvMust.visibility = if (v) VISIBLE else GONE + } + + var enable: Boolean = true + set(v) { + field = v + llContent.isEnabled = v + et.isEnabled = v + spinner.isEnabled = v + } + + init { + orientation = VERTICAL + inflate(context, R.layout.layout_phone_data_layout, this) + + tvTitle = findViewById(R.id.tv_title) + tvMust = findViewById(R.id.tv_must) + llContent = findViewById(R.id.ll_content) + et = findViewById(R.id.et) + tv = findViewById(R.id.tv) + spinner = findViewById(R.id.spinner) + iv = findViewById(R.id.iv) + + et.doOnTextChanged { text, _, _, _ -> + // 仅 INPUT 形态才回写,避免隐藏的 EditText 被系统状态恢复时误清空值 + if (type == PhoneDataLayoutType.INPUT) { + value = text.toString() + } + } + + bindOnSelected(spinner, object : IOnSpinnerSelected { + override fun onSelected(position: Int) { + value = list.getOrNull(position)?.value ?: "" + refreshCallBack?.invoke() + } + }) + + applyType() + } + + private val dateClick: (v: View) -> Unit = { + if (enable) { + Common.onYearMonthDay(context.getActivity(), value) { year, month, day -> + val calendar = Calendar.getInstance() + calendar.set(year, month - 1, day) + value = calendar.time.formatDate() + refreshCallBack?.invoke() + } + } + } + + private fun applyType() { + when (type) { + PhoneDataLayoutType.INPUT -> { + et.visibility = VISIBLE + tv.visibility = GONE + spinner.visibility = GONE + iv.visibility = GONE + llContent.setOnClickListener(null) + llContent.isClickable = false + } + + PhoneDataLayoutType.DATE -> { + et.visibility = GONE + tv.visibility = VISIBLE + spinner.visibility = GONE + iv.visibility = VISIBLE + iv.setImageResource(R.drawable.ic_phone_calendar) + llContent.setOnClickListener(dateClick) + } + + PhoneDataLayoutType.SPINNER -> { + et.visibility = GONE + tv.visibility = GONE + spinner.visibility = VISIBLE + iv.visibility = VISIBLE + iv.setImageResource(R.drawable.ic_phone_chevron_down) + llContent.setOnClickListener(null) + llContent.isClickable = false + } + } + } + + private fun applyValue() { + when (type) { + PhoneDataLayoutType.INPUT -> { + if (et.text.toString() != value) et.setText(value) + } + + PhoneDataLayoutType.DATE -> { + tv.text = value + } + + PhoneDataLayoutType.SPINNER -> { + if (value.isNotEmpty()) { + val position = list.indexOfFirst { it.value == value } + if (position >= 0) spinner.setSelection(position) + } + } + } + } + + /** + * 同一页面往往放置多个本组件实例,而它们的内部子控件(et / tv / spinner)共用同一套 id。 + * 系统按 id 保存/恢复视图状态会发生跨实例串档(典型表现:日期项被邻近输入框的空文本覆盖)。 + * 这里只保存/恢复自身状态、不下发给子控件;控件取值本就由 ViewModel + DataBinding 持有,无需视图级恢复。 + */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } + + /** 重置为空值(筛选弹层「重置」按钮使用) */ + fun reset() { + value = "" + if (type == PhoneDataLayoutType.SPINNER && list.isNotEmpty()) { + spinner.setSelection(if (hint.isEmpty()) list.size - 1 else list.size) + } + } +} + +/** [PhoneDataLayout] 支持的控件形态 */ +enum class PhoneDataLayoutType { + INPUT, // 文本输入 + DATE, // 日期选择 + SPINNER, // 下拉选择 +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt new file mode 100644 index 0000000..f4c2b17 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt @@ -0,0 +1,134 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.util.SparseArray +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.lukouguoji.module_base.R + +/** + * 手机版底部筛选弹层(5.5 寸手持端公共组件) + * + * 半透明遮罩 + 底部白色面板(顶部 20dp 圆角),面板内含标题行、内容插槽、重置/确认按钮行。 + * 对应设计稿各列表页的「筛选条件」弹层。 + * + * **内容插槽**:直接把筛选项(一般是 [PhoneDataLayout])写在本组件标签内即可, + * [onFinishInflate] 会自动把它们移入面板中部,无需关心内部层级: + * + * ```xml + * + * + * + * + * + * + * ``` + * + * 相邻筛选项之间的 20dp 间距由本组件统一施加,使用方无需逐个写 marginTop。 + */ +class PhoneFilterPanel @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + /** + * 同一页面可能出现多个含内部 id 的手机版复合组件,系统按 id 保存/恢复视图状态会跨实例串档。 + * 这里只保存/恢复自身状态、不下发给子控件;筛选值由 ViewModel + DataBinding 持有,无需视图级恢复。 + */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } + + val llPanel: LinearLayout + val llContent: LinearLayout + val tvTitle: TextView + val ivClose: ImageView + val tvReset: TextView + val tvConfirm: TextView + + /** 遮罩点击 / 关闭图标点击回调 */ + private var dismissListener: OnClickListener? = null + + var title: String = "" + set(v) { + field = v + if (v.isNotEmpty()) tvTitle.text = v + } + + /** 弹层显隐 */ + var panelVisible: Boolean = false + set(v) { + field = v + visibility = if (v) VISIBLE else GONE + } + + init { + setBackgroundColor(ContextCompat.getColor(context, R.color.black_tran50)) + visibility = GONE + + inflate(context, R.layout.layout_phone_filter_panel, this) + + llPanel = findViewById(R.id.ll_panel) + llContent = findViewById(R.id.ll_content) + tvTitle = findViewById(R.id.tv_title) + ivClose = findViewById(R.id.iv_close) + tvReset = findViewById(R.id.tv_reset) + tvConfirm = findViewById(R.id.tv_confirm) + + // 遮罩点击关闭;面板自身已 clickable,不会穿透 + setOnClickListener { dismissListener?.onClick(this) } + ivClose.setOnClickListener { dismissListener?.onClick(it) } + } + + /** + * 把 XML 中声明在本组件内的子 View 移入面板内容区,并统一施加 20dp 项间距。 + */ + override fun onFinishInflate() { + super.onFinishInflate() + + // index 0 是内部面板(init 中 inflate 得到),其后才是使用方声明的子 View + val declared = mutableListOf() + for (i in childCount - 1 downTo 1) { + declared.add(0, getChildAt(i)) + removeViewAt(i) + } + + declared.forEachIndexed { index, child -> + llContent.addView(child) + if (index > 0) { + (child.layoutParams as? LinearLayout.LayoutParams)?.let { lp -> + lp.topMargin = (20 * resources.displayMetrics.density).toInt() + child.layoutParams = lp + } + } + } + } + + fun setOnDismiss(listener: OnClickListener?) { + dismissListener = listener + } + + /** 遍历内容区所有 [PhoneDataLayout] 并重置为空值 */ + fun resetAllFields() { + for (i in 0 until llContent.childCount) { + (llContent.getChildAt(i) as? PhoneDataLayout)?.reset() + } + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFormRow.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFormRow.kt new file mode 100644 index 0000000..c3bd10d --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFormRow.kt @@ -0,0 +1,232 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.os.Parcelable +import android.text.InputType +import android.util.AttributeSet +import android.util.SparseArray +import android.view.Gravity +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.widget.doOnTextChanged +import androidx.databinding.InverseBindingListener +import com.lukouguoji.module_base.R +import com.lukouguoji.module_base.ktx.formatDate +import com.lukouguoji.module_base.ktx.getActivity +import com.lukouguoji.module_base.util.Common +import com.lxj.xpopup.XPopup +import dev.utils.app.info.KeyValue +import java.util.Calendar + +/** + * 手机版行式表单项(5.5 寸手持端公共组件) + * + * 结构:左标签(固定 110dp,+ 必填星)+ 右值区 + 可选箭头 + 底部 1px 分隔线。 + * 对应设计稿中新增 / 修改 / 详情三态共用的行式表单(如 01_ULD管理)。 + * 与 [PhoneDataLayout](上下结构,用于筛选弹层)互补,形态见 [PhoneFormRowType]。 + * + * XML 用法(type 按页面状态用三元切换即可覆盖三态页面): + * ```xml + * + * ``` + */ +class PhoneFormRow @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + val tvTitle: TextView + val tvMust: TextView + val et: EditText + val tv: TextView + val ivArrow: ImageView + val llRow: LinearLayout + + /** 双向绑定回调 */ + var onChangeListener: InverseBindingListener? = null + + /** 选择 / 日期确定后的回调 */ + var refreshCallBack: (() -> Unit)? = null + + var type: PhoneFormRowType = PhoneFormRowType.INPUT + set(v) { + field = v + applyType() + applyValue() + } + + var title: String = "" + set(v) { + field = v + tvTitle.text = v + } + + var hint: String = "" + set(v) { + field = v + et.hint = v + tv.hint = v + } + + var value: String = "" + set(v) { + if (field == v) return + field = v + applyValue() + onChangeListener?.onChange() + } + + /** SELECT 形态的选项(key 显示文案 / value 业务值) */ + var list: List = emptyList() + set(v) { + field = v + applyValue() + } + + var required: Boolean = false + set(v) { + field = v + tvMust.visibility = if (v) VISIBLE else GONE + } + + var enable: Boolean = true + set(v) { + field = v + et.isEnabled = v + applyType() + } + + /** INPUT 形态是否数字键盘(自重等数值项) */ + var numeric: Boolean = false + set(v) { + field = v + et.inputType = if (v) { + InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + } else { + InputType.TYPE_CLASS_TEXT + } + } + + init { + orientation = VERTICAL + inflate(context, R.layout.layout_phone_form_row, this) + + llRow = findViewById(R.id.ll_row) + tvTitle = findViewById(R.id.tv_title) + tvMust = findViewById(R.id.tv_must) + et = findViewById(R.id.et) + tv = findViewById(R.id.tv) + ivArrow = findViewById(R.id.iv_arrow) + + et.doOnTextChanged { text, _, _, _ -> + // 仅 INPUT 形态才回写,避免隐藏的 EditText 被系统状态恢复时误清空值 + if (type == PhoneFormRowType.INPUT) { + value = text.toString() + } + } + + applyType() + } + + private val selectClick: (v: View) -> Unit = { + if (enable && list.isNotEmpty()) { + XPopup.Builder(context) + .asBottomList(hint.ifEmpty { title }, list.map { it.key }.toTypedArray()) { position, _ -> + value = list.getOrNull(position)?.value ?: "" + refreshCallBack?.invoke() + } + .show() + } + } + + private val dateClick: (v: View) -> Unit = { + if (enable) { + Common.onYearMonthDay(context.getActivity(), value) { year, month, day -> + val calendar = Calendar.getInstance() + calendar.set(year, month - 1, day) + value = calendar.time.formatDate() + refreshCallBack?.invoke() + } + } + } + + private fun applyType() { + when (type) { + PhoneFormRowType.INPUT -> { + et.visibility = VISIBLE + tv.visibility = GONE + ivArrow.visibility = GONE + llRow.setOnClickListener(null) + llRow.isClickable = false + } + + PhoneFormRowType.SELECT -> { + et.visibility = GONE + tv.visibility = VISIBLE + tv.gravity = Gravity.START or Gravity.CENTER_VERTICAL + ivArrow.visibility = if (enable) VISIBLE else GONE + llRow.setOnClickListener(selectClick) + } + + PhoneFormRowType.DATE -> { + et.visibility = GONE + tv.visibility = VISIBLE + tv.gravity = Gravity.START or Gravity.CENTER_VERTICAL + ivArrow.visibility = GONE + llRow.setOnClickListener(dateClick) + } + + PhoneFormRowType.TEXT -> { + et.visibility = GONE + tv.visibility = VISIBLE + tv.gravity = Gravity.END or Gravity.CENTER_VERTICAL + tv.hint = "" + ivArrow.visibility = GONE + llRow.setOnClickListener(null) + llRow.isClickable = false + } + } + } + + private fun applyValue() { + when (type) { + PhoneFormRowType.INPUT -> { + if (et.text.toString() != value) et.setText(value) + } + + PhoneFormRowType.SELECT -> { + // 显示选中项的文案;列表里找不到就按原值显示(如后端返回的历史值) + tv.text = list.firstOrNull { it.value == value }?.key ?: value + } + + PhoneFormRowType.DATE -> { + tv.text = value + } + + PhoneFormRowType.TEXT -> { + // 只读展示:空值统一占位 "--"(与设计稿一致) + tv.text = value.ifEmpty { "--" } + } + } + } + + /** 只保存/恢复自身状态,避免内部子控件 id 与其他实例串档(公共组件必做,见 components.md) */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFormRowType.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFormRowType.kt new file mode 100644 index 0000000..37900e3 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFormRowType.kt @@ -0,0 +1,18 @@ +package com.lukouguoji.module_base.ui.weight.phone + +/** + * [PhoneFormRow] 的形态。 + * + * | 形态 | 值区控件 | 交互 | + * |----------|--------------------|------| + * | `INPUT` | EditText(左对齐) | 直接输入 | + * | `SELECT` | TextView + 右箭头 | 点击弹底部选择面板(list 提供选项) | + * | `DATE` | TextView | 点击弹日历 | + * | `TEXT` | TextView(右对齐) | 只读展示(详情态) | + */ +enum class PhoneFormRowType { + INPUT, + SELECT, + DATE, + TEXT, +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneKvItem.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneKvItem.kt new file mode 100644 index 0000000..bb78c80 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneKvItem.kt @@ -0,0 +1,112 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.lukouguoji.module_base.R + +/** + * 手机版详情页字段项(5.5 寸手持端公共组件) + * + * 竖排「小号灰标签(12sp)+ 数值(15sp)」,用于详情页两列信息网格。 + * 对应设计稿「板箱详情」等详情卡片内的字段。 + * + * XML 用法: + * ```xml + * + * ``` + * + * 需要突出的字段(如库位)设 `tag="@{true}"`,数值会渲染为橙色高亮标签。 + */ +class PhoneKvItem @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + val tvTitle: TextView + val tvValue: TextView + + var title: String = "" + set(v) { + field = v + tvTitle.text = v + } + + var value: String = "" + set(v) { + field = v + // 详情页空值统一显示为 "-",与设计稿一致 + tvValue.text = v.ifEmpty { "-" } + applyStyle() + } + + /** 是否以橙色高亮标签样式展示数值 */ + var tag: Boolean = false + set(v) { + field = v + applyStyle() + } + + /** 高亮标签配色:"orange"(默认,如库位)/ "green"(如海关放行正常态) */ + var tagColor: String = "orange" + set(v) { + field = v + applyStyle() + } + + /** + * 空值不套高亮标签底 —— 否则占位符 "-"/"--" 会显示成一个孤零零的色块。 + */ + private fun applyStyle() { + if (tag && value.isNotEmpty() && value != "-" && value != "--") { + if (tagColor == "green") { + tvValue.setBackgroundResource(R.drawable.bg_phone_tag_green) + tvValue.setTextColor(ContextCompat.getColor(context, R.color.phone_tag_green_text)) + } else { + tvValue.setBackgroundResource(R.drawable.bg_phone_value_orange) + tvValue.setTextColor(ContextCompat.getColor(context, R.color.phone_orange_text)) + } + tvValue.setTypeface(tvValue.typeface, android.graphics.Typeface.BOLD) + val padH = (4 * resources.displayMetrics.density).toInt() + tvValue.setPadding(padH, 0, padH, 0) + } else { + tvValue.background = null + tvValue.setTextColor(ContextCompat.getColor(context, R.color.phone_text_title)) + tvValue.setPadding(0, 0, 0, 0) + } + } + + init { + orientation = VERTICAL + + tvTitle = TextView(context).apply { + textSize = 12f + setTextColor(ContextCompat.getColor(context, R.color.phone_text_hint)) + } + addView( + tvTitle, + LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + ) + + tvValue = TextView(context).apply { + textSize = 15f + setTextColor(ContextCompat.getColor(context, R.color.phone_text_title)) + } + addView( + tvValue, + LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { + topMargin = (2 * resources.displayMetrics.density).toInt() + gravity = Gravity.START + } + ) + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneSearchBar.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneSearchBar.kt new file mode 100644 index 0000000..e82b501 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneSearchBar.kt @@ -0,0 +1,139 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.util.SparseArray +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.core.widget.doOnTextChanged +import androidx.databinding.InverseBindingListener +import com.lukouguoji.module_base.R +import com.lukouguoji.module_base.ktx.setUpperCaseAlphanumericFilter + +/** + * 手机版搜索栏(5.5 寸手持端公共组件) + * + * 结构:圆角搜索框(放大镜 + 输入框 + 扫码图标)+ 右侧圆形筛选按钮。 + * 对应设计稿「5.5寸手持端设计文件」各列表页顶部搜索区。 + * + * XML 用法: + * ```xml + * + * ``` + * + * 说明:与平板端 [com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout] 并列, + * 二者互不影响;手机布局用本组件,平板布局继续用 PadSearchLayout。 + */ +class PhoneSearchBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + val llSearch: LinearLayout + val ivSearch: ImageView + val et: EditText + val ivScan: ImageView + val ivFilter: ImageView + + /** 双向绑定回调 */ + var onChangeListener: InverseBindingListener? = null + + /** 键盘「搜索」键 / 扫码完成后触发的查询回调 */ + var searchCallBack: (() -> Unit)? = null + + var value: String = "" + set(v) { + if (field == v) return + field = v + if (et.text.toString() != v) et.setText(v) + onChangeListener?.onChange() + } + + var hint: String = "" + set(v) { + field = v + et.hint = v + } + + /** 是否显示扫码图标(默认显示) */ + var showScan: Boolean = true + set(v) { + field = v + ivScan.visibility = if (v) VISIBLE else GONE + } + + /** 是否显示右侧筛选按钮(默认显示) */ + var showFilter: Boolean = true + set(v) { + field = v + ivFilter.visibility = if (v) VISIBLE else GONE + } + + /** 输入内容强制大写字母数字(运单号 / ULD 编号等场景) */ + var upperCase: Boolean = false + set(v) { + field = v + if (v) et.setUpperCaseAlphanumericFilter() + } + + init { + orientation = HORIZONTAL + gravity = android.view.Gravity.CENTER_VERTICAL + inflate(context, R.layout.layout_phone_search_bar, this) + + llSearch = findViewById(R.id.ll_search) + ivSearch = findViewById(R.id.iv_search) + et = findViewById(R.id.et) + ivScan = findViewById(R.id.iv_scan) + ivFilter = findViewById(R.id.iv_filter) + + et.doOnTextChanged { text, _, _, _ -> + value = text.toString() + } + + // 软键盘「搜索」键触发查询 + et.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + searchCallBack?.invoke() + true + } else { + false + } + } + } + + /** + * 只保存/恢复自身状态,不下发给内部子控件。 + * 内部 et / iv_* 的 id 在每个实例中相同,交由系统按 id 恢复会跨实例串档; + * 取值本就由 ViewModel + DataBinding 持有,无需视图级恢复。 + */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } + + /** 供外部(如扫码回调)复用的图标点击入口 */ + fun setOnScanClick(listener: View.OnClickListener?) { + ivScan.setOnClickListener(listener) + } + + fun setOnFilterClick(listener: View.OnClickListener?) { + ivFilter.setOnClickListener(listener) + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatBox.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatBox.kt new file mode 100644 index 0000000..c130e2a --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatBox.kt @@ -0,0 +1,108 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.util.SparseArray +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import com.lukouguoji.module_base.R + +/** + * 手机版卡片统计框(5.5 寸手持端公共组件) + * + * 浅灰底 + 8dp 圆角,横向等宽 2~4 列,每列为「小号灰标签 + 加粗数值」,列间带竖分隔线。 + * 对应设计稿列表卡片中部的统计区(如「架子车号 / 目的港 / 装机重」)。 + * + * XML 用法(列数由是否传 label 决定,未传的列自动隐藏): + * ```xml + * + * ``` + */ +class PhoneStatBox @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val columns: List + private val labels: List + private val values: List + + /** 分隔线(下标 0 对应第 2 列左侧,依次类推) */ + private val dividers: List + + init { + orientation = HORIZONTAL + setBackgroundResource(R.drawable.bg_phone_stat_box) + val pad = (12 * resources.displayMetrics.density).toInt() + setPadding(pad, pad, pad, pad) + + inflate(context, R.layout.layout_phone_stat_box, this) + + columns = listOf( + findViewById(R.id.ll_col1), + findViewById(R.id.ll_col2), + findViewById(R.id.ll_col3), + findViewById(R.id.ll_col4) + ) + labels = listOf( + findViewById(R.id.tv_label1), + findViewById(R.id.tv_label2), + findViewById(R.id.tv_label3), + findViewById(R.id.tv_label4) + ) + values = listOf( + findViewById(R.id.tv_value1), + findViewById(R.id.tv_value2), + findViewById(R.id.tv_value3), + findViewById(R.id.tv_value4) + ) + dividers = listOf( + findViewById(R.id.divider2), + findViewById(R.id.divider3), + findViewById(R.id.divider4) + ) + + // 默认只展示第 1 列,其余待 label 赋值后再显示 + for (i in 1..3) setColumnVisible(i, false) + } + + /** + * 只保存/恢复自身状态。列表中每个卡片都含一个本组件,内部子控件 id 相同, + * 交由系统按 id 恢复会跨实例串档。 + */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } + + /** 设置第 [index] 列(0 起)的标签,传空则隐藏该列 */ + fun setLabel(index: Int, text: String?) { + if (index !in labels.indices) return + labels[index].text = text.orEmpty() + setColumnVisible(index, !text.isNullOrEmpty()) + } + + /** 设置第 [index] 列(0 起)的数值 */ + fun setValue(index: Int, text: String?) { + if (index !in values.indices) return + values[index].text = text.orEmpty() + } + + private fun setColumnVisible(index: Int, visible: Boolean) { + val flag = if (visible) VISIBLE else GONE + columns[index].visibility = flag + // 第 2~4 列连带其左侧分隔线一起显隐 + if (index in 1..3) dividers[index - 1].visibility = flag + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt new file mode 100644 index 0000000..28dd2d0 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt @@ -0,0 +1,182 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.util.SparseArray +import android.view.Gravity +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.databinding.InverseBindingListener +import com.lukouguoji.module_base.R +import dev.utils.app.info.KeyValue + +/** + * 手机版状态 Tab 条(5.5 寸手持端公共组件) + * + * 每个 Tab = 文字 + 可选角标数字,选中态为「深色加粗文字 + 底部 2dp 全宽指示条」。 + * 对应设计稿各列表页的「待交接 / 已交接」「全部 / 待移库 / 已移库」等状态切换栏。 + * + * XML 用法: + * ```xml + * + * ``` + * + * [tabs] 用 [KeyValue]:`key` 为显示文字,`value` 为写回 [value] 的业务值。 + * [counts] 为与 tabs 等长的角标文案列表,传 null 或空串则该 Tab 不显示角标。 + */ +class PhoneStatusTab @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + /** + * 同一页面可能出现多个含内部 id 的手机版复合组件,系统按 id 保存/恢复视图状态会跨实例串档。 + * 这里只保存/恢复自身状态、不下发给子控件;选中值由 ViewModel + DataBinding 持有,无需视图级恢复。 + */ + override fun dispatchSaveInstanceState(container: SparseArray) { + dispatchFreezeSelfOnly(container) + } + + override fun dispatchRestoreInstanceState(container: SparseArray) { + dispatchThawSelfOnly(container) + } + + /** 双向绑定回调 */ + var onChangeListener: InverseBindingListener? = null + + /** Tab 切换回调(用户点击引起的变化才触发,外部赋值不触发) */ + var tabChangedCallBack: (() -> Unit)? = null + + var tabs: List = emptyList() + set(v) { + field = v + buildTabs() + } + + var counts: List = emptyList() + set(v) { + field = v + refreshState() + } + + var value: String = "" + set(v) { + if (field == v) return + field = v + refreshState() + onChangeListener?.onChange() + } + + init { + orientation = HORIZONTAL + setBackgroundColor(ContextCompat.getColor(context, R.color.white)) + } + + private fun buildTabs() { + removeAllViews() + tabs.forEachIndexed { index, tab -> + addView(createTab(tab, index)) + } + refreshState() + } + + private fun createTab(tab: KeyValue, index: Int): View { + val column = LinearLayout(context).apply { + orientation = VERTICAL + layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) + setOnClickListener { + val target = tab.value ?: "" + if (value != target) { + value = target + tabChangedCallBack?.invoke() + } + } + } + + // 文字 + 角标 + val row = LinearLayout(context).apply { + orientation = HORIZONTAL + gravity = Gravity.CENTER + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(42)) + } + row.addView(TextView(context).apply { + id = titleIdOf(index) + text = tab.key + textSize = 14f + setTypeface(typeface, android.graphics.Typeface.BOLD) + }) + row.addView(TextView(context).apply { + id = badgeIdOf(index) + textSize = 11f + gravity = Gravity.CENTER + setBackgroundResource(R.drawable.bg_phone_badge) + setTextColor(ContextCompat.getColor(context, R.color.phone_primary)) + setPadding(dp(6), dp(1), dp(6), dp(1)) + layoutParams = LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ).apply { marginStart = dp(6) } + }) + column.addView(row) + + // 底部指示条 + column.addView(View(context).apply { + id = indicatorIdOf(index) + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(2)) + }) + + return column + } + + private fun refreshState() { + tabs.forEachIndexed { index, tab -> + val selected = (tab.value ?: "") == value + + findViewById(titleIdOf(index))?.setTextColor( + ContextCompat.getColor( + context, + if (selected) R.color.phone_text_title else R.color.phone_text_body + ) + ) + + findViewById(indicatorIdOf(index))?.setBackgroundColor( + if (selected) ContextCompat.getColor(context, R.color.phone_text_title) + else android.graphics.Color.TRANSPARENT + ) + + findViewById(badgeIdOf(index))?.apply { + val count = counts.getOrNull(index).orEmpty() + if (count.isEmpty()) { + visibility = GONE + } else { + visibility = VISIBLE + text = count + } + } + } + } + + private fun dp(value: Int): Int = + (value * resources.displayMetrics.density).toInt() + + // 动态生成的子 View 用固定偏移量作为 id,避免与资源 id 冲突 + private fun titleIdOf(index: Int) = ID_BASE_TITLE + index + private fun badgeIdOf(index: Int) = ID_BASE_BADGE + index + private fun indicatorIdOf(index: Int) = ID_BASE_INDICATOR + index + + companion object { + private const val ID_BASE_TITLE = 0x7E100000 + private const val ID_BASE_BADGE = 0x7E200000 + private const val ID_BASE_INDICATOR = 0x7E300000 + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneWidgetKtx.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneWidgetKtx.kt new file mode 100644 index 0000000..277ffe4 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneWidgetKtx.kt @@ -0,0 +1,310 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.view.View +import androidx.databinding.BindingAdapter +import androidx.databinding.InverseBindingAdapter +import androidx.databinding.InverseBindingListener +import dev.utils.app.info.KeyValue + +/** + * 5.5 寸手持端公共组件的 DataBinding 适配器集合 + * + * 所有属性均为无命名空间写法(与平板端 PadSearchLayout / PadDataLayoutNew 的用法一致)。 + * 同名属性(hint / value / title / list 等)按控件类型解析,与平板端组件互不冲突。 + */ + +/////////////////////////////////////////////////////////////////////////// +// PhoneSearchBar +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("hint", "showScan", "showFilter", "upperCase", requireAll = false) +fun setPhoneSearchBarAttrs( + bar: PhoneSearchBar, + hint: String?, + showScan: Boolean?, + showFilter: Boolean?, + upperCase: Boolean? +) { + hint?.let { bar.hint = it } + showScan?.let { bar.showScan = it } + showFilter?.let { bar.showFilter = it } + upperCase?.let { bar.upperCase = it } +} + +@BindingAdapter("value", requireAll = false) +fun setPhoneSearchBarValue(bar: PhoneSearchBar, value: String?) { + value?.let { bar.value = it } +} + +@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged") +fun getPhoneSearchBarValue(bar: PhoneSearchBar): String = bar.value + +@BindingAdapter("valueAttrChanged", requireAll = false) +fun setPhoneSearchBarValueAttrChanged(bar: PhoneSearchBar, listener: InverseBindingListener) { + bar.onChangeListener = listener +} + +@BindingAdapter("setOnScanClickListener", requireAll = false) +fun setPhoneSearchBarScanClick(bar: PhoneSearchBar, listener: View.OnClickListener?) { + bar.setOnScanClick(listener) +} + +@BindingAdapter("setOnFilterClickListener", requireAll = false) +fun setPhoneSearchBarFilterClick(bar: PhoneSearchBar, listener: View.OnClickListener?) { + bar.setOnFilterClick(listener) +} + +@BindingAdapter("setRefreshCallBack", requireAll = false) +fun setPhoneSearchBarRefreshCallBack(bar: PhoneSearchBar, callBack: (() -> Unit)?) { + bar.searchCallBack = callBack +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneStatusTab +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("tabs", requireAll = false) +fun setPhoneStatusTabTabs(tab: PhoneStatusTab, tabs: List?) { + tabs?.let { tab.tabs = it } +} + +@BindingAdapter("counts", requireAll = false) +fun setPhoneStatusTabCounts(tab: PhoneStatusTab, counts: List?) { + tab.counts = counts ?: emptyList() +} + +@BindingAdapter("value", requireAll = false) +fun setPhoneStatusTabValue(tab: PhoneStatusTab, value: String?) { + value?.let { tab.value = it } +} + +@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged") +fun getPhoneStatusTabValue(tab: PhoneStatusTab): String = tab.value + +@BindingAdapter("valueAttrChanged", requireAll = false) +fun setPhoneStatusTabValueAttrChanged(tab: PhoneStatusTab, listener: InverseBindingListener) { + tab.onChangeListener = listener +} + +@BindingAdapter("setOnTabChanged", requireAll = false) +fun setPhoneStatusTabChanged(tab: PhoneStatusTab, callBack: (() -> Unit)?) { + tab.tabChangedCallBack = callBack +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneDataLayout +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("title", "hint", "type", "required", "enable", requireAll = false) +fun setPhoneDataLayoutAttrs( + layout: PhoneDataLayout, + title: String?, + hint: String?, + type: PhoneDataLayoutType?, + required: Boolean?, + enable: Boolean? +) { + // type 必须先于 value/hint 生效,否则控件形态与取值分支不匹配 + type?.let { layout.type = it } + title?.let { layout.title = it } + hint?.let { layout.hint = it } + required?.let { layout.required = it } + enable?.let { layout.enable = it } +} + +@BindingAdapter("value", requireAll = false) +fun setPhoneDataLayoutValue(layout: PhoneDataLayout, value: String?) { + value?.let { layout.value = it } +} + +@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged") +fun getPhoneDataLayoutValue(layout: PhoneDataLayout): String = layout.value + +@BindingAdapter("valueAttrChanged", requireAll = false) +fun setPhoneDataLayoutValueAttrChanged( + layout: PhoneDataLayout, + listener: InverseBindingListener +) { + layout.onChangeListener = listener +} + +@BindingAdapter("list", requireAll = false) +fun setPhoneDataLayoutList(layout: PhoneDataLayout, list: List?) { + list?.let { layout.list = it } +} + +@BindingAdapter("setRefreshCallBack", requireAll = false) +fun setPhoneDataLayoutRefreshCallBack(layout: PhoneDataLayout, callBack: (() -> Unit)?) { + layout.refreshCallBack = callBack +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneFilterPanel +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("title", "panelVisible", requireAll = false) +fun setPhoneFilterPanelAttrs(panel: PhoneFilterPanel, title: String?, panelVisible: Boolean?) { + title?.let { panel.title = it } + panelVisible?.let { panel.panelVisible = it } +} + +@BindingAdapter("setOnResetClick", requireAll = false) +fun setPhoneFilterPanelResetClick(panel: PhoneFilterPanel, listener: View.OnClickListener?) { + panel.tvReset.setOnClickListener(listener) +} + +@BindingAdapter("setOnConfirmClick", requireAll = false) +fun setPhoneFilterPanelConfirmClick(panel: PhoneFilterPanel, listener: View.OnClickListener?) { + panel.tvConfirm.setOnClickListener(listener) +} + +@BindingAdapter("setOnDismissClick", requireAll = false) +fun setPhoneFilterPanelDismissClick(panel: PhoneFilterPanel, listener: View.OnClickListener?) { + panel.setOnDismiss(listener) +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneBottomBar +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("allChecked", "countText", "actionText", requireAll = false) +fun setPhoneBottomBarAttrs( + bar: PhoneBottomBar, + allChecked: Boolean?, + countText: String?, + actionText: String? +) { + allChecked?.let { bar.allChecked = it } + countText?.let { bar.countText = it } + actionText?.let { bar.actionText = it } +} + +@BindingAdapter("setOnAllCheckClick", requireAll = false) +fun setPhoneBottomBarAllCheckClick(bar: PhoneBottomBar, listener: View.OnClickListener?) { + bar.llCheckAll.setOnClickListener(listener) +} + +@BindingAdapter("setOnActionClick", requireAll = false) +fun setPhoneBottomBarActionClick(bar: PhoneBottomBar, listener: View.OnClickListener?) { + bar.tvAction.setOnClickListener(listener) +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneStatBox +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter( + "label1", "value1", "label2", "value2", + "label3", "value3", "label4", "value4", + requireAll = false +) +fun setPhoneStatBoxAttrs( + box: PhoneStatBox, + label1: String?, value1: String?, + label2: String?, value2: String?, + label3: String?, value3: String?, + label4: String?, value4: String? +) { + box.setLabel(0, label1) + box.setValue(0, value1) + box.setLabel(1, label2) + box.setValue(1, value2) + box.setLabel(2, label3) + box.setValue(2, value3) + box.setLabel(3, label4) + box.setValue(3, value4) +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneKvItem +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("title", "value", "tag", "tagColor", requireAll = false) +fun setPhoneKvItemAttrs( + item: PhoneKvItem, + title: String?, + value: String?, + tag: Boolean?, + tagColor: String? +) { + title?.let { item.title = it } + // tag/tagColor 需先于 value 生效,确保样式切换后再填内容 + tagColor?.let { item.tagColor = it } + tag?.let { item.tag = it } + item.value = value.orEmpty() +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneFormRow +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("title", "hint", "type", "required", "enable", "numeric", requireAll = false) +fun setPhoneFormRowAttrs( + row: PhoneFormRow, + title: String?, + hint: String?, + type: PhoneFormRowType?, + required: Boolean?, + enable: Boolean?, + numeric: Boolean? +) { + // type 先于其余属性生效,确保 value/hint 写进正确的子控件 + type?.let { row.type = it } + title?.let { row.title = it } + hint?.let { row.hint = it } + required?.let { row.required = it } + enable?.let { row.enable = it } + numeric?.let { row.numeric = it } +} + +@BindingAdapter("value", requireAll = false) +fun setPhoneFormRowValue(row: PhoneFormRow, value: String?) { + if (row.value != value.orEmpty()) row.value = value.orEmpty() +} + +@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged") +fun getPhoneFormRowValue(row: PhoneFormRow): String = row.value + +@BindingAdapter("valueAttrChanged", requireAll = false) +fun setPhoneFormRowValueAttrChanged(row: PhoneFormRow, listener: InverseBindingListener) { + row.onChangeListener = listener +} + +@BindingAdapter("list", requireAll = false) +fun setPhoneFormRowList(row: PhoneFormRow, list: List?) { + row.list = list ?: emptyList() +} + +@BindingAdapter("setRefreshCallBack", requireAll = false) +fun setPhoneFormRowRefreshCallBack(row: PhoneFormRow, callBack: (() -> Unit)?) { + row.refreshCallBack = callBack +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneBottomBar(追加:危险操作样式) +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("actionDanger", requireAll = false) +fun setPhoneBottomBarActionDanger(bar: PhoneBottomBar, danger: Boolean?) { + danger?.let { bar.actionDanger = it } +} + +/////////////////////////////////////////////////////////////////////////// +// PhoneBottomBar(追加:次操作按钮,承载平板底部其余批量操作) +/////////////////////////////////////////////////////////////////////////// + +@BindingAdapter("subAction1Text", "subAction2Text", requireAll = false) +fun setPhoneBottomBarSubActions(bar: PhoneBottomBar, subAction1Text: String?, subAction2Text: String?) { + subAction1Text?.let { bar.subAction1Text = it } + subAction2Text?.let { bar.subAction2Text = it } +} + +@BindingAdapter("setOnSubAction1Click", requireAll = false) +fun setPhoneBottomBarSubAction1Click(bar: PhoneBottomBar, listener: View.OnClickListener?) { + bar.tvSubAction1.setOnClickListener(listener) +} + +@BindingAdapter("setOnSubAction2Click", requireAll = false) +fun setPhoneBottomBarSubAction2Click(bar: PhoneBottomBar, listener: View.OnClickListener?) { + bar.tvSubAction2.setOnClickListener(listener) +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/util/DeviceUtil.kt b/module_base/src/main/java/com/lukouguoji/module_base/util/DeviceUtil.kt new file mode 100644 index 0000000..4d4d65e --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/util/DeviceUtil.kt @@ -0,0 +1,116 @@ +package com.lukouguoji.module_base.util + +import android.content.Context +import android.content.res.Resources +import androidx.annotation.LayoutRes +import com.lukouguoji.module_base.MyApplication + +/** + * 设备形态判定工具 + * + * 用于「平板(10 寸横屏)」与「手机(5.5 寸竖屏)」双形态适配: + * 1. 识别当前设备是平板还是手机 + * 2. 提供按形态选择布局的入口(Activity 布局 / RecyclerView item 布局通用) + * 3. 提供手机端 AutoSize 设计基准尺寸常量 + * + * 判定依据:`smallestScreenWidthDp`(最小屏幕宽度,与横竖屏无关,是 Android 官方推荐的设备分类维度) + * - 5.5 寸手机 ≈ 360dp + * - 10 寸平板 ≈ 800dp + * - 分界线取官方标准 600dp + * + * 注意:这里读取的是 [Resources.getSystem](系统资源)而非应用 Resources, + * 目的是绕开 AndroidAutoSize 对 density 的动态改写,保证判定结果稳定。 + */ +object DeviceUtil { + + /** 平板判定阈值:最小屏幕宽度 >= 600dp 视为平板 */ + private const val TABLET_MIN_WIDTH_DP = 600 + + /** 手机端设计稿基准尺寸(对应 5.5 寸手持端设计稿画布 390 × 844) */ + const val PHONE_DESIGN_WIDTH_DP = 390 + const val PHONE_DESIGN_HEIGHT_DP = 844 + + /** 平板端设计稿基准尺寸(沿用现有配置) */ + const val TABLET_DESIGN_WIDTH_DP_LAND = 1152 + const val TABLET_DESIGN_HEIGHT_DP_LAND = 720 + const val TABLET_DESIGN_WIDTH_DP_PORT = 720 + const val TABLET_DESIGN_HEIGHT_DP_PORT = 1280 + + /** + * 调试用:强制指定设备形态 + * + * 开发阶段可在平板上强制走手机布局做验证,正式发布保持 [ForceMode.AUTO]。 + * 用法:`DeviceUtil.forceMode = DeviceUtil.ForceMode.PHONE` + */ + enum class ForceMode { AUTO, PHONE, TABLET } + + @JvmStatic + var forceMode: ForceMode = ForceMode.AUTO + + /** 缓存自动判定结果,避免每次都读 Configuration */ + private var cachedIsTablet: Boolean? = null + + /** + * 是否平板 + * @param context 可不传,默认用 Application 上下文(判定与具体 Activity 无关) + */ + @JvmStatic + @JvmOverloads + fun isTablet(context: Context? = null): Boolean = when (forceMode) { + ForceMode.PHONE -> false + ForceMode.TABLET -> true + ForceMode.AUTO -> cachedIsTablet ?: (smallestWidthDp(context) >= TABLET_MIN_WIDTH_DP) + .also { cachedIsTablet = it } + } + + /** 是否手机 */ + @JvmStatic + @JvmOverloads + fun isPhone(context: Context? = null): Boolean = !isTablet(context) + + /** + * 获取最小屏幕宽度(dp) + * + * 优先读系统资源(不受 AutoSize 影响);异常时回退到应用资源。 + */ + @JvmStatic + @JvmOverloads + fun smallestWidthDp(context: Context? = null): Int { + val fromSystem = Resources.getSystem()?.configuration?.smallestScreenWidthDp ?: 0 + if (fromSystem > 0) return fromSystem + + val res = (context ?: runCatching { MyApplication.context }.getOrNull())?.resources + return res?.configuration?.smallestScreenWidthDp ?: TABLET_MIN_WIDTH_DP + } + + /** + * 按设备形态选择布局 + * + * @param tabletLayout 平板布局(现有布局,必填) + * @param phoneLayout 手机布局;传 0 或未提供时,手机上回退使用平板布局 + * @return 当前设备应加载的布局 id + * + * 用法(ViewModel 中选择 item 布局): + * ``` + * val itemLayoutId = DeviceUtil.layout(R.layout.item_xxx, R.layout.item_xxx_phone) + * ``` + */ + @JvmStatic + @JvmOverloads + fun layout( + @LayoutRes tabletLayout: Int, + @LayoutRes phoneLayout: Int = 0, + context: Context? = null + ): Int = if (phoneLayout != 0 && isPhone(context)) phoneLayout else tabletLayout + + /** 配置发生变化时清空缓存(如分屏、折叠屏形态切换) */ + @JvmStatic + fun clearCache() { + cachedIsTablet = null + } + + /** 设备形态描述,用于日志排查 */ + @JvmStatic + fun describe(): String = + "${if (isTablet()) "TABLET" else "PHONE"}(sw=${smallestWidthDp()}dp, force=$forceMode)" +} diff --git a/module_base/src/main/release/AndroidManifest.xml b/module_base/src/main/release/AndroidManifest.xml index 211c6c2..900bf1d 100644 --- a/module_base/src/main/release/AndroidManifest.xml +++ b/module_base/src/main/release/AndroidManifest.xml @@ -1,53 +1,53 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_base/src/main/res/drawable/bg_phone_badge.xml b/module_base/src/main/res/drawable/bg_phone_badge.xml new file mode 100644 index 0000000..01467fa --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_badge.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_btn_danger.xml b/module_base/src/main/res/drawable/bg_phone_btn_danger.xml new file mode 100644 index 0000000..8845bc3 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_btn_danger.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_btn_disabled.xml b/module_base/src/main/res/drawable/bg_phone_btn_disabled.xml new file mode 100644 index 0000000..e9ee944 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_btn_disabled.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_btn_green.xml b/module_base/src/main/res/drawable/bg_phone_btn_green.xml new file mode 100644 index 0000000..d868fee --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_btn_green.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_btn_outline.xml b/module_base/src/main/res/drawable/bg_phone_btn_outline.xml new file mode 100644 index 0000000..a2539f2 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_btn_outline.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_btn_primary.xml b/module_base/src/main/res/drawable/bg_phone_btn_primary.xml new file mode 100644 index 0000000..79bf36f --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_btn_primary.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_btn_weak.xml b/module_base/src/main/res/drawable/bg_phone_btn_weak.xml new file mode 100644 index 0000000..23c1189 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_btn_weak.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_card.xml b/module_base/src/main/res/drawable/bg_phone_card.xml new file mode 100644 index 0000000..64c2e9c --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_card.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_card_header_blue.xml b/module_base/src/main/res/drawable/bg_phone_card_header_blue.xml new file mode 100644 index 0000000..29e94e1 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_card_header_blue.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_card_header_green.xml b/module_base/src/main/res/drawable/bg_phone_card_header_green.xml new file mode 100644 index 0000000..8a41698 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_card_header_green.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_card_header_orange.xml b/module_base/src/main/res/drawable/bg_phone_card_header_orange.xml new file mode 100644 index 0000000..d9abdd0 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_card_header_orange.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_card_top_accent.xml b/module_base/src/main/res/drawable/bg_phone_card_top_accent.xml new file mode 100644 index 0000000..b551906 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_card_top_accent.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_dot_blue.xml b/module_base/src/main/res/drawable/bg_phone_dot_blue.xml new file mode 100644 index 0000000..f076187 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_dot_blue.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_dot_gray.xml b/module_base/src/main/res/drawable/bg_phone_dot_gray.xml new file mode 100644 index 0000000..204e516 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_dot_gray.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_dot_green.xml b/module_base/src/main/res/drawable/bg_phone_dot_green.xml new file mode 100644 index 0000000..ac4c39d --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_dot_green.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_fab.xml b/module_base/src/main/res/drawable/bg_phone_fab.xml new file mode 100644 index 0000000..4966935 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_fab.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_fab_amber.xml b/module_base/src/main/res/drawable/bg_phone_fab_amber.xml new file mode 100644 index 0000000..7d921e1 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_fab_amber.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_fab_green.xml b/module_base/src/main/res/drawable/bg_phone_fab_green.xml new file mode 100644 index 0000000..cc2e7b6 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_fab_green.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_fab_purple.xml b/module_base/src/main/res/drawable/bg_phone_fab_purple.xml new file mode 100644 index 0000000..15aa096 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_fab_purple.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_fab_red.xml b/module_base/src/main/res/drawable/bg_phone_fab_red.xml new file mode 100644 index 0000000..3afed09 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_fab_red.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_icon_round.xml b/module_base/src/main/res/drawable/bg_phone_icon_round.xml new file mode 100644 index 0000000..968fb82 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_icon_round.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_input.xml b/module_base/src/main/res/drawable/bg_phone_input.xml new file mode 100644 index 0000000..ffcec8d --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_input.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_panel_top.xml b/module_base/src/main/res/drawable/bg_phone_panel_top.xml new file mode 100644 index 0000000..8abbc82 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_panel_top.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_photo_add.xml b/module_base/src/main/res/drawable/bg_phone_photo_add.xml new file mode 100644 index 0000000..a8ff1e7 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_photo_add.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_search.xml b/module_base/src/main/res/drawable/bg_phone_search.xml new file mode 100644 index 0000000..1a9f98e --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_search.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_stat_box.xml b/module_base/src/main/res/drawable/bg_phone_stat_box.xml new file mode 100644 index 0000000..a65e0b2 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_stat_box.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_tag_amber.xml b/module_base/src/main/res/drawable/bg_phone_tag_amber.xml new file mode 100644 index 0000000..abb4e6a --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_tag_amber.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_tag_gray.xml b/module_base/src/main/res/drawable/bg_phone_tag_gray.xml new file mode 100644 index 0000000..0107085 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_tag_gray.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_tag_green.xml b/module_base/src/main/res/drawable/bg_phone_tag_green.xml new file mode 100644 index 0000000..d4a4a6b --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_tag_green.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_tag_indigo.xml b/module_base/src/main/res/drawable/bg_phone_tag_indigo.xml new file mode 100644 index 0000000..8979188 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_tag_indigo.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_tag_orange.xml b/module_base/src/main/res/drawable/bg_phone_tag_orange.xml new file mode 100644 index 0000000..5c72d67 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_tag_orange.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_tag_red.xml b/module_base/src/main/res/drawable/bg_phone_tag_red.xml new file mode 100644 index 0000000..5ebb1de --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_tag_red.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_tag_red_light.xml b/module_base/src/main/res/drawable/bg_phone_tag_red_light.xml new file mode 100644 index 0000000..147e1b6 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_tag_red_light.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/bg_phone_value_orange.xml b/module_base/src/main/res/drawable/bg_phone_value_orange.xml new file mode 100644 index 0000000..cc31f77 --- /dev/null +++ b/module_base/src/main/res/drawable/bg_phone_value_orange.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_airplane.xml b/module_base/src/main/res/drawable/ic_phone_airplane.xml new file mode 100644 index 0000000..68b5b06 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_airplane.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_calendar.xml b/module_base/src/main/res/drawable/ic_phone_calendar.xml new file mode 100644 index 0000000..5cec99e --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_calendar.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_check_circle.xml b/module_base/src/main/res/drawable/ic_phone_check_circle.xml new file mode 100644 index 0000000..656c085 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_check_circle.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_check_round_checked.xml b/module_base/src/main/res/drawable/ic_phone_check_round_checked.xml new file mode 100644 index 0000000..a3742d5 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_check_round_checked.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_check_round_unchecked.xml b/module_base/src/main/res/drawable/ic_phone_check_round_unchecked.xml new file mode 100644 index 0000000..d6d4fa9 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_check_round_unchecked.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_chevron_down.xml b/module_base/src/main/res/drawable/ic_phone_chevron_down.xml new file mode 100644 index 0000000..6ee5c26 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_chevron_down.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_chevron_right.xml b/module_base/src/main/res/drawable/ic_phone_chevron_right.xml new file mode 100644 index 0000000..5147b47 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_chevron_right.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_clipboard.xml b/module_base/src/main/res/drawable/ic_phone_clipboard.xml new file mode 100644 index 0000000..980d0ed --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_clipboard.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_close.xml b/module_base/src/main/res/drawable/ic_phone_close.xml new file mode 100644 index 0000000..d8f7606 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_close.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_edit.xml b/module_base/src/main/res/drawable/ic_phone_edit.xml new file mode 100644 index 0000000..a275720 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_edit.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_location.xml b/module_base/src/main/res/drawable/ic_phone_location.xml new file mode 100644 index 0000000..572cccb --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_location.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_package.xml b/module_base/src/main/res/drawable/ic_phone_package.xml new file mode 100644 index 0000000..45bfe57 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_package.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_person.xml b/module_base/src/main/res/drawable/ic_phone_person.xml new file mode 100644 index 0000000..337908f --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_person.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_tag.xml b/module_base/src/main/res/drawable/ic_phone_tag.xml new file mode 100644 index 0000000..8db7a99 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_tag.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_trash.xml b/module_base/src/main/res/drawable/ic_phone_trash.xml new file mode 100644 index 0000000..8db15c3 --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_trash.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/module_base/src/main/res/drawable/ic_phone_weight.xml b/module_base/src/main/res/drawable/ic_phone_weight.xml new file mode 100644 index 0000000..ebec08a --- /dev/null +++ b/module_base/src/main/res/drawable/ic_phone_weight.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/module_base/src/main/res/layout-sw600dp/activity_customs_response.xml b/module_base/src/main/res/layout-sw600dp/activity_customs_response.xml new file mode 100644 index 0000000..740bf1a --- /dev/null +++ b/module_base/src/main/res/layout-sw600dp/activity_customs_response.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout-sw600dp/title_tool_bar.xml b/module_base/src/main/res/layout-sw600dp/title_tool_bar.xml new file mode 100644 index 0000000..91872c4 --- /dev/null +++ b/module_base/src/main/res/layout-sw600dp/title_tool_bar.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_base/src/main/res/layout/activity_customs_response.xml b/module_base/src/main/res/layout/activity_customs_response.xml index 740bf1a..de706e0 100644 --- a/module_base/src/main/res/layout/activity_customs_response.xml +++ b/module_base/src/main/res/layout/activity_customs_response.xml @@ -1,35 +1,39 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout/item_spinner_list_14sp.xml b/module_base/src/main/res/layout/item_spinner_list_14sp.xml new file mode 100644 index 0000000..739c7e4 --- /dev/null +++ b/module_base/src/main/res/layout/item_spinner_list_14sp.xml @@ -0,0 +1,15 @@ + + + diff --git a/module_base/src/main/res/layout/layout_phone_bottom_bar.xml b/module_base/src/main/res/layout/layout_phone_bottom_bar.xml new file mode 100644 index 0000000..51ce205 --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_bottom_bar.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout/layout_phone_data_layout.xml b/module_base/src/main/res/layout/layout_phone_data_layout.xml new file mode 100644 index 0000000..029f007 --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_data_layout.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout/layout_phone_filter_panel.xml b/module_base/src/main/res/layout/layout_phone_filter_panel.xml new file mode 100644 index 0000000..f4e714a --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_filter_panel.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout/layout_phone_form_row.xml b/module_base/src/main/res/layout/layout_phone_form_row.xml new file mode 100644 index 0000000..435b425 --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_form_row.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout/layout_phone_search_bar.xml b/module_base/src/main/res/layout/layout_phone_search_bar.xml new file mode 100644 index 0000000..a652a92 --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_search_bar.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout/layout_phone_stat_box.xml b/module_base/src/main/res/layout/layout_phone_stat_box.xml new file mode 100644 index 0000000..54470cd --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_stat_box.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/layout/title_tool_bar.xml b/module_base/src/main/res/layout/title_tool_bar.xml index 91872c4..89a10a3 100644 --- a/module_base/src/main/res/layout/title_tool_bar.xml +++ b/module_base/src/main/res/layout/title_tool_bar.xml @@ -1,42 +1,67 @@ - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/module_base/src/main/res/values/colors.xml b/module_base/src/main/res/values/colors.xml index e893c4e..a369cd7 100644 --- a/module_base/src/main/res/values/colors.xml +++ b/module_base/src/main/res/values/colors.xml @@ -47,4 +47,38 @@ #80000000 #B3000000 + + #2563EB + #EAF1FE + #F3F4F6 + #1F2937 + #6B7280 + #9CA3AF + #E5E7EB + #F3F4F6 + #6B7280 + #DCFCE7 + #16A34A + #DC2626 + #FFFFFF + #F5F6FA + #F3F4F6 + #DBEAF6 + #374151 + #111827 + #4B5563 + #10B981 + #CBD5E1 + #FFF7ED + #EA580C + #FFEDD5 + #EA580C + #FEE2E2 + #DC2626 + #E0E7FF + #6366F1 + #FEF3C7 + #D97706 + #F59E0B + \ No newline at end of file diff --git a/module_cargo/src/main/AndroidManifest.xml b/module_cargo/src/main/AndroidManifest.xml index 671905c..f09f4eb 100644 --- a/module_cargo/src/main/AndroidManifest.xml +++ b/module_cargo/src/main/AndroidManifest.xml @@ -1,11 +1,11 @@ - - - - - + + + + + \ No newline at end of file diff --git a/module_cargo/src/main/debug/AndroidManifest.xml b/module_cargo/src/main/debug/AndroidManifest.xml index f4f988b..1959600 100644 --- a/module_cargo/src/main/debug/AndroidManifest.xml +++ b/module_cargo/src/main/debug/AndroidManifest.xml @@ -1,25 +1,25 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_cargo/src/main/release/AndroidManifest.xml b/module_cargo/src/main/release/AndroidManifest.xml index 6814e54..d3796d7 100644 --- a/module_cargo/src/main/release/AndroidManifest.xml +++ b/module_cargo/src/main/release/AndroidManifest.xml @@ -1,17 +1,17 @@ - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/module_gjc/src/main/debug/AndroidManifest.xml b/module_gjc/src/main/debug/AndroidManifest.xml index 79e383b..cbe8e23 100644 --- a/module_gjc/src/main/debug/AndroidManifest.xml +++ b/module_gjc/src/main/debug/AndroidManifest.xml @@ -1,25 +1,25 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryActivity.kt index 004786c..ec20a17 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryActivity.kt @@ -18,6 +18,7 @@ import com.lukouguoji.module_base.ktx.getLifecycleOwner import com.lukouguoji.module_base.ktx.setUpperCaseAlphanumericFilter import com.lukouguoji.module_base.ktx.setUpperCaseLetterFilter import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际出港查询列表页 @@ -31,7 +32,7 @@ class GjcQueryActivity : override fun viewModelClass() = GjcQueryViewModel::class.java override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港查询") + setBackArrow(if (DeviceUtil.isPhone()) "出港查询" else "国际出港查询") binding.viewModel = viewModel @@ -48,23 +49,31 @@ class GjcQueryActivity : viewModel.initAgentList() viewModel.initFilterLists() - // 观察筛选面板显示状态 - viewModel.filterVisible.observe(this) { visible -> - if (visible) showFilterPanel() else hideFilterPanel() - } + // 平板变体才有侧滑筛选面板(filter_panel/filter_overlay 手机变体不存在,Binding 字段为 null) + val padFilterPanel = binding.filterPanel + if (padFilterPanel != null) { + // 观察筛选面板显示状态(滑入/滑出动画) + viewModel.filterVisible.observe(this) { visible -> + if (visible) showFilterPanel() else hideFilterPanel() + } - // 航班号:大写字母+数字 - binding.flightNoSearch.et.setUpperCaseAlphanumericFilter() - // 目的港:仅大写字母 - binding.filterPanel.filterDest.et.setUpperCaseLetterFilter() + // 航班号(搜索栏):大写字母+数字 + binding.flightNoSearch?.et?.setUpperCaseAlphanumericFilter() + // 目的港:仅大写字母 + padFilterPanel.filterDest.et.setUpperCaseLetterFilter() + } else { + // 手机变体:PhoneFilterPanel 的显隐由 XML 直接绑定 viewModel.filterVisible,无需动画代码 + binding.phoneFilterFlightNo?.et?.setUpperCaseAlphanumericFilter() + binding.phoneFilterDest?.et?.setUpperCaseLetterFilter() + } // 初始加载 viewModel.refresh() } private fun showFilterPanel() { - val panel = binding.filterPanel.root - val overlay = binding.filterOverlay + val panel = binding.filterPanel?.root ?: return + val overlay = binding.filterOverlay ?: return val panelWidth = window.decorView.width / 3 panel.layoutParams = (panel.layoutParams as FrameLayout.LayoutParams).apply { @@ -86,8 +95,8 @@ class GjcQueryActivity : } private fun hideFilterPanel() { - val panel = binding.filterPanel.root - val overlay = binding.filterOverlay + val panel = binding.filterPanel?.root ?: return + val overlay = binding.filterOverlay ?: return if (panel.visibility != View.VISIBLE) return diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryDetailsActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryDetailsActivity.kt index eeaeb94..b3bcc28 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryDetailsActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcQueryDetailsActivity.kt @@ -1,90 +1,91 @@ -package com.lukouguoji.gjc.activity - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.Gravity -import android.widget.ImageView -import android.widget.LinearLayout -import androidx.appcompat.widget.Toolbar -import com.alibaba.android.arouter.facade.annotation.Route -import com.alibaba.android.arouter.launcher.ARouter -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityGjcQueryDetailsBinding -import com.lukouguoji.gjc.viewModel.GjcQueryDetailsViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.base.CustomVP2Adapter -import com.lukouguoji.module_base.common.Constant -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港查询详情页面 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_GJC_QUERY_DETAILS) -class GjcQueryDetailsActivity : - BaseBindingActivity() { - - override fun layoutId() = R.layout.activity_gjc_query_details - override fun viewModelClass() = GjcQueryDetailsViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港查询详情") - binding.viewModel = viewModel - - // 初始化ViewModel(传入maWbId) - viewModel.initOnCreated(intent) - - // 配置ViewPager2 - binding.vp.adapter = CustomVP2Adapter( - viewModel.fragmentList, - supportFragmentManager, - lifecycle - ) - binding.vp.isUserInputEnabled = false // 禁用滑动 - binding.vp.offscreenPageLimit = 3 // 预加载3个Fragment - - // 监听Tab索引变化,切换Fragment - viewModel.currentTab.observe(this) { - binding.vp.setCurrentItem(it, false) // false:无动画 - } - - // 标题栏右侧添加日志查询图标 - val toolbar = findViewById(com.lukouguoji.module_base.R.id.toolbar) - val logIcon = ImageView(this).apply { - setImageResource(com.lukouguoji.module_base.R.drawable.img_log) - layoutParams = Toolbar.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).apply { - gravity = Gravity.END or Gravity.CENTER_VERTICAL - marginEnd = 15 - width = 30.dp - height = 30.dp - } - setOnClickListener { - val wbNo = viewModel.maWbData.value?.get("wbNo") as? String ?: "" - ARouter.getInstance() - .build(ARouterConstants.ACTIVITY_URL_LOG_DETAIL) - .withString(Constant.Key.KEY, wbNo) - .withString(Constant.Key.AWB_TYPE, "IO") - .navigation() - } - } - toolbar.addView(logIcon) - - // 加载详情数据 - viewModel.loadDetails() - } - - private val Int.dp: Int - get() = (this * resources!!.displayMetrics.density).toInt() - - companion object { - @JvmStatic - fun start(context: Context, maWbId: Long?) { - val starter = Intent(context, GjcQueryDetailsActivity::class.java) - .putExtra(Constant.Key.ID, maWbId?.toString() ?: "") - context.startActivity(starter) - } - } -} +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Gravity +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.appcompat.widget.Toolbar +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityGjcQueryDetailsBinding +import com.lukouguoji.gjc.viewModel.GjcQueryDetailsViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.base.CustomVP2Adapter +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +/** + * 国际出港查询详情页面 + */ +@Route(path = ARouterConstants.ACTIVITY_URL_GJC_QUERY_DETAILS) +class GjcQueryDetailsActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_gjc_query_details + override fun viewModelClass() = GjcQueryDetailsViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow(if (DeviceUtil.isPhone()) "出港货物" else "国际出港查询详情") + binding.viewModel = viewModel + + // 初始化ViewModel(传入maWbId) + viewModel.initOnCreated(intent) + + // 配置ViewPager2 + binding.vp.adapter = CustomVP2Adapter( + viewModel.fragmentList, + supportFragmentManager, + lifecycle + ) + binding.vp.isUserInputEnabled = false // 禁用滑动 + binding.vp.offscreenPageLimit = 3 // 预加载3个Fragment + + // 监听Tab索引变化,切换Fragment + viewModel.currentTab.observe(this) { + binding.vp.setCurrentItem(it, false) // false:无动画 + } + + // 标题栏右侧添加日志查询图标 + val toolbar = findViewById(com.lukouguoji.module_base.R.id.toolbar) + val logIcon = ImageView(this).apply { + setImageResource(com.lukouguoji.module_base.R.drawable.img_log) + layoutParams = Toolbar.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + gravity = Gravity.END or Gravity.CENTER_VERTICAL + marginEnd = 15 + width = 30.dp + height = 30.dp + } + setOnClickListener { + val wbNo = viewModel.maWbData.value?.get("wbNo") as? String ?: "" + ARouter.getInstance() + .build(ARouterConstants.ACTIVITY_URL_LOG_DETAIL) + .withString(Constant.Key.KEY, wbNo) + .withString(Constant.Key.AWB_TYPE, "IO") + .navigation() + } + } + toolbar.addView(logIcon) + + // 加载详情数据 + viewModel.loadDetails() + } + + private val Int.dp: Int + get() = (this * resources!!.displayMetrics.density).toInt() + + companion object { + @JvmStatic + fun start(context: Context, maWbId: Long?) { + val starter = Intent(context, GjcQueryDetailsActivity::class.java) + .putExtra(Constant.Key.ID, maWbId?.toString() ?: "") + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingListActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingListActivity.kt index a0e37bb..e733adb 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingListActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingListActivity.kt @@ -1,62 +1,104 @@ -package com.lukouguoji.gjc.activity - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import com.alibaba.android.arouter.facade.annotation.Route -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityGjcWeighingListBinding -import com.lukouguoji.gjc.viewModel.GjcWeighingViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.common.ConstantEvent -import com.lukouguoji.module_base.impl.FlowBus -import com.lukouguoji.module_base.impl.observe -import com.lukouguoji.module_base.ktx.getLifecycleOwner -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港计重列表页 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_LIST) -class GjcWeighingListActivity : - BaseBindingActivity() { - - override fun layoutId() = R.layout.activity_gjc_weighing_list - - override fun viewModelClass() = GjcWeighingViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港待计重") - - binding.viewModel = viewModel - - // 初始化代理人列表(从API获取) - viewModel.initAgentList() - - // 初始化特码列表(从API获取) - viewModel.initSpecialCodeList() - - // 绑定分页逻辑 - viewModel.pageModel - .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner()) - - // 监听刷新事件 - FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { viewModel.onItemCheckChanged() } - - FlowBus.with(ConstantEvent.EVENT_REFRESH) - .observe(this) { - viewModel.refresh() - } - - // 初始加载 - viewModel.refresh() - } - - companion object { - @JvmStatic - fun start(context: Context) { - val starter = Intent(context, GjcWeighingListActivity::class.java) - context.startActivity(starter) - } - } -} +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.alibaba.android.arouter.facade.annotation.Route +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityGjcWeighingListBinding +import com.lukouguoji.gjc.viewModel.GjcWeighingViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.GjcWeighingBean +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.observe +import com.lukouguoji.module_base.ktx.getLifecycleOwner +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +/** + * 国际出港计重列表页 + * + * 布局 id 不区分设备,平板/手机由资源限定符自动选择: + * layout-sw600dp/activity_gjc_weighing_list.xml(平板) + * layout/activity_gjc_weighing_list.xml(手机) + */ +@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_LIST) +class GjcWeighingListActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_gjc_weighing_list + + override fun viewModelClass() = GjcWeighingViewModel::class.java + + /** 手机版筛选弹层显隐(仅 UI 状态,不涉及业务逻辑) */ + val filterPanelVisible = ObservableBoolean(false) + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow(if (DeviceUtil.isPhone()) "出港待计重" else "国际出港待计重") + + binding.viewModel = viewModel + binding.activity = this + + // 手机版专属数据:放开默认日期、加载筛选弹层字典、启用三格统计 + // 平板端不调用,查询链路与请求次数与改造前完全一致 + if (DeviceUtil.isPhone()) { + viewModel.initPhoneExtras() + } + + // 初始化代理人列表(从API获取) + viewModel.initAgentList() + + // 初始化特码列表(从API获取) + viewModel.initSpecialCodeList() + + // 绑定分页逻辑 + viewModel.pageModel + .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner()) + + // 监听刷新事件 + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { viewModel.onItemCheckChanged() } + + FlowBus.with(ConstantEvent.EVENT_REFRESH) + .observe(this) { + viewModel.refresh() + } + + // 初始加载 + viewModel.refresh() + } + + // ==================== 手机版专用交互(纯 UI,复用既有 ViewModel 字段) ==================== + + /** 手机版筛选弹层显隐切换 */ + fun toggleFilterPanel() { + filterPanelVisible.set(!filterPanelVisible.get()) + } + + /** 手机版筛选:重置(只清弹层内条件,不影响顶部搜索框) */ + fun resetFilter() { + viewModel.flightDate.value = "" + viewModel.agentCode.value = "" + viewModel.spCode.value = "" + } + + /** 手机版筛选:确认 */ + fun confirmFilter() { + filterPanelVisible.set(false) + viewModel.searchClick() + } + + /** 手机版卡片内「提前运抵」(单票,由 ViewHolder 调用) */ + fun preArriveClick(bean: GjcWeighingBean) { + viewModel.arrivedAheadSingle(bean) + } + + companion object { + @JvmStatic + fun start(context: Context) { + val starter = Intent(context, GjcWeighingListActivity::class.java) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordDetailsActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordDetailsActivity.kt index b9ace48..ca1e9fe 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordDetailsActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordDetailsActivity.kt @@ -1,81 +1,104 @@ -package com.lukouguoji.gjc.activity - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.alibaba.android.arouter.facade.annotation.Route -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityGjcWeighingRecordDetailsBinding -import com.lukouguoji.gjc.holder.GjcCheckInRecordViewHolder -import com.lukouguoji.gjc.viewModel.GjcWeighingRecordDetailsViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.base.CommonAdapter -import com.lukouguoji.module_base.bean.GjcWeighingRecordBean -import com.lukouguoji.module_base.common.Constant -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港计重明细页 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_RECORD_DETAILS) -class GjcWeighingRecordDetailsActivity : - BaseBindingActivity() { - - private lateinit var adapter: CommonAdapter - - // 当前编辑模式(供ViewHolder获取) - var currentEditMode = false - - override fun layoutId() = R.layout.activity_gjc_weighing_record_details - - override fun viewModelClass() = GjcWeighingRecordDetailsViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港计重明细") - - binding.viewModel = viewModel - - // 初始化RecyclerView - val layoutManager = LinearLayoutManager(this) - binding.recyclerView.layoutManager = layoutManager - - // 添加分割线 - val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - binding.recyclerView.addItemDecoration(divider) - - adapter = CommonAdapter( - this, - R.layout.item_gjc_check_in_record, - GjcCheckInRecordViewHolder::class.java - ) - binding.recyclerView.adapter = adapter - - // 监听数据变化更新RecyclerView - viewModel.recordList.observe(this) { records -> - adapter.refresh(records) - } - - // 监听编辑模式变化,更新当前编辑模式并刷新列表 - viewModel.isEditMode.observe(this) { isEditMode -> - // 更新当前编辑模式状态 - currentEditMode = isEditMode - - // 刷新所有可见的 ViewHolder - adapter.notifyDataSetChanged() - } - - // 初始化数据 - viewModel.initOnCreated(intent) - } - - companion object { - @JvmStatic - fun start(context: Context, bean: GjcWeighingRecordBean) { - val starter = Intent(context, GjcWeighingRecordDetailsActivity::class.java) - .putExtra(Constant.Key.BEAN, bean) - context.startActivity(starter) - } - } -} +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.alibaba.android.arouter.facade.annotation.Route +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityGjcWeighingRecordDetailsBinding +import com.lukouguoji.gjc.holder.GjcCheckInRecordViewHolder +import com.lukouguoji.gjc.viewModel.GjcWeighingRecordDetailsViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.base.CommonAdapter +import com.lukouguoji.module_base.bean.GjcCheckInRecord +import com.lukouguoji.module_base.bean.GjcWeighingRecordBean +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +/** + * 国际出港计重明细页 + * + * 布局 id 不区分设备,平板/手机由资源限定符自动选择: + * layout-sw600dp/activity_gjc_weighing_record_details.xml(平板,列表内联编辑) + * layout/activity_gjc_weighing_record_details.xml(手机,单条记录经全屏「修改记录」覆层编辑) + */ +@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_RECORD_DETAILS) +class GjcWeighingRecordDetailsActivity : + BaseBindingActivity() { + + private lateinit var adapter: CommonAdapter + + // 当前编辑模式(供ViewHolder获取) + var currentEditMode = false + + override fun layoutId() = R.layout.activity_gjc_weighing_record_details + + override fun viewModelClass() = GjcWeighingRecordDetailsViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow(if (DeviceUtil.isPhone()) "计重明细" else "国际出港计重明细") + + binding.viewModel = viewModel + + // 初始化RecyclerView + val layoutManager = LinearLayoutManager(this) + binding.recyclerView.layoutManager = layoutManager + + // 添加分割线 + val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + + adapter = CommonAdapter( + this, + R.layout.item_gjc_check_in_record, + GjcCheckInRecordViewHolder::class.java + ) + binding.recyclerView.adapter = adapter + + // 监听数据变化更新RecyclerView + viewModel.recordList.observe(this) { records -> + adapter.refresh(records) + } + + // 监听编辑模式变化,更新当前编辑模式并刷新列表 + viewModel.isEditMode.observe(this) { isEditMode -> + // 更新当前编辑模式状态 + currentEditMode = isEditMode + + // 刷新所有可见的 ViewHolder + adapter.notifyDataSetChanged() + } + + // 初始化数据 + viewModel.initOnCreated(intent) + } + + // ==================== 手机版专用交互(纯 UI,复用既有 ViewModel 编辑链路) ==================== + + /** + * 手机版:打开「修改记录」覆层编辑指定记录。 + * 复用 [GjcWeighingRecordDetailsViewModel.onModifyClick] 的备份/编辑态逻辑, + * 覆层显隐由 isEditMode 驱动(平板端该状态驱动列表内联编辑,互不影响)。 + */ + fun openPhoneEdit(record: GjcCheckInRecord) { + viewModel.onModifyClick() + binding.editRecord = record + } + + /** 手机版:取消编辑(恢复备份并收起覆层) */ + fun closePhoneEdit() { + viewModel.onCancelClick() + } + + companion object { + @JvmStatic + fun start(context: Context, bean: GjcWeighingRecordBean) { + val starter = Intent(context, GjcWeighingRecordDetailsActivity::class.java) + .putExtra(Constant.Key.BEAN, bean) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordListActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordListActivity.kt index 5d6253e..6c1ce64 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordListActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingRecordListActivity.kt @@ -1,60 +1,91 @@ -package com.lukouguoji.gjc.activity - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import com.alibaba.android.arouter.facade.annotation.Route -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityGjcWeighingRecordListBinding -import com.lukouguoji.gjc.viewModel.GjcWeighingRecordViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.common.ConstantEvent -import com.lukouguoji.module_base.impl.FlowBus -import com.lukouguoji.module_base.impl.observe -import com.lukouguoji.module_base.ktx.getLifecycleOwner -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港计重记录列表页 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_RECORD_LIST) -class GjcWeighingRecordListActivity : - BaseBindingActivity() { - - override fun layoutId() = R.layout.activity_gjc_weighing_record_list - - override fun viewModelClass() = GjcWeighingRecordViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港计重记录") - - binding.viewModel = viewModel - - // 初始化代理人列表 - viewModel.initAgentList() - - // 初始化特码列表 - viewModel.initSpecialCodeList() - - // 绑定分页逻辑 - viewModel.pageModel - .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner()) - - // 监听刷新事件 - FlowBus.with(ConstantEvent.EVENT_REFRESH) - .observe(this) { - viewModel.refresh() - } - - // 初始加载 - viewModel.refresh() - } - - companion object { - @JvmStatic - fun start(context: Context) { - val starter = Intent(context, GjcWeighingRecordListActivity::class.java) - context.startActivity(starter) - } - } -} +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.alibaba.android.arouter.facade.annotation.Route +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityGjcWeighingRecordListBinding +import com.lukouguoji.gjc.viewModel.GjcWeighingRecordViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.observe +import com.lukouguoji.module_base.ktx.getLifecycleOwner +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +/** + * 国际出港计重记录列表页 + * + * 布局 id 不区分设备,平板/手机由资源限定符自动选择: + * layout-sw600dp/activity_gjc_weighing_record_list.xml(平板) + * layout/activity_gjc_weighing_record_list.xml(手机) + */ +@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_RECORD_LIST) +class GjcWeighingRecordListActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_gjc_weighing_record_list + + override fun viewModelClass() = GjcWeighingRecordViewModel::class.java + + /** 手机版筛选弹层显隐(仅 UI 状态,不涉及业务逻辑) */ + val filterPanelVisible = ObservableBoolean(false) + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow(if (DeviceUtil.isPhone()) "计重记录" else "国际出港计重记录") + + binding.viewModel = viewModel + binding.activity = this + + // 初始化代理人列表 + viewModel.initAgentList() + + // 初始化特码列表 + viewModel.initSpecialCodeList() + + // 绑定分页逻辑 + viewModel.pageModel + .bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner()) + + // 监听刷新事件 + FlowBus.with(ConstantEvent.EVENT_REFRESH) + .observe(this) { + viewModel.refresh() + } + + // 初始加载 + viewModel.refresh() + } + + // ==================== 手机版专用交互(纯 UI,复用既有 ViewModel 字段) ==================== + + /** 手机版筛选弹层显隐切换 */ + fun toggleFilterPanel() { + filterPanelVisible.set(!filterPanelVisible.get()) + } + + /** 手机版筛选:重置(只清弹层内条件,不影响顶部搜索框) */ + fun resetFilter() { + viewModel.flightDate.value = "" + viewModel.flightNo.value = "" + viewModel.agentCode.value = "" + viewModel.spCode.value = "" + } + + /** 手机版筛选:确认 */ + fun confirmFilter() { + filterPanelVisible.set(false) + viewModel.searchClick() + } + + companion object { + @JvmStatic + fun start(context: Context) { + val starter = Intent(context, GjcWeighingRecordListActivity::class.java) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingStartActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingStartActivity.kt index 168ed21..81d230f 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingStartActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingStartActivity.kt @@ -1,198 +1,216 @@ -package com.lukouguoji.gjc.activity - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.View -import com.alibaba.android.arouter.facade.annotation.Route -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityGjcWeighingStartBinding -import com.lukouguoji.gjc.viewModel.GjcWeighingStartViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.common.Constant -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港计重-开始计重页面 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_START) -class GjcWeighingStartActivity : - BaseBindingActivity() { - - private var isExpanded = false - - override fun layoutId() = R.layout.activity_gjc_weighing_start - - override fun viewModelClass() = GjcWeighingStartViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港开始计重") - binding.viewModel = viewModel - viewModel.initOnCreated(this, intent) - - setupFloatButtons() - } - - private fun setupFloatButtons() { - // 主按钮点击事件 - binding.mainFloatButton.setOnClickListener { - if (!isExpanded) { - expandButtons() - } - } - - // 遮罩层点击事件 - binding.maskView.setOnClickListener { - collapseButtons() - } - - // 子按钮1点击事件 - 跳转到出港运抵页面 - binding.subButton1.setOnClickListener { - com.alibaba.android.arouter.launcher.ARouter.getInstance() - .build(ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE) - .navigation() - collapseButtons() - } - - // 子按钮2点击事件 - 跳转到计重记录 - binding.subButton2.setOnClickListener { - com.alibaba.android.arouter.launcher.ARouter.getInstance() - .build(ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_RECORD_LIST) - .navigation() - collapseButtons() - } - } - - /** - * 展开子按钮 - */ - private fun expandButtons() { - isExpanded = true - - // 显示遮罩层 - binding.maskView.visibility = View.VISIBLE - binding.maskView.alpha = 0f - binding.maskView.animate().alpha(0.3f).setDuration(200).start() - - // 显示子按钮 - binding.subButton1.visibility = View.VISIBLE - binding.subButton2.visibility = View.VISIBLE - - // 子按钮1动画(向左上方移动: 左移15dp, 上移55dp) - val sub1TransX = ObjectAnimator.ofFloat(binding.subButton1, "translationX", 0f, -5f) - val sub1TransY = ObjectAnimator.ofFloat(binding.subButton1, "translationY", 0f, -25f) - val sub1Alpha = ObjectAnimator.ofFloat(binding.subButton1, "alpha", 0f, 1f) - val sub1Scale = ObjectAnimator.ofFloat(binding.subButton1, "scaleX", 0f, 1f) - val sub1ScaleY = ObjectAnimator.ofFloat(binding.subButton1, "scaleY", 0f, 1f) - - val animSet1 = AnimatorSet() - animSet1.playTogether(sub1TransX, sub1TransY, sub1Alpha, sub1Scale, sub1ScaleY) - animSet1.duration = 200 - - // 子按钮2动画(向左下方移动: 左移55dp, 下移15dp) - val sub2TransX = ObjectAnimator.ofFloat(binding.subButton2, "translationX", 0f, -25f) - val sub2TransY = ObjectAnimator.ofFloat(binding.subButton2, "translationY", 0f, -5f) - val sub2Alpha = ObjectAnimator.ofFloat(binding.subButton2, "alpha", 0f, 1f) - val sub2Scale = ObjectAnimator.ofFloat(binding.subButton2, "scaleX", 0f, 1f) - val sub2ScaleY = ObjectAnimator.ofFloat(binding.subButton2, "scaleY", 0f, 1f) - - val animSet2 = AnimatorSet() - animSet2.playTogether(sub2TransX, sub2TransY, sub2Alpha, sub2Scale, sub2ScaleY) - animSet2.duration = 200 - animSet2.startDelay = 50 - - // 主按钮隐藏动画 - binding.mainFloatButton.animate() - .scaleX(0f) - .scaleY(0f) - .alpha(0f) - .setDuration(150) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - binding.mainFloatButton.visibility = View.GONE - } - }) - .start() - - animSet1.start() - animSet2.start() - } - - /** - * 收起子按钮 - */ - private fun collapseButtons() { - isExpanded = false - - // 隐藏遮罩层 - binding.maskView.animate().alpha(0f).setDuration(200).withEndAction { - binding.maskView.visibility = View.GONE - }.start() - - // 子按钮1动画(回到原位) - val sub1TransX = ObjectAnimator.ofFloat(binding.subButton1, "translationX", binding.subButton1.translationX, 0f) - val sub1TransY = ObjectAnimator.ofFloat(binding.subButton1, "translationY", binding.subButton1.translationY, 0f) - val sub1Alpha = ObjectAnimator.ofFloat(binding.subButton1, "alpha", 1f, 0f) - val sub1Scale = ObjectAnimator.ofFloat(binding.subButton1, "scaleX", 1f, 0f) - val sub1ScaleY = ObjectAnimator.ofFloat(binding.subButton1, "scaleY", 1f, 0f) - - val animSet1 = AnimatorSet() - animSet1.playTogether(sub1TransX, sub1TransY, sub1Alpha, sub1Scale, sub1ScaleY) - animSet1.duration = 200 - - // 子按钮2动画(回到原位) - val sub2TransX = ObjectAnimator.ofFloat(binding.subButton2, "translationX", binding.subButton2.translationX, 0f) - val sub2TransY = ObjectAnimator.ofFloat(binding.subButton2, "translationY", binding.subButton2.translationY, 0f) - val sub2Alpha = ObjectAnimator.ofFloat(binding.subButton2, "alpha", 1f, 0f) - val sub2Scale = ObjectAnimator.ofFloat(binding.subButton2, "scaleX", 1f, 0f) - val sub2ScaleY = ObjectAnimator.ofFloat(binding.subButton2, "scaleY", 1f, 0f) - - val animSet2 = AnimatorSet() - animSet2.playTogether(sub2TransX, sub2TransY, sub2Alpha, sub2Scale, sub2ScaleY) - animSet2.duration = 200 - - animSet1.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - binding.subButton1.visibility = View.GONE - } - }) - - animSet2.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - binding.subButton2.visibility = View.GONE - } - }) - - // 主按钮显示动画 - binding.mainFloatButton.visibility = View.VISIBLE - binding.mainFloatButton.animate() - .scaleX(1f) - .scaleY(1f) - .alpha(1f) - .setDuration(200) - .setStartDelay(100) - .setListener(null) - .start() - - animSet1.start() - animSet2.start() - } - - companion object { - /** - * 启动开始计重页面 - * @param context 上下文 - * @param maWbId 运单ID,默认为0(新增模式) - */ - @JvmStatic - fun startForAdd(context: Context, maWbId: Long = 0) { - val starter = Intent(context, GjcWeighingStartActivity::class.java) - .putExtra(Constant.Key.MAWB_ID, maWbId) - context.startActivity(starter) - } - } -} +package com.lukouguoji.gjc.activity + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import com.alibaba.android.arouter.facade.annotation.Route +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityGjcWeighingStartBinding +import com.lukouguoji.gjc.viewModel.GjcWeighingStartViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +/** + * 国际出港计重-开始计重页面 + */ +@Route(path = ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_START) +class GjcWeighingStartActivity : + BaseBindingActivity() { + + private var isExpanded = false + + override fun layoutId() = R.layout.activity_gjc_weighing_start + + override fun viewModelClass() = GjcWeighingStartViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow(if (DeviceUtil.isPhone()) "开始计重" else "国际出港开始计重") + binding.viewModel = viewModel + + viewModel.initOnCreated(this, intent) + + setupFloatButtons() + } + + private fun setupFloatButtons() { + // 浮动按钮组仅平板布局存在(layout-sw600dp),手机布局无这些 id,直接跳过 + val mainFloatButton = binding.mainFloatButton ?: return + val maskView = binding.maskView ?: return + val subButton1 = binding.subButton1 ?: return + val subButton2 = binding.subButton2 ?: return + + // 主按钮点击事件 + mainFloatButton.setOnClickListener { + if (!isExpanded) { + expandButtons() + } + } + + // 遮罩层点击事件 + maskView.setOnClickListener { + collapseButtons() + } + + // 子按钮1点击事件 - 跳转到出港运抵页面 + subButton1.setOnClickListener { + com.alibaba.android.arouter.launcher.ARouter.getInstance() + .build(ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE) + .navigation() + collapseButtons() + } + + // 子按钮2点击事件 - 跳转到计重记录 + subButton2.setOnClickListener { + com.alibaba.android.arouter.launcher.ARouter.getInstance() + .build(ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_RECORD_LIST) + .navigation() + collapseButtons() + } + } + + /** + * 展开子按钮(仅平板,手机布局无浮动按钮组) + */ + private fun expandButtons() { + val mainFloatButton = binding.mainFloatButton ?: return + val maskView = binding.maskView ?: return + val subButton1 = binding.subButton1 ?: return + val subButton2 = binding.subButton2 ?: return + + isExpanded = true + + // 显示遮罩层 + maskView.visibility = View.VISIBLE + maskView.alpha = 0f + maskView.animate().alpha(0.3f).setDuration(200).start() + + // 显示子按钮 + subButton1.visibility = View.VISIBLE + subButton2.visibility = View.VISIBLE + + // 子按钮1动画(向左上方移动: 左移15dp, 上移55dp) + val sub1TransX = ObjectAnimator.ofFloat(subButton1, "translationX", 0f, -5f) + val sub1TransY = ObjectAnimator.ofFloat(subButton1, "translationY", 0f, -25f) + val sub1Alpha = ObjectAnimator.ofFloat(subButton1, "alpha", 0f, 1f) + val sub1Scale = ObjectAnimator.ofFloat(subButton1, "scaleX", 0f, 1f) + val sub1ScaleY = ObjectAnimator.ofFloat(subButton1, "scaleY", 0f, 1f) + + val animSet1 = AnimatorSet() + animSet1.playTogether(sub1TransX, sub1TransY, sub1Alpha, sub1Scale, sub1ScaleY) + animSet1.duration = 200 + + // 子按钮2动画(向左下方移动: 左移55dp, 下移15dp) + val sub2TransX = ObjectAnimator.ofFloat(subButton2, "translationX", 0f, -25f) + val sub2TransY = ObjectAnimator.ofFloat(subButton2, "translationY", 0f, -5f) + val sub2Alpha = ObjectAnimator.ofFloat(subButton2, "alpha", 0f, 1f) + val sub2Scale = ObjectAnimator.ofFloat(subButton2, "scaleX", 0f, 1f) + val sub2ScaleY = ObjectAnimator.ofFloat(subButton2, "scaleY", 0f, 1f) + + val animSet2 = AnimatorSet() + animSet2.playTogether(sub2TransX, sub2TransY, sub2Alpha, sub2Scale, sub2ScaleY) + animSet2.duration = 200 + animSet2.startDelay = 50 + + // 主按钮隐藏动画 + mainFloatButton.animate() + .scaleX(0f) + .scaleY(0f) + .alpha(0f) + .setDuration(150) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mainFloatButton.visibility = View.GONE + } + }) + .start() + + animSet1.start() + animSet2.start() + } + + /** + * 收起子按钮(仅平板,手机布局无浮动按钮组) + */ + private fun collapseButtons() { + val mainFloatButton = binding.mainFloatButton ?: return + val maskView = binding.maskView ?: return + val subButton1 = binding.subButton1 ?: return + val subButton2 = binding.subButton2 ?: return + + isExpanded = false + + // 隐藏遮罩层 + maskView.animate().alpha(0f).setDuration(200).withEndAction { + maskView.visibility = View.GONE + }.start() + + // 子按钮1动画(回到原位) + val sub1TransX = ObjectAnimator.ofFloat(subButton1, "translationX", subButton1.translationX, 0f) + val sub1TransY = ObjectAnimator.ofFloat(subButton1, "translationY", subButton1.translationY, 0f) + val sub1Alpha = ObjectAnimator.ofFloat(subButton1, "alpha", 1f, 0f) + val sub1Scale = ObjectAnimator.ofFloat(subButton1, "scaleX", 1f, 0f) + val sub1ScaleY = ObjectAnimator.ofFloat(subButton1, "scaleY", 1f, 0f) + + val animSet1 = AnimatorSet() + animSet1.playTogether(sub1TransX, sub1TransY, sub1Alpha, sub1Scale, sub1ScaleY) + animSet1.duration = 200 + + // 子按钮2动画(回到原位) + val sub2TransX = ObjectAnimator.ofFloat(subButton2, "translationX", subButton2.translationX, 0f) + val sub2TransY = ObjectAnimator.ofFloat(subButton2, "translationY", subButton2.translationY, 0f) + val sub2Alpha = ObjectAnimator.ofFloat(subButton2, "alpha", 1f, 0f) + val sub2Scale = ObjectAnimator.ofFloat(subButton2, "scaleX", 1f, 0f) + val sub2ScaleY = ObjectAnimator.ofFloat(subButton2, "scaleY", 1f, 0f) + + val animSet2 = AnimatorSet() + animSet2.playTogether(sub2TransX, sub2TransY, sub2Alpha, sub2Scale, sub2ScaleY) + animSet2.duration = 200 + + animSet1.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + subButton1.visibility = View.GONE + } + }) + + animSet2.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + subButton2.visibility = View.GONE + } + }) + + // 主按钮显示动画 + mainFloatButton.visibility = View.VISIBLE + mainFloatButton.animate() + .scaleX(1f) + .scaleY(1f) + .alpha(1f) + .setDuration(200) + .setStartDelay(100) + .setListener(null) + .start() + + animSet1.start() + animSet2.start() + } + + companion object { + /** + * 启动开始计重页面 + * @param context 上下文 + * @param maWbId 运单ID,默认为0(新增模式) + */ + @JvmStatic + fun startForAdd(context: Context, maWbId: Long = 0) { + val starter = Intent(context, GjcWeighingStartActivity::class.java) + .putExtra(Constant.Key.MAWB_ID, maWbId) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingWbDetailActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingWbDetailActivity.kt new file mode 100644 index 0000000..906a9b2 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcWeighingWbDetailActivity.kt @@ -0,0 +1,40 @@ +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityGjcWeighingWbDetailBinding +import com.lukouguoji.gjc.viewModel.GjcWeighingWbDetailViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.GjcWeighingRecordBean +import com.lukouguoji.module_base.common.Constant + +/** + * 国际出港计重-运单详情页(5.5寸手机版专属页面,平板端无对应页面) + * + * 由计重记录列表卡片的「查看详情」进入,只读展示运单基本信息, + * 对应设计稿 documents/5.5寸手持端设计文件/03_出港计重/detail.html。 + */ +class GjcWeighingWbDetailActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_gjc_weighing_wb_detail + + override fun viewModelClass() = GjcWeighingWbDetailViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("运单详情") + binding.viewModel = viewModel + viewModel.initOnCreated(intent) + } + + companion object { + @JvmStatic + fun start(context: Context, bean: GjcWeighingRecordBean) { + val starter = Intent(context, GjcWeighingWbDetailActivity::class.java) + .putExtra(Constant.Key.BEAN, bean) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveActivity.kt index a1ab477..8ae81a2 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveActivity.kt @@ -1,83 +1,170 @@ -package com.lukouguoji.gjc.activity - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import com.alibaba.android.arouter.facade.annotation.Autowired -import com.alibaba.android.arouter.facade.annotation.Route -import com.alibaba.android.arouter.launcher.ARouter -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityIntExpArriveBinding -import com.lukouguoji.gjc.viewModel.IntExpArriveViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.common.Constant -import com.lukouguoji.module_base.common.ConstantEvent -import com.lukouguoji.module_base.impl.FlowBus -import com.lukouguoji.module_base.impl.observe -import com.lukouguoji.module_base.ktx.addOnItemClickListener -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港-出港运抵页面 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE) -class IntExpArriveActivity : - BaseBindingActivity() { - - @JvmField - @Autowired - var wbNoParam: String? = null - - override fun layoutId() = R.layout.activity_int_exp_arrive - override fun viewModelClass() = IntExpArriveViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - // 注入 ARouter 参数 - ARouter.getInstance().inject(this) - - setBackArrow("国际出港运抵") - binding.viewModel = viewModel - - // 如果有传入运单号,自动填充并触发搜索 - if (!wbNoParam.isNullOrEmpty()) { - viewModel.waybillNo.value = wbNoParam - viewModel.searchClick() - } - - // 绑定分页 - viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) - - // 设置item点击监听 - binding.rv.addOnItemClickListener(viewModel) - - // 监听刷新事件 - FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { viewModel.onItemCheckChanged() } - - FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { - viewModel.refresh() - } - - // 初始加载数据(如果没有传入运单号,才执行初始加载) - if (wbNoParam.isNullOrEmpty()) { - viewModel.refresh() - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - Constant.RequestCode.WAYBILL -> { - if (resultCode == Activity.RESULT_OK) { - viewModel.waybillNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT) - viewModel.searchClick() - } - } - Constant.RequestCode.CODE -> { - if (resultCode == Activity.RESULT_OK) { - viewModel.hno.value = data?.getStringExtra(Constant.Result.CODED_CONTENT) - viewModel.searchClick() - } - } - } - } -} +package com.lukouguoji.gjc.activity + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.alibaba.android.arouter.facade.annotation.Autowired +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityIntExpArriveBinding +import com.lukouguoji.gjc.viewModel.IntExpArriveViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.GjcHaWb +import com.lukouguoji.module_base.bean.GjcMaWb +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.observe +import com.lukouguoji.module_base.ktx.addOnItemClickListener +import com.lukouguoji.module_base.ktx.commonAdapter +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil +import java.util.ArrayList + +/** + * 国际出港-出港运抵页面 + * + * 布局 id 不区分设备,平板/手机由资源限定符自动选择: + * layout-sw600dp/activity_int_exp_arrive.xml(平板) + * layout/activity_int_exp_arrive.xml(手机) + */ +@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE) +class IntExpArriveActivity : + BaseBindingActivity() { + + @JvmField + @Autowired + var wbNoParam: String? = null + + override fun layoutId() = R.layout.activity_int_exp_arrive + override fun viewModelClass() = IntExpArriveViewModel::class.java + + /** 手机版筛选弹层显隐(仅 UI 状态,不涉及业务逻辑) */ + val filterPanelVisible = ObservableBoolean(false) + + /** 供 [com.lukouguoji.gjc.holder.IntExpArriveViewHolder] 绑定 item 布局的 viewModel 变量(父类 viewModel 为 protected) */ + val itemViewModel: IntExpArriveViewModel get() = viewModel + + override fun initOnCreate(savedInstanceState: Bundle?) { + // 注入 ARouter 参数 + ARouter.getInstance().inject(this) + + setBackArrow(if (DeviceUtil.isPhone()) "出港运抵" else "国际出港运抵") + binding.viewModel = viewModel + binding.activity = this + + // 手机版专属数据:加载「代理」筛选下拉 + // 平板端不调用,查询链路与请求次数与改造前完全一致 + if (DeviceUtil.isPhone()) { + viewModel.initPhoneExtras() + } + + // 如果有传入运单号,自动填充并触发搜索 + if (!wbNoParam.isNullOrEmpty()) { + viewModel.waybillNo.value = wbNoParam + viewModel.searchClick() + } + + // 绑定分页 + viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) + + // 设置item点击监听 + binding.rv.addOnItemClickListener(viewModel) + + // 监听刷新事件 + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { viewModel.onItemCheckChanged() } + + FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { + viewModel.refresh() + } + + // 初始加载数据(如果没有传入运单号,才执行初始加载) + if (wbNoParam.isNullOrEmpty()) { + viewModel.refresh() + } + } + + // ==================== 手机版专用交互(纯 UI,复用既有 ViewModel 字段与数据) ==================== + + /** + * 手机版 Tab 切换回调:PhoneStatusTab 已通过双向绑定写入 viewModel.currentTab, + * 这里触发 ViewModel 把适配器数据集替换为过滤后的子集(不发起新请求)。 + */ + fun onArriveTabChanged() { + viewModel.onTabChangedPhone() + } + + /** + * 卡片长按:展开操作浮层(View.OnLongClickListener 要求返回 Boolean)。 + * 仅已运抵 Tab 有操作(重置 / 回执);待运抵 Tab 的「补充信息」页后端确认暂不需要,已移除。 + */ + fun showCardActionPhone(bean: GjcMaWb): Boolean { + if (viewModel.currentTab.value != "shipped") return false + bean.actionMenuVisible.set(true) + return true + } + + /** 点击浮层遮罩/空白处:收起操作浮层 */ + fun hideCardActionPhone(bean: GjcMaWb) { + bean.actionMenuVisible.set(false) + } + + /** 选择圈勾选状态变化后回调(由 ViewHolder 调用) */ + fun onItemCheckChanged() { + viewModel.onItemCheckChanged() + } + + /** 手机版筛选弹层显隐切换 */ + fun toggleFilterPanel() { + filterPanelVisible.set(!filterPanelVisible.get()) + } + + /** 手机版筛选:重置 */ + fun resetFilter() { + viewModel.flightDate.value = "" + viewModel.flightNo.value = "" + viewModel.hno.value = "" + } + + /** 手机版筛选:确认 */ + fun confirmFilter() { + filterPanelVisible.set(false) + viewModel.searchClick() + } + + /** + * 进入「分单明细」页:直接传递该运单已加载的 haWbList,零新增请求。 + */ + fun openSubOrder(bean: GjcMaWb) { + val list = ArrayList(bean.haWbList.orEmpty()) + IntExpArriveSubOrderActivity.start(this, bean.wbNo.orEmpty(), list) + } + + /** + * 浮层「回执」:与平板侧滑「回执」同链路(getResponse 查询海关回执 → 共用只读回执页)。 + */ + fun openReceipt(bean: GjcMaWb) { + bean.actionMenuVisible.set(false) + viewModel.queryCustomsResponse(bean) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + Constant.RequestCode.WAYBILL -> { + if (resultCode == Activity.RESULT_OK) { + viewModel.waybillNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT) + viewModel.searchClick() + } + } + Constant.RequestCode.CODE -> { + if (resultCode == Activity.RESULT_OK) { + viewModel.hno.value = data?.getStringExtra(Constant.Result.CODED_CONTENT) + viewModel.searchClick() + } + } + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveReceiptActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveReceiptActivity.kt new file mode 100644 index 0000000..1571b25 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveReceiptActivity.kt @@ -0,0 +1,43 @@ +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityIntExpArriveReceiptBinding +import com.lukouguoji.gjc.viewModel.IntExpArriveReceiptViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.GjcHaWb +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.ktx.refresh + +/** + * 国际出港运抵-回执内容页(5.5寸手机版专属只读新页,平板端无对应页面) + * + * 数据来自主列表页已加载的 haWbList,零新增请求。 + * 对应设计稿 documents/5.5寸手持端设计文件/04_出港运抵/receipt.html。 + */ +class IntExpArriveReceiptActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_int_exp_arrive_receipt + override fun viewModelClass() = IntExpArriveReceiptViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("回执内容") + binding.viewModel = viewModel + + viewModel.initOnCreated(intent) + viewModel.receiptList.observe(this) { binding.rv.refresh(it) } + } + + companion object { + @JvmStatic + fun start(context: Context, wbNo: String, haWbList: ArrayList) { + val starter = Intent(context, IntExpArriveReceiptActivity::class.java) + .putExtra(Constant.Key.CODE, wbNo) + .putExtra(Constant.Key.DATA, haWbList) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveSubOrderActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveSubOrderActivity.kt new file mode 100644 index 0000000..f67af53 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpArriveSubOrderActivity.kt @@ -0,0 +1,98 @@ +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityIntExpArriveSubOrderBinding +import com.lukouguoji.gjc.viewModel.IntExpArriveSubOrderViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.GjcHaWb +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.ktx.commonAdapter +import com.lukouguoji.module_base.ktx.refresh + +/** + * 国际出港运抵-分单明细页(5.5寸手机版专属新页,平板端无对应页面) + * + * 数据来自主列表页已加载的 haWbList(Intent 传入),零新增请求。 + * 对应设计稿 documents/5.5寸手持端设计文件/04_出港运抵/suborder.html。 + */ +class IntExpArriveSubOrderActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_int_exp_arrive_sub_order + override fun viewModelClass() = IntExpArriveSubOrderViewModel::class.java + + /** 筛选弹层显隐(仅 UI 状态) */ + val filterPanelVisible = ObservableBoolean(false) + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("分单明细") + binding.viewModel = viewModel + binding.activity = this + + viewModel.initOnCreated(intent) + + // 监听数据变化刷新列表 + viewModel.displayList.observe(this) { binding.rv.refresh(it) } + } + + /** 子项选中状态变化后回调(由 ViewHolder 调用) */ + fun onItemCheckChanged() { + viewModel.onItemCheckChanged() + } + + /** 全选按钮点击:切换全部选中状态后强制刷新列表(checked 为原地修改,不触发 LiveData) */ + fun checkAllClick() { + viewModel.checkAllClick() + binding.rv.commonAdapter()?.notifyDataSetChanged() + } + + /** 卡片长按:展开操作浮层(View.OnLongClickListener 要求返回 Boolean) */ + fun showActionMenu(bean: GjcHaWb): Boolean { + bean.actionMenuVisible.set(true) + return true + } + + /** 点击浮层遮罩:收起操作浮层 */ + fun hideActionMenu(bean: GjcHaWb) { + bean.actionMenuVisible.set(false) + } + + /** 长按菜单「重置」(单条) */ + fun resetSingleClick(bean: GjcHaWb) { + bean.actionMenuVisible.set(false) + viewModel.resetSingleClick(bean) + } + + /** 长按菜单「回执」(单条):复用回执只读页,仅传当前分单 */ + fun openReceipt(bean: GjcHaWb) { + bean.actionMenuVisible.set(false) + IntExpArriveReceiptActivity.start(this, viewModel.wbNo, arrayListOf(bean)) + } + + fun toggleFilterPanel() { + filterPanelVisible.set(!filterPanelVisible.get()) + } + + fun resetFilter() { + viewModel.hnoFilter.value = "" + } + + fun confirmFilter() { + filterPanelVisible.set(false) + viewModel.searchClick() + } + + companion object { + @JvmStatic + fun start(context: Context, wbNo: String, haWbList: ArrayList) { + val starter = Intent(context, IntExpArriveSubOrderActivity::class.java) + .putExtra(IntExpArriveSubOrderViewModel.KEY_WB_NO, wbNo) + .putExtra(Constant.Key.DATA, haWbList) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverActivity.kt index 447e86d..f38477c 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverActivity.kt @@ -1,56 +1,96 @@ -package com.lukouguoji.gjc.activity - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import com.alibaba.android.arouter.facade.annotation.Route -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityIntExpOutHandoverBinding -import com.lukouguoji.gjc.viewModel.IntExpOutHandoverViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.common.Constant -import com.lukouguoji.module_base.common.ConstantEvent -import com.lukouguoji.module_base.impl.FlowBus -import com.lukouguoji.module_base.impl.observe -import com.lukouguoji.module_base.ktx.addOnItemClickListener -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港-出库交接页面 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_OUT_HANDOVER) -class IntExpOutHandoverActivity : - BaseBindingActivity() { - - override fun layoutId() = R.layout.activity_int_exp_out_handover - override fun viewModelClass() = IntExpOutHandoverViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港出库交接") - binding.viewModel = viewModel - - // 绑定分页 - viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) - - // 设置item点击监听 - binding.rv.addOnItemClickListener(viewModel) - - // 监听刷新事件 - FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { viewModel.onItemCheckChanged() } - - FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { - viewModel.refresh() - } - - // 初始加载数据 - viewModel.refresh() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == Constant.RequestCode.ULD && resultCode == Activity.RESULT_OK) { - viewModel.uldNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT) - viewModel.searchClick() - } - } -} +package com.lukouguoji.gjc.activity + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.alibaba.android.arouter.facade.annotation.Route +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityIntExpOutHandoverBinding +import com.lukouguoji.gjc.viewModel.IntExpOutHandoverViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.observe +import com.lukouguoji.module_base.ktx.addOnItemClickListener +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +/** + * 国际出港-出库交接页面 + * + * 布局 id 不区分设备,平板/手机由资源限定符自动选择: + * layout-sw600dp/activity_int_exp_out_handover.xml(平板) + * layout/activity_int_exp_out_handover.xml(手机) + */ +@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_OUT_HANDOVER) +class IntExpOutHandoverActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_int_exp_out_handover + override fun viewModelClass() = IntExpOutHandoverViewModel::class.java + + /** 手机版筛选弹层显隐(仅 UI 状态,不涉及业务逻辑) */ + val filterPanelVisible = ObservableBoolean(false) + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow(if (DeviceUtil.isPhone()) "出库交接" else "国际出港出库交接") + binding.viewModel = viewModel + binding.activity = this + + // 手机版专属数据:默认「待交接」Tab、Tab 角标计数、交接人下拉 + // 平板端不调用,查询链路与请求次数与改造前完全一致 + if (DeviceUtil.isPhone()) { + viewModel.initPhoneExtras() + } + + // 绑定分页 + viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) + + // 设置item点击监听 + binding.rv.addOnItemClickListener(viewModel) + + // 监听刷新事件 + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { viewModel.onItemCheckChanged() } + + FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { + viewModel.refresh() + } + + // 初始加载数据 + viewModel.refresh() + } + + // ==================== 手机版专用交互(纯 UI,复用既有 ViewModel 字段) ==================== + + /** 手机版筛选弹层显隐切换 */ + fun toggleFilterPanel() { + filterPanelVisible.set(!filterPanelVisible.get()) + } + + /** + * 手机版筛选:重置 + * + * 只清空弹层内的三个条件(航班日期 / 航班号 / 目的站),不影响 Tab 与顶部搜索框。 + */ + fun resetFilter() { + viewModel.flightDate.value = "" + viewModel.flightNo.value = "" + viewModel.fdest.value = "" + } + + /** 手机版筛选:确认 */ + fun confirmFilter() { + filterPanelVisible.set(false) + viewModel.searchClick() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == Constant.RequestCode.ULD && resultCode == Activity.RESULT_OK) { + viewModel.uldNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT) + viewModel.searchClick() + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverDetailActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverDetailActivity.kt new file mode 100644 index 0000000..5ece24d --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverDetailActivity.kt @@ -0,0 +1,40 @@ +package com.lukouguoji.gjc.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.google.gson.Gson +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityIntExpOutHandoverDetailBinding +import com.lukouguoji.gjc.viewModel.IntExpOutHandoverDetailViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.GjcUldUseBean +import com.lukouguoji.module_base.common.Constant + +/** + * 国际出港-出库交接「板箱详情」(手机版专属页面) + * + * 由手机版列表卡片的「查看详情」进入;平板端列表布局不含该入口,故不会走到本页。 + */ +class IntExpOutHandoverDetailActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_int_exp_out_handover_detail + override fun viewModelClass() = IntExpOutHandoverDetailViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("板箱详情") + binding.viewModel = viewModel + viewModel.initOnCreated(intent) + } + + companion object { + @JvmStatic + fun start(context: Context, bean: GjcUldUseBean) { + context.startActivity( + Intent(context, IntExpOutHandoverDetailActivity::class.java) + .putExtra(Constant.Key.DATA, Gson().toJson(bean)) + ) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpStorageUseActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpStorageUseActivity.kt index 076516f..78c8a0d 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpStorageUseActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpStorageUseActivity.kt @@ -19,6 +19,7 @@ import com.lukouguoji.module_base.impl.observe import com.lukouguoji.module_base.ktx.commonAdapter import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际出港-仓库 @@ -31,10 +32,13 @@ class IntExpStorageUseActivity : override fun viewModelClass() = IntExpStorageUseViewModel::class.java override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港仓库") + setBackArrow(if (DeviceUtil.isPhone()) "出港仓库" else "国际出港仓库") binding.viewModel = viewModel binding.activity = this + // 手机形态专属:筛选字典 + 未清仓/已清仓分项统计(平板不调用,行为零变化) + if (DeviceUtil.isPhone()) viewModel.initPhoneExtras() + // 绑定分页 viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) @@ -154,6 +158,71 @@ class IntExpStorageUseActivity : }.show(this) } + // ==================== 5.5寸手机版:卡片级操作 ==================== + // 语义等价于「勾选该主单 + 其全部库位」再执行平板的批量操作,弹框与请求链路完全复用平板。 + + /** 卡片「清仓」(已清仓的卡片按钮置灰,ViewHolder 已拦截) */ + fun cardClear(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + val maWbListForClear = listOf(bean.copy(storageUseList = bean.storageUseList ?: emptyList())) + IntExpMoveClearDialogModel { dialog -> + viewModel.performClear( + dialog.clearNormal.value ?: "", + dialog.clearRemark.value ?: "", + maWbListForClear + ) + }.show(this) + } + + /** 卡片「修改库位」:弹层内先单选原库位(单库位时默认选中)再选新库位 */ + fun cardModifyStorage(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + val storageList = bean.storageUseList ?: emptyList() + if (storageList.isEmpty()) { + showToast("该运单暂无库位") + return + } + IntExpModifyStorageDialogModel(sourceStorageList = storageList) { dialog -> + val sourceStorage = dialog.selectedSourceStorage ?: return@IntExpModifyStorageDialogModel + viewModel.performModifyStorage(dialog.locationName, dialog.locationId, sourceStorage) + }.show(this) + } + + /** 卡片「出库」:该运单全部库位出库(与平板勾选主单后出库同语义,件数/重量默认取库位入库件数/重量) */ + fun cardOutStorage(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + val storageList = bean.storageUseList ?: emptyList() + if (storageList.isEmpty()) { + showToast("该运单暂无库位") + return + } + IntExpOutStorageDialogModel( + storageCount = storageList.size, + defaultPc = storageList.mapNotNull { it.inPc }.takeIf { it.isNotEmpty() }?.sum(), + defaultWeight = storageList.mapNotNull { it.inWeight }.takeIf { it.isNotEmpty() }?.sum() + ) { dialog -> + viewModel.performOutStorage( + dialog.pc.value?.toLongOrNull(), + dialog.weight.value?.toDoubleOrNull(), + storageList + ) + }.show(this) + } + + /** 卡片「入库」:弹层选库位、填件数重量后整票入库 */ + fun cardInStorage(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + val maWbListForInStorage = listOf(bean.copy(storageUseList = bean.storageUseList ?: emptyList())) + IntExpInStorageDialogModel( + defaultPc = bean.pc, + defaultWeight = bean.weight + ) { dialog -> + viewModel.performInStorage( + dialog.locationName, + dialog.locationId, + dialog.pc.value?.toLongOrNull(), + dialog.weight.value?.toDoubleOrNull(), + maWbListForInStorage + ) + }.show(this) + } + /** * 显示入库操作对话框 */ diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveDeleteDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveDeleteDialogModel.kt index 1bbcf11..85962a1 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveDeleteDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveDeleteDialogModel.kt @@ -6,15 +6,19 @@ import com.lukouguoji.gjc.R import com.lukouguoji.gjc.databinding.DialogIntExpArriveDeleteBinding import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际出港运抵 - 删除申报对话框 + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体) */ class IntExpArriveDeleteDialogModel( val changeReasonList: List, // 变更原因列表 private val callback: (IntExpArriveDeleteDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 变更原因代码(存储的是 code) val changeReason = MutableLiveData("") diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt index b18ae7c..3076ebf 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpArriveResetDialogModel.kt @@ -6,14 +6,20 @@ import com.lukouguoji.gjc.R import com.lukouguoji.gjc.databinding.DialogIntExpArriveResetBinding import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际出港运抵 - 状态重置对话框 + * + * 布局按资源限定符自动切换:手机 layout/(底部弹层)、平板 layout-sw600dp/(居中弹框), + * 弹出位置随形态选择 BOTTOM/CENTER。 */ class IntExpArriveResetDialogModel( private val callback: (IntExpArriveResetDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 重置状态列表 val resetStatusList = MutableLiveData>() diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpInStorageDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpInStorageDialogModel.kt index d1c4b12..dcb3f3c 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpInStorageDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpInStorageDialogModel.kt @@ -11,18 +11,22 @@ import com.lukouguoji.module_base.ktx.launchCollect import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.ktx.toPlainText import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际出港仓库 - 入库操作对话框 * @param defaultPc 默认件数(选中运单的件数) * @param defaultWeight 默认重量(选中运单的重量) + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体) */ class IntExpInStorageDialogModel( defaultPc: Long?, defaultWeight: Double?, private val callback: (IntExpInStorageDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 库位列表 val locationList = MutableLiveData>() @@ -48,8 +52,13 @@ class IntExpInStorageDialogModel( override fun onDialogCreated(context: Context) { binding.model = this - binding.pcInput.et.inputType = InputType.TYPE_CLASS_NUMBER - binding.weightInput.et.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + // 平板变体 pc_input/weight_input,手机变体 pc_input_phone/weight_input_phone,单侧 id 在 Binding 中可空 + val pcType = InputType.TYPE_CLASS_NUMBER + val weightType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + binding.pcInput?.et?.inputType = pcType + binding.weightInput?.et?.inputType = weightType + binding.pcInputPhone?.et?.inputType = pcType + binding.weightInputPhone?.et?.inputType = weightType loadLocationList() // 监听选择变化,更新locationId和locationName diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpModifyStorageDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpModifyStorageDialogModel.kt index 2f5fca8..ec11e20 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpModifyStorageDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpModifyStorageDialogModel.kt @@ -8,15 +8,24 @@ import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.http.net.NetApply import com.lukouguoji.module_base.ktx.launchCollect import com.lukouguoji.module_base.ktx.showToast +import com.lukouguoji.module_base.bean.GjcStorageUse import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际出港 - 修改库位对话框 + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体)。 + * + * 平板:列表勾选唯一库位后打开,弹框内只选新库位(sourceStorageList 传 null,原库位项不存在于平板布局)。 + * 手机:卡片级操作,运单可能有多个库位——传入该运单库位列表,弹层内先单选「原库位」再选新库位。 */ class IntExpModifyStorageDialogModel( + private val sourceStorageList: List? = null, private val callback: (IntExpModifyStorageDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 库位列表 val locationList = MutableLiveData>() @@ -30,6 +39,13 @@ class IntExpModifyStorageDialogModel( // 库位名称 (后端需要的name) var locationName: String = "" + // ========== 手机卡片级操作专属:原库位单选 ========== + val sourceList = MutableLiveData>(emptyList()) + val selectedSourceId = MutableLiveData("") + + // 选中的原库位对象(保存后由调用方取用) + var selectedSourceStorage: GjcStorageUse? = null + override fun layoutId(): Int { return R.layout.dialog_int_exp_modify_storage } @@ -44,6 +60,19 @@ class IntExpModifyStorageDialogModel( locationId = selectedItem?.value ?: "" locationName = selectedItem?.key ?: "" } + + // 手机版:初始化原库位列表,单库位时默认选中 + if (sourceStorageList != null) { + sourceList.value = sourceStorageList.map { + KeyValue(it.location ?: "", it.id?.toString() ?: "") + } + selectedSourceId.observeForever { id -> + selectedSourceStorage = sourceStorageList.find { it.id?.toString() == id } + } + if (sourceStorageList.size == 1) { + selectedSourceId.value = sourceStorageList[0].id?.toString() ?: "" + } + } } /** @@ -65,6 +94,10 @@ class IntExpModifyStorageDialogModel( * 保存按钮点击 */ fun onSaveClick() { + if (sourceStorageList != null && selectedSourceStorage == null) { + showToast("请选择原库位") + return + } if (selectedLocationCode.value.verifyNullOrEmpty("请选择库位")) { return } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpMoveClearDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpMoveClearDialogModel.kt index 0ef23c7..149ed67 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpMoveClearDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpMoveClearDialogModel.kt @@ -6,14 +6,18 @@ import com.lukouguoji.gjc.R import com.lukouguoji.gjc.databinding.DialogIntExpMoveClearBinding import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际出港移库 - 清仓操作对话框 + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体) */ class IntExpMoveClearDialogModel( private val callback: (IntExpMoveClearDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 清仓正常(存储的是 code:"0" 或 "1") val clearNormal = MutableLiveData("") diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutStorageDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutStorageDialogModel.kt index 6f2d7ae..097d05c 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutStorageDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutStorageDialogModel.kt @@ -8,9 +8,11 @@ import com.lukouguoji.gjc.databinding.DialogIntExpOutStorageBinding import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.ktx.toPlainText +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际出港仓库 - 出库操作对话框 + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体) * @param storageCount 选中的库位数 * @param defaultPc 默认件数(选中库位的入库件数) * @param defaultWeight 默认重量(选中库位的入库重量) @@ -20,7 +22,9 @@ class IntExpOutStorageDialogModel( defaultPc: Long?, defaultWeight: Double?, private val callback: (IntExpOutStorageDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { val tip = "已选择 $storageCount 个库位执行出库操作" @@ -36,8 +40,13 @@ class IntExpOutStorageDialogModel( override fun onDialogCreated(context: Context) { binding.model = this - binding.pcInput.et.inputType = InputType.TYPE_CLASS_NUMBER - binding.weightInput.et.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + // 平板变体 pc_input/weight_input,手机变体 pc_input_phone/weight_input_phone,单侧 id 在 Binding 中可空 + val pcType = InputType.TYPE_CLASS_NUMBER + val weightType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + binding.pcInput?.et?.inputType = pcType + binding.weightInput?.et?.inputType = weightType + binding.pcInputPhone?.et?.inputType = pcType + binding.weightInputPhone?.et?.inputType = weightType } /** diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutWaitingTransDialogModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutWaitingTransDialogModel.kt index 5c6ba2f..6bf589d 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutWaitingTransDialogModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/dialog/IntExpOutWaitingTransDialogModel.kt @@ -9,14 +9,18 @@ import com.lukouguoji.module_base.http.net.NetApply import com.lukouguoji.module_base.ktx.launchCollect import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际出港出库交接 - 待配运操作对话框 + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体) */ class IntExpOutWaitingTransDialogModel( private val callback: (IntExpOutWaitingTransDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 库位列表 val locationList = MutableLiveData>() diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcCheckInRecordViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcCheckInRecordViewHolder.kt index 1b2a843..78f0876 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcCheckInRecordViewHolder.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcCheckInRecordViewHolder.kt @@ -23,5 +23,10 @@ class GjcCheckInRecordViewHolder(view: View) : binding.isEditMode = isEditMode binding.position = position // 传入位置用于显示序号 binding.executePendingBindings() + + // 「修改」按钮(手机独有):打开全屏修改覆层编辑本条记录 + binding.btnEdit?.setOnClickListener { + activity?.openPhoneEdit(record) + } } } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingRecordViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingRecordViewHolder.kt index 93b41e5..5966e9c 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingRecordViewHolder.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingRecordViewHolder.kt @@ -2,12 +2,18 @@ package com.lukouguoji.gjc.holder import android.view.View import com.lukouguoji.gjc.activity.GjcWeighingRecordDetailsActivity +import com.lukouguoji.gjc.activity.GjcWeighingWbDetailActivity import com.lukouguoji.gjc.databinding.ItemGjcWeighingRecordBinding +import com.lukouguoji.gjc.viewModel.GjcWeighingRecordViewModel import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.bean.GjcWeighingRecordBean /** * 国际出港计重记录列表 ViewHolder + * + * item 布局分平板(layout-sw600dp/item_gjc_weighing_record.xml,含选择图标 iv_icon) + * 与手机(layout/item_gjc_weighing_record.xml,卡片式,地磅图标 iv_scale 进计重明细、 + * 「查看详情」tv_detail 进运单详情)两个变体,单侧独有的 id 需判空调用。 */ class GjcWeighingRecordViewHolder(view: View) : BaseViewHolder(view) { @@ -17,8 +23,8 @@ class GjcWeighingRecordViewHolder(view: View) : binding.bean = bean binding.executePendingBindings() - // 图标点击 - 切换选择状态 - binding.ivIcon.setOnClickListener { + // 图标点击 - 切换选择状态(平板独有) + binding.ivIcon?.setOnClickListener { // 反转checked状态 bean.checked.set(!bean.checked.get()) @@ -26,6 +32,21 @@ class GjcWeighingRecordViewHolder(view: View) : binding.executePendingBindings() } + // 地磅图标点击进计重明细(手机独有) + binding.ivScale?.setOnClickListener { + GjcWeighingRecordDetailsActivity.start(it.context, bean) + } + + // 「运抵申报」携带运单号进入出港运抵(手机独有入口,对应平板勾选单条后的「运抵申报」) + binding.tvDeclare?.setOnClickListener { + GjcWeighingRecordViewModel.openArriveDeclare(bean.wbNo) + } + + // 「查看详情」进运单详情(手机独有页面) + binding.tvDetail?.setOnClickListener { + GjcWeighingWbDetailActivity.start(it.context, bean) + } + // 整行点击跳转到计重明细页 binding.ll.setOnClickListener { GjcWeighingRecordDetailsActivity.start(it.context, bean) diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingViewHolder.kt index 54e8513..9de45ad 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingViewHolder.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/GjcWeighingViewHolder.kt @@ -1,15 +1,20 @@ package com.lukouguoji.gjc.holder import android.view.View +import com.lukouguoji.gjc.activity.GjcWeighingListActivity import com.lukouguoji.gjc.activity.GjcWeighingStartActivity import com.lukouguoji.gjc.databinding.ItemGjcWeighingBinding import com.lukouguoji.module_base.base.BaseViewHolder -import com.lukouguoji.module_base.common.ConstantEvent -import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus import com.lukouguoji.module_base.bean.GjcWeighingBean /** * 国际出港计重列表 ViewHolder + * + * item 布局分平板(layout-sw600dp/item_gjc_weighing.xml,含选择图标 iv_icon) + * 与手机(layout/item_gjc_weighing.xml,卡片式,含单票提前运抵按钮 btn_pre_arrive)两个变体, + * 单侧独有的 id 在 Binding 基类中为可空字段,需判空调用。 */ class GjcWeighingViewHolder(view: View) : BaseViewHolder(view) { @@ -20,8 +25,8 @@ class GjcWeighingViewHolder(view: View) : binding.position = position binding.executePendingBindings() - // 选择图标点击事件 - 切换选择状态 - binding.ivIcon.setOnClickListener { + // 选择图标点击事件 - 切换选择状态(平板独有) + binding.ivIcon?.setOnClickListener { // 反转checked状态 bean.checked.set(!bean.checked.get()) FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") @@ -30,6 +35,11 @@ class GjcWeighingViewHolder(view: View) : binding.executePendingBindings() } + // 卡片内「提前运抵」按钮(手机独有,单票操作) + binding.btnPreArrive?.setOnClickListener { + (it.context as? GjcWeighingListActivity)?.preArriveClick(bean) + } + // 整行点击跳转到开始计重页面 binding.ll.setOnClickListener { GjcWeighingStartActivity.startForAdd(it.context, bean.maWbId) diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveReceiptViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveReceiptViewHolder.kt new file mode 100644 index 0000000..d757114 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveReceiptViewHolder.kt @@ -0,0 +1,19 @@ +package com.lukouguoji.gjc.holder + +import android.view.View +import com.lukouguoji.gjc.databinding.ItemIntExpArriveReceiptBinding +import com.lukouguoji.module_base.base.BaseViewHolder +import com.lukouguoji.module_base.bean.GjcHaWb + +/** + * 国际出港运抵-回执内容列表 ViewHolder(5.5寸手机版专属只读页) + */ +class IntExpArriveReceiptViewHolder(view: View) : + BaseViewHolder(view) { + + override fun onBind(item: Any?, position: Int) { + val bean = getItemBean(item) ?: return + binding.bean = bean + binding.executePendingBindings() + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveSubOrderViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveSubOrderViewHolder.kt new file mode 100644 index 0000000..ca6450e --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveSubOrderViewHolder.kt @@ -0,0 +1,32 @@ +package com.lukouguoji.gjc.holder + +import android.view.View +import com.lukouguoji.gjc.activity.IntExpArriveSubOrderActivity +import com.lukouguoji.gjc.databinding.ItemIntExpArriveSubOrderBinding +import com.lukouguoji.module_base.base.BaseViewHolder +import com.lukouguoji.module_base.bean.GjcHaWb + +/** + * 国际出港运抵-分单明细列表 ViewHolder(5.5寸手机版专属页面) + * + * 选中状态变化只影响本页自身的 ViewModel(不复用全局 FlowBus.EVENT_CHECK_CHANGED, + * 避免与主列表页共享同一事件、产生跨页面的无意义联动)。 + */ +class IntExpArriveSubOrderViewHolder(view: View) : + BaseViewHolder(view) { + + override fun onBind(item: Any?, position: Int) { + val bean = getItemBean(item) ?: return + val activity = itemView.context as? IntExpArriveSubOrderActivity + binding.bean = bean + binding.activity = activity + binding.executePendingBindings() + + // 整卡点击切换选择状态 + binding.ll.setOnClickListener { + bean.checked.set(!bean.checked.get()) + activity?.onItemCheckChanged() + binding.executePendingBindings() + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveViewHolder.kt index a40f876..7165afe 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveViewHolder.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpArriveViewHolder.kt @@ -2,6 +2,7 @@ package com.lukouguoji.gjc.holder import android.view.View import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.activity.IntExpArriveActivity import com.lukouguoji.gjc.databinding.ItemIntExpArriveBinding import com.lukouguoji.module_base.adapter.setCommonAdapter import com.lukouguoji.module_base.base.BaseViewHolder @@ -13,58 +14,79 @@ import com.lukouguoji.module_base.ktx.ITEM_CLICK_TYPE_CUSTOMS_RESPONSE /** * 国际出港-出港运抵 列表项ViewHolder + * + * item 布局分平板(layout-sw600dp/item_int_exp_arrive.xml,内嵌分单子列表 rv_sub 展开/收起) + * 与手机(layout/item_int_exp_arrive.xml,分单改为独立「分单明细」页,无 rv_sub/iv_show 等 id) + * 两个变体,单侧独有的 id 在 Binding 基类中为 `@Nullable`,需判空调用。 */ class IntExpArriveViewHolder(view: View) : BaseViewHolder(view) { override fun onBind(item: Any?, position: Int) { val bean = getItemBean(item) ?: return + val activity = itemView.context as? IntExpArriveActivity binding.bean = bean binding.position = position + binding.activity = activity + binding.viewModel = activity?.itemViewModel binding.executePendingBindings() - // 侧滑菜单 - 回执按钮(关闭侧滑后分发到 ViewModel) - binding.btnResponse.setOnClickListener { - binding.swipeMenu.quickClose() + // 侧滑菜单 - 回执按钮(关闭侧滑后分发到 ViewModel,平板独有) + binding.btnResponse?.setOnClickListener { + binding.swipeMenu?.quickClose() clickListener?.onItemClick(position, ITEM_CLICK_TYPE_CUSTOMS_RESPONSE) } - // 添加图标点击事件 - 切换选择状态 - binding.ivIcon.setOnClickListener { + // 添加图标点击事件 - 切换选择状态(平板独有) + binding.ivIcon?.setOnClickListener { bean.checked.set(!bean.checked.get()) FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") binding.executePendingBindings() } - // 展开按钮点击事件 - binding.ivShow.setOnClickListener { + // 展开按钮点击事件(平板独有) + binding.ivShow?.setOnClickListener { bean.showMore.set(!bean.showMore.get()) } + // 选择圈点击事件(手机独有,仅待运抵 Tab 可见) + binding.ivSelect?.setOnClickListener { + bean.checked.set(!bean.checked.get()) + activity?.onItemCheckChanged() + binding.executePendingBindings() + } + + // 子列表 RecyclerView 仅平板存在,手机版分单改走独立页面 + val rvSub = binding.rvSub ?: return + // 初始化子列表 RecyclerView setCommonAdapter( - binding.rvSub, + rvSub, IntExpArriveSubViewHolder::class.java, R.layout.item_int_exp_arrive_sub ) // 刷新子列表数据 val subList = bean.haWbList ?: emptyList() - binding.rvSub.refresh(subList) + rvSub.refresh(subList) updateSubListVisibility(subList.isNotEmpty()) } private fun updateSubListVisibility(hasData: Boolean) { + val llHeader = binding.llHeader ?: return + val dividerHeader = binding.dividerHeader ?: return + val rvSub = binding.rvSub ?: return + val tvEmpty = binding.tvEmpty ?: return if (hasData) { - binding.llHeader.visibility = View.VISIBLE - binding.dividerHeader.visibility = View.VISIBLE - binding.rvSub.visibility = View.VISIBLE - binding.tvEmpty.visibility = View.GONE + llHeader.visibility = View.VISIBLE + dividerHeader.visibility = View.VISIBLE + rvSub.visibility = View.VISIBLE + tvEmpty.visibility = View.GONE } else { - binding.llHeader.visibility = View.GONE - binding.dividerHeader.visibility = View.GONE - binding.rvSub.visibility = View.GONE - binding.tvEmpty.visibility = View.VISIBLE + llHeader.visibility = View.GONE + dividerHeader.visibility = View.GONE + rvSub.visibility = View.GONE + tvEmpty.visibility = View.VISIBLE } } } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpMoveViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpMoveViewHolder.kt index b9f6417..b086c59 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpMoveViewHolder.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpMoveViewHolder.kt @@ -21,6 +21,14 @@ class IntExpMoveViewHolder(view: View) : binding.bean = bean binding.position = position + // 手机版「查看详情」/「编辑」→ 与平板点击卡片 / 侧滑编辑同一页面(平板变体无此控件,为 null 时跳过) + binding.llDetail?.setOnClickListener { + IntExpMoveEditActivity.startForDetails(itemView.context, bean) + } + binding.tvEdit?.setOnClickListener { + IntExpMoveEditActivity.startForEdit(itemView.context, bean) + } + // 添加图标点击事件 - 切换选择状态 binding.ivIcon.setOnClickListener { bean.checked.set(!bean.checked.get()) // 反转选择状态 @@ -33,13 +41,13 @@ class IntExpMoveViewHolder(view: View) : } // 点击列表项进入移库详情 - binding.ll.setOnClickListener { + binding.ll?.setOnClickListener { IntExpMoveEditActivity.startForDetails(itemView.context, bean) } - // 侧滑菜单 - 编辑按钮 - binding.btnEdit.setOnClickListener { - binding.swipeMenu.quickClose() + // 侧滑菜单 - 编辑按钮(平板独有) + binding.btnEdit?.setOnClickListener { + binding.swipeMenu?.quickClose() IntExpMoveEditActivity.startForEdit(itemView.context, bean) } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpOutHandoverViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpOutHandoverViewHolder.kt index 66b86bd..ce5ac61 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpOutHandoverViewHolder.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpOutHandoverViewHolder.kt @@ -1,32 +1,41 @@ -package com.lukouguoji.gjc.holder - -import android.view.View -import com.lukouguoji.gjc.databinding.ItemIntExpOutHandoverBinding -import com.lukouguoji.module_base.base.BaseViewHolder +package com.lukouguoji.gjc.holder + +import android.view.View +import com.lukouguoji.gjc.activity.IntExpOutHandoverDetailActivity +import com.lukouguoji.gjc.databinding.ItemIntExpOutHandoverBinding +import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.common.ConstantEvent import com.lukouguoji.module_base.impl.FlowBus -import com.lukouguoji.module_base.bean.GjcUldUseBean - -/** - * 国际出港-出库交接 列表项ViewHolder - */ -class IntExpOutHandoverViewHolder(view: View) : - BaseViewHolder(view) { - - override fun onBind(item: Any?, position: Int) { - val bean = getItemBean(item) ?: return - binding.bean = bean - binding.position = position - binding.executePendingBindings() - - // 添加图标点击事件 - 切换选择状态 - binding.ivIcon.setOnClickListener { - // 反转checked状态 - bean.checked.set(!bean.checked.get()) - FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") - - // 立即更新UI(图片自动切换) - binding.executePendingBindings() - } - } -} +import com.lukouguoji.module_base.bean.GjcUldUseBean + +/** + * 国际出港-出库交接 列表项ViewHolder + * + * 平板 / 手机共用:布局按资源限定符切换,本类不做设备判断。 + * `llDetail`(查看详情)仅存在于手机变体,平板变体中为 null,因此需判空。 + */ +class IntExpOutHandoverViewHolder(view: View) : + BaseViewHolder(view) { + + override fun onBind(item: Any?, position: Int) { + val bean = getItemBean(item) ?: return + binding.bean = bean + binding.position = position + binding.executePendingBindings() + + // 添加图标点击事件 - 切换选择状态 + binding.ivIcon.setOnClickListener { + // 反转checked状态 + bean.checked.set(!bean.checked.get()) + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") + + // 立即更新UI(图片自动切换) + binding.executePendingBindings() + } + + // 手机版「查看详情」→ 板箱详情页(平板变体无此控件,为 null 时跳过) + binding.llDetail?.setOnClickListener { + IntExpOutHandoverDetailActivity.start(itemView.context, bean) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpStorageUseViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpStorageUseViewHolder.kt index 7013a5a..8109f37 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpStorageUseViewHolder.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpStorageUseViewHolder.kt @@ -2,6 +2,8 @@ package com.lukouguoji.gjc.holder import android.view.View import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.activity.GjcQueryDetailsActivity +import com.lukouguoji.gjc.activity.IntExpStorageUseActivity import com.lukouguoji.gjc.databinding.ItemIntExpStorageUseBinding import com.lukouguoji.module_base.adapter.setCommonAdapter import com.lukouguoji.module_base.base.BaseViewHolder @@ -12,6 +14,10 @@ import com.lukouguoji.module_base.ktx.refresh /** * 国际出港-仓库 ViewHolder + * + * 双形态布局:平板变体含 iv_icon/iv_show/rv_sub(多选 + 库位子列表),手机变体含 + * ll/btn_clear/btn_modify/btn_out/btn_in(整卡进详情 + 卡片级四操作按钮)。 + * 单侧独有的 id 在 Binding 基类中为 @Nullable,必须判空访问。 */ class IntExpStorageUseViewHolder(view: View) : BaseViewHolder(view) { @@ -22,8 +28,10 @@ class IntExpStorageUseViewHolder(view: View) : binding.position = position binding.executePendingBindings() + // ==================== 平板:多选 + 子列表 ==================== + // 图标点击切换选择状态(联动子列表) - binding.ivIcon.setOnClickListener { + binding.ivIcon?.setOnClickListener { // 切换主列表项的选择状态 val newCheckedState = !bean.checked.get() bean.checked.set(newCheckedState) @@ -37,25 +45,45 @@ class IntExpStorageUseViewHolder(view: View) : // 刷新UI binding.executePendingBindings() - binding.rvSub.adapter?.notifyDataSetChanged() + binding.rvSub?.adapter?.notifyDataSetChanged() } // 展开按钮点击事件 - binding.ivShow.setOnClickListener { + binding.ivShow?.setOnClickListener { bean.showMore.set(!bean.showMore.get()) } // 初始化库位明细子列表 RecyclerView - setCommonAdapter( - binding.rvSub, - IntExpStorageUseSubViewHolder::class.java, - R.layout.item_int_exp_storage_use_sub - ) + binding.rvSub?.let { rvSub -> + setCommonAdapter( + rvSub, + IntExpStorageUseSubViewHolder::class.java, + R.layout.item_int_exp_storage_use_sub + ) - // 刷新库位明细数据(传递父Bean引用) - val storageUseList = bean.storageUseList ?: emptyList() - // 为每个子列表项设置父Bean引用(通过tag传递) - binding.rvSub.tag = bean - binding.rvSub.refresh(storageUseList) + // 刷新库位明细数据(传递父Bean引用) + val storageUseList = bean.storageUseList ?: emptyList() + // 为每个子列表项设置父Bean引用(通过tag传递) + rvSub.tag = bean + rvSub.refresh(storageUseList) + } + + // ==================== 手机:整卡进详情 + 卡片级操作 ==================== + + val activity = itemView.context as? IntExpStorageUseActivity + + // 整卡点击进详情(复用出港查询详情页,3 Tab 运单/仓库/库位信息) + binding.ll?.setOnClickListener { + GjcQueryDetailsActivity.start(itemView.context, bean.maWbId) + } + + // 清仓(已清仓时置灰不可点,样式由 XML 按 bean.isCleared 切换) + binding.btnClear?.setOnClickListener { + if (!bean.isCleared) activity?.cardClear(bean) + } + + binding.btnModify?.setOnClickListener { activity?.cardModifyStorage(bean) } + binding.btnOut?.setOnClickListener { activity?.cardOutStorage(bean) } + binding.btnIn?.setOnClickListener { activity?.cardInStorage(bean) } } } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveActivity.kt index d9edbf7..56baa54 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveActivity.kt @@ -1,129 +1,171 @@ -package com.lukouguoji.gjc.page.move - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.appcompat.app.AlertDialog -import com.alibaba.android.arouter.facade.annotation.Route -import com.lukouguoji.gjc.R -import com.lukouguoji.gjc.databinding.ActivityIntExpMoveBinding -import com.lukouguoji.gjc.viewModel.IntExpMoveViewModel -import com.lukouguoji.module_base.base.BaseBindingActivity -import com.lukouguoji.module_base.common.Constant -import com.lukouguoji.module_base.common.ConstantEvent -import com.lukouguoji.module_base.impl.FlowBus -import com.lukouguoji.module_base.impl.observe -import com.lukouguoji.module_base.ktx.addOnItemClickListener -import com.lukouguoji.module_base.ktx.showToast -import com.lukouguoji.module_base.model.ScanModel -import com.lukouguoji.module_base.router.ARouterConstants - -/** - * 国际出港-出港移库 - */ -@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE) -class IntExpMoveActivity : BaseBindingActivity() { - - override fun layoutId() = R.layout.activity_int_exp_move - override fun viewModelClass() = IntExpMoveViewModel::class.java - - override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际出港移库") - binding.viewModel = viewModel - binding.activity = this - - initRecyclerView() - initListeners() - observeData() - } - - /** - * 初始化RecyclerView - */ - private fun initRecyclerView() { - viewModel.pageModel.bindSmartRefreshLayout( - binding.srl, - binding.rv, - viewModel, - this - ) - binding.rv.addOnItemClickListener(viewModel) - } - - /** - * 初始化监听器 - */ - private fun initListeners() { - // 移库按钮 - binding.btnMove.setOnClickListener { - showMoveConfirmDialog() - } - } - - /** - * 观察数据变化 - */ - private fun observeData() { - // 监听item选择变化事件,更新全选状态 - FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { - viewModel.onItemCheckChanged() - } - - // 监听刷新事件 - FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { - viewModel.refresh() - } - - // 初始加载数据 - viewModel.refresh() - } - - /** - * 扫码运单号 - */ - fun scanWaybill() { - ScanModel.startScan(this, Constant.RequestCode.WAYBILL) - } - - /** - * 显示移库确认对话框 - */ - private fun showMoveConfirmDialog() { - val selectedItems = viewModel.getSelectedItems() - - if (selectedItems.isEmpty()) { - showToast("请至少选择一条运单") - return - } - - AlertDialog.Builder(this) - .setTitle("移库确认") - .setMessage("确定要移库选中的 ${selectedItems.size} 条记录吗?") - .setPositiveButton("确定") { _, _ -> - viewModel.submitMove() - } - .setNegativeButton("取消", null) - .show() - } - - /** - * 扫码回调 - */ - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == Constant.RequestCode.WAYBILL && resultCode == Activity.RESULT_OK) { - val code = data?.getStringExtra(Constant.Result.CODED_CONTENT) - viewModel.waybillNo.value = code - viewModel.searchClick() - } - } - - companion object { - @JvmStatic - fun start(context: Context) { - val starter = Intent(context, IntExpMoveActivity::class.java) - context.startActivity(starter) - } - } -} +package com.lukouguoji.gjc.page.move + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.databinding.ObservableBoolean +import com.alibaba.android.arouter.facade.annotation.Route +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.databinding.ActivityIntExpMoveBinding +import com.lukouguoji.gjc.viewModel.IntExpMoveViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.impl.observe +import com.lukouguoji.module_base.ktx.addOnItemClickListener +import com.lukouguoji.module_base.ktx.showToast +import com.lukouguoji.module_base.model.ConfirmDialogModel +import com.lukouguoji.module_base.model.ScanModel +import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil + +/** + * 国际出港-出港移库 + */ +@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE) +class IntExpMoveActivity : BaseBindingActivity() { + + // 布局 id 不区分设备,平板/手机由资源限定符自动选择: + // layout-sw600dp/activity_int_exp_move.xml(平板) / layout/activity_int_exp_move.xml(手机) + override fun layoutId() = R.layout.activity_int_exp_move + + override fun viewModelClass() = IntExpMoveViewModel::class.java + + /** 手机版筛选弹层显隐(仅 UI 状态,不涉及业务逻辑) */ + val filterPanelVisible = ObservableBoolean(false) + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow(if (DeviceUtil.isPhone()) "出港移库" else "国际出港移库") + binding.viewModel = viewModel + binding.activity = this + + // 手机版专属数据:默认「待移库」Tab、Tab 角标计数、特码下拉 + // 平板端不调用,查询链路与请求次数与改造前完全一致 + if (DeviceUtil.isPhone()) { + viewModel.initPhoneExtras() + } + + initRecyclerView() + initListeners() + observeData() + } + + /** + * 初始化RecyclerView + */ + private fun initRecyclerView() { + viewModel.pageModel.bindSmartRefreshLayout( + binding.srl, + binding.rv, + viewModel, + this + ) + binding.rv.addOnItemClickListener(viewModel) + } + + /** + * 初始化监听器 + */ + private fun initListeners() { + // 移库按钮:btnMove 只存在于平板变体(手机版底部条走 setOnActionClick),故判空 + binding.btnMove?.setOnClickListener { + showMoveConfirmDialog() + } + } + + /** + * 观察数据变化 + */ + private fun observeData() { + // 监听item选择变化事件,更新全选状态 + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) { + viewModel.onItemCheckChanged() + } + + // 监听刷新事件 + FlowBus.with(ConstantEvent.EVENT_REFRESH).observe(this) { + viewModel.refresh() + } + + // 初始加载数据 + viewModel.refresh() + } + + /** + * 扫码运单号 + */ + fun scanWaybill() { + ScanModel.startScan(this, Constant.RequestCode.WAYBILL) + } + + // ==================== 手机版专用交互(复用既有 ViewModel 字段,无新增业务逻辑) ==================== + + /** + * 手机版筛选弹层显隐切换 + */ + fun toggleFilterPanel() { + filterPanelVisible.set(!filterPanelVisible.get()) + } + + /** + * 手机版筛选:重置 + * + * 只清空弹层内的三个条件(承运人 / 运单类型 / 特码),不影响 Tab 与顶部搜索框。 + */ + fun resetFilter() { + viewModel.by1.value = "" + viewModel.dest1.value = "" + viewModel.awbType.value = "" + } + + /** + * 手机版筛选:确定 + */ + fun confirmFilter() { + filterPanelVisible.set(false) + viewModel.searchClick() + } + + /** + * 显示移库确认对话框 + * + * 手机版底部操作条通过 XML 的 setOnActionClick 调用,故为 public。 + */ + fun showMoveConfirmDialog() { + val selectedItems = viewModel.getSelectedItems() + + if (selectedItems.isEmpty()) { + showToast("请至少选择一条运单") + return + } + + ConfirmDialogModel( + message = "确定要移库选中的 ${selectedItems.size} 条运单吗?", + title = "移库确认" + ) { + viewModel.submitMove() + }.show() + } + + /** + * 扫码回调 + */ + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == Constant.RequestCode.WAYBILL && resultCode == Activity.RESULT_OK) { + val code = data?.getStringExtra(Constant.Result.CODED_CONTENT) + viewModel.waybillNo.value = code + viewModel.searchClick() + } + } + + companion object { + @JvmStatic + fun start(context: Context) { + val starter = Intent(context, IntExpMoveActivity::class.java) + context.startActivity(starter) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveEditActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveEditActivity.kt index 776efc6..81efe26 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveEditActivity.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveEditActivity.kt @@ -15,10 +15,12 @@ import com.lukouguoji.module_base.common.DetailsPageType import com.lukouguoji.module_base.ktx.addOnItemClickListener import com.lukouguoji.module_base.ktx.commonAdapter import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际出港移库编辑/详情页 * 编辑模式仅支持拍照上传与备注修改 + * 布局双变体:layout-sw600dp/(平板)与 layout/(手机,设计稿「运单详情」样式),共用本 Activity 与 ViewModel */ @Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE_EDIT) class IntExpMoveEditActivity : @@ -31,9 +33,10 @@ class IntExpMoveEditActivity : override fun initOnCreate(savedInstanceState: Bundle?) { viewModel.initOnCreated(intent) + val isPhone = DeviceUtil.isPhone() when (viewModel.pageType.value) { - DetailsPageType.Modify -> setBackArrow("国际出港移库编辑") - else -> setBackArrow("国际出港移库详情") + DetailsPageType.Modify -> setBackArrow(if (isPhone) "移库编辑" else "国际出港移库编辑") + else -> setBackArrow(if (isPhone) "运单详情" else "国际出港移库详情") } binding.viewModel = viewModel diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryDetailsViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryDetailsViewModel.kt index c9bec27..e77286e 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryDetailsViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryDetailsViewModel.kt @@ -34,6 +34,11 @@ class GjcQueryDetailsViewModel : BaseViewModel() { // 库位列表 (storageUseList) val storageUseList = MutableLiveData>>(emptyList()) + // ==================== 5.5寸手机版专属(展示用,平板布局不绑定) ==================== + // 入库件数/入库重量 = warehouseList 各批次求和(设计稿「件重信息」区块) + val inPcTotal = MutableLiveData("--") + val inWeightTotal = MutableLiveData("--") + // ==================== Fragment列表 ==================== val fragmentList by lazy { listOf( @@ -59,6 +64,40 @@ class GjcQueryDetailsViewModel : BaseViewModel() { currentTab.value = index } + // ==================== 5.5寸手机版取值帮助方法(布局绑定用) ==================== + // 表达式里必须把 map(maWbData)作为参数传入,DataBinding 才能追踪依赖并在数据到达后刷新 + + /** + * 从详情 Map 取字符串展示值;数字自动去掉无意义的 .0;空值兜底 "--" + */ + fun str(map: Map?, key: String): String { + val v = map?.get(key) ?: return "--" + val s = when (v) { + is Double -> if (v % 1.0 == 0.0) v.toLong().toString() else v.toString() + else -> v.toString() + } + return s.ifBlank { "--" } + } + + /** + * 依次取第一个非空 key 的展示值(如 agentName 优先于 agentCode) + */ + fun strOr(map: Map?, key: String, fallbackKey: String): String { + val first = str(map, key) + return if (first != "--") first else str(map, fallbackKey) + } + + /** + * 取重量类数值(保留小数形式),空值兜底 "--" + */ + fun dec(map: Map?, key: String): String { + val v = map?.get(key) ?: return "--" + return when (v) { + is Double -> v.toString() + else -> v.toString().ifBlank { "--" } + } + } + /** * 加载详情数据 */ @@ -111,6 +150,17 @@ class GjcQueryDetailsViewModel : BaseViewModel() { val whList = data["warehouseList"] as? List> ?: emptyList() warehouseList.value = whList + // 手机版「入库件数/入库重量」= 各批次求和(Gson 反序列化数字均为 Double) + if (whList.isNotEmpty()) { + val pcSum = whList.sumOf { (it["pc"] as? Double) ?: 0.0 } + val weightSum = whList.sumOf { (it["weight"] as? Double) ?: 0.0 } + inPcTotal.value = pcSum.toLong().toString() + inWeightTotal.value = weightSum.toString() + } else { + inPcTotal.value = "--" + inWeightTotal.value = "--" + } + // 解析 storageUseList 列表 @Suppress("UNCHECKED_CAST") val suList = data["storageUseList"] as? List> ?: emptyList() diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryViewModel.kt index c166f41..f9067a9 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcQueryViewModel.kt @@ -79,6 +79,12 @@ class GjcQueryViewModel : BasePageViewModel() { val businessType = MutableLiveData("") // 业务类型 val goodsCn = MutableLiveData("") // 品名(中) + // ==================== 5.5寸手机版三格统计 ==================== + // 总票数 / 已入库 / 已离港,均取 pageQueryTotal 出参(wbNumber / storedNumber / departedNumber), + // 与列表同条件、服务端全量口径(平板布局不绑定) + val storedCount = MutableLiveData("0") + val departedCount = MutableLiveData("0") + // 是否有筛选条件(任意一个非空则为 true,用于筛选按钮红点) val hasFilter: MediatorLiveData = MediatorLiveData().apply { val update = { _: Any? -> @@ -216,6 +222,9 @@ class GjcQueryViewModel : BasePageViewModel() { totalPc.value = (data?.totalPc ?: 0).toString() totalWeight.value = (data?.totalWeight ?: 0.0).toString() totalFlightNumber.value = (data?.flightNumber ?: 0).toString() + // 手机三格统计分项(平板布局不绑定,无影响) + storedCount.value = (data?.storedNumber ?: 0).toString() + departedCount.value = (data?.departedNumber ?: 0).toString() } } } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordDetailsViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordDetailsViewModel.kt index 7204d4e..1f04bb8 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordDetailsViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordDetailsViewModel.kt @@ -53,6 +53,7 @@ class GjcWeighingRecordDetailsViewModel : BaseViewModel() { val totalPc = MutableLiveData("0") // 总件数 val totalWeight = MutableLiveData("0.0") // 总重量 val weightError = MutableLiveData("0.0%") // 重量误差百分比 + val totalCarWeight = MutableLiveData("0") // 托盘总重(5.5寸手机版顶部卡片,平板布局不引用) // 传递参数(用于API调用) private var prefix = "" @@ -143,6 +144,10 @@ class GjcWeighingRecordDetailsViewModel : BaseViewModel() { val sumWeight = records.sumOf { it.weight } totalWeight.value = String.format("%.2f", sumWeight) + // 计算托盘总重(手机版顶部卡片) + val sumCarWeight = records.sumOf { it.trayTotalWeight } + totalCarWeight.value = String.format("%.2f", sumCarWeight) + // 计算重量误差百分比 val preWeightValue = preWeight.value?.toDoubleOrNull() ?: 0.0 val error = if (sumWeight > 0 && preWeightValue > 0) { @@ -191,6 +196,39 @@ class GjcWeighingRecordDetailsViewModel : BaseViewModel() { saveRecords(records) } + /** + * 5.5寸手机版「修改记录」覆层保存:与 [onSaveClick] 相同的批量更新接口, + * 但保存成功后留在本页(收起覆层 + 重新加载),不关闭页面。仅手机布局调用。 + */ + fun onPhoneSaveClick() { + val records = recordList.value ?: return + + if (!validateRecords(records)) return + + launchLoadingCollect({ + NetApply.api.updateGjcCheckInRecordList(records) + }) { + onSuccess = { result -> + if (result.data == true) { + showToast("保存成功") + // 收起编辑覆层 + isEditMode.value = false + // 通知列表页刷新 + viewModelScope.launch { + FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh") + } + // 重新加载本页数据 + loadData() + } else { + showToast("保存失败") + } + } + onFailed = { code, msg -> + showToast("保存失败:$msg") + } + } + } + /////////////////////////////////////////////////////////////////////////// // 业务逻辑 /////////////////////////////////////////////////////////////////////////// diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordViewModel.kt index 43897e9..ba7947f 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingRecordViewModel.kt @@ -46,6 +46,11 @@ class GjcWeighingRecordViewModel : BasePageViewModel() { val totalWeight = MutableLiveData("0") // 总重量 val cargoWeight = MutableLiveData("0") // 总货重 + // ========== 5.5寸手机版 ========== + + // 手机版筛选弹层与平板端查询参数保持一致(后端确认):航班日期 / 代理人 / 特码, + // 运单号走顶部搜索框;dest / opId / 收运时间范围后端均不支持(dest、opId 内网实测被忽略) + /////////////////////////////////////////////////////////////////////////// // 方法区 /////////////////////////////////////////////////////////////////////////// @@ -107,17 +112,11 @@ class GjcWeighingRecordViewModel : BasePageViewModel() { when (selectedItems.size) { 0 -> { // 没有选择单据:直接跳转(不带参数) - com.alibaba.android.arouter.launcher.ARouter.getInstance() - .build(com.lukouguoji.module_base.router.ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE) - .navigation() + openArriveDeclare(null) } 1 -> { // 选择一个单据:跳转到出港运抵页面,携带运单号 - val waybillNo = selectedItems[0].wbNo - com.alibaba.android.arouter.launcher.ARouter.getInstance() - .build(com.lukouguoji.module_base.router.ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE) - .withString("wbNoParam", waybillNo) - .navigation() + openArriveDeclare(selectedItems[0].wbNo) } else -> { // 选择多个单据:Toast 提示 @@ -126,29 +125,40 @@ class GjcWeighingRecordViewModel : BasePageViewModel() { } } + companion object { + /** + * 跳转出港运抵(运抵申报);wbNo 非空时携带运单号自动查询。 + * 平板「运抵申报」按钮与手机卡片「运抵申报」(ViewHolder 调用)共用。 + */ + fun openArriveDeclare(wbNo: String?) { + val postcard = com.alibaba.android.arouter.launcher.ARouter.getInstance() + .build(com.lukouguoji.module_base.router.ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE) + if (!wbNo.isNullOrEmpty()) postcard.withString("wbNoParam", wbNo) + postcard.navigation() + } + } + /** * 获取列表数据 */ override fun getData() { + // 公共查询条件(列表与统计共用) + val baseParams = mutableMapOf( + "fdate" to flightDate.value!!.ifEmpty { null }, + "fno" to flightNo.value!!.ifEmpty { null }, + "agentCode" to agentCode.value!!.ifEmpty { null }, + "spCode" to spCode.value!!.ifEmpty { null }, + "wbNo" to waybillNo.value!!.ifEmpty { null }, + ) + // 构建查询参数(列表接口) - val listParams = mapOf( + val listParams = (baseParams + mapOf( "pageNum" to pageModel.page, "pageSize" to pageModel.limit, - "fdate" to flightDate.value!!.ifEmpty { null }, - "fno" to flightNo.value!!.ifEmpty { null }, - "agentCode" to agentCode.value!!.ifEmpty { null }, - "spCode" to spCode.value!!.ifEmpty { null }, - "wbNo" to waybillNo.value!!.ifEmpty { null }, - ).toRequestBody() + )).toRequestBody() // 构建查询参数(统计接口 - 使用相同的搜索条件) - val totalParams = mapOf( - "fdate" to flightDate.value!!.ifEmpty { null }, - "fno" to flightNo.value!!.ifEmpty { null }, - "agentCode" to agentCode.value!!.ifEmpty { null }, - "spCode" to spCode.value!!.ifEmpty { null }, - "wbNo" to waybillNo.value!!.ifEmpty { null }, - ).toRequestBody() + val totalParams = baseParams.toRequestBody() // 获取列表数据(显示loading) launchLoadingCollect({ diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingViewModel.kt index 0559c43..84865a8 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingViewModel.kt @@ -55,15 +55,69 @@ class GjcWeighingViewModel : BasePageViewModel() { // 全选状态 val isAllChecked = MutableLiveData(false) - - fun onItemCheckChanged() { - updateCheckAllStatus() - } - - fun updateCheckAllStatus() { - val list = pageModel.rv?.commonAdapter()?.items as? List ?: return - isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } - } + + // ========== 5.5寸手机版专属字段(平板不调用 initPhoneExtras,行为零变化) ========== + + /** 手机版顶部搜索关键字(运单号/航班号混输,getData 内分流:含字母→fno,其余→wbNo) */ + val searchKey = MutableLiveData("") + + /** 主页三格统计:待计重 / 计重中数量(取 pageQueryTotal 出参 waitCheckInNumber / checkingNumber) */ + val pendingCount = MutableLiveData("0") + val weighingCount = MutableLiveData("0") + + // 手机版筛选弹层与平板端查询参数保持一致(后端确认):航班日期 / 代理人 / 特码, + // 运单号与航班号走顶部搜索框,均复用上方既有字段 + + /** 由手机形态的 Activity 置 true,平板保持 false(不产生任何额外请求与参数) */ + private var phoneExtrasEnabled = false + + /** + * 开启手机版专属数据:顶部「运单号/航班号」合并搜索框分流(见 getData)。 + * 仅由手机形态的 Activity 调用。 + */ + fun initPhoneExtras() { + phoneExtrasEnabled = true + } + + /** + * 单票提前运抵(手机版卡片内按钮;平板走底部批量 [arrivedAhead]) + */ + fun arrivedAheadSingle(bean: GjcWeighingBean) { + if (bean.arriveFlag == "1") { + showToast("该运单已提前运抵") + return + } + val maWbList = listOf( + GjcMaWb( + maWbId = bean.maWbId, + no = bean.no, + prefix = bean.prefix, + wbNo = bean.wbNo, + arriveFlag = "1" + ) + ) + launchLoadingCollect({ NetApply.api.preArrive(maWbList) }) { + onSuccess = { + showToast("提前运抵成功") + viewModelScope.launch { + FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh") + } + refresh() + } + onFailed = { code, msg -> + showToast(msg.noNull("提前运抵失败")) + } + } + } + + fun onItemCheckChanged() { + updateCheckAllStatus() + } + + fun updateCheckAllStatus() { + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } + } /////////////////////////////////////////////////////////////////////////// // 方法区 @@ -185,25 +239,32 @@ class GjcWeighingViewModel : BasePageViewModel() { * 获取列表数据 */ override fun getData() { + // 公共查询条件(列表与统计共用) + val baseParams = mutableMapOf( + "fdate" to flightDate.value!!.ifEmpty { null }, + "fno" to flightNo.value!!.ifEmpty { null }, + "agentCode" to agentCode.value!!.ifEmpty { null }, + "spCode" to spCode.value!!.ifEmpty { null }, + "wbNo" to waybillNo.value!!.ifEmpty { null }, + ) + + // 手机版专属条件(平板 phoneExtrasEnabled 恒为 false,请求体与改造前完全一致) + if (phoneExtrasEnabled) { + val key = searchKey.value?.trim() ?: "" + if (key.isNotEmpty()) { + // 「运单号/航班号」合并搜索框,只分流到平板同名入参:含字母 → fno,其余 → wbNo + if (key.any { it.isLetter() }) baseParams["fno"] = key else baseParams["wbNo"] = key + } + } + // 构建查询参数(列表接口) - val listParams = mapOf( + val listParams = (baseParams + mapOf( "pageNum" to pageModel.page, "pageSize" to pageModel.limit, - "fdate" to flightDate.value!!.ifEmpty { null }, - "fno" to flightNo.value!!.ifEmpty { null }, - "agentCode" to agentCode.value!!.ifEmpty { null }, - "spCode" to spCode.value!!.ifEmpty { null }, - "wbNo" to waybillNo.value!!.ifEmpty { null }, - ).toRequestBody() + )).toRequestBody() // 构建查询参数(统计接口 - 使用相同的搜索条件) - val totalParams = mapOf( - "fdate" to flightDate.value!!.ifEmpty { null }, - "fno" to flightNo.value!!.ifEmpty { null }, - "agentCode" to agentCode.value!!.ifEmpty { null }, - "spCode" to spCode.value!!.ifEmpty { null }, - "wbNo" to waybillNo.value!!.ifEmpty { null }, - ).toRequestBody() + val totalParams = baseParams.toRequestBody() // 获取列表数据(显示loading) launchLoadingCollect({ @@ -223,6 +284,9 @@ class GjcWeighingViewModel : BasePageViewModel() { totalCount.value = (data?.wbNumber ?: 0).toString() totalPc.value = (data?.totalPc ?: 0).toString() totalWeight.value = (data?.totalWeight ?: 0.0).toString() + // 手机版三格统计:后端统计出参直接给出分项计数,无需额外请求 + pendingCount.value = (data?.waitCheckInNumber ?: 0).toString() + weighingCount.value = (data?.checkingNumber ?: 0).toString() } } } @@ -234,7 +298,10 @@ class GjcWeighingViewModel : BasePageViewModel() { if (resultCode == Activity.RESULT_OK && data != null) { when (requestCode) { Constant.RequestCode.WAYBILL -> { - waybillNo.value = data.getStringExtra(Constant.Result.CODED_CONTENT) + val content = data.getStringExtra(Constant.Result.CODED_CONTENT) + // 手机版扫码写入顶部搜索框,平板写入原运单号搜索条 + if (phoneExtrasEnabled) searchKey.value = content + else waybillNo.value = content refresh() } } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingWbDetailViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingWbDetailViewModel.kt new file mode 100644 index 0000000..0de37cf --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/GjcWeighingWbDetailViewModel.kt @@ -0,0 +1,30 @@ +package com.lukouguoji.gjc.viewModel + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.GjcWeighingRecordBean +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.ktx.showToast + +/** + * 国际出港计重-运单详情 ViewModel(5.5寸手机版专属页面) + * + * 只读展示计重记录列表传入的运单信息,无接口请求。 + * 注:设计稿中的「通道号」字段在 /IntExpCheckIn/checked/pageQuery 出参中不存在(后端缺口, + * 见接口问题清单),布局侧空值兜底显示 "-"。 + */ +class GjcWeighingWbDetailViewModel : BaseViewModel() { + + // 运单信息(从计重记录列表页传入) + val bean = MutableLiveData(GjcWeighingRecordBean()) + + fun initOnCreated(intent: Intent) { + val data = intent.getSerializableExtra(Constant.Key.BEAN) as? GjcWeighingRecordBean + if (data != null) { + bean.value = data + } else { + showToast("参数错误") + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveReceiptViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveReceiptViewModel.kt new file mode 100644 index 0000000..f303f50 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveReceiptViewModel.kt @@ -0,0 +1,34 @@ +package com.lukouguoji.gjc.viewModel + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.holder.IntExpArriveReceiptViewHolder +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.GjcHaWb +import com.lukouguoji.module_base.common.Constant + +/** + * 国际出港运抵-回执内容 ViewModel(5.5寸手机版专属只读页) + * + * 数据直接复用主列表页已加载的 haWbList.response / arrivalOpDate 字段,无需任何新增请求。 + * 对应设计稿 documents/5.5寸手持端设计文件/04_出港运抵/receipt.html。 + */ +class IntExpArriveReceiptViewModel : BaseViewModel() { + + val receiptList = MutableLiveData>(emptyList()) + + // 适配器配置 + val itemViewHolder = IntExpArriveReceiptViewHolder::class.java + val itemLayoutId = R.layout.item_int_exp_arrive_receipt + + @Suppress("UNCHECKED_CAST") + fun initOnCreated(intent: Intent) { + val list = intent.getSerializableExtra(Constant.Key.DATA) as? ArrayList + // .copy() 补上被 Java 序列化跳过的 @Transient 字段(checked 等),见分单明细页同款注释 + receiptList.value = (list ?: emptyList()) + .map { it.copy() } + .filter { !it.response.isNullOrEmpty() } + .sortedByDescending { it.arrivalOpDate } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveSubOrderViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveSubOrderViewModel.kt new file mode 100644 index 0000000..1c564e6 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveSubOrderViewModel.kt @@ -0,0 +1,137 @@ +package com.lukouguoji.gjc.viewModel + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.lukouguoji.gjc.R +import com.lukouguoji.gjc.dialog.IntExpArriveResetDialogModel +import com.lukouguoji.gjc.holder.IntExpArriveSubOrderViewHolder +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.GjcHaWb +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.common.ConstantEvent +import com.lukouguoji.module_base.http.net.NetApply +import com.lukouguoji.module_base.impl.FlowBus +import com.lukouguoji.module_base.ktx.launchLoadingCollect +import com.lukouguoji.module_base.ktx.showToast +import com.lukouguoji.module_base.ktx.toRequestBody +import kotlinx.coroutines.launch + +/** + * 国际出港运抵-分单明细 ViewModel(5.5寸手机版专属页面) + * + * 数据直接复用主列表页已加载的 haWbList(Intent 传入),无需任何新增请求。 + * 筛选仅「分单号」在客户端生效——航班日期/航班号/代理等字段在单个运单的分单集合内 + * 恒为同一值,筛选它们没有实际意义(设计稿疑似沿用主列表筛选组件模板,未按此页场景定制)。 + */ +class IntExpArriveSubOrderViewModel : BaseViewModel() { + + // 运单号(页面标题辅助展示,非必需) + var wbNo: String = "" + private set + + // 完整分单列表(Intent 传入,不随筛选变化) + private var fullList: List = emptyList() + + // 当前展示列表(按分单号筛选后的结果) + val displayList = MutableLiveData>(emptyList()) + + // 筛选:分单号(客户端 contains 匹配) + val hnoFilter = MutableLiveData("") + + // 全选状态与已选计数 + val isAllChecked = MutableLiveData(false) + val selectedCount = MutableLiveData("0") + + // 适配器配置 + val itemViewHolder = IntExpArriveSubOrderViewHolder::class.java + val itemLayoutId = R.layout.item_int_exp_arrive_sub_order + + @Suppress("UNCHECKED_CAST") + fun initOnCreated(intent: Intent) { + wbNo = intent.getStringExtra(KEY_WB_NO).orEmpty() + val list = intent.getSerializableExtra(Constant.Key.DATA) as? ArrayList + // Java 序列化会跳过 checked/actionMenuVisible(标了 @Transient,即 JVM transient 修饰符), + // 反序列化后这两个 ObservableBoolean 字段为 null。.copy() 会重新走一遍类体初始化逻辑, + // 用全新的 ObservableBoolean(false) 补上,避免全选/长按时 NPE(已实机崩溃验证过)。 + fullList = (list ?: emptyList()).map { it.copy() } + displayList.value = fullList + } + + /** + * 分单号筛选(客户端 contains 匹配,非后端查询)。 + */ + fun searchClick() { + val key = hnoFilter.value?.trim().orEmpty() + displayList.value = if (key.isEmpty()) { + fullList + } else { + fullList.filter { it.hno?.contains(key, ignoreCase = true) == true } + } + updateCheckAllStatus() + } + + fun onItemCheckChanged() { + updateCheckAllStatus() + } + + fun updateCheckAllStatus() { + val list = displayList.value ?: return + isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } + selectedCount.value = list.count { it.checked.get() }.toString() + } + + fun checkAllClick() { + val list = displayList.value ?: return + val shouldCheckAll = !isAllChecked.value!! + list.forEach { it.checked.set(shouldCheckAll) } + isAllChecked.value = shouldCheckAll + selectedCount.value = if (shouldCheckAll) list.size.toString() else "0" + } + + /** + * 长按菜单「重置」(单条分单)。 + */ + fun resetSingleClick(bean: GjcHaWb) { + val dialog = IntExpArriveResetDialogModel { dialogModel -> + val params = mutableMapOf("haWbList" to listOf(bean)) + if (dialogModel.resetStatusCode != null) { + params["restStatus"] = dialogModel.resetStatusCode + } + launchLoadingCollect({ NetApply.api.resetArriveDeclare(params.toRequestBody()) }) { + onSuccess = { + showToast("状态重置成功,请返回列表页下拉刷新查看最新状态") + viewModelScope.launch { + FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh") + } + } + } + } + dialog.show() + } + + /** + * 底部「申报」:批量申报当前选中的分单。 + */ + fun declareClick() { + val selected = displayList.value?.filter { it.isSelected } ?: emptyList() + if (selected.isEmpty()) { + showToast("请选择要申报的分单") + return + } + val params = mapOf("haWbList" to selected).toRequestBody() + launchLoadingCollect({ NetApply.api.arriveDeclare(params) }) { + onSuccess = { + showToast("运抵申报成功") + viewModelScope.launch { + FlowBus.with(ConstantEvent.EVENT_REFRESH).emit("refresh") + } + getTopActivity().finish() + } + } + } + + companion object { + const val KEY_WB_NO = "wbNo" + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt index f60cd4d..f90a28b 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpArriveViewModel.kt @@ -59,6 +59,7 @@ class IntExpArriveViewModel : BasePageViewModel() { fun onItemCheckChanged() { updateCheckAllStatus() + updateCheckAllStatusPhone() } fun updateCheckAllStatus() { @@ -66,6 +67,97 @@ class IntExpArriveViewModel : BasePageViewModel() { isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } } + // ========== 5.5寸手机版专属(平板布局不引用,零影响) ========== + + /** 当前 Tab:"pending"(待运抵) / "shipped"(已运抵)。查询时映射为入参 arrivalStatus=0/1(与平板端运抵状态筛选一致) */ + val currentTab = MutableLiveData("pending") + + /** Tab 配置 */ + val arriveTabs = MutableLiveData( + listOf(KeyValue("待运抵", "pending"), KeyValue("已运抵", "shipped")) + ) + + /** 两个 Tab 的角标计数(同条件 + arrivalStatus=0/1 各调一次 pageQueryTotal 取 wbNumber,全量口径) */ + val arriveTabCounts = MutableLiveData(listOf("0", "0")) + + /** 手机版空态(当前 Tab 过滤后无数据时显示,对应设计稿 empty-pending) */ + val emptyVisiblePhone = MutableLiveData(false) + val emptyTextPhone = MutableLiveData("暂无待运抵的运单") + + /** 手机版全选状态(仅统计待运抵 Tab 内的项,与 Pad 端 [isAllChecked] 统计口径不同) */ + val isAllCheckedPhone = MutableLiveData(false) + + /** 手机版已选数量(底部操作条「已选 N 项」) */ + val selectedCountPhone = MutableLiveData("0") + + private var phoneExtrasEnabled = false + + /** + * 开启手机版专属数据:Tab(arrivalStatus)与角标统计。仅由手机形态的 Activity 调用。 + * 筛选弹层与平板端查询参数一致(航班日期/航班号),agentCode 内网实测被后端忽略,已移除「代理」。 + */ + fun initPhoneExtras() { + phoneExtrasEnabled = true + } + + /** + * 手机版 Tab 切换:按 arrivalStatus 重新查询(服务端过滤,分页与角标均为全量口径)。 + * 由 Activity 在 [com.lukouguoji.module_base.ui.weight.phone.PhoneStatusTab] 的 + * 双向绑定写入 currentTab 后调用。 + */ + fun onTabChangedPhone() { + refresh() + } + + /** 当前 Tab 对应的运抵状态入参:待运抵=0 / 已运抵=1 */ + private fun currentArrivalStatus(): String = + if (currentTab.value == "shipped") "1" else "0" + + /** 列表加载完成后刷新空态文案(服务端已按 Tab 过滤,adapter 即当前 Tab 数据) */ + private fun updateEmptyStatePhone() { + val isPendingTab = currentTab.value != "shipped" + emptyVisiblePhone.value = pageModel.rv?.commonAdapter()?.items.isNullOrEmpty() + emptyTextPhone.value = if (isPendingTab) "暂无待运抵的运单" else "暂无已运抵的运单" + } + + /** + * 加载 Tab 角标:同条件 + arrivalStatus=0/1 各调一次 pageQueryTotal,取 wbNumber。 + */ + private fun loadPhoneTabCounts(filterParams: Map) { + listOf("0", "1").forEachIndexed { index, status -> + launchCollect({ + NetApply.api.getIntExpArriveTotal((filterParams + mapOf("arrivalStatus" to status)).toRequestBody()) + }) { + onSuccess = { result -> + val current = arriveTabCounts.value?.toMutableList() ?: mutableListOf("0", "0") + current[index] = (result.data?.wbNumber ?: 0).toString() + arriveTabCounts.value = current + } + } + } + } + + /** + * 手机版全选(适配器此时已只包含当前 Tab 的子集,待运抵 Tab 下即为全部可选项)。 + */ + fun checkAllClickPhone() { + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + val shouldCheckAll = !isAllCheckedPhone.value!! + list.forEach { it.checked.set(shouldCheckAll) } + isAllCheckedPhone.value = shouldCheckAll + updateCheckAllStatusPhone() + pageModel.rv?.commonAdapter()?.notifyDataSetChanged() + } + + /** + * 更新手机版全选状态与已选计数(基于适配器当前子集,即当前 Tab 的数据)。 + */ + fun updateCheckAllStatusPhone() { + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + isAllCheckedPhone.value = list.isNotEmpty() && list.all { it.checked.get() } + selectedCountPhone.value = list.count { it.checked.get() }.toString() + } + // ========== 全局展开状态 ========== /** * 全局展开状态 @@ -156,6 +248,13 @@ class IntExpArriveViewModel : BasePageViewModel() { return } + showResetDeclareDialog(selectedMaWbList, selectedHaWbList) + } + + /** + * 状态重置弹框 + 请求(平板批量「状态重置」与手机卡片浮层「重置」共用) + */ + private fun showResetDeclareDialog(selectedMaWbList: List, selectedHaWbList: List) { // 创建并显示弹框 val dialog = IntExpArriveResetDialogModel { dialogModel -> // 弹框确认后的回调 @@ -216,6 +315,13 @@ class IntExpArriveViewModel : BasePageViewModel() { return } + deleteDeclare(selectedMaWbList, selectedHaWbList) + } + + /** + * 删除申报校验 + 弹框 + 请求(平板批量「删除申报」与手机卡片浮层「删除申报」共用) + */ + private fun deleteDeclare(selectedMaWbList: List, selectedHaWbList: List) { // 校验申报状态:只有申报状态为 01 的记录才能删除申报 if (selectedMaWbList.any { it.declareStatus != "01" } || selectedHaWbList.any { it.arrivalStatus != "01" } @@ -306,12 +412,28 @@ class IntExpArriveViewModel : BasePageViewModel() { } } + /** + * 手机版:卡片长按菜单「重置」(单条,已运抵 Tab 专用,对应设计稿长按浮层)。 + */ + fun resetSingleDeclareClick(bean: GjcMaWb) { + bean.actionMenuVisible.set(false) + showResetDeclareDialog(listOf(bean), emptyList()) + } + + /** + * 手机版:卡片长按菜单「删除申报」(单条,对应平板批量「删除申报」)。 + */ + fun deleteSingleDeclareClick(bean: GjcMaWb) { + bean.actionMenuVisible.set(false) + deleteDeclare(listOf(bean), emptyList()) + } + /** * 获取数据 (重写BasePageViewModel) */ override fun getData() { // 构建搜索条件 - val filterParams = mapOf( + val filterParams = mutableMapOf( "fdate" to flightDate.value?.ifEmpty { null }, "fno" to flightNo.value?.ifEmpty { null }, "wbNo" to waybillNo.value?.ifEmpty { null }, @@ -319,19 +441,30 @@ class IntExpArriveViewModel : BasePageViewModel() { "arrivalStatus" to arrivalStatus.value?.ifEmpty { null } ) + // 手机版 Tab 维度(运抵状态),角标需在不含该条件的基础上分别统计 + val tabParams = if (phoneExtrasEnabled) { + filterParams + mapOf("arrivalStatus" to currentArrivalStatus()) + } else filterParams + // 列表参数 (含分页) - val listParams = (filterParams + mapOf( + val listParams = (tabParams + mapOf( "pageNum" to pageModel.page, "pageSize" to pageModel.limit )).toRequestBody() // 统计参数 (无分页) - val totalParams = filterParams.toRequestBody() + val totalParams = tabParams.toRequestBody() // 获取列表 (带Loading) launchLoadingCollect({ NetApply.api.getIntExpArriveList(listParams) }) { - onSuccess = { - pageModel.handleListBean(it) { updateCheckAllStatus() } + onSuccess = { result -> + pageModel.handleListBean(result) { + updateCheckAllStatus() + if (phoneExtrasEnabled) { + updateEmptyStatePhone() + updateCheckAllStatusPhone() + } + } // 数据加载完成后,重置全局展开状态为收起 isAllExpanded.value = false } @@ -346,6 +479,11 @@ class IntExpArriveViewModel : BasePageViewModel() { totalWeight.value = (data?.totalWeight ?: 0.0).toString() } } + + // 手机版 Tab 角标(平板不触发) + if (phoneExtrasEnabled) { + loadPhoneTabCounts(filterParams) + } } /** @@ -354,7 +492,12 @@ class IntExpArriveViewModel : BasePageViewModel() { override fun onItemClick(position: Int, type: Int) { val bean = pageModel.rv?.commonAdapter()?.getItem(position) as? GjcMaWb ?: return if (type == ITEM_CLICK_TYPE_CUSTOMS_RESPONSE) { - showCustomsResponse(CustomsResponseUrl.INT_EXP_ARRIVE, bean.prefix, bean.no, "出港运抵回执") + queryCustomsResponse(bean) } } + + /** 查询海关回执并跳转只读回执页(平板侧滑「回执」与手机长按浮层「回执」共用) */ + fun queryCustomsResponse(bean: GjcMaWb) { + showCustomsResponse(CustomsResponseUrl.INT_EXP_ARRIVE, bean.prefix, bean.no, "出港运抵回执") + } } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveEditViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveEditViewModel.kt index e276ab9..c73de07 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveEditViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveEditViewModel.kt @@ -35,8 +35,10 @@ class IntExpMoveEditViewModel : BaseViewModel(), IOnItemClickListener { val pageType = MutableLiveData(DetailsPageType.Details) private var maWbId: Long = 0L - // 列表页传入的运单数据,用于回填 detail 接口返回为空的展示字段(承运人、出运路径等) - private var listBean: GjcMove? = null + // 列表页传入的运单数据,用于回填 detail 接口返回为空的展示字段(承运人、出运路径等); + // 手机布局另用它展示移库人/移库时间(initOnCreated 在绑定前完成赋值) + var listBean: GjcMove? = null + private set val dataBean = MutableLiveData(GjcMoveDetail()) diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveViewModel.kt index cf9b62e..f351c28 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveViewModel.kt @@ -14,6 +14,7 @@ import com.lukouguoji.module_base.ktx.noNull import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.ktx.toRequestBody import com.lukouguoji.module_base.util.CheckUtil +import com.lukouguoji.module_base.util.DictUtils import dev.utils.app.info.KeyValue /** @@ -24,10 +25,38 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { // ========== 搜索字段 ========== val awbType = MutableLiveData("") // 运单类型 val by1 = MutableLiveData("") // 承运人 - val dest1 = MutableLiveData("") // 卸货站 + val dest1 = MutableLiveData("") // 卸货站(仅平板筛选,手机版设计稿无此项) val moveState = MutableLiveData("") // 移库状态 val waybillNo = MutableLiveData("") // 运单号 + // ========== 手机版(5.5 寸手持端)专用 ========== + + /** 手机版状态 Tab 配置(设计稿只有两个 Tab,无「全部」) */ + val moveStateTabs = MutableLiveData( + listOf(KeyValue("待移库", "0"), KeyValue("已移库", "1")) + ) + + /** 手机版状态 Tab 角标(与 [moveStateTabs] 等长) */ + val moveStateCounts = MutableLiveData(listOf("", "")) + + /** 已选数量(手机版底部操作条「已选 N 项」) */ + val selectedCount = MutableLiveData("0") + + /** + * 是否启用手机版专属数据(Tab 角标计数)。 + * 平板端保持 false,不产生任何额外接口请求。 + */ + private var phoneExtrasEnabled = false + + /** + * 开启手机版专属数据:默认停留「待移库」Tab、启用 Tab 角标计数。 + * 仅由手机形态的 Activity 调用。 + */ + fun initPhoneExtras() { + phoneExtrasEnabled = true + moveState.value = "0" + } + // ========== 运单类型下拉数据 ========== val awbTypeList = MutableLiveData>().apply { value = listOf( @@ -55,6 +84,8 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { // ========== 适配器配置 ========== val itemViewHolder = IntExpMoveViewHolder::class.java + + // 布局 id 不区分设备,平板/手机由资源限定符自动选择(layout-sw600dp/ 与 layout/ 下的同名布局) val itemLayoutId = R.layout.item_int_exp_move /** @@ -100,6 +131,8 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { } else { isAllChecked.value = false } + + selectedCount.value = list.count { it.checked.get() }.toString() } /** @@ -172,6 +205,33 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { // 获取统计(后台调用) getTotalData(totalParams) + + // 手机版 Tab 角标计数(平板端不触发,无额外请求) + if (phoneExtrasEnabled) { + loadTabCounts(filterParams) + } + } + + /** + * 加载「待移库 / 已移库」Tab 角标数量。 + * + * 复用统计接口,分别以 moveState=0 / 1 各查一次,并沿用当前其余筛选条件, + * 保证角标与列表口径一致。仅手机版调用。 + */ + private fun loadTabCounts(filterParams: Map) { + listOf("0", "1").forEachIndexed { index, state -> + val params = (filterParams + mapOf("moveState" to state)).toRequestBody() + launchCollect({ NetApply.api.getIntExpMoveTotal(params) }) { + onSuccess = { result -> + val count = (result.data?.wbNumber ?: 0).toString() + val current = moveStateCounts.value?.toMutableList() ?: mutableListOf("", "") + if (index < current.size) { + current[index] = count + moveStateCounts.value = current + } + } + } + } } /** diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverDetailViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverDetailViewModel.kt new file mode 100644 index 0000000..02b398a --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverDetailViewModel.kt @@ -0,0 +1,25 @@ +package com.lukouguoji.gjc.viewModel + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import com.google.gson.Gson +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.GjcUldUseBean +import com.lukouguoji.module_base.common.Constant + +/** + * 国际出港-出库交接「板箱详情」ViewModel(手机版专属页面) + * + * 列表项数据已包含详情页所需全部字段,故直接由列表带入,不额外请求接口。 + */ +class IntExpOutHandoverDetailViewModel : BaseViewModel() { + + val dataBean = MutableLiveData(GjcUldUseBean()) + + fun initOnCreated(intent: Intent) { + val json = intent.getStringExtra(Constant.Key.DATA).orEmpty() + if (json.isNotEmpty()) { + dataBean.value = Gson().fromJson(json, GjcUldUseBean::class.java) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverViewModel.kt index f1ead8c..437ddf3 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverViewModel.kt @@ -18,6 +18,7 @@ import com.lukouguoji.module_base.ktx.launchLoadingCollect import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.ktx.toRequestBody import com.lukouguoji.module_base.model.ScanModel +import dev.utils.app.info.KeyValue import dev.utils.common.DateUtils import com.lukouguoji.module_base.ktx.formatDate import kotlinx.coroutines.launch @@ -33,21 +34,60 @@ class IntExpOutHandoverViewModel : BasePageViewModel() { val fdest = MutableLiveData("") // 目的站 val uldNo = MutableLiveData("") // ULD编号 + // ========== 手机版(5.5 寸手持端)专用筛选条件 ========== + // 平板布局不含对应控件,保持空值 → 不会进入请求参数,平板查询行为与改造前完全一致 + val handoverState = MutableLiveData("") // 交接状态:"" 全部 / "0" 待交接 / "1" 已交接 + + /** 手机版状态 Tab 配置 */ + val handoverStateTabs = MutableLiveData( + listOf(KeyValue("待交接", "0"), KeyValue("已交接", "1")) + ) + + /** 手机版状态 Tab 角标(与 [handoverStateTabs] 等长) */ + val handoverStateCounts = MutableLiveData(listOf("", "")) + + /** + * 是否启用手机版专属数据(Tab 角标计数)。 + * + * 由 Activity 在手机形态下置 true。平板端保持 false, + * 因此不会产生任何额外接口请求,平板行为零变化。 + */ + private var phoneExtrasEnabled = false + // ========== 统计信息 ========== val totalCount = MutableLiveData("0") // 合计票数 val totalWeight = MutableLiveData("0") // 总重量 // ========== 全选状态 ========== val isAllChecked = MutableLiveData(false) - - fun onItemCheckChanged() { - updateCheckAllStatus() - } - - fun updateCheckAllStatus() { - val list = pageModel.rv?.commonAdapter()?.items as? List ?: return - isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } - } + + /** 已选数量(手机版底部操作条「已选 N 项」) */ + val selectedCount = MutableLiveData("0") + + /** + * 开启手机版专属数据:默认停留「待交接」Tab、启用 Tab 角标计数。 + * 仅由手机形态的 Activity 调用。 + */ + fun initPhoneExtras() { + phoneExtrasEnabled = true + handoverState.value = "0" + } + + fun onItemCheckChanged() { + updateCheckAllStatus() + } + + fun updateCheckAllStatus() { + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } + updateSelectedCount() + } + + /** 刷新已选数量 */ + private fun updateSelectedCount() { + val list = pageModel.rv?.commonAdapter()?.items as? List ?: return + selectedCount.value = list.count { it.checked.get() }.toString() + } // ========== 适配器配置 ========== val itemViewHolder = IntExpOutHandoverViewHolder::class.java @@ -70,6 +110,7 @@ class IntExpOutHandoverViewModel : BasePageViewModel() { val shouldCheckAll = !isAllChecked.value!! list.forEach { it.checked.set(shouldCheckAll) } isAllChecked.value = shouldCheckAll + updateSelectedCount() pageModel.rv?.commonAdapter()?.notifyDataSetChanged() } @@ -198,7 +239,11 @@ class IntExpOutHandoverViewModel : BasePageViewModel() { "fdate" to flightDate.value?.ifEmpty { null }, "fno" to flightNo.value?.ifEmpty { null }, "fdest" to fdest.value?.ifEmpty { null }, - "uld" to uldNo.value?.ifEmpty { null } + "uld" to uldNo.value?.ifEmpty { null }, + // 手机版 Tab 维度,平板端保持空值故不参与查询 + // 参数名以 api-doc 为准:交接状态为 handoverState(integer,0 未交接 / 1 已交接)。 + // 其余筛选与平板端口径一致(fdate/fno/fdest/uld),后端不提供交接人查询入参 + "handoverState" to handoverState.value?.ifEmpty { null } ) // 列表参数 (含分页) @@ -223,5 +268,33 @@ class IntExpOutHandoverViewModel : BasePageViewModel() { totalWeight.value = (data?.totalWeight ?: 0.0).toString() } } + + // 手机版 Tab 角标计数(平板端不触发,无额外请求) + if (phoneExtrasEnabled) { + loadTabCounts(filterParams) + } + } + + /** + * 加载「待交接 / 已交接」Tab 角标数量。 + * + * 复用统计接口,分别以 handoverState=0 / 1 各查一次,且沿用当前其余筛选条件; + * 角标取 uldNumber(板箱数,与列表 pageQuery 的 total 同口径)。仅手机版调用。 + */ + private fun loadTabCounts(filterParams: Map) { + listOf("0", "1").forEachIndexed { index, state -> + val params = (filterParams + mapOf("handoverState" to state)).toRequestBody() + launchCollect({ NetApply.api.getIntExpOutHandoverTotal(params) }) { + onSuccess = { result -> + val count = (result.data?.uldNumber ?: 0).toString() + val current = handoverStateCounts.value?.toMutableList() + ?: mutableListOf("", "") + if (index < current.size) { + current[index] = count + handoverStateCounts.value = current + } + } + } + } } } diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpStorageUseViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpStorageUseViewModel.kt index f42638b..fdfa196 100644 --- a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpStorageUseViewModel.kt +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpStorageUseViewModel.kt @@ -38,6 +38,52 @@ class IntExpStorageUseViewModel : BasePageViewModel() { val totalPc = MutableLiveData("0") // 总件数 val totalWeight = MutableLiveData("0") // 总重量 + // ========== 5.5寸手机版专属 ========== + // 筛选弹层显隐 + val filterVisible = MutableLiveData(false) + + // 筛选弹层与平板端查询参数保持一致(后端确认):航班日期 / 航班号 / 库位 / 清仓结果,运单号走顶部搜索框。 + // 代理/特码/目的站/运单类型/业务类型/品名 后端均不支持(内网实测 agentCode/spCode/dest 亦被忽略),已移除 + + // 三格统计:未清仓 / 已清仓(总票数复用 totalWbNumber),直接取 pageQueryTotal 出参 + // notClearNumber / clearNumber(与总票数同一次请求、同条件) + val unclearedCount = MutableLiveData("0") + val clearedCount = MutableLiveData("0") + + private var phoneExtrasEnabled = false + + /** + * 手机形态专属初始化(Activity 在 DeviceUtil.isPhone() 时调用)。 + * 平板端不调用,请求次数与行为零变化。 + */ + fun initPhoneExtras() { + phoneExtrasEnabled = true + } + + /** 筛选按钮点击 - 打开筛选面板 */ + fun filterClick() { + filterVisible.value = true + } + + /** 关闭筛选面板 */ + fun closeFilter() { + filterVisible.value = false + } + + /** 重置筛选条件 */ + fun resetFilter() { + flightDate.value = "" + flightNo.value = "" + location.value = "" + clearResult.value = "" + } + + /** 确认筛选 - 关闭面板并刷新 */ + fun confirmFilter() { + filterVisible.value = false + refresh() + } + // ========== 全选状态 ========== val isAllChecked = MutableLiveData(false) @@ -319,7 +365,7 @@ class IntExpStorageUseViewModel : BasePageViewModel() { * 获取数据 (重写BasePageViewModel) */ override fun getData() { - // 构建搜索条件 + // 构建搜索条件(与平板端一致) val filterParams = mapOf( "fdate" to flightDate.value?.ifEmpty { null }, "fno" to flightNo.value?.ifEmpty { null }, @@ -355,6 +401,9 @@ class IntExpStorageUseViewModel : BasePageViewModel() { totalWbNumber.value = (data?.wbNumber ?: 0).toString() totalPc.value = (data?.totalPc ?: 0).toString() totalWeight.value = (data?.totalWeight ?: 0.0).toString() + // 手机三格统计分项(平板布局不绑定,无影响) + unclearedCount.value = (data?.notClearNumber ?: 0).toString() + clearedCount.value = (data?.clearNumber ?: 0).toString() } } } diff --git a/module_gjc/src/main/release/AndroidManifest.xml b/module_gjc/src/main/release/AndroidManifest.xml index 6191e52..ab1dfc6 100644 --- a/module_gjc/src/main/release/AndroidManifest.xml +++ b/module_gjc/src/main/release/AndroidManifest.xml @@ -1,82 +1,82 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_gjc_query.xml b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_query.xml new file mode 100644 index 0000000..5768961 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_query.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_gjc_query_details.xml b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_query_details.xml new file mode 100644 index 0000000..34b34b7 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_query_details.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_list.xml b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_list.xml new file mode 100644 index 0000000..353f09f --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_list.xml @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_record_details.xml b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_record_details.xml new file mode 100644 index 0000000..f3fca7f --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_record_details.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_record_list.xml b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_record_list.xml new file mode 100644 index 0000000..0f8db9d --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_record_list.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_start.xml b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_start.xml new file mode 100644 index 0000000..816f54e --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_gjc_weighing_start.xml @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_arrive.xml b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_arrive.xml new file mode 100644 index 0000000..a6869d2 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_arrive.xml @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_move.xml b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_move.xml new file mode 100644 index 0000000..2cef614 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_move.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_move_edit.xml b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_move_edit.xml new file mode 100644 index 0000000..c6ed597 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_move_edit.xml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_out_handover.xml b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_out_handover.xml new file mode 100644 index 0000000..b0f41b1 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_out_handover.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_storage_use.xml b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_storage_use.xml new file mode 100644 index 0000000..7719350 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_storage_use.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_delete.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_delete.xml new file mode 100644 index 0000000..a8f4ec4 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_delete.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_reset.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_reset.xml new file mode 100644 index 0000000..de6675c --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_arrive_reset.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_in_storage.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_in_storage.xml new file mode 100644 index 0000000..9896633 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_in_storage.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_modify_storage.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_modify_storage.xml new file mode 100644 index 0000000..31919b7 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_modify_storage.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_move_clear.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_move_clear.xml new file mode 100644 index 0000000..f53169b --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_move_clear.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_out_storage.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_out_storage.xml new file mode 100644 index 0000000..faefc73 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_out_storage.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_out_waiting_trans.xml b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_out_waiting_trans.xml new file mode 100644 index 0000000..e56f877 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/dialog_int_exp_out_waiting_trans.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_storage.xml b/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_storage.xml new file mode 100644 index 0000000..43d5ba2 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_storage.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_warehouse.xml b/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_warehouse.xml new file mode 100644 index 0000000..c9f7f93 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_warehouse.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_waybill.xml b/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_waybill.xml new file mode 100644 index 0000000..0917122 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/fragment_gjc_query_waybill.xml @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_gjc_check_in_record.xml b/module_gjc/src/main/res/layout-sw600dp/item_gjc_check_in_record.xml new file mode 100644 index 0000000..c01dccf --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_gjc_check_in_record.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_gjc_query.xml b/module_gjc/src/main/res/layout-sw600dp/item_gjc_query.xml new file mode 100644 index 0000000..ce02a29 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_gjc_query.xml @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_gjc_query_storage.xml b/module_gjc/src/main/res/layout-sw600dp/item_gjc_query_storage.xml new file mode 100644 index 0000000..9689254 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_gjc_query_storage.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_gjc_query_warehouse.xml b/module_gjc/src/main/res/layout-sw600dp/item_gjc_query_warehouse.xml new file mode 100644 index 0000000..b6d098b --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_gjc_query_warehouse.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_gjc_weighing.xml b/module_gjc/src/main/res/layout-sw600dp/item_gjc_weighing.xml new file mode 100644 index 0000000..606773f --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_gjc_weighing.xml @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_gjc_weighing_record.xml b/module_gjc/src/main/res/layout-sw600dp/item_gjc_weighing_record.xml new file mode 100644 index 0000000..4360e53 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_gjc_weighing_record.xml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_int_exp_arrive.xml b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_arrive.xml new file mode 100644 index 0000000..d340fcf --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_arrive.xml @@ -0,0 +1,549 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_int_exp_move.xml b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_move.xml new file mode 100644 index 0000000..9786ec5 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_move.xml @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_int_exp_out_handover.xml b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_out_handover.xml new file mode 100644 index 0000000..b47c521 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_out_handover.xml @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout-sw600dp/item_int_exp_storage_use.xml b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_storage_use.xml new file mode 100644 index 0000000..2c2e3b6 --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_storage_use.xml @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_storage_use_sub.xml b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_storage_use_sub.xml similarity index 100% rename from module_gjc/src/main/res/layout/item_int_exp_storage_use_sub.xml rename to module_gjc/src/main/res/layout-sw600dp/item_int_exp_storage_use_sub.xml diff --git a/module_gjc/src/main/res/layout/layout_gjc_query_filter.xml b/module_gjc/src/main/res/layout-sw600dp/layout_gjc_query_filter.xml similarity index 100% rename from module_gjc/src/main/res/layout/layout_gjc_query_filter.xml rename to module_gjc/src/main/res/layout-sw600dp/layout_gjc_query_filter.xml diff --git a/module_gjc/src/main/res/layout/activity_gjc_query.xml b/module_gjc/src/main/res/layout/activity_gjc_query.xml index 5768961..a85a39b 100644 --- a/module_gjc/src/main/res/layout/activity_gjc_query.xml +++ b/module_gjc/src/main/res/layout/activity_gjc_query.xml @@ -1,228 +1,201 @@ + + - + + - + android:background="@color/phone_page_bg"> - - - - - + android:layout_height="match_parent" + android:orientation="vertical"> - + + + + android:layout_height="wrap_content" + android:background="@color/white" + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="16dp" + android:paddingBottom="12dp"> - - + + + + + + + + - - + - - - - - - - - - - - - - - - - + android:orientation="vertical" + android:paddingHorizontal="12dp" + android:paddingVertical="10dp"> - - + - - - - - - - - + - - - - - - - - + + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="10dp" + android:layout_weight="1" + android:background="@drawable/bg_phone_card" + android:orientation="horizontal"> + + - + android:orientation="vertical" + android:paddingHorizontal="12dp" + android:paddingVertical="10dp"> + android:text="已入库" + android:textColor="@color/phone_text_body" + android:textSize="12sp" /> + tools:text="2" /> + + + + + + + + + + + + android:text="已离港" + android:textColor="@color/phone_text_body" + android:textSize="12sp" /> + tools:text="2" /> @@ -230,28 +203,141 @@ - - + + android:layout_height="0dp" + android:layout_weight="1"> - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/module_gjc/src/main/res/layout/activity_gjc_query_details.xml b/module_gjc/src/main/res/layout/activity_gjc_query_details.xml index 34b34b7..99ccbf2 100644 --- a/module_gjc/src/main/res/layout/activity_gjc_query_details.xml +++ b/module_gjc/src/main/res/layout/activity_gjc_query_details.xml @@ -1,125 +1,137 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml b/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml index 165cefc..3931638 100644 --- a/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml +++ b/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml @@ -1,222 +1,310 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_gjc_weighing_record_details.xml b/module_gjc/src/main/res/layout/activity_gjc_weighing_record_details.xml index 0c5d9f8..68dc0dc 100644 --- a/module_gjc/src/main/res/layout/activity_gjc_weighing_record_details.xml +++ b/module_gjc/src/main/res/layout/activity_gjc_weighing_record_details.xml @@ -1,270 +1,258 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_gjc_weighing_record_list.xml b/module_gjc/src/main/res/layout/activity_gjc_weighing_record_list.xml index 381001f..83e5824 100644 --- a/module_gjc/src/main/res/layout/activity_gjc_weighing_record_list.xml +++ b/module_gjc/src/main/res/layout/activity_gjc_weighing_record_list.xml @@ -1,183 +1,283 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_gjc_weighing_start.xml b/module_gjc/src/main/res/layout/activity_gjc_weighing_start.xml index 816f54e..cec2360 100644 --- a/module_gjc/src/main/res/layout/activity_gjc_weighing_start.xml +++ b/module_gjc/src/main/res/layout/activity_gjc_weighing_start.xml @@ -1,527 +1,329 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_gjc_weighing_wb_detail.xml b/module_gjc/src/main/res/layout/activity_gjc_weighing_wb_detail.xml new file mode 100644 index 0000000..fb4c677 --- /dev/null +++ b/module_gjc/src/main/res/layout/activity_gjc_weighing_wb_detail.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml b/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml index 62875d2..cc3dacb 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_arrive.xml @@ -1,249 +1,193 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_arrive_receipt.xml b/module_gjc/src/main/res/layout/activity_int_exp_arrive_receipt.xml new file mode 100644 index 0000000..772a63e --- /dev/null +++ b/module_gjc/src/main/res/layout/activity_int_exp_arrive_receipt.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_arrive_sub_order.xml b/module_gjc/src/main/res/layout/activity_int_exp_arrive_sub_order.xml new file mode 100644 index 0000000..76c3559 --- /dev/null +++ b/module_gjc/src/main/res/layout/activity_int_exp_arrive_sub_order.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_move.xml b/module_gjc/src/main/res/layout/activity_int_exp_move.xml index 2cef614..6c288de 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_move.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_move.xml @@ -1,206 +1,167 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_move_edit.xml b/module_gjc/src/main/res/layout/activity_int_exp_move_edit.xml index c6ed597..b468460 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_move_edit.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_move_edit.xml @@ -1,13 +1,21 @@ + - - + + + android:layout_height="0dp" + android:layout_weight="1" + android:fillViewport="true" + android:overScrollMode="never"> + android:padding="16dp"> + + android:background="@drawable/bg_phone_card" + android:orientation="vertical"> - + + android:background="@drawable/bg_phone_card_header_blue" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingHorizontal="16dp" + android:paddingVertical="12dp"> - + android:text="@{viewModel.dataBean.wbNo}" + android:textColor="@color/white" + android:textSize="17sp" + android:textStyle="bold" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + android:text="@{viewModel.dataBean.moveStateText}" + android:textColor="@color/white" + android:textSize="14sp" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + title='@{"移库人"}' + value="@{viewModel.listBean.opUserDisplay}" /> + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_out_handover.xml b/module_gjc/src/main/res/layout/activity_int_exp_out_handover.xml index 7103831..ccb678e 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_out_handover.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_out_handover.xml @@ -1,198 +1,165 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_out_handover_detail.xml b/module_gjc/src/main/res/layout/activity_int_exp_out_handover_detail.xml new file mode 100644 index 0000000..4a93ad6 --- /dev/null +++ b/module_gjc/src/main/res/layout/activity_int_exp_out_handover_detail.xml @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_storage_use.xml b/module_gjc/src/main/res/layout/activity_int_exp_storage_use.xml index 7719350..f495932 100644 --- a/module_gjc/src/main/res/layout/activity_int_exp_storage_use.xml +++ b/module_gjc/src/main/res/layout/activity_int_exp_storage_use.xml @@ -1,253 +1,288 @@ + - + - + + - + android:background="@color/phone_page_bg"> - - - + android:layout_height="match_parent" + android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + hint='@{"请选择航班日期"}' + title='@{"航班日期"}' + type="@{PhoneDataLayoutType.DATE}" + value="@={viewModel.flightDate}" /> - + hint='@{"请输入航班号"}' + title='@{"航班号"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={viewModel.flightNo}" /> - - + + + + + title='@{"清仓结果"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={viewModel.clearResult}" /> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_arrive_delete.xml b/module_gjc/src/main/res/layout/dialog_int_exp_arrive_delete.xml index a8f4ec4..ae94ec6 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_arrive_delete.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_arrive_delete.xml @@ -1,8 +1,13 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml b/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml index de6675c..53d390d 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_arrive_reset.xml @@ -1,8 +1,16 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - - - - + + android:layout_marginBottom="24dp"> - - + android:gravity="center" + android:text="重置状态" + android:textColor="@color/phone_text_strong" + android:textSize="18sp" + android:textStyle="bold" /> - + - + - - - - - - + + hint='@{"请选择状态"}' + list="@{model.resetStatusList}" + title='@{"状态"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={model.selectedResetStatus}" /> - - - - - + + diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_in_storage.xml b/module_gjc/src/main/res/layout/dialog_int_exp_in_storage.xml index 9896633..6da6368 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_in_storage.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_in_storage.xml @@ -1,8 +1,13 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - + + android:layout_height="wrap_content" + android:layout_marginBottom="24dp"> - + + + + + + + + + + - - + android:layout_marginTop="20dp" + hint='@{"请输入件数"}' + title='@{"件数"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={model.pc}" /> - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginTop="20dp" + hint='@{"请输入重量"}' + title='@{"重量"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={model.weight}" /> - - - - - - - - + + diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_modify_storage.xml b/module_gjc/src/main/res/layout/dialog_int_exp_modify_storage.xml index 31919b7..c8c192b 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_modify_storage.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_modify_storage.xml @@ -1,8 +1,14 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - - - - + + android:layout_marginBottom="24dp"> - - + android:gravity="center" + android:text="修改库位" + android:textColor="@color/phone_text_strong" + android:textSize="18sp" + android:textStyle="bold" /> - + - + - - - - - - + + hint='@{"请选择原库位"}' + list="@{model.sourceList}" + title='@{"原库位"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={model.selectedSourceId}" /> - + + - - - + + diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_move_clear.xml b/module_gjc/src/main/res/layout/dialog_int_exp_move_clear.xml index f53169b..d45e1d6 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_move_clear.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_move_clear.xml @@ -1,8 +1,13 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_out_storage.xml b/module_gjc/src/main/res/layout/dialog_int_exp_out_storage.xml index faefc73..a051bf3 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_out_storage.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_out_storage.xml @@ -1,8 +1,13 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - + + android:layout_height="wrap_content" + android:layout_marginBottom="16dp"> - + + + + + + + + + + - - - - - + hint='@{"请输入件数"}' + title='@{"件数"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={model.pc}" /> - - - - - - - - - - - - - - + android:layout_marginTop="20dp" + hint='@{"请输入重量"}' + title='@{"重量"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={model.weight}" /> - - - - - - - - + + diff --git a/module_gjc/src/main/res/layout/dialog_int_exp_out_waiting_trans.xml b/module_gjc/src/main/res/layout/dialog_int_exp_out_waiting_trans.xml index e56f877..af7189b 100644 --- a/module_gjc/src/main/res/layout/dialog_int_exp_out_waiting_trans.xml +++ b/module_gjc/src/main/res/layout/dialog_int_exp_out_waiting_trans.xml @@ -1,8 +1,13 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - - - - + + android:layout_marginBottom="24dp"> - - + android:gravity="center" + android:text="待配运操作" + android:textColor="@color/phone_text_strong" + android:textSize="18sp" + android:textStyle="bold" /> - + - + - - - - - - + + hint='@{"请选择库位"}' + list="@{model.locationList}" + title='@{"库位号"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={model.selectedLocationCode}" /> - - - - - + + diff --git a/module_gjc/src/main/res/layout/fragment_gjc_query_storage.xml b/module_gjc/src/main/res/layout/fragment_gjc_query_storage.xml index 43d5ba2..4211277 100644 --- a/module_gjc/src/main/res/layout/fragment_gjc_query_storage.xml +++ b/module_gjc/src/main/res/layout/fragment_gjc_query_storage.xml @@ -1,137 +1,53 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/fragment_gjc_query_warehouse.xml b/module_gjc/src/main/res/layout/fragment_gjc_query_warehouse.xml index c9f7f93..4a16117 100644 --- a/module_gjc/src/main/res/layout/fragment_gjc_query_warehouse.xml +++ b/module_gjc/src/main/res/layout/fragment_gjc_query_warehouse.xml @@ -1,157 +1,53 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/fragment_gjc_query_waybill.xml b/module_gjc/src/main/res/layout/fragment_gjc_query_waybill.xml index 0917122..4bc2c81 100644 --- a/module_gjc/src/main/res/layout/fragment_gjc_query_waybill.xml +++ b/module_gjc/src/main/res/layout/fragment_gjc_query_waybill.xml @@ -1,319 +1,437 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_gjc_check_in_record.xml b/module_gjc/src/main/res/layout/item_gjc_check_in_record.xml index c01dccf..0e621fc 100644 --- a/module_gjc/src/main/res/layout/item_gjc_check_in_record.xml +++ b/module_gjc/src/main/res/layout/item_gjc_check_in_record.xml @@ -1,97 +1,196 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_gjc_query.xml b/module_gjc/src/main/res/layout/item_gjc_query.xml index ce02a29..f41cdc7 100644 --- a/module_gjc/src/main/res/layout/item_gjc_query.xml +++ b/module_gjc/src/main/res/layout/item_gjc_query.xml @@ -1,339 +1,226 @@ + + + + - - - - - - - - + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:padding="14dp"> + + android:gravity="center_vertical" + android:orientation="horizontal"> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:text="@{bean.wbNo}" + android:textColor="@color/phone_text_title" + android:textSize="16sp" + android:textStyle="bold" + tools:text="78133363396" /> - - + + - + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - diff --git a/module_gjc/src/main/res/layout/item_gjc_query_storage.xml b/module_gjc/src/main/res/layout/item_gjc_query_storage.xml index 9689254..dcf07d5 100644 --- a/module_gjc/src/main/res/layout/item_gjc_query_storage.xml +++ b/module_gjc/src/main/res/layout/item_gjc_query_storage.xml @@ -1,99 +1,113 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_gjc_query_warehouse.xml b/module_gjc/src/main/res/layout/item_gjc_query_warehouse.xml index b6d098b..acac4de 100644 --- a/module_gjc/src/main/res/layout/item_gjc_query_warehouse.xml +++ b/module_gjc/src/main/res/layout/item_gjc_query_warehouse.xml @@ -1,119 +1,136 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_gjc_weighing.xml b/module_gjc/src/main/res/layout/item_gjc_weighing.xml index 606773f..643b6dd 100644 --- a/module_gjc/src/main/res/layout/item_gjc_weighing.xml +++ b/module_gjc/src/main/res/layout/item_gjc_weighing.xml @@ -1,338 +1,200 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_gjc_weighing_record.xml b/module_gjc/src/main/res/layout/item_gjc_weighing_record.xml index 4360e53..6157f65 100644 --- a/module_gjc/src/main/res/layout/item_gjc_weighing_record.xml +++ b/module_gjc/src/main/res/layout/item_gjc_weighing_record.xml @@ -1,291 +1,159 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_arrive.xml b/module_gjc/src/main/res/layout/item_int_exp_arrive.xml index 794a343..2ecabd4 100644 --- a/module_gjc/src/main/res/layout/item_int_exp_arrive.xml +++ b/module_gjc/src/main/res/layout/item_int_exp_arrive.xml @@ -1,540 +1,294 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_arrive_receipt.xml b/module_gjc/src/main/res/layout/item_int_exp_arrive_receipt.xml new file mode 100644 index 0000000..b9b9915 --- /dev/null +++ b/module_gjc/src/main/res/layout/item_int_exp_arrive_receipt.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_arrive_sub_order.xml b/module_gjc/src/main/res/layout/item_int_exp_arrive_sub_order.xml new file mode 100644 index 0000000..e421cdd --- /dev/null +++ b/module_gjc/src/main/res/layout/item_int_exp_arrive_sub_order.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_move.xml b/module_gjc/src/main/res/layout/item_int_exp_move.xml index 9786ec5..f517a55 100644 --- a/module_gjc/src/main/res/layout/item_int_exp_move.xml +++ b/module_gjc/src/main/res/layout/item_int_exp_move.xml @@ -1,372 +1,225 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_out_handover.xml b/module_gjc/src/main/res/layout/item_int_exp_out_handover.xml index b47c521..c3ef32f 100644 --- a/module_gjc/src/main/res/layout/item_int_exp_out_handover.xml +++ b/module_gjc/src/main/res/layout/item_int_exp_out_handover.xml @@ -1,352 +1,238 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/item_int_exp_storage_use.xml b/module_gjc/src/main/res/layout/item_int_exp_storage_use.xml index 2c2e3b6..7e5f463 100644 --- a/module_gjc/src/main/res/layout/item_int_exp_storage_use.xml +++ b/module_gjc/src/main/res/layout/item_int_exp_storage_use.xml @@ -1,7 +1,23 @@ + + xmlns:tools="http://schemas.android.com/tools"> @@ -16,488 +32,254 @@ type="Integer" /> - - + android:layout_marginHorizontal="16dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:padding="14dp"> - + + android:gravity="center_vertical" + android:orientation="horizontal"> - - + + + android:layout_weight="1" + android:text="@{bean.wbNo}" + android:textColor="@color/phone_text_title" + android:textSize="16sp" + android:textStyle="bold" + tools:text="78133363396" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + android:layout_marginStart="8dp" + android:background="@drawable/bg_phone_tag_green" + android:gravity="center" + android:paddingHorizontal="8dp" + android:text="已清仓" + android:textColor="@color/phone_tag_green_text" + android:textSize="11sp" + android:visibility="@{bean.isCleared ? View.VISIBLE : View.GONE}" /> + + + + + - + + android:layout_marginTop="12dp" + android:orientation="horizontal"> - + android:layout_weight="1" + android:gravity="center_vertical" + android:orientation="horizontal"> + + - - - - - - - - - - - - - - - - - - + android:layout_marginStart="6dp" + android:text='@{"件数:" + (bean.pc != null ? String.valueOf(bean.pc) : "--")}' + android:textColor="@color/phone_text_body" + android:textSize="13sp" + tools:text="件数:111" /> - - - - + android:layout_weight="1" + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/debug/AndroidManifest.xml b/module_gjj/src/main/debug/AndroidManifest.xml index 35bf7ed..d1931f9 100644 --- a/module_gjj/src/main/debug/AndroidManifest.xml +++ b/module_gjj/src/main/debug/AndroidManifest.xml @@ -1,25 +1,25 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpPickUpDLVActivity.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpPickUpDLVActivity.kt index dd97ed4..4126b31 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpPickUpDLVActivity.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpPickUpDLVActivity.kt @@ -13,6 +13,7 @@ import com.lukouguoji.module_base.common.ConstantEvent import com.lukouguoji.module_base.impl.FlowBus import com.lukouguoji.module_base.impl.observe import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际进港-提取出库 @@ -25,9 +26,12 @@ class IntImpPickUpDLVActivity : override fun viewModelClass() = IntImpPickUpDLVViewModel::class.java override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际进港出库") + setBackArrow(if (DeviceUtil.isPhone()) "提取出库" else "国际进港出库") binding.viewModel = viewModel + // 手机形态专属:待出库 Tab 默认态 + 角标计数(平板不调用,行为零变化) + if (DeviceUtil.isPhone()) viewModel.initPhoneExtras() + // 绑定分页 viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpPickUpDetailActivity.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpPickUpDetailActivity.kt new file mode 100644 index 0000000..4a00505 --- /dev/null +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpPickUpDetailActivity.kt @@ -0,0 +1,45 @@ +package com.lukouguoji.gjj.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.lukouguoji.gjj.R +import com.lukouguoji.gjj.databinding.ActivityIntImpPickUpDetailBinding +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.IntImpPickUpDLVBean +import com.lukouguoji.module_base.common.Constant + +/** + * 国际进港提取出库-运单详情(5.5 寸手机版专属页面) + * + * 设计稿 09_提取出库「查看详情」内嵌页:蓝头运单卡(品名/件重/特码/代理/港站/航班/库位/提货单号) + * + 绿头提货信息卡(柜台办理人/时间,已出库追加出库人/出库时间)。 + * 数据直接复用列表已加载的 bean(Intent 传 Serializable),零新增请求;平板端无入口不可达。 + */ +class IntImpPickUpDetailActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_int_imp_pick_up_detail + override fun viewModelClass() = BaseViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("运单详情") + val bean = intent.getSerializableExtra(Constant.Key.DATA) as? IntImpPickUpDLVBean + if (bean == null) { + finish() + return + } + binding.bean = bean + } + + companion object { + @JvmStatic + fun start(context: Context, bean: IntImpPickUpDLVBean) { + context.startActivity( + Intent(context, IntImpPickUpDetailActivity::class.java) + .putExtra(Constant.Key.DATA, bean) + ) + } + } +} diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryActivity.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryActivity.kt index 77c8ef1..9c92bd5 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryActivity.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryActivity.kt @@ -19,6 +19,7 @@ import com.lukouguoji.module_base.ktx.getLifecycleOwner import com.lukouguoji.module_base.ktx.setUpperCaseAlphanumericFilter import com.lukouguoji.module_base.ktx.setUpperCaseLetterFilter import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际进港查询列表页 @@ -32,7 +33,7 @@ class IntImpQueryActivity : override fun viewModelClass() = IntImpQueryViewModel::class.java override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际进港查询") + setBackArrow(if (DeviceUtil.isPhone()) "进港查询" else "国际进港查询") binding.viewModel = viewModel @@ -50,22 +51,30 @@ class IntImpQueryActivity : viewModel.initSpecialCodeList() viewModel.initFilterLists() - // 观察筛选面板显示状态 - viewModel.filterVisible.observe(this) { visible -> - if (visible) showFilterPanel() else hideFilterPanel() - } + // 平板变体才有侧滑筛选面板(filter_panel/filter_overlay 手机变体不存在,Binding 字段为 null) + val padFilterPanel = binding.filterPanel + if (padFilterPanel != null) { + // 观察筛选面板显示状态(滑入/滑出动画) + viewModel.filterVisible.observe(this) { visible -> + if (visible) showFilterPanel() else hideFilterPanel() + } - // 航班号(顶部搜索区):大写字母+数字 - binding.flightNoSearch.et.setUpperCaseAlphanumericFilter() - // 始发港:仅大写字母 - binding.filterPanel.filterOrigin.et.setUpperCaseLetterFilter() + // 航班号(顶部搜索区):大写字母+数字 + binding.flightNoSearch?.et?.setUpperCaseAlphanumericFilter() + // 始发港:仅大写字母 + padFilterPanel.filterOrigin.et.setUpperCaseLetterFilter() + } else { + // 手机变体:PhoneFilterPanel 的显隐由 XML 直接绑定 viewModel.filterVisible,无需动画代码 + binding.phoneFilterFlightNo?.et?.setUpperCaseAlphanumericFilter() + binding.phoneFilterOrigin?.et?.setUpperCaseLetterFilter() + } viewModel.refresh() } private fun showFilterPanel() { - val panel = binding.filterPanel.root - val overlay = binding.filterOverlay + val panel = binding.filterPanel?.root ?: return + val overlay = binding.filterOverlay ?: return val panelWidth = window.decorView.width / 3 panel.layoutParams = (panel.layoutParams as FrameLayout.LayoutParams).apply { @@ -87,8 +96,8 @@ class IntImpQueryActivity : } private fun hideFilterPanel() { - val panel = binding.filterPanel.root - val overlay = binding.filterOverlay + val panel = binding.filterPanel?.root ?: return + val overlay = binding.filterOverlay ?: return if (panel.visibility != View.VISIBLE) return diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryDetailsActivity.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryDetailsActivity.kt index d1437a1..06c7e98 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryDetailsActivity.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpQueryDetailsActivity.kt @@ -16,6 +16,7 @@ import com.lukouguoji.module_base.base.BaseBindingActivity import com.lukouguoji.module_base.base.CustomVP2Adapter import com.lukouguoji.module_base.common.Constant import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际进港查询详情页面 @@ -28,7 +29,7 @@ class IntImpQueryDetailsActivity : override fun viewModelClass() = IntImpQueryDetailsViewModel::class.java override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际进港查询详情") + setBackArrow(if (DeviceUtil.isPhone()) "进港货物" else "国际进港查询详情") binding.viewModel = viewModel // 初始化ViewModel(传入maWbId) diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpStorageUseActivity.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpStorageUseActivity.kt index eeb5c61..9081785 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpStorageUseActivity.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpStorageUseActivity.kt @@ -19,6 +19,7 @@ import com.lukouguoji.module_base.impl.observe import com.lukouguoji.module_base.ktx.commonAdapter import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际进港-仓库 @@ -31,10 +32,13 @@ class IntImpStorageUseActivity : override fun viewModelClass() = IntImpStorageUseViewModel::class.java override fun initOnCreate(savedInstanceState: Bundle?) { - setBackArrow("国际进港仓库") + setBackArrow(if (DeviceUtil.isPhone()) "进港仓库" else "国际进港仓库") binding.viewModel = viewModel binding.activity = this + // 手机形态专属:筛选字典 + 未清仓/已清仓分项统计(平板不调用,行为零变化) + if (DeviceUtil.isPhone()) viewModel.initPhoneExtras() + // 绑定分页 viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this) @@ -198,6 +202,74 @@ class IntImpStorageUseActivity : }.show(this) } + // ==================== 5.5寸手机版:卡片级操作 ==================== + // 语义等价于「勾选该主单 + 其全部库位」再执行平板的批量操作,弹框与请求链路完全复用平板。 + + /** 卡片「清仓」(已清仓的卡片按钮置灰,ViewHolder 已拦截) */ + fun cardClear(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + val maWbListForClear = listOf(bean.copy(storageUseList = bean.storageUseList ?: emptyList())) + IntImpMoveClearDialogModel { dialog -> + viewModel.performClear( + dialog.clearNormal.value ?: "", + dialog.clearRemark.value ?: "", + maWbListForClear + ) + }.show(this) + } + + /** 卡片「修改库位」:弹层内先单选原库位(未出库的才可选,单库位时默认选中)再选新库位 */ + fun cardModifyStorage(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + // 已出库的分单不允许修改库位(与平板校验同口径) + val modifiableList = (bean.storageUseList ?: emptyList()) + .filter { it.outOpId.isNullOrEmpty() && it.outDate.isNullOrEmpty() } + if (modifiableList.isEmpty()) { + showToast(if (bean.storageUseList.isNullOrEmpty()) "该运单暂无库位" else "该运单库位均已出库,不允许修改库位") + return + } + IntImpModifyStorageDialogModel(sourceStorageList = modifiableList) { dialog -> + val sourceStorage = dialog.selectedSourceStorage ?: return@IntImpModifyStorageDialogModel + viewModel.performModifyStorage(dialog.locationName, dialog.locationId, sourceStorage) + }.show(this) + } + + /** 卡片「出库」:该运单全部库位出库(与平板勾选主单后出库同语义,件数/重量默认取库位入库件数/重量) */ + fun cardOutStorage(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + val storageList = bean.storageUseList ?: emptyList() + if (storageList.isEmpty()) { + showToast("该运单暂无库位") + return + } + IntImpOutStorageDialogModel( + storageCount = storageList.size, + defaultPc = storageList.mapNotNull { it.inPc }.takeIf { it.isNotEmpty() }?.sum(), + defaultWeight = storageList.mapNotNull { it.inWeight }.takeIf { it.isNotEmpty() }?.sum() + ) { dialog -> + viewModel.performOutStorage( + dialog.pc.value?.toLongOrNull(), + dialog.weight.value?.toDoubleOrNull(), + storageList + ) + }.show(this) + } + + /** 卡片「入库」:弹层选库位、填件数重量后整票入库 */ + fun cardInStorage(bean: com.lukouguoji.module_base.bean.GjcMaWb) { + val maWbListForInStorage = listOf(bean.copy(storageUseList = bean.storageUseList ?: emptyList())) + IntImpInStorageDialogModel( + showPcWeight = true, + defaultPc = bean.pc, + defaultWeight = bean.weight + ) { dialog -> + viewModel.performInStorage( + dialog.locationName, + dialog.locationId, + dialog.pc.value?.toLongOrNull(), + dialog.weight.value?.toDoubleOrNull(), + maWbListForInStorage + ) + }.show(this) + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == Constant.RequestCode.WAYBILL && resultCode == Activity.RESULT_OK) { diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpInStorageDialogModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpInStorageDialogModel.kt index 098e191..007d5c9 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpInStorageDialogModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpInStorageDialogModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import com.lukouguoji.gjj.R import com.lukouguoji.gjj.databinding.DialogIntImpInStorageBinding import com.lukouguoji.module_base.base.BaseDialogModel +import com.lukouguoji.module_base.util.DeviceUtil import com.lukouguoji.module_base.http.net.NetApply import com.lukouguoji.module_base.ktx.launchCollect import com.lukouguoji.module_base.ktx.showToast @@ -24,7 +25,9 @@ class IntImpInStorageDialogModel( defaultPc: Long? = null, defaultWeight: Double? = null, private val callback: (IntImpInStorageDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 库位列表 val locationList = MutableLiveData>() @@ -50,8 +53,13 @@ class IntImpInStorageDialogModel( override fun onDialogCreated(context: Context) { binding.model = this - binding.pcInput.et.inputType = InputType.TYPE_CLASS_NUMBER - binding.weightInput.et.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + // 平板变体 pc_input/weight_input,手机变体 pc_input_phone/weight_input_phone,单侧 id 在 Binding 中可空 + val pcType = InputType.TYPE_CLASS_NUMBER + val weightType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + binding.pcInput?.et?.inputType = pcType + binding.weightInput?.et?.inputType = weightType + binding.pcInputPhone?.et?.inputType = pcType + binding.weightInputPhone?.et?.inputType = weightType loadLocationList() // 监听选择变化,更新locationId和locationName diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpModifyStorageDialogModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpModifyStorageDialogModel.kt index a3abae6..ba2905c 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpModifyStorageDialogModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpModifyStorageDialogModel.kt @@ -8,17 +8,26 @@ import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.http.net.NetApply import com.lukouguoji.module_base.ktx.launchCollect import com.lukouguoji.module_base.ktx.showToast +import com.lukouguoji.module_base.bean.GjcStorageUse import com.lukouguoji.module_base.ktx.verifyNullOrEmpty +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.info.KeyValue /** * 国际进港 - 修改库位对话框 + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体)。 + * + * 平板:列表勾选唯一库位后打开,弹框内只选新库位(sourceStorageList 传 null,原库位项不存在于平板布局)。 + * 手机:卡片级操作,运单可能有多个库位——传入该运单未出库的库位列表,弹层内先单选「原库位」再选新库位。 */ class IntImpModifyStorageDialogModel( private val currentLocationId: String = "", private val currentLocationName: String = "", + private val sourceStorageList: List? = null, private val callback: (IntImpModifyStorageDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 库位列表 val locationList = MutableLiveData>() @@ -32,6 +41,13 @@ class IntImpModifyStorageDialogModel( // 库位名称 (后端需要的name) var locationName: String = "" + // ========== 手机卡片级操作专属:原库位单选 ========== + val sourceList = MutableLiveData>(emptyList()) + val selectedSourceId = MutableLiveData("") + + // 选中的原库位对象(保存后由调用方取用) + var selectedSourceStorage: GjcStorageUse? = null + override fun layoutId(): Int { return R.layout.dialog_int_imp_modify_storage } @@ -46,6 +62,19 @@ class IntImpModifyStorageDialogModel( locationId = selectedItem?.value ?: "" locationName = selectedItem?.key ?: "" } + + // 手机版:初始化原库位列表,单库位时默认选中 + if (sourceStorageList != null) { + sourceList.value = sourceStorageList.map { + KeyValue(it.location ?: "", it.id?.toString() ?: "") + } + selectedSourceId.observeForever { id -> + selectedSourceStorage = sourceStorageList.find { it.id?.toString() == id } + } + if (sourceStorageList.size == 1) { + selectedSourceId.value = sourceStorageList[0].id?.toString() ?: "" + } + } } /** @@ -76,6 +105,10 @@ class IntImpModifyStorageDialogModel( * 保存按钮点击 */ fun onSaveClick() { + if (sourceStorageList != null && selectedSourceStorage == null) { + showToast("请选择原库位") + return + } if (selectedLocationCode.value.verifyNullOrEmpty("请选择库位")) { return } diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpMoveClearDialogModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpMoveClearDialogModel.kt index 4b344ba..c060bb5 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpMoveClearDialogModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpMoveClearDialogModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData import com.lukouguoji.gjj.R import com.lukouguoji.gjj.databinding.DialogIntImpMoveClearBinding import com.lukouguoji.module_base.base.BaseDialogModel +import com.lukouguoji.module_base.util.DeviceUtil import com.lukouguoji.module_base.ktx.verifyNullOrEmpty import dev.utils.app.info.KeyValue @@ -13,7 +14,9 @@ import dev.utils.app.info.KeyValue */ class IntImpMoveClearDialogModel( private val callback: (IntImpMoveClearDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { // 清仓正常(存储的是 code:"0" 或 "1") val clearNormal = MutableLiveData("") diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpOutStorageDialogModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpOutStorageDialogModel.kt index 5610d96..ab5dd57 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpOutStorageDialogModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/dialog/IntImpOutStorageDialogModel.kt @@ -8,9 +8,11 @@ import com.lukouguoji.gjj.databinding.DialogIntImpOutStorageBinding import com.lukouguoji.module_base.base.BaseDialogModel import com.lukouguoji.module_base.ktx.showToast import com.lukouguoji.module_base.ktx.toPlainText +import com.lukouguoji.module_base.util.DeviceUtil /** * 国际进港仓库 - 出库操作对话框 + * 手机为底部弹层(layout/ 变体),平板为居中弹框(layout-sw600dp/ 变体) * @param storageCount 选中的库位数 * @param defaultPc 默认件数(选中库位的入库件数) * @param defaultWeight 默认重量(选中库位的入库重量) @@ -20,7 +22,9 @@ class IntImpOutStorageDialogModel( defaultPc: Long?, defaultWeight: Double?, private val callback: (IntImpOutStorageDialogModel) -> Unit -) : BaseDialogModel(DIALOG_TYPE_CENTER) { +) : BaseDialogModel( + if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER +) { val tip = "已选择 $storageCount 个库位执行出库操作" @@ -36,8 +40,13 @@ class IntImpOutStorageDialogModel( override fun onDialogCreated(context: Context) { binding.model = this - binding.pcInput.et.inputType = InputType.TYPE_CLASS_NUMBER - binding.weightInput.et.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + // 平板变体 pc_input/weight_input,手机变体 pc_input_phone/weight_input_phone,单侧 id 在 Binding 中可空 + val pcType = InputType.TYPE_CLASS_NUMBER + val weightType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + binding.pcInput?.et?.inputType = pcType + binding.weightInput?.et?.inputType = weightType + binding.pcInputPhone?.et?.inputType = pcType + binding.weightInputPhone?.et?.inputType = weightType } /** diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpPickUpDLVViewHolder.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpPickUpDLVViewHolder.kt index 3a28b70..cc1514d 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpPickUpDLVViewHolder.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpPickUpDLVViewHolder.kt @@ -1,6 +1,7 @@ package com.lukouguoji.gjj.holder import android.view.View +import com.lukouguoji.gjj.activity.IntImpPickUpDetailActivity import com.lukouguoji.gjj.databinding.ItemIntImpPickUpDlvBinding import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.common.ConstantEvent @@ -9,6 +10,9 @@ import com.lukouguoji.module_base.bean.IntImpPickUpDLVBean /** * 国际进港-提取出库 列表项ViewHolder + * + * 双形态布局:平板变体含 iv_icon(多选图标),手机变体含 iv_check(圆形勾选框,仅待出库形态显示) + * 与 tv_detail(查看详情)。单侧独有的 id 在 Binding 基类中为 @Nullable,必须判空访问。 */ class IntImpPickUpDLVViewHolder(view: View) : BaseViewHolder(view) { @@ -19,11 +23,25 @@ class IntImpPickUpDLVViewHolder(view: View) : binding.position = position binding.executePendingBindings() - // 点击图标切换选中状态 - binding.ivIcon.setOnClickListener { + // 平板:点击图标切换选中状态 + binding.ivIcon?.setOnClickListener { bean.checked.set(!bean.checked.get()) FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") binding.executePendingBindings() } + + // 手机:圆形勾选框(已出库形态隐藏,无勾选语义) + binding.ivCheck?.setOnClickListener { + if (!bean.isPickedUp) { + bean.checked.set(!bean.checked.get()) + FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") + binding.executePendingBindings() + } + } + + // 手机:查看详情(手机专属新页,数据直接传当前 bean,零新增请求) + binding.tvDetail?.setOnClickListener { + IntImpPickUpDetailActivity.start(itemView.context, bean) + } } } diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpQueryViewHolder.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpQueryViewHolder.kt index 5c618c7..67bd5dc 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpQueryViewHolder.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpQueryViewHolder.kt @@ -19,9 +19,9 @@ class IntImpQueryViewHolder(view: View) : // 注册整行点击事件 notifyItemClick(position, binding.ll) - // 侧滑菜单 - 修改按钮 - binding.btnEdit.setOnClickListener { - binding.swipeMenu.quickClose() + // 侧滑菜单 - 修改按钮(平板变体独有;手机变体无侧滑编辑入口,字段为 null) + binding.btnEdit?.setOnClickListener { + binding.swipeMenu?.quickClose() clickListener?.onItemClick(position, 2000) // type=2000表示修改操作 } } diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpStorageUseViewHolder.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpStorageUseViewHolder.kt index 68c8fb6..d40ee7e 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpStorageUseViewHolder.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/IntImpStorageUseViewHolder.kt @@ -2,6 +2,8 @@ package com.lukouguoji.gjj.holder import android.view.View import com.lukouguoji.gjj.R +import com.lukouguoji.gjj.activity.IntImpQueryDetailsActivity +import com.lukouguoji.gjj.activity.IntImpStorageUseActivity import com.lukouguoji.gjj.databinding.ItemIntImpStorageUseBinding import com.lukouguoji.module_base.adapter.setCommonAdapter import com.lukouguoji.module_base.base.BaseViewHolder @@ -12,6 +14,10 @@ import com.lukouguoji.module_base.ktx.refresh /** * 国际进港-仓库 ViewHolder + * + * 双形态布局:平板变体含 iv_icon/iv_show/rv_sub/ll_header/divider_header/tv_empty + * (多选 + 库位子列表),手机变体含 ll/btn_clear/btn_modify/btn_out/btn_in + * (整卡进详情 + 卡片级四操作按钮)。单侧独有的 id 在 Binding 基类中为 @Nullable,必须判空访问。 */ class IntImpStorageUseViewHolder(view: View) : BaseViewHolder(view) { @@ -22,43 +28,58 @@ class IntImpStorageUseViewHolder(view: View) : binding.position = position binding.executePendingBindings() + // ==================== 平板:多选 + 子列表 ==================== + // 图标点击切换选择状态 - binding.ivIcon.setOnClickListener { + binding.ivIcon?.setOnClickListener { bean.checked.set(!bean.checked.get()) FlowBus.with(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed") binding.executePendingBindings() } // 展开按钮点击事件 - binding.ivShow.setOnClickListener { + binding.ivShow?.setOnClickListener { bean.showMore.set(!bean.showMore.get()) } // 初始化库位明细子列表 RecyclerView - setCommonAdapter( - binding.rvSub, - IntImpStorageUseSubViewHolder::class.java, - R.layout.item_int_imp_storage_use_sub - ) + binding.rvSub?.let { rvSub -> + setCommonAdapter( + rvSub, + IntImpStorageUseSubViewHolder::class.java, + R.layout.item_int_imp_storage_use_sub + ) - // 刷新库位明细数据(传递父Bean引用) - val storageUseList = bean.storageUseList ?: emptyList() - binding.rvSub.tag = bean - binding.rvSub.refresh(storageUseList) - updateSubListVisibility(storageUseList.isNotEmpty()) + // 刷新库位明细数据(传递父Bean引用) + val storageUseList = bean.storageUseList ?: emptyList() + rvSub.tag = bean + rvSub.refresh(storageUseList) + updateSubListVisibility(storageUseList.isNotEmpty()) + } + + // ==================== 手机:整卡进详情 + 卡片级操作 ==================== + + val activity = itemView.context as? IntImpStorageUseActivity + + // 整卡点击进详情(复用进港查询详情页,3 Tab 运单/仓库/库位信息) + binding.ll?.setOnClickListener { + IntImpQueryDetailsActivity.start(itemView.context, bean.prefix, bean.no) + } + + // 清仓(已清仓时置灰不可点,样式由 XML 按 bean.isCleared 切换) + binding.btnClear?.setOnClickListener { + if (!bean.isCleared) activity?.cardClear(bean) + } + + binding.btnModify?.setOnClickListener { activity?.cardModifyStorage(bean) } + binding.btnOut?.setOnClickListener { activity?.cardOutStorage(bean) } + binding.btnIn?.setOnClickListener { activity?.cardInStorage(bean) } } private fun updateSubListVisibility(hasData: Boolean) { - if (hasData) { - binding.llHeader.visibility = View.VISIBLE - binding.dividerHeader.visibility = View.VISIBLE - binding.rvSub.visibility = View.VISIBLE - binding.tvEmpty.visibility = View.GONE - } else { - binding.llHeader.visibility = View.GONE - binding.dividerHeader.visibility = View.GONE - binding.rvSub.visibility = View.GONE - binding.tvEmpty.visibility = View.VISIBLE - } + binding.llHeader?.visibility = if (hasData) View.VISIBLE else View.GONE + binding.dividerHeader?.visibility = if (hasData) View.VISIBLE else View.GONE + binding.rvSub?.visibility = if (hasData) View.VISIBLE else View.GONE + binding.tvEmpty?.visibility = if (hasData) View.GONE else View.VISIBLE } } diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpPickUpDLVViewModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpPickUpDLVViewModel.kt index 17175b7..b693442 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpPickUpDLVViewModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpPickUpDLVViewModel.kt @@ -56,6 +56,57 @@ class IntImpPickUpDLVViewModel : BasePageViewModel() { fun updateCheckAllStatus() { val list = pageModel.rv?.commonAdapter()?.items as? List ?: return isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() } + selectedCountText.value = "已选 ${list.count { it.checked.get() }} 项" + } + + // ========== 5.5寸手机版专属 ========== + // 状态 Tab:待出库/已出库(outState 为 pageQuery 真实入参,0:未出库,1:已出库;平板恒空不进请求) + val outState = MutableLiveData("") + val tabList = MutableLiveData(listOf(KeyValue("待出库", "0"), KeyValue("已出库", "1"))) + val tabCounts = MutableLiveData(listOf("0", "0")) + + // 底部操作条「已选 N 项」 + val selectedCountText = MutableLiveData("已选 0 项") + + // 筛选弹层显隐 + val filterVisible = MutableLiveData(false) + + // 筛选弹层与平板端查询参数保持一致(后端确认):缴费日期起止 / 代理人 / 特码,运单号走顶部搜索框 + + private var phoneExtrasEnabled = false + + /** + * 手机形态专属初始化(Activity 在 DeviceUtil.isPhone() 时调用)。 + * 平板端不调用,请求次数与行为零变化。 + */ + fun initPhoneExtras() { + phoneExtrasEnabled = true + // 默认进「待出库」Tab + outState.value = "0" + } + + /** 筛选按钮点击 - 打开筛选面板 */ + fun filterClick() { + filterVisible.value = true + } + + /** 关闭筛选面板 */ + fun closeFilter() { + filterVisible.value = false + } + + /** 重置筛选条件 */ + fun resetFilter() { + paymentDateStart.value = "" + paymentDateEnd.value = "" + agentCode.value = "" + spCode.value = "" + } + + /** 确认筛选 - 关闭面板并刷新 */ + fun confirmFilter() { + filterVisible.value = false + refresh() } // ========== 适配器配置 ========== @@ -113,6 +164,8 @@ class IntImpPickUpDLVViewModel : BasePageViewModel() { list.forEach { it.checked.set(shouldCheckAll) } isAllChecked.value = shouldCheckAll pageModel.rv?.commonAdapter()?.notifyDataSetChanged() + // 同步手机版底部条「已选 N 项」(平板布局不绑定该字段,无影响) + selectedCountText.value = "已选 ${list.count { it.checked.get() }} 项" } /** @@ -147,12 +200,15 @@ class IntImpPickUpDLVViewModel : BasePageViewModel() { * 获取列表数据 */ override fun getData() { + // 查询条件与平板端一致;outState 为手机 Tab 维度(平板恒空不进请求)。 + // 运单号入参用 wbNo(no 为 8 位编号,与主干平板端修复一致) val filterParams = mapOf( "beginDate" to paymentDateStart.value?.ifEmpty { null }, "endDate" to paymentDateEnd.value?.ifEmpty { null }, "agentCode" to agentCode.value?.ifEmpty { null }, "wbNo" to wbNo.value?.ifEmpty { null }, - "spCode" to spCode.value?.ifEmpty { null } + "spCode" to spCode.value?.ifEmpty { null }, + "outState" to outState.value?.ifEmpty { null } ) val listParams = (filterParams + mapOf( @@ -177,6 +233,28 @@ class IntImpPickUpDLVViewModel : BasePageViewModel() { totalWeight.value = (data?.totalWeight ?: 0.0).toString() } } + + // 手机专属:Tab 角标计数(同条件 + outState=0/1 各查一次,平板不发) + if (phoneExtrasEnabled) { + val pendingParams = (filterParams + mapOf("outState" to "0")).toRequestBody() + launchCollect({ NetApply.api.getIntImpPickUpDLVTotal(pendingParams) }) { + onSuccess = { result -> + tabCounts.value = listOf( + (result.data?.wbNumber ?: 0).toString(), + tabCounts.value?.getOrNull(1) ?: "0" + ) + } + } + val shippedParams = (filterParams + mapOf("outState" to "1")).toRequestBody() + launchCollect({ NetApply.api.getIntImpPickUpDLVTotal(shippedParams) }) { + onSuccess = { result -> + tabCounts.value = listOf( + tabCounts.value?.getOrNull(0) ?: "0", + (result.data?.wbNumber ?: 0).toString() + ) + } + } + } } /** diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryDetailsViewModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryDetailsViewModel.kt index 05f7480..f0383b9 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryDetailsViewModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryDetailsViewModel.kt @@ -61,6 +61,39 @@ class IntImpQueryDetailsViewModel : BaseViewModel() { currentTab.value = index } + // ==================== 5.5寸手机版取值帮助(与出港 GjcQueryDetailsViewModel 同口径) ==================== + + /** + * 从详情 Map 取字符串展示值;数字自动去掉无意义的 .0;空值兜底 "--" + */ + fun str(map: Map?, key: String): String { + val v = map?.get(key) ?: return "--" + val s = when (v) { + is Double -> if (v % 1.0 == 0.0) v.toLong().toString() else v.toString() + else -> v.toString() + } + return s.ifBlank { "--" } + } + + /** + * 依次取第一个非空 key 的展示值(如 by1 优先于 by0) + */ + fun strOr(map: Map?, key: String, fallbackKey: String): String { + val first = str(map, key) + return if (first != "--") first else str(map, fallbackKey) + } + + /** + * 取重量类数值(保留小数形式),空值兜底 "--" + */ + fun dec(map: Map?, key: String): String { + val v = map?.get(key) ?: return "--" + return when (v) { + is Double -> v.toString() + else -> v.toString().ifBlank { "--" } + } + } + /** * 加载详情数据 */ diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryViewModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryViewModel.kt index 58fe54f..fc77fb9 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryViewModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpQueryViewModel.kt @@ -65,6 +65,12 @@ class IntImpQueryViewModel : BasePageViewModel(), IOnItemClickListener { val awbTypeList = MutableLiveData>(emptyList()) val businessTypeList = MutableLiveData>(emptyList()) + // ==================== 5.5寸手机版专属 ==================== + // 三格统计:总票数 / 已入库 / 已出库,均取 pageQueryTotal 出参 + // (wbNumber / storedNumber / departedNumber),服务端全量口径 + val storedCount = MutableLiveData("0") + val outboundCount = MutableLiveData("0") + // ==================== 筛选条件 ==================== val spCode = MutableLiveData("") val outStatus = MutableLiveData("") @@ -178,6 +184,9 @@ class IntImpQueryViewModel : BasePageViewModel(), IOnItemClickListener { totalPc.value = (data?.totalPc ?: 0).toString() totalWeight.value = (data?.totalWeight ?: 0.0).toString() totalFlightNumber.value = (data?.flightNumber ?: 0).toString() + // 手机三格统计分项(平板布局不绑定,无影响) + storedCount.value = (data?.storedNumber ?: 0).toString() + outboundCount.value = (data?.departedNumber ?: 0).toString() } } } diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpStorageUseViewModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpStorageUseViewModel.kt index b177992..192aa51 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpStorageUseViewModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpStorageUseViewModel.kt @@ -39,6 +39,76 @@ class IntImpStorageUseViewModel : BasePageViewModel() { val totalPc = MutableLiveData("0") // 总件数 val totalWeight = MutableLiveData("0") // 总重量 + // ========== 5.5寸手机版专属 ========== + // 搜索框统一入口(placeholder「搜索运单号/航班号」,phoneSearchClick 分流) + val phoneSearchKey = MutableLiveData("") + + // 筛选弹层显隐 + val filterVisible = MutableLiveData(false) + + // 筛选弹层与平板端查询参数保持一致(后端确认):航班日期 / 航班号 / 库位 / 清仓结果,运单号/航班号走顶部搜索框。 + // 始发站/代理/特码/运单类型/业务类型/品名 后端不支持(内网实测 fdep/agentCode 被忽略),已移除 + + // 三格统计:未清仓 / 已清仓,直接取 pageQueryTotal 出参 notClearNumber / clearNumber + val unclearedCount = MutableLiveData("0") + val clearedCount = MutableLiveData("0") + + private var phoneExtrasEnabled = false + + /** + * 手机形态专属初始化(Activity 在 DeviceUtil.isPhone() 时调用)。 + * 平板端不调用,请求次数与行为零变化。 + */ + fun initPhoneExtras() { + phoneExtrasEnabled = true + } + + /** + * 手机搜索框查询:按出港计重实测规则分流——含字母→航班号,其余→运单号 + */ + fun phoneSearchClick() { + val key = phoneSearchKey.value?.trim() ?: "" + when { + key.isEmpty() -> { + wbNo.value = "" + flightNo.value = "" + } + key.any { it.isLetter() } -> { + flightNo.value = key + wbNo.value = "" + } + else -> { + wbNo.value = key + flightNo.value = "" + } + } + refresh() + } + + /** 筛选按钮点击 - 打开筛选面板 */ + fun filterClick() { + filterVisible.value = true + } + + /** 关闭筛选面板 */ + fun closeFilter() { + filterVisible.value = false + } + + /** 重置筛选条件 */ + fun resetFilter() { + flightDate.value = "" + flightNo.value = "" + location.value = "" + clearResult.value = "" + } + + /** 确认筛选 - 关闭面板并刷新 */ + fun confirmFilter() { + filterVisible.value = false + refresh() + } + // ========== 全选状态 ========== val isAllChecked = MutableLiveData(false) @@ -270,6 +340,7 @@ class IntImpStorageUseViewModel : BasePageViewModel() { } override fun getData() { + // 查询条件与平板端一致 val filterParams = mapOf( "fdate" to flightDate.value?.ifEmpty { null }, "fno" to flightNo.value?.ifEmpty { null }, @@ -299,6 +370,9 @@ class IntImpStorageUseViewModel : BasePageViewModel() { totalWbNumber.value = (data?.wbNumber ?: 0).toString() totalPc.value = (data?.totalPc ?: 0).toString() totalWeight.value = (data?.totalWeight ?: 0.0).toString() + // 手机三格统计分项(平板布局不绑定,无影响) + unclearedCount.value = (data?.notClearNumber ?: 0).toString() + clearedCount.value = (data?.clearNumber ?: 0).toString() } } } diff --git a/module_gjj/src/main/release/AndroidManifest.xml b/module_gjj/src/main/release/AndroidManifest.xml index 64c654b..1556983 100644 --- a/module_gjj/src/main/release/AndroidManifest.xml +++ b/module_gjj/src/main/release/AndroidManifest.xml @@ -1,99 +1,99 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_pick_up_dlv.xml b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_pick_up_dlv.xml new file mode 100644 index 0000000..71bf416 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_pick_up_dlv.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_query.xml b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_query.xml new file mode 100644 index 0000000..392304e --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_query.xml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_query_details.xml b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_query_details.xml new file mode 100644 index 0000000..ad3219c --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_query_details.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_storage_use.xml b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_storage_use.xml new file mode 100644 index 0000000..1d0eab7 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/activity_int_imp_storage_use.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_in_storage.xml b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_in_storage.xml new file mode 100644 index 0000000..f6b6d20 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_in_storage.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_modify_storage.xml b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_modify_storage.xml new file mode 100644 index 0000000..d3bc331 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_modify_storage.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_move_clear.xml b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_move_clear.xml new file mode 100644 index 0000000..d8aba12 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_move_clear.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_out_storage.xml b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_out_storage.xml new file mode 100644 index 0000000..e297cf9 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/dialog_int_imp_out_storage.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_storage.xml b/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_storage.xml new file mode 100644 index 0000000..6a02988 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_storage.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_warehouse.xml b/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_warehouse.xml new file mode 100644 index 0000000..e2cd6b3 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_warehouse.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_waybill.xml b/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_waybill.xml new file mode 100644 index 0000000..7f5dabd --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/fragment_int_imp_query_waybill.xml @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/item_int_imp_pick_up_dlv.xml b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_pick_up_dlv.xml new file mode 100644 index 0000000..ce35bb9 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_pick_up_dlv.xml @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query.xml b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query.xml new file mode 100644 index 0000000..a5d1d76 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query.xml @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query_storage.xml b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query_storage.xml new file mode 100644 index 0000000..73eb1b4 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query_storage.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query_warehouse.xml b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query_warehouse.xml new file mode 100644 index 0000000..9fc40f3 --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_query_warehouse.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout-sw600dp/item_int_imp_storage_use.xml b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_storage_use.xml new file mode 100644 index 0000000..a732a5f --- /dev/null +++ b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_storage_use.xml @@ -0,0 +1,517 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout/item_int_imp_storage_use_sub.xml b/module_gjj/src/main/res/layout-sw600dp/item_int_imp_storage_use_sub.xml similarity index 100% rename from module_gjj/src/main/res/layout/item_int_imp_storage_use_sub.xml rename to module_gjj/src/main/res/layout-sw600dp/item_int_imp_storage_use_sub.xml diff --git a/module_gjj/src/main/res/layout/activity_int_imp_pick_up_detail.xml b/module_gjj/src/main/res/layout/activity_int_imp_pick_up_detail.xml new file mode 100644 index 0000000..b761ccc --- /dev/null +++ b/module_gjj/src/main/res/layout/activity_int_imp_pick_up_detail.xml @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout/activity_int_imp_pick_up_dlv.xml b/module_gjj/src/main/res/layout/activity_int_imp_pick_up_dlv.xml index 71bf416..2d8516a 100644 --- a/module_gjj/src/main/res/layout/activity_int_imp_pick_up_dlv.xml +++ b/module_gjj/src/main/res/layout/activity_int_imp_pick_up_dlv.xml @@ -1,203 +1,165 @@ + - + + + - + android:background="@color/phone_page_bg"> - - - + android:layout_height="match_parent" + android:orientation="vertical"> - - + + + + android:background="@color/white" + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="16dp" + android:paddingBottom="12dp"> - - + + + + + + counts="@{viewModel.tabCounts}" + setOnTabChanged="@{()-> viewModel.searchClick()}" + tabs="@{viewModel.tabList}" + value="@={viewModel.outState}" /> - - + + + + + + + + + + + + + + + + + + + + + + title='@{"代理人"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={viewModel.agentCode}" /> - - + - - - + title='@{"特码"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={viewModel.spCode}" /> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/module_gjj/src/main/res/layout/activity_int_imp_query.xml b/module_gjj/src/main/res/layout/activity_int_imp_query.xml index 392304e..79422ef 100644 --- a/module_gjj/src/main/res/layout/activity_int_imp_query.xml +++ b/module_gjj/src/main/res/layout/activity_int_imp_query.xml @@ -1,215 +1,201 @@ + + - + + - + android:background="@color/phone_page_bg"> - - - - - + android:layout_height="match_parent" + android:orientation="vertical"> - + + + + android:layout_height="wrap_content" + android:background="@color/white" + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="16dp" + android:paddingBottom="12dp"> - - + + + + + + + + - - + - - - - - - - - - - - - - + android:orientation="vertical" + android:paddingHorizontal="12dp" + android:paddingVertical="10dp"> - + - - - - - - - + - - - - - - - - + + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="10dp" + android:layout_weight="1" + android:background="@drawable/bg_phone_card" + android:orientation="horizontal"> + + + android:orientation="vertical" + android:paddingHorizontal="12dp" + android:paddingVertical="10dp"> + android:text="已入库" + android:textColor="@color/phone_text_body" + android:textSize="12sp" /> + tools:text="2" /> + + + + + + + + + + + + android:text="已出库" + android:textColor="@color/phone_text_body" + android:textSize="12sp" /> + tools:text="2" /> @@ -217,28 +203,141 @@ - - + + android:layout_height="0dp" + android:layout_weight="1"> - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/module_gjj/src/main/res/layout/activity_int_imp_query_details.xml b/module_gjj/src/main/res/layout/activity_int_imp_query_details.xml index ad3219c..2a14f83 100644 --- a/module_gjj/src/main/res/layout/activity_int_imp_query_details.xml +++ b/module_gjj/src/main/res/layout/activity_int_imp_query_details.xml @@ -1,6 +1,18 @@ - + + @@ -14,27 +26,25 @@ - - + + android:textColor="@{viewModel.currentTab == 0 ? @color/phone_primary : @color/phone_text_body}" + android:textSize="14sp" + android:textStyle="bold" /> + + android:textColor="@{viewModel.currentTab == 1 ? @color/phone_primary : @color/phone_text_body}" + android:textSize="14sp" + android:textStyle="bold" /> + + android:textColor="@{viewModel.currentTab == 2 ? @color/phone_primary : @color/phone_text_body}" + android:textSize="14sp" + android:textStyle="bold" /> + + android:layout_height="1px" + android:background="@color/phone_divider" /> + - + - + + - + android:background="@color/phone_page_bg"> - - - + android:layout_height="match_parent" + android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + hint='@{"请选择航班日期"}' + title='@{"航班日期"}' + type="@{PhoneDataLayoutType.DATE}" + value="@={viewModel.flightDate}" /> - + hint='@{"请输入航班号"}' + title='@{"航班号"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={viewModel.flightNo}" /> - - + + + + + title='@{"清仓结果"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={viewModel.clearResult}" /> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/module_gjj/src/main/res/layout/dialog_int_imp_in_storage.xml b/module_gjj/src/main/res/layout/dialog_int_imp_in_storage.xml index f6b6d20..1d7f65a 100644 --- a/module_gjj/src/main/res/layout/dialog_int_imp_in_storage.xml +++ b/module_gjj/src/main/res/layout/dialog_int_imp_in_storage.xml @@ -1,8 +1,14 @@ + - + + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/module_gjj/src/main/res/layout/dialog_int_imp_modify_storage.xml b/module_gjj/src/main/res/layout/dialog_int_imp_modify_storage.xml index d3bc331..9151ebf 100644 --- a/module_gjj/src/main/res/layout/dialog_int_imp_modify_storage.xml +++ b/module_gjj/src/main/res/layout/dialog_int_imp_modify_storage.xml @@ -1,8 +1,14 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - - - - + + android:layout_marginBottom="24dp"> - - + android:gravity="center" + android:text="修改库位" + android:textColor="@color/phone_text_strong" + android:textSize="18sp" + android:textStyle="bold" /> - + - + - - - - - - + + hint='@{"请选择原库位"}' + list="@{model.sourceList}" + title='@{"原库位"}' + type="@{PhoneDataLayoutType.SPINNER}" + value="@={model.selectedSourceId}" /> - + + - - - + + diff --git a/module_gjj/src/main/res/layout/dialog_int_imp_move_clear.xml b/module_gjj/src/main/res/layout/dialog_int_imp_move_clear.xml index d8aba12..d50e956 100644 --- a/module_gjj/src/main/res/layout/dialog_int_imp_move_clear.xml +++ b/module_gjj/src/main/res/layout/dialog_int_imp_move_clear.xml @@ -1,8 +1,13 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/module_gjj/src/main/res/layout/dialog_int_imp_out_storage.xml b/module_gjj/src/main/res/layout/dialog_int_imp_out_storage.xml index e297cf9..5ecbe94 100644 --- a/module_gjj/src/main/res/layout/dialog_int_imp_out_storage.xml +++ b/module_gjj/src/main/res/layout/dialog_int_imp_out_storage.xml @@ -1,8 +1,13 @@ + - + + android:background="@drawable/bg_phone_panel_top" + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="24dp" + android:paddingBottom="32dp"> - - + + android:layout_height="wrap_content" + android:layout_marginBottom="16dp"> - + + + + + + + + + + - - - - - + hint='@{"请输入件数"}' + title='@{"件数"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={model.pc}" /> - - - - - - - - - - - - - - + android:layout_marginTop="20dp" + hint='@{"请输入重量"}' + title='@{"重量"}' + type="@{PhoneDataLayoutType.INPUT}" + value="@={model.weight}" /> - - - - - - - - + + diff --git a/module_gjj/src/main/res/layout/fragment_int_imp_query_storage.xml b/module_gjj/src/main/res/layout/fragment_int_imp_query_storage.xml index 6a02988..77a821b 100644 --- a/module_gjj/src/main/res/layout/fragment_int_imp_query_storage.xml +++ b/module_gjj/src/main/res/layout/fragment_int_imp_query_storage.xml @@ -1,4 +1,14 @@ + @@ -7,130 +17,37 @@ - + android:background="@color/phone_page_bg"> - + android:clipToPadding="false" + android:overScrollMode="never" + android:paddingBottom="16dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + diff --git a/module_gjj/src/main/res/layout/fragment_int_imp_query_warehouse.xml b/module_gjj/src/main/res/layout/fragment_int_imp_query_warehouse.xml index e2cd6b3..f9cf2fb 100644 --- a/module_gjj/src/main/res/layout/fragment_int_imp_query_warehouse.xml +++ b/module_gjj/src/main/res/layout/fragment_int_imp_query_warehouse.xml @@ -1,4 +1,14 @@ + @@ -7,140 +17,37 @@
- + android:background="@color/phone_page_bg"> - + android:clipToPadding="false" + android:overScrollMode="never" + android:paddingBottom="16dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + diff --git a/module_gjj/src/main/res/layout/fragment_int_imp_query_waybill.xml b/module_gjj/src/main/res/layout/fragment_int_imp_query_waybill.xml index 7f5dabd..626e03a 100644 --- a/module_gjj/src/main/res/layout/fragment_int_imp_query_waybill.xml +++ b/module_gjj/src/main/res/layout/fragment_int_imp_query_waybill.xml @@ -1,9 +1,24 @@ + - - + @@ -12,322 +27,372 @@ + android:background="@color/phone_page_bg" + android:fillViewport="true" + android:overScrollMode="never"> + android:padding="12dp"> - + + android:paddingHorizontal="14dp" + android:paddingBottom="14dp"> - + + + + + + + + + + + + - + value='@{viewModel.str(viewModel.maWbData, "wbNo")}' /> - + value='@{viewModel.strOr(viewModel.maWbData, "agentName", "agentCode")}' /> - + + + + + + value='@{viewModel.str(viewModel.maWbData, "spCode")}' /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + value='@{viewModel.strOr(viewModel.maWbData, "by1", "by0")}' /> - + - + title='@{"航程"}' + value='@{viewModel.str(viewModel.maWbData, "range")}' /> - + value='@{viewModel.str(viewModel.maWbData, "unNumber")}' /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingVertical="12dp"> - + - + android:layout_marginStart="8dp" + android:text="回执信息" + android:textColor="@color/phone_text_title" + android:textSize="15sp" + android:textStyle="bold" /> - + + + - - - - - - - - - - + value='@{viewModel.str(viewModel.maWbData, "mftStatus")}' /> - + value='@{viewModel.str(viewModel.maWbData, "tallyStatus")}' /> - + value='@{viewModel.str(viewModel.maWbData, "command")}' /> - - - diff --git a/module_gjj/src/main/res/layout/item_int_imp_pick_up_dlv.xml b/module_gjj/src/main/res/layout/item_int_imp_pick_up_dlv.xml index ce35bb9..2165bc7 100644 --- a/module_gjj/src/main/res/layout/item_int_imp_pick_up_dlv.xml +++ b/module_gjj/src/main/res/layout/item_int_imp_pick_up_dlv.xml @@ -1,9 +1,27 @@ + + xmlns:tools="http://schemas.android.com/tools"> + + @@ -17,268 +35,164 @@ android:id="@+id/ll" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginHorizontal="10dp" - android:layout_marginTop="10dp" - android:background="@drawable/bg_white_radius_8" - android:gravity="center_vertical" - android:orientation="horizontal" - android:paddingVertical="20dp" - android:paddingHorizontal="10dp"> - - + android:layout_marginHorizontal="16dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:padding="14dp"> - + + android:gravity="center_vertical" + android:orientation="horizontal"> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:text="@{bean.wbNo}" + android:textColor="@{bean.isPickedUp ? @color/phone_text_title : @color/phone_primary}" + android:textSize="16sp" + android:textStyle="bold" + tools:text="78133363573" /> - - + + + + + + + + + android:layout_marginStart="4dp" + android:text="已出库" + android:textColor="@color/phone_text_title" + android:textSize="13sp" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout/item_int_imp_query.xml b/module_gjj/src/main/res/layout/item_int_imp_query.xml index a5d1d76..241864a 100644 --- a/module_gjj/src/main/res/layout/item_int_imp_query.xml +++ b/module_gjj/src/main/res/layout/item_int_imp_query.xml @@ -1,346 +1,226 @@ + + + + - - - - - - - - - - + android:layout_marginHorizontal="16dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:padding="14dp"> + + android:gravity="center_vertical" + android:orientation="horizontal"> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:text="@{bean.wbNo}" + android:textColor="@color/phone_text_title" + android:textSize="16sp" + android:textStyle="bold" + tools:text="78133363396" /> - - + + - + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - - - - - + + + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="6dp" + android:text='@{"件数:" + (bean.inPc != null ? String.valueOf(bean.inPc) : "--")}' + android:textColor="@color/phone_text_body" + android:textSize="13sp" + tools:text="件数:444" /> - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout/item_int_imp_query_storage.xml b/module_gjj/src/main/res/layout/item_int_imp_query_storage.xml index 73eb1b4..c73b07d 100644 --- a/module_gjj/src/main/res/layout/item_int_imp_query_storage.xml +++ b/module_gjj/src/main/res/layout/item_int_imp_query_storage.xml @@ -1,5 +1,16 @@ - + + @@ -15,85 +26,88 @@ + android:layout_marginHorizontal="12dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:paddingHorizontal="14dp" + android:paddingBottom="14dp"> + + android:paddingVertical="12dp"> - - + - - - - - - - - - - - - - - + - - + android:layout_marginBottom="12dp" + android:background="@color/phone_divider" /> + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout/item_int_imp_query_warehouse.xml b/module_gjj/src/main/res/layout/item_int_imp_query_warehouse.xml index 9fc40f3..044195a 100644 --- a/module_gjj/src/main/res/layout/item_int_imp_query_warehouse.xml +++ b/module_gjj/src/main/res/layout/item_int_imp_query_warehouse.xml @@ -1,5 +1,16 @@ - + + @@ -15,95 +26,111 @@ + android:layout_marginHorizontal="12dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:paddingHorizontal="14dp" + android:paddingBottom="14dp"> + + android:paddingVertical="12dp"> - - + - - - - - - - - - - - - - - - - - + - - + android:layout_marginBottom="12dp" + android:background="@color/phone_divider" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjj/src/main/res/layout/item_int_imp_storage_use.xml b/module_gjj/src/main/res/layout/item_int_imp_storage_use.xml index a732a5f..df42734 100644 --- a/module_gjj/src/main/res/layout/item_int_imp_storage_use.xml +++ b/module_gjj/src/main/res/layout/item_int_imp_storage_use.xml @@ -1,7 +1,23 @@ + + xmlns:tools="http://schemas.android.com/tools"> @@ -16,502 +32,254 @@ type="Integer" /> - - + android:layout_marginHorizontal="16dp" + android:layout_marginTop="12dp" + android:background="@drawable/bg_phone_card" + android:orientation="vertical" + android:padding="14dp"> - + + android:gravity="center_vertical" + android:orientation="horizontal"> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + android:paddingHorizontal="8dp" + android:text="已清仓" + android:textColor="@color/phone_tag_green_text" + android:textSize="11sp" + android:visibility="@{bean.isCleared ? View.VISIBLE : View.GONE}" /> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gnc/src/main/debug/AndroidManifest.xml b/module_gnc/src/main/debug/AndroidManifest.xml index b6f1468..3817219 100644 --- a/module_gnc/src/main/debug/AndroidManifest.xml +++ b/module_gnc/src/main/debug/AndroidManifest.xml @@ -1,25 +1,25 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_gnc/src/main/release/AndroidManifest.xml b/module_gnc/src/main/release/AndroidManifest.xml index 97d68cb..787036a 100644 --- a/module_gnc/src/main/release/AndroidManifest.xml +++ b/module_gnc/src/main/release/AndroidManifest.xml @@ -1,99 +1,99 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_gnj/src/main/AndroidManifest.xml b/module_gnj/src/main/AndroidManifest.xml index 7676f00..3647888 100644 --- a/module_gnj/src/main/AndroidManifest.xml +++ b/module_gnj/src/main/AndroidManifest.xml @@ -1,26 +1,26 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/module_gnj/src/main/debug/AndroidManifest.xml b/module_gnj/src/main/debug/AndroidManifest.xml index 2c3b8a2..59e94b0 100644 --- a/module_gnj/src/main/debug/AndroidManifest.xml +++ b/module_gnj/src/main/debug/AndroidManifest.xml @@ -1,44 +1,44 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_gnj/src/main/release/AndroidManifest.xml b/module_gnj/src/main/release/AndroidManifest.xml index a989e04..90f0d3e 100644 --- a/module_gnj/src/main/release/AndroidManifest.xml +++ b/module_gnj/src/main/release/AndroidManifest.xml @@ -1,86 +1,86 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_hangban/src/main/debug/AndroidManifest.xml b/module_hangban/src/main/debug/AndroidManifest.xml index d4c68cd..2156499 100644 --- a/module_hangban/src/main/debug/AndroidManifest.xml +++ b/module_hangban/src/main/debug/AndroidManifest.xml @@ -1,25 +1,25 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_hangban/src/main/release/AndroidManifest.xml b/module_hangban/src/main/release/AndroidManifest.xml index ceccc24..4c2b014 100644 --- a/module_hangban/src/main/release/AndroidManifest.xml +++ b/module_hangban/src/main/release/AndroidManifest.xml @@ -1,33 +1,33 @@ - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/module_mit/src/main/debug/AndroidManifest.xml b/module_mit/src/main/debug/AndroidManifest.xml index 6bfb851..58f89d9 100644 --- a/module_mit/src/main/debug/AndroidManifest.xml +++ b/module_mit/src/main/debug/AndroidManifest.xml @@ -1,25 +1,25 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_mit/src/main/release/AndroidManifest.xml b/module_mit/src/main/release/AndroidManifest.xml index be048cb..d9c0500 100644 --- a/module_mit/src/main/release/AndroidManifest.xml +++ b/module_mit/src/main/release/AndroidManifest.xml @@ -1,43 +1,43 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_p/src/main/java/com/sixin/module_p/ui/enter/gj/GjViewModel.kt b/module_p/src/main/java/com/sixin/module_p/ui/enter/gj/GjViewModel.kt index a6b6813..45b2972 100644 --- a/module_p/src/main/java/com/sixin/module_p/ui/enter/gj/GjViewModel.kt +++ b/module_p/src/main/java/com/sixin/module_p/ui/enter/gj/GjViewModel.kt @@ -1,48 +1,134 @@ -package com.sixin.module_p.ui.enter.gj - -import com.lukouguoji.module_base.base.BaseViewModel -import com.lukouguoji.module_base.bean.ActionBean -import com.lukouguoji.module_base.common.Constant -import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil -import com.lukouguoji.module_base.ktx.loge -import com.sixin.module_p.R -import com.sixin.module_p.ui.guobang.PdaGuoBangActivity -import com.sixin.module_p.ui.lihuo.PdaLiHuoActivity -import com.sixin.module_p.ui.shouyun.PdaShouYunActivity -import com.sixin.module_p.ui.zuzhuang.PdaZuZhuangActivity -import dev.DevUtils - -class GjViewModel : BaseViewModel() { - - val actionList = listOf( - ActionBean("普货收运", R.drawable.gjc_shou_yun_icon, Constant.AuthName.GjcAppDomExpCheckin), - ActionBean("板箱组装", R.drawable.img_bxzz, Constant.AuthName.GjcBanXListActivity), - ActionBean("板箱过磅", R.drawable.gjc_fu_bang_icon, Constant.AuthName.GjcFuBangActivity), - ActionBean("进港理货", R.drawable.img_jglh, Constant.AuthName.GjjTallyListActivity), - ).filter { SharedPreferenceUtil.getString(Constant.Share.authList).contains(it.key) } - - fun onItemClick(position: Int) { - when (val title = actionList[position].title) { - "普货收运" -> { - PdaShouYunActivity.start(DevUtils.getTopActivity()) - } - - "板箱组装" -> { - PdaZuZhuangActivity.start(DevUtils.getTopActivity()) - } - - "板箱过磅" -> { - PdaGuoBangActivity.start(DevUtils.getTopActivity()) - } - - "进港理货" -> { - PdaLiHuoActivity.start(DevUtils.getTopActivity()) - } - - else -> { - loge("点击事件 $title 未处理") - } - } - } - -} \ No newline at end of file +package com.sixin.module_p.ui.enter.gj + +import com.alibaba.android.arouter.launcher.ARouter +import com.lukouguoji.module_base.base.BaseViewModel +import com.lukouguoji.module_base.bean.ActionBean +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil +import com.lukouguoji.module_base.ktx.loge +import com.lukouguoji.module_base.router.ARouterConstants +import com.sixin.module_p.R +import com.sixin.module_p.ui.guobang.PdaGuoBangActivity +import com.sixin.module_p.ui.lihuo.PdaLiHuoActivity +import com.sixin.module_p.ui.shouyun.PdaShouYunActivity +import com.sixin.module_p.ui.zuzhuang.PdaZuZhuangActivity +import dev.DevUtils + +/** + * 手持端首页「国际业务」Tab + * + * 菜单项是**手写清单**(不是接口下发),只用登录返回的 authList 做权限过滤: + * 某项没配 [ActionBean.key] 对应的权限串时不显示。所以新页面适配完之后 + * **必须回来这里加一行**,否则手机端首页根本没有入口。 + * + * 两类跳转: + * - 带 [ActionBean.route] → 通用 ARouter 跳转(module_p 不依赖 module_gjc 等业务模块, + * 跨模块只能走路由)。**5.5 寸适配页一律用这种,加一行即可。** + * - 不带 route → 落到 [onItemClick] 的 when 分支(module_p 内部的 PDA 专用页面) + */ +class GjViewModel : BaseViewModel() { + + val actionList = listOf( + // ===== module_p 内的 PDA 专用页面(无 route,走 when 分支) ===== + ActionBean("普货收运", R.drawable.gjc_shou_yun_icon, Constant.AuthName.GjcAppDomExpCheckin), + ActionBean("板箱组装", R.drawable.img_bxzz, Constant.AuthName.GjcBanXListActivity), + ActionBean("板箱过磅", R.drawable.gjc_fu_bang_icon, Constant.AuthName.GjcFuBangActivity), + ActionBean("进港理货", R.drawable.img_jglh, Constant.AuthName.GjjTallyListActivity), + + // ===== 5.5 寸手机版已适配页面(图标复用 Pad 端同款,跳转走 ARouter) ===== + ActionBean( + "出港移库", + R.drawable.gjc_yi_ku_icon, + Constant.AuthName.GjcYiKuListActivity, + ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE + ), + ActionBean( + "出库交接", + R.drawable.img_gjc_chuku_jiaojie, + Constant.AuthName.GjcIntExpOutHandover, + ARouterConstants.ACTIVITY_URL_INT_EXP_OUT_HANDOVER + ), + ActionBean( + "出港计重", + R.drawable.img_gjc_chugangjizhong, + Constant.AuthName.GjcCheckWeighing, + ARouterConstants.ACTIVITY_URL_GJC_WEIGHING_LIST + ), + ActionBean( + "出港运抵", + R.drawable.img_gjc_chugang_diyun, + Constant.AuthName.GjcIntExpArrive, + ARouterConstants.ACTIVITY_URL_INT_EXP_ARRIVE + ), + ActionBean( + "出港查询", + R.drawable.gjc_query_icon, + Constant.AuthName.GjcQueryListActivity, + ARouterConstants.ACTIVITY_URL_GJC_QUERY_LIST + ), + ActionBean( + "出港仓库", + R.drawable.gjc_cang_ku_icon, + Constant.AuthName.GjcIntExpStorageUse, + ARouterConstants.ACTIVITY_URL_INT_EXP_STORAGE_USE + ), + ActionBean( + "进港仓库", + R.drawable.gjc_cang_ku_icon, // Pad 端进港仓库菜单同样复用出港仓库图标 + Constant.AuthName.GjjWareHouseActivity, + ARouterConstants.ACTIVITY_URL_GJJ_WARE_HOUSE + ), + ActionBean( + "进港查询", + R.drawable.gjj_query_icon, + Constant.AuthName.GjjQueryListActivity, + ARouterConstants.ACTIVITY_URL_GJJ_QUERY_LIST + ), + ActionBean( + "提取出库", + R.drawable.gjj_chu_ku_icon, + Constant.AuthName.IntImpPickUpDLV, + ARouterConstants.ACTIVITY_URL_INT_IMP_PICK_UP_DLV + ), + // ULD管理属综合管理业务(Pad 端在「综合管理」菜单组),手机端首页暂无综合 Tab,挂在国际 Tab 下 + ActionBean( + "ULD管理", + com.lukouguoji.module_base.R.mipmap.cargo, // 图标复用 Pad 端同款(在 module_base,直接引用) + Constant.AuthName.ComprehensiveUld, // 权限串与 Pad 端菜单一致 + ARouterConstants.ACTIVITY_URL_ULD_LIST + ), + ).filter { SharedPreferenceUtil.getString(Constant.Share.authList).contains(it.key) } + + fun onItemClick(position: Int) { + val bean = actionList.getOrNull(position) ?: return + + // 配了路由的直接跳(5.5 寸适配页走这条) + if (bean.route.isNotEmpty()) { + ARouter.getInstance().build(bean.route).navigation() + return + } + + when (bean.title) { + "普货收运" -> { + PdaShouYunActivity.start(DevUtils.getTopActivity()) + } + + "板箱组装" -> { + PdaZuZhuangActivity.start(DevUtils.getTopActivity()) + } + + "板箱过磅" -> { + PdaGuoBangActivity.start(DevUtils.getTopActivity()) + } + + "进港理货" -> { + PdaLiHuoActivity.start(DevUtils.getTopActivity()) + } + + else -> { + loge("点击事件 ${bean.title} 未处理") + } + } + } + +} diff --git a/module_p/src/main/java/com/sixin/module_p/ui/enter/gn/GnViewModel.kt b/module_p/src/main/java/com/sixin/module_p/ui/enter/gn/GnViewModel.kt index c4873be..907c894 100644 --- a/module_p/src/main/java/com/sixin/module_p/ui/enter/gn/GnViewModel.kt +++ b/module_p/src/main/java/com/sixin/module_p/ui/enter/gn/GnViewModel.kt @@ -1,12 +1,14 @@ package com.sixin.module_p.ui.enter.gn import android.content.Intent +import com.alibaba.android.arouter.launcher.ARouter import com.lukouguoji.module_base.base.BaseViewModel import com.lukouguoji.module_base.bean.ActionBean import com.lukouguoji.module_base.common.Constant import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil import com.lukouguoji.module_base.ktx.logd import com.lukouguoji.module_base.ktx.loge +import com.lukouguoji.module_base.router.ARouterConstants import com.sixin.module_p.R import com.sixin.module_p.ui.enter.gn.document.PdaDocumentHandoverActivity import com.sixin.module_p.ui.gnc.cunfang.list.PdaGncCunFangListActivity @@ -57,11 +59,25 @@ class GnViewModel : BaseViewModel() { R.mipmap.gnc_document_handover, Constant.AuthName.DocumentHandover ), + ActionBean( + "进港移库", + R.drawable.gnj_yi_ku_icon, // 图标复制自 module_gnj(module_p 看不到业务模块资源) + Constant.AuthName.GnjYiKu, // 权限串与 Pad 端菜单一致(AppDomImpMove) + ARouterConstants.ACTIVITY_URL_GNJ_MOVE_STASH_LIST // 与 Pad 首页菜单同一跳转页(app 模块,走路由) + ), ) .filter { SharedPreferenceUtil.getString(Constant.Share.authList).contains(it.key) } fun onItemClick(position: Int) { + val bean = actionList.getOrNull(position) ?: return + + // 配了路由的直接跳(5.5 寸适配页走这条,与 GjViewModel 同模式) + if (bean.route.isNotEmpty()) { + ARouter.getInstance().build(bean.route).navigation() + return + } + when (val title = actionList[position].title) { "出港收运" -> { GncUnShouYunActivity.start(DevUtils.getTopActivity()) diff --git a/module_p/src/main/res/drawable-xxhdpi/gjc_cang_ku_icon.png b/module_p/src/main/res/drawable-xxhdpi/gjc_cang_ku_icon.png new file mode 100644 index 0000000..6dceea0 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/gjc_cang_ku_icon.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/gjc_query_icon.png b/module_p/src/main/res/drawable-xxhdpi/gjc_query_icon.png new file mode 100644 index 0000000..76433e2 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/gjc_query_icon.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/gjc_yi_ku_icon.png b/module_p/src/main/res/drawable-xxhdpi/gjc_yi_ku_icon.png new file mode 100644 index 0000000..8962030 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/gjc_yi_ku_icon.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/gjj_chu_ku_icon.png b/module_p/src/main/res/drawable-xxhdpi/gjj_chu_ku_icon.png new file mode 100644 index 0000000..7cca5d2 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/gjj_chu_ku_icon.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/gjj_query_icon.png b/module_p/src/main/res/drawable-xxhdpi/gjj_query_icon.png new file mode 100644 index 0000000..0e98155 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/gjj_query_icon.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/gnj_yi_ku_icon.png b/module_p/src/main/res/drawable-xxhdpi/gnj_yi_ku_icon.png new file mode 100644 index 0000000..26454c6 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/gnj_yi_ku_icon.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/img_gjc_chugang_diyun.png b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chugang_diyun.png new file mode 100644 index 0000000..430eb62 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chugang_diyun.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/img_gjc_chugangjizhong.png b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chugangjizhong.png new file mode 100644 index 0000000..57ab8e4 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chugangjizhong.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/img_gjc_chuku_jiaojie.png b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chuku_jiaojie.png new file mode 100644 index 0000000..20d20c8 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chuku_jiaojie.png differ