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 0000000..b04fdfb Binary files /dev/null and b/documents/航空货运APP-5.5寸手持端UI适配-工时报价明细.xlsx differ diff --git a/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx b/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx new file mode 100644 index 0000000..8fb6f7b Binary files /dev/null and b/documents/航空货运APP-5.5寸手持端适配报价单(对外).xlsx differ diff --git a/documents/航空货运APP-5.5寸手持端适配报价单.xlsx b/documents/航空货运APP-5.5寸手持端适配报价单.xlsx new file mode 100644 index 0000000..0d78202 Binary files /dev/null and b/documents/航空货运APP-5.5寸手持端适配报价单.xlsx differ diff --git a/documents/长卿科技-航空货运作业管理系统报价.xlsx b/documents/长卿科技-航空货运作业管理系统报价.xlsx new file mode 100644 index 0000000..717ba27 Binary files /dev/null and b/documents/长卿科技-航空货运作业管理系统报价.xlsx differ diff --git a/module_base/src/main/debug/AndroidManifest.xml b/module_base/src/main/debug/AndroidManifest.xml index f6ebe9f..3bc8754 100644 --- a/module_base/src/main/debug/AndroidManifest.xml +++ b/module_base/src/main/debug/AndroidManifest.xml @@ -1,33 +1,33 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt b/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt index 193ceb2..3efee48 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt @@ -2,6 +2,7 @@ package com.lukouguoji.module_base import android.content.Context import android.content.Intent +import android.content.pm.ActivityInfo import android.content.res.Configuration import android.content.res.Resources import android.net.Uri @@ -26,6 +27,7 @@ import com.yanzhenjie.permission.runtime.Permission import com.yzq.zxinglibrary.android.CaptureActivity import com.yzq.zxinglibrary.bean.ZxingConfig import com.yzq.zxinglibrary.common.Constant +import com.lukouguoji.module_base.util.DeviceUtil import dev.utils.app.HandlerUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -49,15 +51,48 @@ open class BaseActivity : AppCompatActivity(), CoroutineScope { private var timerTask: TimerTask? = null override fun onCreate(savedInstanceState: Bundle?) { + // 必须在 super.onCreate 之前设置,避免进入页面后再旋转造成闪屏与重建 + applyDeviceOrientation() super.onCreate(savedInstanceState) -// requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE setContentView(R.layout.activity_base) - Log.d("BaseActivity", javaClass.simpleName) + Log.d("BaseActivity", "${javaClass.simpleName} ${DeviceUtil.describe()}") ActivityCollector.addActivity(this) ARouter.getInstance().inject(this); /////////必须要写这行,自动注入解析 loadingDialog = LoadingDialog(this) } + /** + * 按设备形态锁定屏幕方向 —— **兜底手段** + * + * 方向的主控权在 Manifest:各 Activity 统一写 + * `android:screenOrientation="@integer/screen_orientation"`, + * 由资源限定符取值(`values/` = portrait,`values-sw600dp/` = userLandscape)。 + * 这样方向在**窗口创建阶段**就已确定,不会出现"先横屏、约 1 秒后转竖屏"的闪屏。 + * + * 本方法只作为安全网:万一某个 Activity 的 Manifest 漏配(仍写死 userLandscape), + * 至少还能在手机上纠正为竖屏——但那种情况下闪屏依旧存在,应当去补 Manifest。 + * + * 子类如需特殊方向(如扫码页),覆写 [lockOrientationByDevice] 返回 false 即可跳过。 + */ + protected open fun applyDeviceOrientation() { + if (!lockOrientationByDevice()) return + try { + requestedOrientation = if (DeviceUtil.isPhone(this)) { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } else { + ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + } + } catch (e: Exception) { + // 半透明 / 悬浮类 Activity 不允许指定方向,忽略即可 + Log.w("BaseActivity", "setRequestedOrientation failed: ${e.message}") + } + } + + /** + * 是否由基类按设备形态自动锁定方向,默认开启 + */ + protected open fun lockOrientationByDevice(): Boolean = true + override fun onDestroy() { super.onDestroy() job.cancel() diff --git a/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt b/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt index 4bad6a1..8bdaf41 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/MyApplication.kt @@ -13,6 +13,7 @@ import com.cretin.www.cretinautoupdatelibrary.model.UpdateConfig import com.cretin.www.cretinautoupdatelibrary.utils.AppUpdateUtils import com.cretin.www.cretinautoupdatelibrary.utils.SSLUtils.TrustAllHostnameVerifier import com.lukouguoji.module_base.handler.MyCrashHandler +import com.lukouguoji.module_base.util.DeviceUtil import com.lukouguoji.module_base.util.OkHttp3Connection import com.scwang.smart.refresh.footer.ClassicsFooter import com.scwang.smart.refresh.header.ClassicsHeader @@ -79,20 +80,26 @@ class MyApplication : Application() { .setScreenHeight(activity.resources.displayMetrics.heightPixels) //根据屏幕方向,设置设计尺寸 if (activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - //设置横屏设计尺寸 + //设置横屏设计尺寸(平板) AutoSizeConfig.getInstance() - .setDesignWidthInDp(1152) - .setDesignHeightInDp(720) + .setDesignWidthInDp(DeviceUtil.TABLET_DESIGN_WIDTH_DP_LAND) + .setDesignHeightInDp(DeviceUtil.TABLET_DESIGN_HEIGHT_DP_LAND) } else { if(activity.javaClass.simpleName.startsWith("PictureSelectorSupporterActivity")){ AutoSizeConfig.getInstance() .setDesignWidthInDp(360) .setDesignHeightInDp(480) - }else{ - //设置竖屏设计尺寸 + } else if (DeviceUtil.isPhone(activity)) { + //手机竖屏:按 5.5 寸手持端设计稿画布 390 × 844 换算 + //若沿用平板竖屏的 720 × 1280 基准,手机上所有 dp 会被压缩约一半,布局全部错位 AutoSizeConfig.getInstance() - .setDesignWidthInDp(720) - .setDesignHeightInDp(1280) + .setDesignWidthInDp(DeviceUtil.PHONE_DESIGN_WIDTH_DP) + .setDesignHeightInDp(DeviceUtil.PHONE_DESIGN_HEIGHT_DP) + } else { + //设置竖屏设计尺寸(平板) + AutoSizeConfig.getInstance() + .setDesignWidthInDp(DeviceUtil.TABLET_DESIGN_WIDTH_DP_PORT) + .setDesignHeightInDp(DeviceUtil.TABLET_DESIGN_HEIGHT_DP_PORT) } } } diff --git a/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt b/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt index 29b8f95..b7eec00 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt @@ -21,7 +21,17 @@ abstract class BaseBindingActivity : BaseAc override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - //dataBinding初始化 + /* + * dataBinding初始化 + * + * 平板 / 手机双形态的布局切换由 Android 资源限定符自动完成,此处无需分支: + * res/layout-sw600dp/xxx.xml → 平板(最小宽度 >= 600dp) + * res/layout/xxx.xml → 手机(兜底) + * 两个变体同名,DataBinding 会生成同一个 Binding 类,因此 Activity 与 ViewModel 无需改动。 + * + * 注意:不要按「不同文件名 + 代码判断」的方式切换布局, + * 那样会生成两个不同的 Binding 类,泛型擦除下编译期无法发现,运行时必然 ClassCastException。 + */ binding = DataBindingUtil.setContentView(this, layoutId()) binding.lifecycleOwner = this @@ -37,6 +47,9 @@ abstract class BaseBindingActivity : BaseAc /** * 布局ID + * + * 双形态适配时保持同一个 id 即可,具体加载哪份布局由资源限定符决定: + * res/layout-sw600dp/ 平板 res/layout/ 手机 */ abstract fun layoutId(): Int diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt index fb29587..6d62372 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/ActionBean.kt @@ -1,8 +1,19 @@ -package com.lukouguoji.module_base.bean - -class ActionBean( - val title: String, - val icon: Int, - val key: String = "" -) { -} \ No newline at end of file +package com.lukouguoji.module_base.bean + +/** + * 手持端(手机版)首页菜单项 + * + * @param title 菜单显示名称 + * @param icon 菜单图标 + * @param key 权限串(取 [com.lukouguoji.module_base.common.Constant.AuthName] 中的常量)。 + * 菜单按登录返回的 authList 是否包含该串来决定显不显示 + * @param route ARouter 路由路径。填了就由通用逻辑直接跳转,无需再写 when 分支; + * 留空表示由所在 ViewModel 的 onItemClick 自行处理(module_p 内的 PDA 页面走这条) + */ +class ActionBean( + val title: String, + val icon: Int, + val key: String = "", + val route: String = "" +) { +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/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 0000000..8962030 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/gjc_yi_ku_icon.png differ diff --git a/module_p/src/main/res/drawable-xxhdpi/img_gjc_chuku_jiaojie.png b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chuku_jiaojie.png new file mode 100644 index 0000000..20d20c8 Binary files /dev/null and b/module_p/src/main/res/drawable-xxhdpi/img_gjc_chuku_jiaojie.png differ