From db062c8ee36b8af2b9b04506af9ffad4f71cf7b8 Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Wed, 29 Jul 2026 14:18:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=87=BA=E6=B8=AF=E7=A7=BB=E5=BA=93/=E5=87=BA=E5=BA=93?= =?UTF-8?q?=E4=BA=A4=E6=8E=A5=E9=A1=B5=E9=9D=A25.5=E5=AF=B8=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E7=AB=AF=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 落地手机端双形态适配框架:同名布局+资源限定符(layout/手机、 layout-sw600dp/平板)自动切换,DeviceUtil 设备形态判定,7个 手机版公共组件(PhoneSearchBar/StatusTab/DataLayout/FilterPanel/ BottomBar/StatBox/KvItem) - 完成「国际出港移库」「国际出港出库交接」两页手机端适配,新增 各自详情页(IntExpMoveDetailActivity/IntExpOutHandoverDetailActivity) - 手机端首页菜单接入"出港移库""出库交接"入口 - 修复手机端进入页面先横屏后转竖屏的闪屏问题:Manifest 全项目 192 处改为 screenOrientation="unspecified",由 BaseActivity 按设备形态运行时锁定方向 - 修复该方案的中间版本在平板端引入的回归(先竖后横 + UI放大1.6倍) - AutoSize 补充手机竖屏 390×844 设计基准 - 修复 CHANGELOG.md 因脚本异常导致的内容重复损坏(膨胀至12万行), 恢复正常结构并补充本次变更记录 Co-Authored-By: Claude Sonnet 5 --- .claude/mcp.json | 14 +- .claude/skills/phone-adapt/SKILL.md | 277 ++++ .../phone-adapt/references/components.md | 120 ++ .../phone-adapt/references/pad-safety.md | 167 +++ .../phone-adapt/references/verification.md | 166 +++ .../phone-adapt/scripts/design_shots.py | 137 ++ .../skills/phone-adapt/scripts/ui_probe.sh | 119 ++ CHANGELOG.md | 223 ++-- CLAUDE.md | 307 ++++- README.md | 627 ++++----- app/src/debug/AndroidManifest.xml | 42 + app/src/main/AndroidManifest.xml | 1154 +++++++++-------- documents/5.5寸手持端-后端字段补充清单.md | 55 + .../5.5寸手持端设计文件/00_全部入口总览.html | 195 +++ .../01_ULD管理/ULD管理.html | 650 ++++++++++ .../02_出库交接/出库交接.html | 715 ++++++++++ .../03_出港计重/detail.html | 151 +++ .../5.5寸手持端设计文件/03_出港计重/main.html | 419 ++++++ .../03_出港计重/records.html | 282 ++++ .../03_出港计重/weighing.html | 256 ++++ .../03_出港计重/weight-detail.html | 306 +++++ .../5.5寸手持端设计文件/04_出港运抵/main.html | 535 ++++++++ .../04_出港运抵/receipt.html | 73 ++ .../04_出港运抵/suborder.html | 413 ++++++ .../04_出港运抵/supplement.html | 175 +++ .../05_出港移库/出港移库.html | 668 ++++++++++ .../06_出港查询/出港查询.html | 963 ++++++++++++++ .../06_出港查询/运单追踪.html | 139 ++ .../07_出港仓库/出港仓库.html | 932 +++++++++++++ .../07_出港仓库/运单追踪.html | 139 ++ .../08_进港仓库/运单追踪.html | 139 ++ .../08_进港仓库/进港仓库.html | 924 +++++++++++++ .../09_提取出库/提取出库.html | 773 +++++++++++ .../10_进港查询/运单追踪.html | 147 +++ .../10_进港查询/进港查询.html | 951 ++++++++++++++ .../11_进港移库/photo.html | 134 ++ .../11_进港移库/进港移库.html | 831 ++++++++++++ documents/5.5寸手持端适配技术方案.md | 312 +++++ ...货运APP-5.5寸手持端UI适配-工时报价明细.xlsx | Bin 0 -> 17364 bytes ...空货运APP-5.5寸手持端适配报价单(对外).xlsx | Bin 0 -> 7521 bytes .../航空货运APP-5.5寸手持端适配报价单.xlsx | Bin 0 -> 11067 bytes .../长卿科技-航空货运作业管理系统报价.xlsx | Bin 0 -> 13433 bytes .../src/main/debug/AndroidManifest.xml | 64 +- .../lukouguoji/module_base/BaseActivity.kt | 39 +- .../lukouguoji/module_base/MyApplication.kt | 21 +- .../module_base/base/BaseBindingActivity.kt | 15 +- .../lukouguoji/module_base/bean/ActionBean.kt | 27 +- .../lukouguoji/module_base/bean/GjcMove.kt | 17 + .../module_base/bean/GjcUldUseBean.kt | 15 + .../ui/weight/phone/PhoneBottomBar.kt | 92 ++ .../ui/weight/phone/PhoneDataLayout.kt | 238 ++++ .../ui/weight/phone/PhoneFilterPanel.kt | 120 ++ .../ui/weight/phone/PhoneKvItem.kt | 100 ++ .../ui/weight/phone/PhoneSearchBar.kt | 139 ++ .../ui/weight/phone/PhoneStatBox.kt | 108 ++ .../ui/weight/phone/PhoneStatusTab.kt | 168 +++ .../ui/weight/phone/PhoneWidgetKtx.kt | 228 ++++ .../lukouguoji/module_base/util/DeviceUtil.kt | 116 ++ .../src/main/release/AndroidManifest.xml | 90 +- .../src/main/res/drawable/bg_phone_badge.xml | 7 + .../res/drawable/bg_phone_btn_outline.xml | 22 + .../res/drawable/bg_phone_btn_primary.xml | 16 + .../src/main/res/drawable/bg_phone_card.xml | 10 + .../drawable/bg_phone_card_header_blue.xml | 9 + .../drawable/bg_phone_card_header_green.xml | 9 + .../main/res/drawable/bg_phone_dot_green.xml | 6 + .../main/res/drawable/bg_phone_icon_round.xml | 9 + .../src/main/res/drawable/bg_phone_input.xml | 10 + .../main/res/drawable/bg_phone_panel_top.xml | 9 + .../src/main/res/drawable/bg_phone_search.xml | 10 + .../main/res/drawable/bg_phone_stat_box.xml | 7 + .../main/res/drawable/bg_phone_tag_gray.xml | 7 + .../main/res/drawable/bg_phone_tag_green.xml | 7 + .../main/res/drawable/bg_phone_tag_orange.xml | 11 + .../main/res/drawable/bg_phone_tag_red.xml | 7 + .../res/drawable/bg_phone_value_orange.xml | 11 + .../main/res/drawable/ic_phone_airplane.xml | 12 + .../main/res/drawable/ic_phone_calendar.xml | 12 + .../res/drawable/ic_phone_check_circle.xml | 12 + .../drawable/ic_phone_check_round_checked.xml | 23 + .../ic_phone_check_round_unchecked.xml | 16 + .../res/drawable/ic_phone_chevron_down.xml | 12 + .../res/drawable/ic_phone_chevron_right.xml | 12 + .../src/main/res/drawable/ic_phone_close.xml | 12 + .../main/res/drawable/ic_phone_location.xml | 12 + .../src/main/res/drawable/ic_phone_person.xml | 12 + .../res/layout-sw600dp/title_tool_bar.xml | 42 + .../res/layout/item_spinner_list_14sp.xml | 15 + .../res/layout/layout_phone_bottom_bar.xml | 64 + .../res/layout/layout_phone_data_layout.xml | 94 ++ .../res/layout/layout_phone_filter_panel.xml | 87 ++ .../res/layout/layout_phone_search_bar.xml | 65 + .../main/res/layout/layout_phone_stat_box.xml | 138 ++ .../src/main/res/layout/title_tool_bar.xml | 109 +- module_base/src/main/res/values/colors.xml | 27 + module_cargo/src/main/AndroidManifest.xml | 20 +- .../src/main/debug/AndroidManifest.xml | 48 +- .../src/main/release/AndroidManifest.xml | 32 +- module_gjc/src/main/debug/AndroidManifest.xml | 48 +- .../gjc/activity/IntExpOutHandoverActivity.kt | 152 ++- .../IntExpOutHandoverDetailActivity.kt | 40 + .../gjc/holder/IntExpMoveViewHolder.kt | 6 + .../gjc/holder/IntExpOutHandoverViewHolder.kt | 69 +- .../gjc/page/move/IntExpMoveActivity.kt | 66 +- .../gjc/page/move/IntExpMoveDetailActivity.kt | 40 + .../viewModel/IntExpMoveDetailViewModel.kt | 25 + .../gjc/viewModel/IntExpMoveViewModel.kt | 74 +- .../IntExpOutHandoverDetailViewModel.kt | 25 + .../viewModel/IntExpOutHandoverViewModel.kt | 100 +- .../src/main/release/AndroidManifest.xml | 162 +-- .../layout-sw600dp/activity_int_exp_move.xml | 206 +++ .../activity_int_exp_out_handover.xml | 197 +++ .../res/layout-sw600dp/item_int_exp_move.xml | 319 +++++ .../item_int_exp_out_handover.xml | 300 +++++ .../main/res/layout/activity_int_exp_move.xml | 374 +++--- .../layout/activity_int_exp_move_detail.xml | 268 ++++ .../layout/activity_int_exp_out_handover.xml | 354 +++-- .../activity_int_exp_out_handover_detail.xml | 263 ++++ .../src/main/res/layout/item_int_exp_move.xml | 521 +++----- .../res/layout/item_int_exp_out_handover.xml | 525 ++++---- module_gjj/src/main/debug/AndroidManifest.xml | 48 +- .../src/main/release/AndroidManifest.xml | 196 +-- module_gnc/src/main/debug/AndroidManifest.xml | 48 +- .../src/main/release/AndroidManifest.xml | 196 +-- module_gnj/src/main/AndroidManifest.xml | 52 +- module_gnj/src/main/debug/AndroidManifest.xml | 86 +- .../src/main/release/AndroidManifest.xml | 170 +-- .../src/main/debug/AndroidManifest.xml | 48 +- .../src/main/release/AndroidManifest.xml | 54 +- module_mit/src/main/debug/AndroidManifest.xml | 48 +- .../src/main/release/AndroidManifest.xml | 84 +- .../sixin/module_p/ui/enter/gj/GjViewModel.kt | 133 +- .../res/drawable-xxhdpi/gjc_yi_ku_icon.png | Bin 0 -> 32732 bytes .../drawable-xxhdpi/img_gjc_chuku_jiaojie.png | Bin 0 -> 32239 bytes 134 files changed, 19602 insertions(+), 2965 deletions(-) create mode 100644 .claude/skills/phone-adapt/SKILL.md create mode 100644 .claude/skills/phone-adapt/references/components.md create mode 100644 .claude/skills/phone-adapt/references/pad-safety.md create mode 100644 .claude/skills/phone-adapt/references/verification.md create mode 100755 .claude/skills/phone-adapt/scripts/design_shots.py create mode 100755 .claude/skills/phone-adapt/scripts/ui_probe.sh create mode 100644 app/src/debug/AndroidManifest.xml create mode 100644 documents/5.5寸手持端-后端字段补充清单.md create mode 100644 documents/5.5寸手持端设计文件/00_全部入口总览.html create mode 100644 documents/5.5寸手持端设计文件/01_ULD管理/ULD管理.html create mode 100644 documents/5.5寸手持端设计文件/02_出库交接/出库交接.html create mode 100644 documents/5.5寸手持端设计文件/03_出港计重/detail.html create mode 100644 documents/5.5寸手持端设计文件/03_出港计重/main.html create mode 100644 documents/5.5寸手持端设计文件/03_出港计重/records.html create mode 100644 documents/5.5寸手持端设计文件/03_出港计重/weighing.html create mode 100644 documents/5.5寸手持端设计文件/03_出港计重/weight-detail.html create mode 100644 documents/5.5寸手持端设计文件/04_出港运抵/main.html create mode 100644 documents/5.5寸手持端设计文件/04_出港运抵/receipt.html create mode 100644 documents/5.5寸手持端设计文件/04_出港运抵/suborder.html create mode 100644 documents/5.5寸手持端设计文件/04_出港运抵/supplement.html create mode 100644 documents/5.5寸手持端设计文件/05_出港移库/出港移库.html create mode 100644 documents/5.5寸手持端设计文件/06_出港查询/出港查询.html create mode 100644 documents/5.5寸手持端设计文件/06_出港查询/运单追踪.html create mode 100644 documents/5.5寸手持端设计文件/07_出港仓库/出港仓库.html create mode 100644 documents/5.5寸手持端设计文件/07_出港仓库/运单追踪.html create mode 100644 documents/5.5寸手持端设计文件/08_进港仓库/运单追踪.html create mode 100644 documents/5.5寸手持端设计文件/08_进港仓库/进港仓库.html create mode 100644 documents/5.5寸手持端设计文件/09_提取出库/提取出库.html create mode 100644 documents/5.5寸手持端设计文件/10_进港查询/运单追踪.html create mode 100644 documents/5.5寸手持端设计文件/10_进港查询/进港查询.html create mode 100644 documents/5.5寸手持端设计文件/11_进港移库/photo.html create mode 100644 documents/5.5寸手持端设计文件/11_进港移库/进港移库.html create mode 100644 documents/5.5寸手持端适配技术方案.md create mode 100644 documents/航空货运APP-5.5寸手持端UI适配-工时报价明细.xlsx create mode 100644 documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx create mode 100644 documents/航空货运APP-5.5寸手持端适配报价单.xlsx create mode 100644 documents/长卿科技-航空货运作业管理系统报价.xlsx create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneBottomBar.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneDataLayout.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneKvItem.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneSearchBar.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatBox.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneWidgetKtx.kt create mode 100644 module_base/src/main/java/com/lukouguoji/module_base/util/DeviceUtil.kt create mode 100644 module_base/src/main/res/drawable/bg_phone_badge.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_btn_outline.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_btn_primary.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_card.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_card_header_blue.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_card_header_green.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_dot_green.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_icon_round.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_input.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_panel_top.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_search.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_stat_box.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_tag_gray.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_tag_green.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_tag_orange.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_tag_red.xml create mode 100644 module_base/src/main/res/drawable/bg_phone_value_orange.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_airplane.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_calendar.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_check_circle.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_check_round_checked.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_check_round_unchecked.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_chevron_down.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_chevron_right.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_close.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_location.xml create mode 100644 module_base/src/main/res/drawable/ic_phone_person.xml create mode 100644 module_base/src/main/res/layout-sw600dp/title_tool_bar.xml create mode 100644 module_base/src/main/res/layout/item_spinner_list_14sp.xml create mode 100644 module_base/src/main/res/layout/layout_phone_bottom_bar.xml create mode 100644 module_base/src/main/res/layout/layout_phone_data_layout.xml create mode 100644 module_base/src/main/res/layout/layout_phone_filter_panel.xml create mode 100644 module_base/src/main/res/layout/layout_phone_search_bar.xml create mode 100644 module_base/src/main/res/layout/layout_phone_stat_box.xml create mode 100644 module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverDetailActivity.kt create mode 100644 module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveDetailActivity.kt create mode 100644 module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveDetailViewModel.kt create mode 100644 module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverDetailViewModel.kt create mode 100644 module_gjc/src/main/res/layout-sw600dp/activity_int_exp_move.xml create mode 100644 module_gjc/src/main/res/layout-sw600dp/activity_int_exp_out_handover.xml create mode 100644 module_gjc/src/main/res/layout-sw600dp/item_int_exp_move.xml create mode 100644 module_gjc/src/main/res/layout-sw600dp/item_int_exp_out_handover.xml create mode 100644 module_gjc/src/main/res/layout/activity_int_exp_move_detail.xml create mode 100644 module_gjc/src/main/res/layout/activity_int_exp_out_handover_detail.xml create mode 100644 module_p/src/main/res/drawable-xxhdpi/gjc_yi_ku_icon.png create mode 100644 module_p/src/main/res/drawable-xxhdpi/img_gjc_chuku_jiaojie.png diff --git a/.claude/mcp.json b/.claude/mcp.json index 9270fdd..e5ee220 100644 --- a/.claude/mcp.json +++ b/.claude/mcp.json @@ -1,14 +1,10 @@ { "mcpServers": { - "空港集团 - API 文档": { - "command": "/Users/kid/.version-fox/sdks/nodejs/bin/npx", - "args": [ - "-y", - "apifox-mcp-server@latest", - "--project-id=7382863" - ], - "env": { - "APIFOX_ACCESS_TOKEN": "APS-S2aVVwqasbdByzPLgSqryRC8BB0ZFqhQ" + "api-doc": { + "type": "http", + "url": "https://www.agentfoxapp.com/mcp/976caff7-5f98-4487-bccf-3aa20c92cf1f", + "headers": { + "Authorization": "Bearer afk_G_VKmHTdXUi3GjQdg5AeHeH-0BSYQEPP" } } } diff --git a/.claude/skills/phone-adapt/SKILL.md b/.claude/skills/phone-adapt/SKILL.md new file mode 100644 index 0000000..011853a --- /dev/null +++ b/.claude/skills/phone-adapt/SKILL.md @@ -0,0 +1,277 @@ +--- +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 状态、新下拉、新排序)→ **停下来问用户**要用什么接口参数名, + 连带问数据源(如「交接人」用 `DictUtils.getWHSUserList()`) + +这一步不能猜。出库交接就是实测发现后端 `pageQuery` 根本不认 `hoState`,两个 Tab 返回同一批数据—— +参数名对不对只有后端知道。用 `AskUserQuestion` 一次问清,并在阶段 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( + /* 原有字段… */ + "hoState" to handoverState.value?.ifEmpty { null }, +) + +// 手机专属的额外请求(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 +$P req pageQuery # 核对请求体参数确实带上了新字段 +``` + +逐项走查:列表/卡片两种形态、详情页、筛选弹层(重置+确认)、Tab 切换、单选与全选联动、 +下拉真实取数、扫码入口。每步 `crash` 都要为 0。 + +点完**一定要断言效果**(`texts` 或截图),不要假设点中了——加载弹窗盖屏时 `input tap` 会被静默吞掉。 + +手机端还要确认方向没闪屏(冷启动采样 `mRotation` 应恒为 0),命令见 `references/verification.md`。 + +**Pad 端回归**(必做):同样安装到平板模拟器打开同一页面截图,与改造前逐项对比—— +搜索条数量、默认值、按钮、底部统计都应一致。改过 `module_base` 公共组件时这步尤其不能省。 + +## 阶段 6:与设计稿二次核对 + +把阶段 1 的设计稿截图和阶段 5 的实机截图放在一起,用 Read 工具对照看,逐项核对: + +配色 / 字号 / 字重 / 圆角 / 内外间距 / 图标形状与大小 / 控件顺序 / 选中态 / 状态标签配色 / +**空值兜底**(真实数据空字段很常见——出库交接就出现过「库位」为空时橙色高亮标签只剩一个孤零零的 +小色块,需要空值时不套标签底)。 + +服务端当天没数据时(列表空)先放宽条件重查;仍为空就临时在 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,把**定位手法**也写上,不只写结论 +- `CHANGELOG.md` 追加条目(新增 / 改进 / 修复 / 验证 四段),记忆文件 + `phone-5.5inch-adaptation.md` 同步 + +向用户汇报时明确写出这四块: + +1. 新增/修改的文件清单(布局、组件、Activity/ViewModel/Bean) +2. Pad 端零破坏的具体做法 + 回归结论 +3. 复用与新增的公共组件 +4. **待确认事项**:后端未生效的参数、自己推断的业务规则(如 IMP 代码红/绿的判定)、 + 设计稿与平板功能的差异。如实说,别含糊。 + +## 参考文件 + +- `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/components.md b/.claude/skills/phone-adapt/references/components.md new file mode 100644 index 0000000..e3a26a0 --- /dev/null +++ b/.claude/skills/phone-adapt/references/components.md @@ -0,0 +1,120 @@ +# 手机版公共组件(`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 → 橙色高亮标签) | + +`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_badge` + `bg_phone_tag_green/gray/red/orange` `bg_phone_card_header_blue/green` +- 图标:`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`(圆形复选框) +- 复用 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..9cc07d5 --- /dev/null +++ b/.claude/skills/phone-adapt/references/pad-safety.md @@ -0,0 +1,167 @@ +# 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`。 + +## 业务层零破坏的三个模式 + +### 新增查询维度:默认空值 + 仅非空进请求 + +平板布局没有对应控件 → 字段恒为空 → 不进请求体 → 平板查询与改造前逐字节一致。 + +```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 必须写资源引用 + +新增 Activity 注册时: + +```xml + +``` + +**不要写死 `userLandscape`。** `screenOrientation` 由系统在创建 Activity 窗口时生效, +那时业务代码还没跑;写死横屏后即使 `BaseActivity` 在 `super.onCreate` 之前改成竖屏, +用户也会看到「先横屏、约 1 秒后转竖屏」的闪屏和重建。 + +资源取值:`module_base/res/values/integers.xml` = 1(portrait), +`values-sw600dp/integers.xml` = 11(userLandscape)。全项目已统一替换。 + +## 改 module_base 公共代码时 + +`module_base` 被所有业务模块依赖,改动会影响全部页面。原则: + +- **只做增量**:加新组件、新颜色、新 drawable,别改既有组件的行为 +- 确实要改公共基类(`BaseActivity` / `BaseBindingActivity` / Pad 系组件)时, + **必须在平板上回归**至少 2~3 个不同类型的既有页面 +- Bean 上加只读计算属性是安全的(Gson 只序列化字段,不动 getter); + 加**字段**要谨慎,会进请求体 + +## 已知的既有问题(不是你改坏的) + +用 `am start` 直启页面时,若**先经过首页**再启目标页,会崩在 +`LoadingModel.showLoading` → XPopup `popupInfo is null`。在**未改动**的页面上同样复现, +属于既有缺陷。**冷启动直启不受影响**(`force-stop` 后直接 `am start`)。 + +遇到崩溃先做这个对照实验再下结论:拿一个你没碰过的页面用同样方式打开,看是否同样崩。 diff --git a/.claude/skills/phone-adapt/references/verification.md b/.claude/skills/phone-adapt/references/verification.md new file mode 100644 index 0000000..0499784 --- /dev/null +++ b/.claude/skills/phone-adapt/references/verification.md @@ -0,0 +1,166 @@ +# 端到端验证手册 + +目标:在**真实接口**下把手机端走通,并确认平板端没退化。只看编译通过是不够的—— +本次适配里"筛选条件被静默清空"和"后端不认新参数"两个问题,都只有跑真机才暴露。 + +## 环境 + +```bash +~/Library/Android/sdk/emulator/emulator -list-avds # Medium_Phone_API_36.1(手机) / Aerologic_Tablet(平板) +adb devices -l +adb -s shell wm size && adb -s shell wm density +``` + +判定形态:手机 1080×2400@420dpi → `PHONE(sw=411dp)`;平板 1280×800@160dpi → `TABLET(sw=800dp)`。 + +`ui_probe.sh` 封装了常用操作,比手写 adb 省事且更可靠: + +```bash +P=.claude/skills/phone-adapt/scripts/ui_probe.sh +$P form # 打印形态识别结果 +$P texts # 当前界面所有可见文字(做断言用) +$P bounds "已交接" # 元素 bounds + 中心点 +$P tap-text "确认" # 按文字点击(自动取中心点) +$P wait-text "待交接" # 等页面就绪 +$P wait-gone "请稍候……" # 等加载弹窗消失 +$P shot /tmp/a.png # 截图 +$P req pageQuery # 抓请求/响应体 +$P crash # 崩溃次数 + 栈 +``` + +## 构建安装 + +```bash +./gradlew assembleDebug 2>&1 | grep -E "^(BUILD|FAILURE)|error:" +adb -s install -r -d app/build/outputs/apk/debug/app-debug.apk +``` + +注意 gradle 输出里大量 `e: 注: ARouter::Compiler …` 是注解处理器的**提示**被误标成 `e:`, +不是错误;只看 `BUILD SUCCESSFUL` / `error:` 就行。 + +## 进入目标页面的两条路 + +### 正规路径(优先):登录 → 首页菜单 + +```bash +adb -s shell monkey -p com.lukouguoji.aerologic -c android.intent.category.LAUNCHER 1 +``` + +登录页账号密码需要用户提供。填表用 `input text`,注意 `#` 等字符要转义(`Aa123456\#`)。 +底部有「首页 / 国内 / 国际 / 我的」Tab,国际业务页找对应入口。 + +**首页菜单是按权限过滤的**:当前账号角色没有该权限时,入口根本不显示(本次 ADMIN 登录后 +国际业务只有「板箱过磅」「进港理货」,没有「出库交接」)。这时走下面的直启。 + +### 直启(权限缺失时的替代):冷启动 + am start + +`app/src/debug/AndroidManifest.xml` 里把调试页临时 `exported="true"`,就能直接拉起。 +**必须冷启动**(先 force-stop),否则会撞上既有的 LoadingModel 崩溃: + +```bash +adb -s shell am force-stop com.lukouguoji.aerologic +sleep 2 +adb -s shell am start -n com.lukouguoji.aerologic/<完整类名> +``` + +登录态存在 SharedPreference 里,冷启动直启同样带 token,接口能正常返回真实数据。 + +> 先经首页再 `am start` 会崩在 `LoadingModel.showLoading` → XPopup `popupInfo is null`, +> 这是既有缺陷(未改动的页面也复现),不是本次改动引入的。 + +## 点击前先等就绪 + +加载弹窗("请稍候……")会盖住整屏并**吞掉 `input tap`**,表现为"点了没反应"。 +本次验证就因此白跑了几轮。所以: + +```bash +$P wait-text "待交接" # 等目标元素出现 +$P tap-text "已交接" +$P texts | grep -c "全选" # 用文字变化确认点击确实生效 +``` + +点完**一定要断言效果**(用 `texts` 或截图),别假设点中了。 + +## 逐项走查清单 + +每一步做完都跑一次 `crash`(必须为 0): + +- [ ] 列表加载:卡片字段、各种状态形态(如待处理 / 已完成两种卡片) +- [ ] Tab 切换:指示条位置、角标数字、底部条按设计显隐 +- [ ] 筛选弹层:打开、各控件取值、重置、确认后重新查询 +- [ ] 下拉字典:真实取到数据(不是空列表) +- [ ] 多选:单选、全选联动、"已选 N 项"计数 +- [ ] 详情页/二级页:字段绑定、条件显示的区块 +- [ ] 扫码入口能拉起 + +## 核对请求参数是否真的生效 + +新增的查询字段要确认两件事:客户端**发出去了**,服务端**认**。 + +```bash +$P req pageQuery +``` + +OkHttp 日志是多行 pretty JSON。看到请求体里有你的字段(如 `"hoState": "0"`)只说明客户端对了。 +**服务端是否生效要用行为验证**:切到另一个 Tab,看返回数据是否真的变了。 + +本次实测:请求正确携带 `hoState:"0"/"1"`,但两个 Tab 返回同一批数据 → 后端没做过滤。 +这种情况**必须在交付说明里如实标注**,不能让它看起来是通的。 + +## 无数据时怎么核对 UI + +服务端当天可能没数据(列表空)。放宽条件(清空日期)再查;仍为空时,临时注入样例 Bean: + +```kotlin +// 临时:覆盖各种形态各一条,验证后删除 +binding.rv.postDelayed({ + binding.rv.commonAdapter()?.refresh(listOf(/* 待处理 / 危险品 / 已完成 各一条 */)) +}, 2500) +``` + +截图核对完**必须删除**,然后重新构建 + grep 确认无残留: + +```bash +grep -rn "injectMockForVisualCheck\|TODO 临时" module_base/src module_gjc/src | wc -l # 应为 0 +``` + +## 屏幕方向验证 + +新增/修改 Activity 后,确认手机端不会「先横屏再转竖屏」: + +```bash +# 冷启动时连续采样:应全程 mRotation=0 +adb -s shell am force-stop com.lukouguoji.aerologic; sleep 2 +adb -s shell monkey -p com.lukouguoji.aerologic -c android.intent.category.LAUNCHER 1 +for i in $(seq 1 10); do adb -s shell dumpsys window | grep -m1 -oE "mRotation=[0-9]+"; sleep 0.4; done +``` + +平板则应首屏就是横屏(截图宽 > 高)。若手机出现横屏帧,检查该 Activity 的 Manifest +是否漏写 `android:screenOrientation="@integer/screen_orientation"`。 + +## Pad 端回归(必做) + +```bash +adb -s install -r -d app/build/outputs/apk/debug/app-debug.apk +adb -s shell am start -n com.lukouguoji.aerologic/<同一个类名> +$P shot /tmp/tablet.png +$P form # 应为 TABLET(sw=800dp…) +``` + +对比改造前后:搜索条数量与顺序、日期默认值、按钮(平板可能比手机多按钮)、底部统计文案。 +改过 `module_base` 里的公共基类或 Pad 系组件时,这一步不能省,且要多看几个页面。 + +## 定位"值被神秘修改"类问题 + +在 setter 里打带堆栈的日志,一次就能看到调用来源: + +```kotlin +android.util.Log.d("DBG", "value '$field' -> '$v'", Throwable("trace")) +``` + +```bash +adb -s logcat -d -s DBG | grep -A14 "'旧值' -> ''" | grep "at com.lukouguoji\|at android" +``` + +本次靠这招定位到 `TextView.onRestoreInstanceState → EditText.setText → doOnTextChanged`, +即复合自定义 View 的 id 串档(见 `components.md`)。**排完删日志。** 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..5fb81c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,101 +1,122 @@ -# 版本变更日志 (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/)。 + +## [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 fd8b34d..fe62cfb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -839,6 +839,281 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志 --- +## 5.5 寸手持端(手机版)双形态适配规范 + +项目需同时支持 **10 寸平板横屏** 与 **5.5 寸手机竖屏**。核心原则:**业务逻辑层完全复用,只重写布局**。 + +### 核心机制:布局按资源限定符自动切换 + +``` +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. **回到手机端首页加菜单入口**(最易遗忘,见下节)——不加的话页面在手机上根本进不去 + +### 手机端首页菜单入口(适配完必须加,否则没入口) + +手机端首页是 `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` | + +**`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`(框架参考页,筛选弹层内暂仍用 `PadSearchLayout`) + - 「国际出港出库交接」`IntExpOutHandoverActivity` + 「板箱详情」`IntExpOutHandoverDetailActivity` + (公共组件首个完整落地页,双端实机验证通过) +- ⏳ **待办**: + - 把「移库」页筛选弹层内的 `PadSearchLayout` 换成 `PhoneDataLayout`,视觉统一 + - `PhoneSearchBar` 接入 AutoQuery 联想(`AutoQueryManager` 目前只对接了 Pad 系组件) + +### 调试便利:直接拉起页面 + +`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 路径速查 ### 基类与常用类 @@ -870,6 +1145,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` | @@ -896,6 +1172,12 @@ 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`(见「屏幕方向」小节的 ⛔ 说明) | --- @@ -961,10 +1243,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 寸手持端双形态适配规范」 ### 常见业务操作 @@ -1112,6 +1400,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,禁止直接写死色值 ### 环境配置 @@ -1125,4 +1416,16 @@ 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 # 可用:Medium_Phone_API_36.1(手机)、Aerologic_Tablet(平板) +adb -s <设备> shell wm size && adb -s <设备> shell wm density # 确认屏幕参数 +adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果 PHONE(sw=411dp) / TABLET(sw=800dp) +adb -s <设备> exec-out screencap -p > /tmp/shot.png # 截图比对 +``` + +> 改动 `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/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..f0988cb --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 932eace..e0d5ebd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,572 +1,584 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/航空货运APP-5.5寸手持端UI适配-工时报价明细.xlsx b/documents/航空货运APP-5.5寸手持端UI适配-工时报价明细.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b04fdfb8ac063ab97fb273e6b1171ce20fac4ab4 GIT binary patch literal 17364 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

@Kp|qKxHDm0RLJV~F@?&C6$x5dG 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 literal 0 HcmV?d00001 diff --git a/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx b/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8fb6f7bbee51b78abc7c1d62e0c8c0b1a7c4d625 GIT binary patch literal 7521 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 b{#UnBQ$%^3r~v@zk6*FJ_>M*P$KC$`>wllu literal 0 HcmV?d00001 diff --git a/documents/航空货运APP-5.5寸手持端适配报价单.xlsx b/documents/航空货运APP-5.5寸手持端适配报价单.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0d7820217ae95de1ea3556e1e018fc10415b7ee6 GIT binary patch literal 11067 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%0D)7_LugwA)d|IYn^=!+ghc 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 ISblx_KV9iud;kCd literal 0 HcmV?d00001 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/GjcMove.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjcMove.kt index 36276c7..deded4b 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 @@ -56,4 +56,21 @@ 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 } + + /** + * 详情页字段统一空值占位。 + * 接口对 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 ccb0add..3bf0e32 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 @@ -108,4 +108,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/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..8d1c88c --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneBottomBar.kt @@ -0,0 +1,92 @@ +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 寸手持端公共组件) + * + * 结构:全选(圆形勾选框 + 文字)+ 已选统计 + 主操作按钮。 + * 对应设计稿各多选列表页底部的白色操作条。 + * + * 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 + + /** 全选状态(驱动勾选框图标切换) */ + 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 + } + + 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) + } + + /** 只保存/恢复自身状态,避免内部子控件 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..ba4c69e --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneFilterPanel.kt @@ -0,0 +1,120 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.util.AttributeSet +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) { + + 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/PhoneKvItem.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneKvItem.kt new file mode 100644 index 0000000..aaa1ac0 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneKvItem.kt @@ -0,0 +1,100 @@ +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() + } + + /** + * 空值不套高亮标签底 —— 否则占位符 "-" 会显示成一个孤零零的橙色小方块。 + */ + private fun applyStyle() { + if (tag && value.isNotEmpty()) { + 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..b16b0b3 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneStatusTab.kt @@ -0,0 +1,168 @@ +package com.lukouguoji.module_base.ui.weight.phone + +import android.content.Context +import android.util.AttributeSet +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) { + + /** 双向绑定回调 */ + 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..326658b --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/phone/PhoneWidgetKtx.kt @@ -0,0 +1,228 @@ +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", requireAll = false) +fun setPhoneKvItemAttrs(item: PhoneKvItem, title: String?, value: String?, tag: Boolean?) { + title?.let { item.title = it } + // tag 需先于 value 生效,确保样式切换后再填内容 + tag?.let { item.tag = it } + item.value = value.orEmpty() +} 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 8201453..5dd7009 100644 --- a/module_base/src/main/release/AndroidManifest.xml +++ b/module_base/src/main/release/AndroidManifest.xml @@ -1,46 +1,46 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + \ 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_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_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_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_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_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_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_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_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_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_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_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/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/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..aa8dc8c --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_bottom_bar.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + 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..1499ea2 --- /dev/null +++ b/module_base/src/main/res/layout/layout_phone_filter_panel.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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..d88b3ee 100644 --- a/module_base/src/main/res/values/colors.xml +++ b/module_base/src/main/res/values/colors.xml @@ -47,4 +47,31 @@ #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 + \ 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/IntExpOutHandoverActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverActivity.kt index 447e86d..25e1f83 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("国际出港出库交接") + 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.hoId.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/holder/IntExpMoveViewHolder.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpMoveViewHolder.kt index e94899a..9a1aa54 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 @@ -2,6 +2,7 @@ package com.lukouguoji.gjc.holder import android.view.View import com.lukouguoji.gjc.databinding.ItemIntExpMoveBinding +import com.lukouguoji.gjc.page.move.IntExpMoveDetailActivity import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.bean.GjcMove import com.lukouguoji.module_base.common.ConstantEvent @@ -20,6 +21,11 @@ class IntExpMoveViewHolder(view: View) : binding.bean = bean binding.position = position + // 手机版「查看详情」→ 运单详情页(平板变体无此控件,为 null 时跳过) + binding.llDetail?.setOnClickListener { + IntExpMoveDetailActivity.start(itemView.context, bean) + } + // 添加图标点击事件 - 切换选择状态 binding.ivIcon.setOnClickListener { bean.checked.set(!bean.checked.get()) // 反转选择状态 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/page/move/IntExpMoveActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveActivity.kt index d9edbf7..26dc0a1 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 @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AlertDialog +import androidx.databinding.ObservableBoolean import com.alibaba.android.arouter.facade.annotation.Route import com.lukouguoji.gjc.R import com.lukouguoji.gjc.databinding.ActivityIntExpMoveBinding @@ -16,8 +16,10 @@ 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 /** * 国际出港-出港移库 @@ -25,14 +27,26 @@ import com.lukouguoji.module_base.router.ARouterConstants @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("国际出港移库") binding.viewModel = viewModel binding.activity = this + // 手机版专属数据:默认「待移库」Tab、Tab 角标计数、特码下拉 + // 平板端不调用,查询链路与请求次数与改造前完全一致 + if (DeviceUtil.isPhone()) { + viewModel.initPhoneExtras() + } + initRecyclerView() initListeners() observeData() @@ -55,8 +69,8 @@ class IntExpMoveActivity : BaseBindingActivity - viewModel.submitMove() - } - .setNegativeButton("取消", null) - .show() + ConfirmDialogModel( + message = "确定要移库选中的 ${selectedItems.size} 条运单吗?", + title = "移库确认" + ) { + viewModel.submitMove() + }.show() } /** diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveDetailActivity.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveDetailActivity.kt new file mode 100644 index 0000000..80b3f91 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/page/move/IntExpMoveDetailActivity.kt @@ -0,0 +1,40 @@ +package com.lukouguoji.gjc.page.move + +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.ActivityIntExpMoveDetailBinding +import com.lukouguoji.gjc.viewModel.IntExpMoveDetailViewModel +import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.bean.GjcMove +import com.lukouguoji.module_base.common.Constant + +/** + * 国际出港移库「运单详情」(手机版专属页面) + * + * 由手机版列表卡片的「查看详情」进入;平板端列表布局不含该入口,故不会走到本页。 + */ +class IntExpMoveDetailActivity : + BaseBindingActivity() { + + override fun layoutId() = R.layout.activity_int_exp_move_detail + override fun viewModelClass() = IntExpMoveDetailViewModel::class.java + + override fun initOnCreate(savedInstanceState: Bundle?) { + setBackArrow("运单详情") + binding.viewModel = viewModel + viewModel.initOnCreated(intent) + } + + companion object { + @JvmStatic + fun start(context: Context, bean: GjcMove) { + context.startActivity( + Intent(context, IntExpMoveDetailActivity::class.java) + .putExtra(Constant.Key.DATA, Gson().toJson(bean)) + ) + } + } +} diff --git a/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveDetailViewModel.kt b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveDetailViewModel.kt new file mode 100644 index 0000000..f411354 --- /dev/null +++ b/module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpMoveDetailViewModel.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.GjcMove +import com.lukouguoji.module_base.common.Constant + +/** + * 国际出港移库「运单详情」ViewModel(手机版专属页面) + * + * 列表项数据已包含详情页所需全部字段,故直接由列表带入,不额外请求接口。 + */ +class IntExpMoveDetailViewModel : BaseViewModel() { + + val dataBean = MutableLiveData(GjcMove()) + + fun initOnCreated(intent: Intent) { + val json = intent.getStringExtra(Constant.Key.DATA).orEmpty() + if (json.isNotEmpty()) { + dataBean.value = Gson().fromJson(json, GjcMove::class.java) + } + } +} 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..d494e50 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,48 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { // ========== 搜索字段 ========== val awbType = MutableLiveData("") // 运单类型 val by1 = MutableLiveData("") // 承运人 - val dest1 = MutableLiveData("") // 卸货站 + val dest1 = MutableLiveData("") // 卸货站(仅平板筛选,手机版设计稿无此项) val moveState = MutableLiveData("") // 移库状态 val waybillNo = MutableLiveData("") // 运单号 + // ========== 手机版(5.5 寸手持端)专用 ========== + // 平板布局无对应控件,spCode 恒为空 → 不进请求,平板查询行为与改造前一致 + val spCode = MutableLiveData("") // 特码 + + /** 特码下拉数据(仅手机版筛选弹层使用,由 [initPhoneExtras] 触发加载) */ + val spCodeList = MutableLiveData>(emptyList()) + + /** 手机版状态 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" + // flag=1 国际;ieFlag 必须留空——传 "E" 后端返回空数组, + // 与 GjcWeighingStartViewModel 等既有国际出港页面的调用方式保持一致 + DictUtils.getSpecialCodeList(addAll = false, flag = 1, ieFlag = "") { list -> + spCodeList.postValue(list) + } + } + // ========== 运单类型下拉数据 ========== val awbTypeList = MutableLiveData>().apply { value = listOf( @@ -55,6 +94,8 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { // ========== 适配器配置 ========== val itemViewHolder = IntExpMoveViewHolder::class.java + + // 布局 id 不区分设备,平板/手机由资源限定符自动选择(layout-sw600dp/ 与 layout/ 下的同名布局) val itemLayoutId = R.layout.item_int_exp_move /** @@ -100,6 +141,8 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { } else { isAllChecked.value = false } + + selectedCount.value = list.count { it.checked.get() }.toString() } /** @@ -153,6 +196,8 @@ class IntExpMoveViewModel : BasePageViewModel(), IOnItemClickListener { dest1.value?.takeIf { it.isNotEmpty() }?.let { filterParams["dest1"] = it } moveState.value?.takeIf { it.isNotEmpty() }?.let { filterParams["moveState"] = it } waybillNo.value?.takeIf { it.isNotEmpty() }?.let { filterParams["wbNo"] = it } + // 手机版新增筛选维度,平板端恒为空故不参与查询 + spCode.value?.takeIf { it.isNotEmpty() }?.let { filterParams["spCode"] = it } // 列表参数(含分页) val listParams = (filterParams + mapOf( @@ -172,6 +217,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 c62d51f..51e3efe 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,8 @@ 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 com.lukouguoji.module_base.util.DictUtils +import dev.utils.app.info.KeyValue import dev.utils.common.DateUtils import com.lukouguoji.module_base.ktx.formatDate import kotlinx.coroutines.launch @@ -33,21 +35,67 @@ class IntExpOutHandoverViewModel : BasePageViewModel() { val fdest = MutableLiveData("") // 目的站 val uldNo = MutableLiveData("") // ULD编号 + // ========== 手机版(5.5 寸手持端)专用筛选条件 ========== + // 平板布局不含对应控件,两者保持空值 → 不会进入请求参数,平板查询行为与改造前完全一致 + val handoverState = MutableLiveData("") // 交接状态:"" 全部 / "0" 待交接 / "1" 已交接 + val hoId = MutableLiveData("") // 交接人(仓管编码) + + /** 交接人下拉数据(仅手机版筛选弹层使用,由 [initPhoneExtras] 触发加载) */ + val hoUserList = MutableLiveData>(emptyList()) + + /** 手机版状态 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" + DictUtils.getWHSUserList(addAll = false) { list -> + hoUserList.postValue(list) + } + } + + 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 +118,7 @@ class IntExpOutHandoverViewModel : BasePageViewModel() { val shouldCheckAll = !isAllChecked.value!! list.forEach { it.checked.set(shouldCheckAll) } isAllChecked.value = shouldCheckAll + updateSelectedCount() pageModel.rv?.commonAdapter()?.notifyDataSetChanged() } @@ -168,7 +217,10 @@ 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 }, + // 以下两项为手机版新增筛选维度,平板端保持空值故不参与查询 + "hoState" to handoverState.value?.ifEmpty { null }, + "hoId" to hoId.value?.ifEmpty { null } ) // 列表参数 (含分页) @@ -193,5 +245,33 @@ class IntExpOutHandoverViewModel : BasePageViewModel() { totalWeight.value = (data?.totalWeight ?: 0.0).toString() } } + + // 手机版 Tab 角标计数(平板端不触发,无额外请求) + if (phoneExtrasEnabled) { + loadTabCounts(filterParams) + } + } + + /** + * 加载「待交接 / 已交接」Tab 角标数量。 + * + * 复用统计接口,分别以 hoState=0 / 1 各查一次,且沿用当前其余筛选条件, + * 保证角标与列表口径一致。仅手机版调用。 + */ + private fun loadTabCounts(filterParams: Map) { + listOf("0", "1").forEachIndexed { index, state -> + val params = (filterParams + mapOf("hoState" to state)).toRequestBody() + launchCollect({ NetApply.api.getIntExpOutHandoverTotal(params) }) { + onSuccess = { result -> + val count = (result.data?.wbNumber ?: 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/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_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_out_handover.xml b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_out_handover.xml new file mode 100644 index 0000000..38541af --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/activity_int_exp_out_handover.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..2ecdcdf --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_move.xml @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..de4737a --- /dev/null +++ b/module_gjc/src/main/res/layout-sw600dp/item_int_exp_out_handover.xml @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..27e942d 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,168 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/module_gjc/src/main/res/layout/activity_int_exp_move_detail.xml b/module_gjc/src/main/res/layout/activity_int_exp_move_detail.xml new file mode 100644 index 0000000..7698a48 --- /dev/null +++ b/module_gjc/src/main/res/layout/activity_int_exp_move_detail.xml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 a78b0ba..6d781bf 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,192 +1,162 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..bcb110d --- /dev/null +++ b/module_gjc/src/main/res/layout/activity_int_exp_out_handover_detail.xml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 2ecdcdf..7beda0a 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,319 +1,202 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 de4737a..37cb6b5 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,300 +1,225 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/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_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 268892c..7305989 100644 --- a/module_hangban/src/main/release/AndroidManifest.xml +++ b/module_hangban/src/main/release/AndroidManifest.xml @@ -1,28 +1,28 @@ - - - - - - - - - - + + + + + + + + + + \ 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..81dcab1 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,85 @@ -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 + ), + ).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/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 0000000000000000000000000000000000000000..8962030876f328948b40f6c8ecceb91796594049 GIT binary patch literal 32732 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