Compare commits

..

19 Commits

Author SHA1 Message Date
623ebc22f7 feat: opt pic 2026-04-17 14:59:16 +08:00
1157a0c4ed fix: 修复图片上传字段语义颠倒及加载缺失鉴权头导致的 403
- 修正 UploadUtil 返回字段到 FileBean 的映射:
  newName 是原图(较大)、zipFileName 是缩略图(较小)
- 保证 bean.pic 存缩略图、bean.originalPic 存原图
- 全局 loadImage BindingAdapter 对 http(s) URL 自动包装
  GlideUrl + Authorization,避免 /file/getImg/ 接口 403
- ImageSelectViewHolder 缩略图带鉴权加载,点击预览传原图
- 覆盖国内/国际事故签证、国内进港移库/移交编辑页面
- CLAUDE.md 同步修正 UploadBean 字段语义文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 14:57:26 +08:00
6ad7f0d3d4 fix: fix 国内出港出库 2026-04-16 17:10:07 +08:00
9d7453d3ee fix: 进港舱单编辑页运单号改用 prefix+no 拼接取值
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 17:47:21 +08:00
60478327e2 fix: 修复图片二次编辑丢失旧图及预览模糊问题
- 国内进港移交编辑:loadData 时同时从 pic/originalPic 构建
  FileBean,确保二次编辑保存时原有图片不被覆盖
- 国际事故签证:loadDetail 改用 originalPic URL 作为 FileBean.path,
  保证全屏预览取原图而非缩略图;同时修正保存时 pic/originalPic 字段赋值颠倒的问题

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 15:02:54 +08:00
1d2b11bfd2 refactor: 确保舱单编辑页保存时始终传递 no 和 prefix 参数
重构 GjjManifestAddViewModel 的参数构建方式:
- 改用 mutableMapOf 手动处理可选字段,替代 removeEmptyOrNull = true
- 编辑模式下 mfId、no、prefix 单独追加,不受空字符串过滤影响

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 14:50:28 +08:00
4117cbb489 fix: 修复国际进港仓库入库接口参数格式,去除主子列表选中联动,更新分单理货报告字段
- 入库接口请求体改为 {location, locationId, warehouseList} 结构
- 移除主列表与子列表之间的双向选中联动,保留全选按钮同时选中两者
- 舱单子列表理货报告字段从 lastMftStatus 改为 tallyStatus
- GjjHaWb 新增 tallyStatus 字段

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 14:13:22 +08:00
4451b790de fix: 国际进港装机单编辑页库位号取 locationTally,查询详情运单信息取 awbPc/awbWeight
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 13:42:03 +08:00
1fa0f6dde4 fix: 操作日志详情时间线调整显示顺序
第一行改为显示操作环节名称(status 映射)加时间,
第二行改为灰色小字展示操作内容(content)。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 12:07:23 +08:00
faf343301f fix: 事故签证列表查询改用 fdate+fno,理货主分单取消联动选择
- IntImpAccidentVisaViewModel: 移除 fid 逻辑,始终使用 fdate+fno+fdep+fdest 查询
- IntImpTallyViewHolder/SubViewHolder: 主单与分单选择状态改为独立,不再互相联动

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 12:00:18 +08:00
27d6e55cbe feat: new-page skill 2026-04-12 20:23:26 +08:00
262ce8f099 feat: fix @Deprecated 2026-04-11 10:44:52 +08:00
c7ab89b836 feat: improve claude 2026-04-10 21:19:49 +08:00
ecbe7d9a5c feat: improve claude 2026-04-10 20:44:01 +08:00
1ec08b4847 fix: 国际进港修改舱单接口补充 no 和 prefix 字段
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 15:11:09 +08:00
2c7cf0c255 feat: update claude conf 2026-04-07 10:57:33 +08:00
2865e2fd2d feat: add jks file 2026-04-02 17:05:22 +08:00
76ace545cd feat: 国际进港原始舱单补充信息页按始发站/目的站自动匹配国家代码
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 16:48:40 +08:00
cf8a7f38fb fix: 修复图片上传字段、航班查询接口及图片鉴权加载问题
- 国际进港舱单列表页航班查询接口改为 /flt/searchFlightList,支持多航班校验
- 修复国内进港移库编辑/交接页图片上传缺少 pic、picNumber 字段
- 国际进港舱单详情页对接交接图片展示
- 图片缩略图和大图预览加载带 Authorization header 解决 403
- CLAUDE.md 新增图片上传与展示规范

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 16:25:24 +08:00
47 changed files with 1204 additions and 287 deletions

159
.claude.local.md Normal file
View File

@@ -0,0 +1,159 @@
# 首页菜单页面地图
> **重要**:修改任何业务页面前,先在此处确认目标 Activity 是否为当前活跃页面。
> 若存在新旧两个同名/近名实现,以本文件中列出的为准(旧版已加 `@Deprecated`)。
>
> 菜单入口代码:`app/src/main/java/com/lukouguoji/aerologic/ui/fragment/HomeFragment.kt`
> 路由常量:`module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt`
---
## 一级菜单总览
| 菜单ID | 名称 | 模块 |
|--------|------|------|
| `DomExp` | 国内出港 | `module_gnc` |
| `DomImp` | 国内进港 | `app/page/gnj` |
| `IntExp` | 国际出港 | `module_gjc` |
| `IntImp` | 国际进港 | `module_gjj` |
| `Flight` | 航班查询 | `module_hangban` |
| `CargoStatus` | 货物追踪 | `module_cargo` |
| `Supervision` | 监装监卸 | `module_mit` + `app/page` |
| `Comprehensive` | 综合管理 | `app/page` |
---
## 国内出港DomExp& 国内进港DomImp
> ⛔ **不在维护范围内**:国内出港(`module_gnc`)和国内进港(`module_gnj` / `app/page/gnj`)不会涉及历史修改,也不会有新的需求。**无需关注这两个模块的任何页面。**
---
## 国际出港IntExp
路由前缀:`/gjc/`
### 二级菜单
| 菜单名 | 权限ID | Activity | 路由常量 |
|--------|--------|----------|---------|
| 收运检查 | `AppIntExpInspection` | `GjcInspectionActivity` | `ACTIVITY_URL_GJC_INSPECTION` |
| 出港计重 | `AppIntExpCheckWeighing` | `GjcWeighingListActivity` | `ACTIVITY_URL_GJC_WEIGHING_LIST` |
| 出港运抵 | `AppIntExpArrive` | `IntExpArriveActivity` | `ACTIVITY_URL_INT_EXP_ARRIVE` |
| 组装分配 | `AppIntExpAssembleAllocate` | `GjcAssembleAllocateActivity` | `ACTIVITY_URL_GJC_ASSEMBLE_ALLOCATE` |
| 出港组装 | `AppIntExpAssemble` | `IntExpAssembleActivity` | `ACTIVITY_URL_INT_EXP_ASSEMBLE` |
| 板箱过磅 | `GjcFuBangActivity`权限ID非类名| `GjcBoxWeighingActivity` | `ACTIVITY_URL_GJC_BOX_WEIGHING` |
| 出港装载 | `AppIntExpLoad` | `IntExpLoadActivity` | `ACTIVITY_URL_INT_EXP_LOAD` |
| 出库交接 | `AppIntExpOutHandover` | `IntExpOutHandoverActivity` | `ACTIVITY_URL_INT_EXP_OUT_HANDOVER` |
| 出港理货 | `AppIntExpTally` | `IntExpTallyActivity` | `ACTIVITY_URL_INT_EXP_TALLY` |
| 出港移库 | `AppIntExpMove` | `IntExpMoveActivity` | `ACTIVITY_URL_INT_EXP_MOVE` |
| 出港仓库 | `AppIntExpStorageUse` | `IntExpStorageUseActivity` | `ACTIVITY_URL_INT_EXP_STORAGE_USE` |
| 出港查询 | `AppIntExpSearch` | `GjcQueryListActivity` | `ACTIVITY_URL_GJC_QUERY_LIST` |
> ⚠️ 旧版文件(已加 `@Deprecated``GjcFuBangActivity``module_gjc/.../activity/`),已被 `GjcBoxWeighingActivity` 替代
---
## 国际进港IntImp
路由前缀:`/gjj/`
### 二级菜单
| 菜单名 | 权限ID | Activity | 路由常量 |
|--------|--------|----------|---------|
| 电报解析 | `AppIntImpMsgParse` | `IntImpMsgParseActivity` | `ACTIVITY_URL_INT_IMP_MSG_PARSE` |
| 进港舱单 | `AppIntImpManifest` | `IntImpManifestActivity` | `ACTIVITY_URL_INT_IMP_MANIFEST` |
| 原始舱单 | `AppIntArrAirManifest` | `IntArrAirManifestActivity` | `ACTIVITY_URL_INT_ARR_AIR_MANIFEST` |
| 理货报告 | `AppIntImpTally` | `IntImpTallyActivity` | `ACTIVITY_URL_INT_IMP_TALLY` |
| 进港仓库 | `AppIntImpWareHouse` | `GjjWareHouseActivity` | `ACTIVITY_URL_GJJ_WARE_HOUSE` |
| 提取记录 | `AppIntImpPickUpRecord` | `IntImpPickUpRecordActivity` | `ACTIVITY_URL_INT_IMP_PICK_UP_RECORD` |
| 提取出库 | `AppIntImpPickUpDLV` | `IntImpPickUpDLVActivity` | `ACTIVITY_URL_INT_IMP_PICK_UP_DLV` |
| 进港查询 | `AppIntImpSearch` | `IntImpQueryActivity` ⚡ | `ACTIVITY_URL_GJJ_QUERY_LIST` |
| 事故签证 | `AppIntImpAccidentVisa` | `IntImpAccidentVisaActivity` | `ACTIVITY_URL_INT_IMP_ACCIDENT_VISA` |
### 三级页面(进港舱单)
| 页面名 | Activity | 说明 |
|--------|----------|------|
| 舱单列表(入口) | `IntImpManifestActivity` | 二级入口页 |
| 舱单子列表 | `GjjManifestListActivity` | 活跃,独立路由 `ACTIVITY_URL_GJJ_MANIFEST` |
| 舱单详情 | `GjjManifestDetailsActivity` / `IntImpManifestDetailsActivity` | 两个活跃详情页 |
| 分单编辑 | `GjjManifestAddActivity` / `IntImpManifestSubEditActivity` | 活跃编辑页 |
### 三级页面(进港查询)
| 页面名 | Activity | 说明 |
|--------|----------|------|
| 查询列表(入口) | `IntImpQueryActivity` | 二级入口页(注意:非 GjjQueryListActivity|
| 查询详情 | `IntImpQueryDetailsActivity` | 活跃 |
| 运单修改 | `IntImpQueryEditActivity` | 活跃 |
> ⚠️ 旧版(已加 `@Deprecated``GjjQueryListActivity`、`GjjQueryInfoActivity`
---
## 航班查询Flight
直接跳转,无二级菜单。
| Activity | 路由 | 文件路径 |
|----------|------|---------|
| `HbQueryListActivity` | `/hb/HbQueryListActivity` | `module_hangban/...` |
> ⚠️ 旧版:`FlightQueryListActivity``app/.../page/flight/query/list/`)已加 `@Deprecated`,由 HomeFragment 注释确认废弃
---
## 货物追踪CargoStatus
直接跳转,无二级菜单。
| Activity | 路由 |
|----------|------|
| `CargoTrackingActivity` | `/cg/CargoTrackingActivity` |
---
## 监装监卸Supervision
### 二级菜单
| 菜单名 | 权限ID | Activity | 文件路径 |
|--------|--------|----------|---------|
| 进港卸机 | `AppImpLoad` | `GnjUnloadListActivity` | `app/.../page/gnj/unload/list/` |
| 事故签证(监装) | `AppAccidentVisa` | `AccidentVisaActivity` | 路由 `/mit/AccidentVisaActivity` |
| 事故签证(综合) | `AppComprehensiveAccidentVisa` | `AccidentVisaListActivity` | `app/.../page/accident/visa/list/` |
| 出港装板 | `AppExpLoad` | `OutLoadingActivity` | 路由 `/mit/OutLoadingActivity` |
---
## 综合管理Comprehensive
### 二级菜单
| 菜单名 | 权限ID | Activity | 文件路径 |
|--------|--------|----------|---------|
| 消息管理 | `AppComprehensiveMessage` | `MessageListActivity` | `app/.../page/message/list/` |
| 电报管理 | `AppComprehensiveTelegram` | `TelegramListActivity` | `app/.../page/telegram/list/` |
| 货物转运 | `AppComprehensiveTransfer` | `GoodsTransportActivity` | `app/.../page/transport/` |
| 平板车管理 | `AppComprehensiveCar` | `CarListActivity` | `app/.../page/car/list/` |
| 日志查询 | `AppComprehensiveLog` | `LogQueryActivity` | `app/.../page/log/list/` |
| 转运记录 | `AppTransportLog` | `TransportLogActivity` | `app/.../page/transportLog/list/` |
| ULD管理 | `AppComprehensiveUld` | `UldListActivity` | `app/.../page/uld/list/` |
| 冷库登记 | `AppComprehensiveColdStorage` | `ColdStorageActivity` | `app/.../page/coldStorage/list/` |
---
## 废弃页面清单(已加 @Deprecated
以下页面确认废弃,勿修改、勿参考:
| 废弃类名 | 文件路径 | 替代页面 | 依据 |
|----------|---------|---------|------|
| `FlightQueryListActivity` | `app/.../page/flight/query/list/` | `HbQueryListActivity` | HomeFragment 注释"已废弃" |
| `HangBanQueryActivity` | `module_hangban/.../activity/` | `HbQueryListActivity` | 路由注释"已废弃,使用 HbQueryListActivity" |
| `HangBanQueryInfoActivity` | `module_hangban/.../activity/` | — | 仅被废弃的 HangBanQueryActivity 调用,实际不可达 |
| `GjcFuBangActivity` | `module_gjc/.../activity/` | `GjcBoxWeighingActivity` | HomeFragment 注释"旧版跳转" |
| `GjjQueryListActivity` | `module_gjj/.../activity/` | `IntImpQueryActivity` | 路由被注释IntImpQueryActivity 接管同一路由 |
| `GjjQueryInfoActivity` | `module_gjj/.../activity/` | `IntImpQueryDetailsActivity` | 路由注释"已替换为 IntImpQueryDetailsActivity" |

View File

@@ -119,13 +119,32 @@
"mcp__apifox__read_project_oas_ruugy8",
"mcp__apifox__read_project_oas_ref_resources_ldmedm",
"mcp__apifox__read_project_oas_ldmedm",
"mcp__apifox__refresh_project_oas_ldmedm"
"mcp__apifox__refresh_project_oas_ldmedm",
"Skill(update-config)",
"mcp__apidoc__get_project_overview",
"mcp__apidoc__search_endpoints",
"mcp__apidoc__list_endpoints",
"mcp__apidoc__get_endpoint_detail",
"mcp__api-doc__search_endpoints",
"mcp__api-doc__get_endpoint_detail",
"mcp__api-doc__list_modules",
"mcp__api-doc__list_endpoints",
"Bash(sed -n '11,15p' /Users/kid/Development/Fusion/Projects/aerologic-app/app/src/main/java/com/lukouguoji/aerologic/page/flight/query/list/FlightQueryListActivity.kt)",
"Bash(sed -n '23,27p' /Users/kid/Development/Fusion/Projects/aerologic-app/module_gjc/src/main/java/com/lukouguoji/gjc/activity/GjcFuBangActivity.kt)",
"Bash(sed -i '' '/@Deprecated\\(\"旧版国内出港出库,已由 Gnc 前缀新版页面替代\"\\)/d' /Users/kid/Development/Fusion/Projects/aerologic-app/module_gnc/src/main/java/com/lukouguoji/gnc/activity/GoutChuKuListActivity.kt)",
"Bash(sed -i '' '/@Deprecated\\(\"旧版国内出港收运,已由 Gnc 前缀新版页面替代\"\\)/d' /Users/kid/Development/Fusion/Projects/aerologic-app/module_gnc/src/main/java/com/lukouguoji/gnc/activity/GoutCollectionActivity.kt)",
"Bash(sed -i '' '/@Deprecated\\(\"旧版国内出港复磅,使用 GncFuBangListActivity 替代\"\\)/d' /Users/kid/Development/Fusion/Projects/aerologic-app/module_gnc/src/main/java/com/lukouguoji/gnc/activity/GoutFuBangActivity.kt)",
"Bash(sed -i '' '/@Deprecated\\(\"旧版国内出港复磅(组合),使用 GncFuBangListActivity 替代\"\\)/d' /Users/kid/Development/Fusion/Projects/aerologic-app/module_gnc/src/main/java/com/lukouguoji/gnc/activity/GoutFuBangCotActivity.kt)",
"Bash(sed -i '' '/@Deprecated\\(\"旧版国内出港收运开始,使用 GncShouYunUnListActivity 替代\"\\)/d' /Users/kid/Development/Fusion/Projects/aerologic-app/module_gnc/src/main/java/com/lukouguoji/gnc/activity/GoutStartCotActivity.kt)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [
"空港集团 - API 文档",
"apifox"
"apifox",
"aerologic-app",
"apidoc"
]
}

View File

@@ -0,0 +1,465 @@
---
name: new-page
description: 根据设计截图和菜单入口名称,在 AirLogistics 项目中创建新的业务页面。自动分析截图判断页面类型6种典型类型之一规划所需文件然后完整实现Kotlin + XML + 路由注册 + 菜单接入)。当用户说"创建页面"、"新建页面"、"实现这个页面"、"做这个页面"或提供设计截图要求开发时触发。
---
# 新建业务页面
根据设计截图和菜单入口名称,在 AirLogistics 项目中完整创建一个新的业务页面。
## 前置要求
用户需要提供:
1. **设计截图**(必须)— 页面 UI 设计图
2. **菜单入口名称**(必须)— 页面在首页菜单中显示的名称
3. **所属模块**(可选)— 如 module_gjj国际进港、module_gjc国际出港可从截图标题推断
## 执行步骤
### 第 1 步:分析截图,判定页面类型
仔细阅读截图,提取以下信息:
1. **页面标题**(标题栏文字)
2. **搜索区字段**(每个搜索控件的 hint 文本、类型:日期/输入/下拉/扫码)
3. **列表项字段**(每个数据字段的标签名和显示格式)
4. **底部操作栏**(统计项 + 操作按钮)
5. **特殊交互**(全选、展开/收起、右箭头、子列表等)
然后对照 CLAUDE.md 中的 **6 种典型页面类型**判定:
| 类型 | 关键特征 | 判定依据 |
|------|----------|----------|
| **类型 1列表查询页** | 搜索 + 分页列表 + 底部统计 | 无全选、无勾选,纯查看 |
| **类型 2多选列表 + 批量操作页** | 类型 1 + 全选按钮 + 飞机图标选中态 + 操作按钮 | 有全选、有批量操作按钮(如"清除提货" |
| **类型 3嵌套多选列表页** | 类型 2 + 子列表(展开/收起)+ 主子联动全选 | 有展开按钮、列表项内含子 RecyclerView |
| **类型 4Tab 详情页** | 自定义 Tab 栏 + ViewPager2 + 多 Fragment | 有 Tab 切换、无列表搜索 |
| **类型 5编辑表单页** | ScrollView + PadDataLayoutNew 表单 + 保存/取消 | 有可编辑字段、有保存按钮 |
| **类型 6添加表单页** | 类型 5 + 输入回调 + 实时计算 | 有 setRefreshCallBack 联动 |
**向用户确认**判定结果,格式:
```
📋 页面分析结果:
页面标题XXX
页面类型:类型 N — XXXX
所属模块module_xxx
搜索条件:
1. XXXDATE
2. XXXSPINNER
3. XXXINPUT + 扫码)
列表字段第一行XXX | XXX | XXX
列表字段第二行XXX | XXX | XXX
底部操作:全选 + 统计(合计/总件数/总重量) + [操作按钮名称]
是否正确?确认后开始实现。
```
### 第 2 步:查找参考模板并提取 UI 设计规范
根据判定的页面类型,在项目中找到**同类型的最新参考实现**,并严格提取 UI 设计规范。
**查找策略**(优先级从高到低):
1. 优先查找**最近提交的同类型页面**(最新的页面代表最新的 UI 规范)
2. 其次在**同模块**内查找同类型页面
3. 最后在 **module_gjc**(国际出港)查找
**最新典型参考页面**2024年后新增代表当前 UI 规范):
| 页面类型 | 参考页面 | 布局文件 |
|----------|----------|----------|
| 列表查询页 | 航班查询列表 | `activity_flight_query_list.xml` / `item_flight_query_list.xml` |
| 列表查询页 | 日志查询页 | `activity_log_query.xml` / `item_log_query.xml` |
| 详情页 | 航班查询详情 | `activity_flight_query_details.xml` |
| 详情页 | 日志详情 | `activity_log_detail.xml` |
**必须读取的参考文件**
- 上述最新典型参考页面中**与当前页面类型匹配的布局 XML**Activity 布局 + Item 布局)
- 对应的 Activity、ViewModel、ViewHolder Kotlin 代码
- Dialog如有批量操作弹窗
**UI 设计规范提取(必做)**
读取参考布局后,必须逐一确认以下规范项,并在开发计划中明确列出:
| 规范项 | 必须确认的内容 |
|--------|---------------|
| 页面背景色 | 根容器 `background` 属性(新规范:`@color/color_f2` |
| 搜索区样式 | 使用的搜索控件类型、间距、布局方式 |
| 搜索按钮样式 | 图标资源、尺寸、style新规范`@drawable/img_search`36dp |
| 底部栏背景色 | 背景色 + 文字颜色(新规范有两种变体) |
| 底部栏文字样式 | 字号、粗细、颜色 |
| 列表项背景 | 背景、间距、padding |
| 详情页卡片样式 | 背景、圆角、padding、行间距 |
| 表单控件 | 使用 PadDataLayoutNew非旧版 PadDataLayout |
**同时检查**
- 该页面需要的**下拉列表数据源 API** 是否已存在(代理人、特码等)
- 该页面需要的 **Bean 类**是否已存在,或需要新建
- `ARouterConstants` 中是否已有对应路由常量
- `Constant.AuthName` 中是否已有对应权限名
### 第 3 步:制定文件清单
列出所有需要**新建**和**修改**的文件。
**新建文件清单**(根据页面类型调整):
| 类别 | 文件 | 路径 |
|------|------|------|
| Bean | `XxxBean.kt`(如需) | `module_base/.../bean/` |
| Activity | `XxxActivity.kt` | `module_xxx/.../activity/` |
| ViewModel | `XxxViewModel.kt` | `module_xxx/.../viewModel/` |
| ViewHolder | `XxxViewHolder.kt` | `module_xxx/.../holder/` |
| Dialog | `XxxDialogModel.kt`(如需) | `module_xxx/.../dialog/` |
| Activity 布局 | `activity_xxx.xml` | `module_xxx/.../res/layout/` |
| Item 布局 | `item_xxx.xml` | `module_xxx/.../res/layout/` |
| Dialog 布局 | `dialog_xxx.xml`(如需) | `module_xxx/.../res/layout/` |
**修改文件清单**(固定):
| 文件 | 修改内容 |
|------|----------|
| `Api.kt` | 添加 API 接口方法 + import |
| `ARouterConstants.kt` | 添加路由常量(如不存在) |
| `Constant.kt` | 添加 AuthName 常量(如不存在) |
| `AndroidManifest.xml` | 注册 Activity |
| `HomeFragment.kt` | 添加菜单项 + 点击路由处理 |
| 旧版 Activity如有 | 注释掉 `@Route` 注解避免冲突 |
### 第 4 步:创建 Bean如需
如果截图中的列表字段与现有 Bean 不匹配,创建新的 Bean 类。
**规则**
- 放在 `module_base/.../bean/` 目录下
- 如果是类型 2/3多选必须包含 `ObservableBoolean` 选中状态:
```kotlin
val checked: ObservableBoolean = ObservableBoolean(false)
var isSelected: Boolean
get() = checked.get()
set(value) = checked.set(value)
```
- 字段类型映射:数字用 `Int/Long/Double`,文本用 `String = ""`,时间用 `String = ""`
-`Api.kt` 的 import 区按字母顺序添加 import
### 第 5 步:添加 API 接口
`Api.kt` 中添加 API 方法。
**标准 API 组合**(根据页面需要选取):
- `getXxxList(@Body)` — 分页查询,返回 `PageInfo<XxxBean>`
- `getXxxTotal(@Body)` — 分页合计,返回 `BaseResultBean<ManifestTotalDto>`
- 批量操作 API — 返回 `BaseResultBean<Boolean>`
- 下拉列表 API代理人、特码等— 如不存在则添加
**API 路径命名**:先使用 `ModuleName/methodName` 格式占位(如 `IntImpPickUpRecord/pageQuery`),后续由第 13 步替换为真实接口路径
### 第 6 步:添加路由和权限常量
1.`ARouterConstants.kt` 添加路由(如不存在):
```kotlin
const val ACTIVITY_URL_XXX = "/module/XxxActivity"
```
2.`Constant.kt``AuthName` 中添加权限名(如不存在):
```kotlin
const val XxxPage = "AppXxxPage"
```
### 第 7 步:创建 ViewHolder
根据 item 布局创建 ViewHolder。
**类型 1**:基础绑定
**类型 2**:增加图标点击切换 `checked` 状态
**类型 3**:增加子列表 `setCommonAdapter` + 展开按钮 + 父子联动
### 第 8 步:创建 ViewModel
**必须包含的元素**(根据截图):
- 搜索条件 `MutableLiveData`(与搜索区对应)
- 下拉列表数据源 `MutableLiveData<List<KeyValue>>`
- 统计字段 `MutableLiveData<String>`
- 适配器配置:`itemViewHolder` + `itemLayoutId`
- `searchClick()` 方法
- `getData()` override调用列表 API + 统计 API
**类型 2 额外**`isAllChecked` + `checkAllClick()` + 批量操作方法
**类型 3 额外**`isAllExpanded` + `toggleAllExpand()` + 联动全选逻辑
**下拉列表初始化**
- 代理人:`DictUtils.getAgentList()` / `NetApply.api.getIntImpAgentList()` / `getIntExpAgentList()`
- 特码:`DictUtils.getSpecialCodeList(flag, ieFlag, parentcode)`
- 其他字典:`DictUtils` 或自定义 API
### 第 9 步:创建布局文件
**⚠️ 重要:所有布局必须严格遵循最新 UI 设计规范,参照第 2 步中提取的规范项。**
---
#### 新版 UI 设计规范(强制执行)
**1. 页面背景色**
```xml
<!-- 根容器必须使用 color_f2 背景色 -->
<LinearLayout
android:background="@color/color_f2"
android:orientation="vertical">
```
**2. 搜索区规范**
```xml
<!-- 搜索区marginHorizontal=10dp, marginTop=10dp, gravity=center_vertical -->
<LinearLayout
android:layout_marginHorizontal="10dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical">
<!-- 搜索控件使用 PadSearchLayout -->
<PadSearchLayout
type="@{SearchLayoutType.DATE}"
hint='@{"请选择航班日期"}'
icon="@{@drawable/img_date}"
value="@={viewModel.date}"
android:layout_width="0dp"
android:layout_weight="1" />
<!-- 搜索按钮img_search 图标36x36dppadding=2dp -->
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:paddingHorizontal="24dp">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="2dp"
android:onClick="@{()-> viewModel.searchClick()}"
android:src="@drawable/img_search" />
</LinearLayout>
</LinearLayout>
```
**3. 底部统计栏规范**(两种变体,根据截图选择):
**变体 A — 深蓝色底部栏**(多数列表页使用):
```xml
<LinearLayout
android:layout_height="50dp"
android:background="@color/color_bottom_layout"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:text='@{"合计:"+viewModel.count+"条"}'
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
```
**变体 B — 白色底部栏**
```xml
<LinearLayout
android:layout_height="50dp"
android:background="@color/white"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:text='@{"合计:" + viewModel.count + "条"}'
android:textColor="@color/bottom_tool_tips_text_color"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
```
**4. 详情页/表单页规范**
- 必须使用 **PadDataLayoutNew**(非旧版 PadDataLayout
- 卡片背景:`@drawable/bg_white_radius_8`padding `15dp`
- 行间距:`marginTop="8dp"`
- 三列标准布局,每列 `layout_weight="1"`
```xml
<ScrollView android:fillViewport="true">
<LinearLayout android:padding="15dp" android:orientation="vertical">
<LinearLayout android:background="@drawable/bg_white_radius_8"
android:padding="15dp" android:orientation="vertical">
<LinearLayout android:orientation="horizontal">
<PadDataLayoutNew layout_weight="1" enable="@{false}"
title='@{"航班日期"}' type="@{DataLayoutType.INPUT}"
value='@{viewModel.dataBean.fdate}' />
<!-- 更多列... -->
</LinearLayout>
<LinearLayout android:orientation="horizontal" android:layout_marginTop="8dp">
<!-- 第二行... -->
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
```
---
#### Activity 布局完整结构
```xml
<layout>
<data>
<import SearchLayoutType />
<variable viewModel />
<variable activity /> <!-- 类型 3 或有 Dialog 操作时 -->
</data>
<LinearLayout background="@color/color_f2" vertical> <!-- ← 新规范:背景色 -->
<include title_tool_bar />
<!-- 搜索区marginHorizontal=10dp, marginTop=10dp -->
<LinearLayout horizontal marginHorizontal="10dp" marginTop="10dp">
PadSearchLayout × N + 搜索按钮(img_search, 36dp) <!-- ← 新规范:搜索按钮 -->
</LinearLayout>
<!-- 列表 -->
<SmartRefreshLayout> <RecyclerView /> </SmartRefreshLayout>
<!-- 底部栏50dp根据截图选择深蓝/白色变体 --> <!-- ← 新规范:底部栏 -->
<LinearLayout height="50dp" background="..." paddingHorizontal="15dp">
<TextView textSize="18sp" textStyle="bold" />
</LinearLayout>
</LinearLayout>
</layout>
```
#### Item 布局(从截图精确还原每一行每一列)
- 逐行对照截图中的字段顺序和标签文本
- 使用 `completeSpace` 对齐 Key 文本
- 运单号等关键字段用 `@color/colorPrimary`
- 类型 2/3 左侧有飞机图标:`loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}"`
- 列表项背景统一使用 `@drawable/bg_item`
- 间距统一:`marginHorizontal="15dp"`, `marginVertical="5dp"`, `padding="10dp"`
**关键原则:务必尽可能还原截图上的页面设计,不推测不假想。**
### 第 10 步:创建 Activity
**固定结构**
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_XXX)
class XxxActivity : BaseBindingActivity<XxxBinding, XxxViewModel>() {
override fun layoutId() = R.layout.activity_xxx
override fun viewModelClass() = XxxViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("页面标题") // 与截图标题一致
binding.viewModel = viewModel
binding.activity = this // 类型 3 或有 Dialog 时
// 类型 2/3观察全选状态
viewModel.isAllChecked.observe(this) { binding.checkIcon.alpha = if (it) 1.0f else 0.5f }
// 绑定分页
viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this)
// 监听刷新事件
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) { viewModel.refresh() }
// 初始化下拉列表(如有)
viewModel.initAgentList()
viewModel.initSpecialCodeList()
viewModel.refresh()
}
}
```
### 第 11 步:注册 Activity + 菜单入口
1. **AndroidManifest.xml**:在其他 gjj Activity 注册附近添加:
```xml
<activity android:name="com.lukouguoji.xxx.activity.XxxActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false" android:screenOrientation="userLandscape" />
```
2. **HomeFragment.kt**
- 在对应模块的菜单列表区添加 `RightMenu` 项(图标 + 标题)
- 在 onClick 处理区添加路由跳转
3. **旧版 Activity**(如有):注释掉 `@Route` 注解
### 第 12 步:编译验证
```bash
./gradlew assembleDebug
```
编译必须通过0 errors。如有错误立即修复后重新编译。
### 第 13 步:查找并对接 API 接口
页面创建并编译通过后,通过"空港集团 - API 文档"Apifox MCP 工具查找真实接口,替换第 5 步中的占位路径。
**A. 基础接口查找(每次必查):**
1. 根据页面所属业务路径(如"国际进港 → 原始舱单"),在 Apifox MCP 中按模块目录搜索
2. 查找以下基础接口:
- **列表接口**(分页查询,如 `pageQuery``list`
- **合计接口**(统计,如 `total``count``statistics`
- **修改/保存接口**(如 `update``save``edit`
- **删除接口**(如 `delete``remove`
**B. 智能接口匹配(根据页面分析):**
3. 根据**页面类型特性**查找对应接口:
- 类型 2多选批量操作→ 查找批量操作接口(如批量删除、批量确认等)
- 类型 3嵌套列表→ 查找子列表相关接口
- 类型 5/6表单页→ 查找详情查询接口、下拉选项字典接口
- 类型 4Tab 详情)→ 查找各 Tab 对应的数据接口
4. 根据**页面中的按钮和文案**,逐一匹配对应接口:
- 例如页面有"审核"按钮 → 查找审核接口
- 例如页面有"导出"按钮 → 查找导出接口
- 例如页面有"打印"按钮 → 查找打印相关接口
- 例如底部栏有"清除提货"按钮 → 查找清除提货接口
- 例如搜索区有下拉框(代理人、状态等)→ 查找对应的字典/下拉数据接口
5. 根据**截图中可见的交互元素**,推断可能需要的接口:
- 列表项有右箭头 → 可能需要详情接口
- 列表项有编辑图标 → 可能需要编辑/更新接口
- 有扫码图标 → 可能需要扫码查询接口
**C. 对接与校准:**
6. 找到接口后,更新 `Api.kt` 中的占位路径为真实路径
7. 根据接口的请求参数和返回字段结构,校准 Bean 类的字段名和类型
8. 遵循 memory 中的 API 搜索原则:不跨模块混用接口,遇到不确定的接口询问用户确认
### 第 14 步:重新编译验证
对接真实 API 后重新编译,确保无错误:
```bash
./gradlew assembleDebug
```
编译必须通过0 errors。如有错误立即修复后重新编译。
## 注意事项
### UI 设计规范(强制)
- **必须使用最新 UI 规范**:不管是新增页面、覆盖旧页面还是修改旧页面,都必须完全采用最新典型参考页面的 UI 设计规范
- **页面背景色**:根容器必须使用 `@color/color_f2``#F2F2F2`),禁止使用旧的白色或其他背景
- **搜索按钮**:使用 `@drawable/img_search` 图标36x36dp + padding 2dp不使用旧的 `iv_search_action` style
- **底部栏**:高度 50dp文字 18sp bold根据截图选择深蓝色`@color/color_bottom_layout` + 白字)或白色(`@color/white` + `@color/bottom_tool_tips_text_color`
- **详情页/表单页**:必须使用 **PadDataLayoutNew**(非旧版 PadDataLayout卡片用 `bg_white_radius_8`
- **严格匹配典型页面**:在写开发计划前,必须先读取最新典型参考页面(如航班查询列表、日志查询页等),总结并列出 UI 设计规范要点
### 通用规则
- **资源引用必须存在**:使用 drawable/color/mipmap 前确认资源存在,不存在则换用已有资源
- **import 路径查阅 CLAUDE.md**:基类和扩展函数的正确 import 路径参见开发指南的 Import 速查表
- **不创建不需要的文件**:如果截图中没有 Dialog 弹窗,不要创建 Dialog 文件
- **Bean 复用优先**:如果现有 Bean 的字段足以覆盖截图需求,直接复用
- **API 接口对接**:页面创建完成后,通过空港集团 API 文档 MCP 查找真实接口并替换占位路径。遵循 API 搜索原则,按业务模块目录查找,不跨模块混用
- **DataBinding 规则**:遵循 CLAUDE.md 中的 DataBinding 关键规则双向绑定、字符串拼接、View 导入等)

2
.gitignore vendored
View File

@@ -113,7 +113,7 @@ captures/
*.swp
# Keystore files
*.jks
# *.jks
*.keystore
# Google Services (e.g. APIs or Firebase)

View File

@@ -1,14 +1,10 @@
{
"mcpServers": {
"apifox": {
"command": "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"
}
}
}

139
CLAUDE.md
View File

@@ -687,6 +687,8 @@ fun resetClick() {
基于 `BaseDialogModel`XPopup 封装),支持 5 种弹窗类型。
> ⚠️ **强制规则**:所有二次确认弹框**必须**使用 `ConfirmDialogModel``com.lukouguoji.module_base.model.ConfirmDialogModel`**禁止**使用系统 `AlertDialog`。
### 基础模板
```kotlin
@@ -812,6 +814,25 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
`completeSpace="@{5}"` 设置 Key 文本宽度(以"一"字宽度为单位),用于 Key-Value 布局对齐。
### AutoQuery 自动查询PadSearchLayout / PadDataLayoutNew
两个组件均支持输入时自动联想查询,只需在 XML 添加属性,无需修改 Kotlin
```xml
<PadSearchLayout
autoQueryEnabled="@{true}"
autoQueryUrl="@{`/IntExpSearch/queryWbNoList`}"
autoQueryParamKey="@{`wbNo`}"
autoQueryMinLength="@{4}"
autoQueryMaxLength="@{8}"
autoQueryTitle="@{`选择运单号`}"
... />
```
- 1条结果 → 直接填充;多条结果 → 弹出选择列表0条结果 → 无处理
- 通用 API 方法:`Api.getWbNoList(@Url url, @Body data)` 返回 `BaseResultBean<List<String>>`
- 关键文件:`module_base/.../ui/weight/data/layout/AutoQueryManager.kt`
---
## Import 路径速查
@@ -838,7 +859,20 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
### 扩展函数(均在 `com.lukouguoji.module_base.ktx` 包下)
`launchCollect``launchLoadingCollect``showToast``toRequestBody``verifyNullOrEmpty``noNull``formatDate`
`launchCollect``launchLoadingCollect``showToast``toRequestBody``verifyNullOrEmpty``noNull``formatDate``setUpperCaseAlphanumericFilter`
### 工具类
| 类 | 正确路径 |
|----|----------|
| `DictUtils` | `com.lukouguoji.module_base.util.DictUtils` |
| `MediaUtil` | `com.lukouguoji.module_base.util.MediaUtil` |
| `UploadUtil` | `com.lukouguoji.module_base.util.UploadUtil` |
| `KeyValue` | `dev.utils.app.info.KeyValue` |
| `DateUtils` | `dev.utils.common.DateUtils` |
| `SharedPreferenceUtil` | `com.lukouguoji.module_base.db.perference.SharedPreferenceUtil` |
| `ScanModel` | `com.lukouguoji.module_base.model.ScanModel` |
| `ConfirmDialogModel` | `com.lukouguoji.module_base.model.ConfirmDialogModel` |
---
@@ -956,6 +990,109 @@ companion object {
}
```
### API 接口目录对应规则
为某页面查找接口时,**必须按业务路径匹配对应 API 目录**,不能跨模块借用。
| API 前缀 | 所属模块 |
|----------|---------|
| `IntImpManiFest/` | 国际进港-进港舱单(增删改查) |
| `IntImpAirManifest/` | 国际进港-原始舱单(申报、补充信息等) |
不同前缀代表不同业务,即使功能语义相似(如"更新"),也不能混用。不确定时询问用户。
### 页面定位规则
修改代码前,必须确认目标文件是**首页菜单实际跳转到的 Activity/ViewModel**,而非同名旧版文件。同一业务有多个实现时,以首页菜单入口链路为准。
---
## 图片上传与展示规范
### 图片上传三字段规范
上传图片后提交表单时,**必须同时传 `pic``originalPic``picNumber` 三个字段**,缺一不可。
**`UploadUtil.upload()` 返回值**(注意:**与字面意思相反**:
- `data?.newName` — **原图**文件名(较大)
- `data?.zipFileName` — **缩略图/压缩图**文件名(较小)
**提交时字段映射**(参考事故签证 `AccidentVisaDetailsViewModel``IntImpAccidentVisaEditViewModel`:
```kotlin
// FileBean 字段含义(约定用途,与 UploadBean 字段名不一致):
// - FileBean.url 作缩略图标识(提交到 bean.pic
// - FileBean.originalPic 作原图标识(提交到 bean.originalPic
// 上传新图片(注意 UploadBean 字段名的误导性,按实际含义赋值)
val data = UploadUtil.upload(fileBean.path).data
fileBean.url = data?.zipFileName ?: "" // 缩略图
fileBean.originalPic = data?.newName ?: "" // 原图
// 提交时设置三个字段
bean.picNumber = list.size.toString()
bean.pic = list.joinToString(",") { MediaUtil.removeUrl(it.url) } // 缩略图
bean.originalPic = list.joinToString(",") { MediaUtil.removeUrl(it.originalPic) } // 原图
```
**常见错误**:
- ❌ 只传 `images``originalPic` 单个字段 — 接口不认或数据不完整
- ❌ 只取 `newName` 不取 `zipFileName` — 丢失缩略图/原图之一
- ❌ 按 `UploadBean` 字段字面含义赋值(`url = newName`)— 会导致 pic/originalPic 内容和字段语义颠倒(缩略图字段装原图、原图字段装缩略图)
- ❌ 用 `fileBean.path.startsWith("http")` 判断已上传 — 应该用 `fileBean.url.isNotEmpty()`
### 编辑页加载已有图片
从详情接口获取图片后,需要同时解析 `pic`(缩略图)和 `originalPic`(原图),构建完整的 `FileBean`
```kotlin
val picList = bean.pic.split(",").filter { it.isNotEmpty() }
val originalPicList = bean.originalPic.split(",").filter { it.isNotEmpty() }
val images = picList.mapIndexed { index, picUrl ->
val originalFile = originalPicList.getOrElse(index) { picUrl }
FileBean(
path = MediaUtil.fillUrl(picUrl), // 完整URL用于显示
url = picUrl, // 相对路径,提交时用
originalPic = MediaUtil.fillUrl(originalFile) // 原图完整URL
)
}.toMutableList()
```
### 图片加载必须带 Authorization Header
`/file/getImg/` 接口需要鉴权Glide 默认不带 token直接用 `loadImage` BindingAdapter 会 **403 Forbidden**
**正确做法** — 在 ViewHolder 中使用 `GlideUrl` + `LazyHeaders`
```kotlin
// 缩略图加载ViewHolder 中)
val glideUrl = GlideUrl(
bean.path,
LazyHeaders.Builder()
.addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token))
.build()
)
Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail)
```
**同时必须去掉 XML 布局中的 `loadImage` 属性**,否则 BindingAdapter 会触发不带 token 的请求覆盖手动加载:
```xml
<!-- ❌ 错误:会触发不带 token 的 Glide 请求 -->
<ImageView loadImage="@{bean.path}" />
<!-- ✅ 正确:只保留 id由 ViewHolder 手动加载 -->
<ImageView android:id="@+id/iv_thumbnail" />
```
**大图预览同理**`PreviewImageViewHolder` 也需要用 `GlideUrl` 带 token 加载网络图片。
**参考文件**:
- 缩略图加载: `module_gjj/.../GjjManifestPicViewHolder.kt`
- 大图预览: `module_base/.../PreviewImageViewHolder.kt`
- 图片上传提交: `app/.../AccidentVisaDetailsViewModel.kt`
- 带 token 的 Glide 加载: `module_mit/.../PictureAdapter.kt`
---
## 开发原则

View File

@@ -75,23 +75,18 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener {
onSuccess = {
dataBean.value = it.data ?: AccidentVisaBean()
// 渲染图片
val list = dataBean.value!!.pic.split(",")
.filter { url -> url.isNotEmpty() }
.map { url ->
FileBean(MediaUtil.fillUrl(url), url)
}
val zipList = dataBean.value!!.originalPic.split(",")
.filter { url -> url.isNotEmpty() }
.map { url ->
FileBean(MediaUtil.fillUrl(url))
}
for ((index, fileBean) in list.withIndex()) {
val originalPic = zipList.get(index).path
list.get(index).originalPic = originalPic
// 渲染图片pic 存缩略图文件名originalPic 存原图文件名
val picList = dataBean.value!!.pic.split(",").filter { it.isNotEmpty() }
val originalPicList = dataBean.value!!.originalPic.split(",").filter { it.isNotEmpty() }
val list = picList.mapIndexed { index, picFilename ->
val originalFilename = originalPicList.getOrElse(index) { picFilename }
FileBean(
path = MediaUtil.fillUrl(picFilename),
url = picFilename,
originalPic = MediaUtil.fillUrl(originalFilename)
)
}
rv?.commonAdapter()
?.loadMore(list)
rv?.commonAdapter()?.loadMore(list)
}
}
}
@@ -110,8 +105,10 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener {
.filter { it.path.isNotEmpty() && it.url.isEmpty() }
.onEach {
val data = UploadUtil.upload(it.path).data
it.url = data?.newName ?: ""
it.originalPic = data?.zipFileName ?: ""
// UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
// FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
it.url = data?.zipFileName ?: ""
it.originalPic = data?.newName ?: ""
}
.flowOn(Dispatchers.IO)
.onStart { showLoading() }
@@ -124,8 +121,8 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener {
val list =
(rv?.commonAdapter()?.items as List<FileBean>).filter { it.path.isNotEmpty() }
bean.picnumber = list.size.toString()
bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) }
bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) }
bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) }
bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) }
NetApply.api.anyPost(
url = if (pageType.value == DetailsPageType.Add) "GnAccidentVisa/saveVisa" else "GnAccidentVisa/updateVisa",

View File

@@ -44,7 +44,8 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
viewModel.latestStepCode.observe(this) { rebuildSteps() }
viewModel.statusLogList.observe(this) { list ->
timelineAdapter.setData(list)
val stepMap = viewModel.allSteps.value?.associate { it.code to it.name } ?: emptyMap()
timelineAdapter.setData(list, stepMap)
}
viewModel.initOnCreated(intent)

View File

@@ -11,9 +11,11 @@ import com.lukouguoji.module_base.bean.StatusLogBean
class LogDetailTimelineAdapter : RecyclerView.Adapter<LogDetailTimelineAdapter.TimelineViewHolder>() {
private var items: List<StatusLogBean> = emptyList()
private var statusNameMap: Map<String, String> = emptyMap()
fun setData(list: List<StatusLogBean>) {
fun setData(list: List<StatusLogBean>, stepMap: Map<String, String> = emptyMap()) {
items = list
statusNameMap = stepMap
notifyDataSetChanged()
}
@@ -28,8 +30,9 @@ class LogDetailTimelineAdapter : RecyclerView.Adapter<LogDetailTimelineAdapter.T
val isFirst = position == 0
val isLast = position == items.size - 1
holder.tvContent.text = item.content
holder.tvTime.text = item.opDate
val statusName = statusNameMap[item.status] ?: item.status
holder.tvContent.text = if (statusName.isNotEmpty()) "$statusName ${item.opDate}" else item.opDate
holder.tvTime.text = item.content
// 最后一项(当前步骤)用绿色圆点
holder.dotView.setBackgroundResource(

View File

@@ -37,6 +37,7 @@ import com.lukouguoji.gnc.page.deposit.list.GncDepositListActivity
import com.lukouguoji.gnc.page.distribution.home.GncDistributionHomeActivity
import com.lukouguoji.gnc.page.fubang.list.GncFuBangListActivity
import com.lukouguoji.gnc.page.shouyun.unlist.GncShouYunUnListActivity
import com.lukouguoji.gnj.activity.GnjChuKuListActivity
import com.lukouguoji.module_base.MyApplication
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil
@@ -314,7 +315,8 @@ class HomeFragment : Fragment() {
}
//出库
Constant.AuthName.GnjChuKuList -> {
GnjMoveStashListActivity.start(requireContext())
ARouter.getInstance().build(ARouterConstants.ACTIVITY_URL_GNJ_CHU_KU_LIST)
.navigation()
}
//仓库管理
Constant.AuthName.GnjWareHouse -> {

BIN
key.jks Normal file

Binary file not shown.

View File

@@ -12,12 +12,16 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.base.CommonAdapter
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.util.SizeUtils
@@ -111,10 +115,22 @@ fun loadImage(
com.bumptech.glide.request.target.Target.SIZE_ORIGINAL
)
// 对 http(s) 字符串 URL自动包装为带 Authorization header 的 GlideUrl避免 /file/getImg/ 接口 403
val actualSource: Any? = if (source is String && (source.startsWith("http://") || source.startsWith("https://"))) {
GlideUrl(
source,
LazyHeaders.Builder()
.addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token))
.build()
)
} else {
source
}
// 设置图片加载
val load = Glide.with(imageView)
.setDefaultRequestOptions(requestOptions)
.load(source)
.load(actualSource)
.diskCacheStrategy(diskCacheStrategy)
.encodeFormat(encodeFormat)

View File

@@ -29,7 +29,8 @@ data class GjjHaWb(
var opDate: String = "",
var billsNo: String = "",
var remark: String = "",
var response: String = ""
var response: String = "",
var tallyStatus: String = ""
) : Serializable {
@Transient
val checked: ObservableBoolean = ObservableBoolean(false)

View File

@@ -56,7 +56,10 @@ data class GjjManifest(
var subCode: String = "", // 子代码
var unNumber: String = "", // 危险品编号
var activeId: Long = 0, // 活动ID
var locationTally: String = "" // 理货库位号
var locationTally: String = "", // 理货库位号
var pic: String = "", // 交接图片缩略图路径
var originalPic: String = "", // 交接图片原图地址
var picNumber: String = "" // 交接图片数量
) : Serializable {
// 分单列表
var haWbList: List<GjjHaWb>? = null

View File

@@ -308,6 +308,12 @@ interface Api {
@POST("typeCode/countryCode")
suspend fun getCountryCodeList(): DictListBean
/**
* 获取国家代码(带始发站筛选)
*/
@POST("typeCode/countryCode")
suspend fun getCountryCodeListByFdep(@Query("fDep") fDep: String): DictListBean
/**
* 获取通讯方式类型
*/
@@ -957,7 +963,7 @@ interface Api {
* 接口路径: /IntImpStorage/inStorage
*/
@POST("IntImpStorage/inStorage")
suspend fun inIntImpStorage(@Query("location") location: String, @Body data: RequestBody): BaseResultBean<Boolean>
suspend fun inIntImpStorage(@Body data: RequestBody): BaseResultBean<Boolean>
/**
* 国际进港提取记录-分页查询
@@ -1367,6 +1373,12 @@ interface Api {
@POST("flt/queryFlight")
suspend fun queryFlightByDateAndNo(@Body data: RequestBody): BaseResultBean<FlightBean>
/**
* 根据航班日期、航班号、地区类型、进出港查询航班(返回列表)
*/
@POST("flt/searchFlightList")
suspend fun searchFlightList(@Body data: RequestBody): BaseResultBean<List<FlightBean>>
/**
* 获取航班目的站、经停站
*/

View File

@@ -1,51 +1,69 @@
package com.lukouguoji.module_base.impl
import android.view.View
import com.luck.picture.lib.adapter.holder.PreviewImageHolder
import com.luck.picture.lib.basic.PictureSelector
import com.lukouguoji.module_base.adapter.loadImage
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.FileBean
import com.lukouguoji.module_base.databinding.ItemImageSelectBinding
import com.lukouguoji.module_base.ktx.commonAdapter
import com.lukouguoji.module_base.ktx.logd
import com.lukouguoji.module_base.ktx.loge
import com.lukouguoji.module_base.ui.page.preview.PreviewActivity
import com.lukouguoji.module_base.util.MediaUtil
class ImageSelectViewHolder(view: View) : BaseViewHolder<FileBean, ItemImageSelectBinding>(view) {
override fun onBind(item: Any?, position: Int) {
val bean = getItemBean(item)!!
binding.bean = bean
binding.rl.setOnClickListener {
if (bean.path.isEmpty()) {
MediaUtil.pickImage(itemView.context, maxNum = 10) {
it.forEach {
logd("添加了图片 : ${it.realPath}")
getRecyclerView()?.commonAdapter()?.addItem(FileBean(path = it.realPath))
}
}
} else {
PreviewActivity.start(itemView.context, listOf(bean))
}
}
// 长按事件
binding.rl.setOnLongClickListener {
clickListener?.onItemClick(bindingAdapterPosition, binding.rl.id)
true
}
notifyItemClick(position, binding.ivDelete)
if (bean.isOnlineResource()) {
loge("开始下载 : ${bean.path}")
bean.download {
loadImage(binding.iv, it)
}
}
}
}
package com.lukouguoji.module_base.impl
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.FileBean
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.databinding.ItemImageSelectBinding
import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil
import com.lukouguoji.module_base.ktx.commonAdapter
import com.lukouguoji.module_base.ktx.logd
import com.lukouguoji.module_base.ui.page.preview.PreviewActivity
import com.lukouguoji.module_base.util.MediaUtil
import java.io.File
class ImageSelectViewHolder(view: View) : BaseViewHolder<FileBean, ItemImageSelectBinding>(view) {
override fun onBind(item: Any?, position: Int) {
val bean = getItemBean(item)!!
binding.bean = bean
// 加载缩略图
if (bean.path.isNotEmpty()) {
if (bean.isOnlineResource()) {
val glideUrl = GlideUrl(
bean.path,
LazyHeaders.Builder()
.addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token))
.build()
)
Glide.with(itemView.context).load(glideUrl).into(binding.iv)
} else {
Glide.with(itemView.context).load(File(bean.path)).into(binding.iv)
}
}
binding.rl.setOnClickListener {
if (bean.path.isEmpty()) {
MediaUtil.pickImage(itemView.context, maxNum = 10) {
it.forEach {
logd("添加了图片 : ${it.realPath}")
getRecyclerView()?.commonAdapter()?.addItem(FileBean(path = it.realPath))
}
}
} else {
val items = getRecyclerView()?.commonAdapter()?.items
?.filterIsInstance<FileBean>()
?.filter { it.path.isNotEmpty() }
?: listOf(bean)
val previewList = items.map { fb ->
FileBean(path = if (fb.originalPic.isNotEmpty()) fb.originalPic else fb.path)
}
val previewPosition = items.indexOfFirst { it === bean }.coerceAtLeast(0)
PreviewActivity.start(itemView.context, previewList, previewPosition)
}
}
// 长按事件
binding.rl.setOnLongClickListener {
clickListener?.onItemClick(bindingAdapterPosition, binding.rl.id)
true
}
notifyItemClick(position, binding.ivDelete)
}
}

View File

@@ -1,9 +1,14 @@
package com.lukouguoji.module_base.ui.page.preview
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.FileBean
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.databinding.ItemPreviewImageBinding
import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil
/**
* @author孟凡华
@@ -14,8 +19,26 @@ class PreviewImageViewHolder(view: View) :
BaseViewHolder<FileBean, ItemPreviewImageBinding>(view) {
override fun onBind(item: Any?, position: Int) {
binding.bean = getItemBean(item)
val bean = getItemBean(item) ?: return
binding.bean = bean
// 加载图片
val path = bean.path
if (path.isNotEmpty()) {
if (path.startsWith("http")) {
// 网络图片带 Authorization header
val glideUrl = GlideUrl(
path,
LazyHeaders.Builder()
.addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token))
.build()
)
Glide.with(itemView.context).load(glideUrl).into(binding.photoView)
} else {
// 本地图片直接加载
Glide.with(itemView.context).load(path).into(binding.photoView)
}
}
}
}

View File

@@ -24,7 +24,6 @@
<ImageView
android:id="@+id/iv"
loadImage="@{bean.path}"
visible="@{bean.path}"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -13,7 +13,7 @@
android:layout_height="match_parent">
<com.luck.picture.lib.photoview.PhotoView
loadImage="@{bean.path}"
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -21,6 +21,7 @@ import com.lukouguoji.module_base.util.Common
import java.text.SimpleDateFormat
import java.util.*
@Deprecated("旧版板箱过磅,使用 GjcBoxWeighingActivity 替代")
@Route(path = ARouterConstants.ACTIVITY_URL_GJC_FU_BANG)
class GjcFuBangActivity : BaseActivity(), View.OnClickListener {
private lateinit var viewModel: GjcFuBangViewModel

View File

@@ -13,6 +13,7 @@ import com.lukouguoji.module_base.base.CommonAdapter
import com.lukouguoji.module_base.ktx.addOnItemClickListener
import com.lukouguoji.module_base.router.ARouterConstants
@Deprecated("旧的实现")
@Route(path = ARouterConstants.ACTIVITY_URL_GJJ_MANIFEST)
class GjjManifestListActivity :
BaseBindingActivity<ActivityGjjManifestBinding, GjjManifestListViewModel>() {

View File

@@ -24,6 +24,7 @@ import com.lukouguoji.module_base.BaseActivity
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.router.ARouterConstants
@Deprecated("旧版国际进港查询详情,使用 IntImpQueryDetailsActivity 替代")
// @Route(path = ARouterConstants.ACTIVITY_URL_GJJ_QUERY_INFO) // 已替换为 IntImpQueryDetailsActivity
class GjjQueryInfoActivity : BaseActivity(), View.OnClickListener {

View File

@@ -27,6 +27,7 @@ import com.scwang.smart.refresh.layout.api.RefreshLayout
import java.text.SimpleDateFormat
import java.util.*
@Deprecated("旧版国际进港查询列表,使用 IntImpQueryActivity 替代")
//@Route(path = ARouterConstants.ACTIVITY_URL_GJJ_QUERY_LIST)
class GjjQueryListActivity : BaseActivity(), View.OnClickListener {
private val currentTitleName = "国际进港查询"

View File

@@ -3,14 +3,20 @@ package com.lukouguoji.gjj.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.alibaba.android.arouter.facade.annotation.Route
import com.lukouguoji.gjj.R
import com.lukouguoji.gjj.databinding.ActivityIntImpManifestDetailsBinding
import com.lukouguoji.gjj.holder.GjjManifestPicViewHolder
import com.lukouguoji.gjj.viewModel.IntImpManifestDetailsViewModel
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.base.CommonAdapter
import com.lukouguoji.module_base.bean.FileBean
import com.lukouguoji.module_base.bean.GjjManifest
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.router.ARouterConstants
import com.lukouguoji.module_base.util.MediaUtil
import com.lukouguoji.module_base.ktx.noNull
/**
* 国际进港舱单详情
@@ -26,6 +32,29 @@ class IntImpManifestDetailsActivity :
setBackArrow("进港舱单详情")
binding.viewModel = viewModel
viewModel.initOnCreated(intent)
// 交接图片
val picAdapter = CommonAdapter(
this,
R.layout.item_gjj_manifest_pic,
GjjManifestPicViewHolder::class.java
)
binding.rvPic.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
binding.rvPic.adapter = picAdapter
viewModel.dataBean.observe(this) { bean ->
val picList = bean.pic.noNull().split(",").filter { it.isNotEmpty() }
val originalPicList = bean.originalPic.noNull().split(",").filter { it.isNotEmpty() }
val list = picList.mapIndexed { index, picUrl ->
val originalFile = originalPicList.getOrElse(index) { picUrl }
FileBean(
path = MediaUtil.fillUrl(picUrl),
url = picUrl,
originalPic = MediaUtil.fillUrl(originalFile)
)
}
picAdapter.refresh(list)
}
}
companion object {

View File

@@ -1,9 +1,14 @@
package com.lukouguoji.gjj.holder
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.lukouguoji.gjj.databinding.ItemGjjManifestPicBinding
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.FileBean
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil
import com.lukouguoji.module_base.ktx.commonAdapter
import com.lukouguoji.module_base.ui.page.preview.PreviewActivity
@@ -14,6 +19,17 @@ class GjjManifestPicViewHolder(view: View) :
val bean = getItemBean(item)!!
binding.bean = bean
// 带 Authorization header 加载图片
if (bean.path.isNotEmpty()) {
val glideUrl = GlideUrl(
bean.path,
LazyHeaders.Builder()
.addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token))
.build()
)
Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail)
}
binding.ivThumbnail.setOnClickListener {
val items = getRecyclerView()?.commonAdapter()?.items
?.filterIsInstance<FileBean>() ?: listOf(bean)

View File

@@ -1,10 +1,8 @@
package com.lukouguoji.gjj.holder
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.lukouguoji.gjj.databinding.ItemIntImpStorageUseSubBinding
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.GjcMaWb
import com.lukouguoji.module_base.bean.GjcStorageUse
/**
@@ -19,27 +17,10 @@ class IntImpStorageUseSubViewHolder(view: View) :
binding.position = position
binding.executePendingBindings()
// 单选框点击切换选择状态(反向联动主列表)
// 单选框点击切换选择状态
binding.ivCheckbox.setOnClickListener {
// 切换子列表项的选择状态
val newCheckedState = !bean.checked.get()
bean.checked.set(newCheckedState)
bean.checked.set(!bean.checked.get())
binding.executePendingBindings()
// 反向联动主列表项(仅在勾选时联动)
updateParentCheckState(newCheckedState)
}
}
/**
* 更新父列表项的选择状态
*/
private fun updateParentCheckState(newCheckedState: Boolean) {
val recyclerView = itemView.parent as? RecyclerView ?: return
val parentBean = recyclerView.tag as? GjcMaWb ?: return
if (newCheckedState) {
parentBean.checked.set(true)
}
}
}

View File

@@ -20,18 +20,10 @@ class IntImpStorageUseViewHolder(view: View) :
binding.position = position
binding.executePendingBindings()
// 图标点击切换选择状态(联动子列表)
// 图标点击切换选择状态
binding.ivIcon.setOnClickListener {
val newCheckedState = !bean.checked.get()
bean.checked.set(newCheckedState)
// 联动勾选/取消所有子列表项
bean.storageUseList?.forEach { storageUse ->
storageUse.checked.set(newCheckedState)
}
bean.checked.set(!bean.checked.get())
binding.executePendingBindings()
binding.rvSub.adapter?.notifyDataSetChanged()
}
// 展开按钮点击事件

View File

@@ -1,7 +1,6 @@
package com.lukouguoji.gjj.holder
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.lukouguoji.gjj.databinding.ItemIntImpTallySubBinding
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.GjjImportTally
@@ -18,26 +17,10 @@ class IntImpTallySubViewHolder(view: View) :
binding.position = position
binding.executePendingBindings()
// 单选框点击切换选择状态(反向联动主列表
// 单选框点击切换选择状态(独立选择,不联动主
binding.ivCheckbox.setOnClickListener {
val newCheckedState = !bean.checked.get()
bean.checked.set(newCheckedState)
bean.checked.set(!bean.checked.get())
binding.executePendingBindings()
// 反向联动主列表项(勾选时联动主项也勾选)
updateParentCheckState(newCheckedState)
}
}
/**
* 更新父列表项的选择状态
*/
private fun updateParentCheckState(newCheckedState: Boolean) {
val recyclerView = itemView.parent as? RecyclerView ?: return
val parentBean = recyclerView.tag as? GjjImportTally ?: return
if (newCheckedState) {
parentBean.checked.set(true)
}
}
}

View File

@@ -25,18 +25,10 @@ class IntImpTallyViewHolder(view: View) :
binding.position = position
binding.executePendingBindings()
// 选中图标点击 - 切换选择状态(联动子列表
// 选中图标点击 - 切换选择状态(独立选择,不联动分单
binding.ivIcon.setOnClickListener {
val newCheckedState = !bean.checked.get()
bean.checked.set(newCheckedState)
// 联动勾选/取消所有子列表项
bean.haWbList?.forEach { sub ->
sub.checked.set(newCheckedState)
}
bean.checked.set(!bean.checked.get())
binding.executePendingBindings()
binding.rvSub.adapter?.notifyDataSetChanged()
}
// 整个内容区域点击 - 跳转到详情页

View File

@@ -32,6 +32,10 @@ class GjjManifestAddViewModel : BaseViewModel() {
// 舱单ID编辑时使用
var mfId: Long = 0
// 编号和前缀(编辑时使用)
var no: String = ""
var prefix: String = ""
// 航班ID
var fid: String = ""
@@ -271,9 +275,11 @@ class GjjManifestAddViewModel : BaseViewModel() {
// 保存舱单ID
mfId = manifest.mfId
fid = manifest.fid.toString()
no = manifest.no
prefix = manifest.prefix
// 填充表单字段
waybillNo.value = manifest.wbNo
waybillNo.value = manifest.getWaybillNo()
waybillNum.value = manifest.totalPc.toString()
actualNum.value = manifest.pc.toString()
actualWeight.value = manifest.weight.toString()
@@ -299,9 +305,11 @@ class GjjManifestAddViewModel : BaseViewModel() {
*/
private fun loadManifestFromImportBean(manifest: com.lukouguoji.module_base.bean.GjjImportManifest) {
fid = manifest.fid.toString()
no = manifest.no
prefix = manifest.prefix
// 填充表单字段
waybillNo.value = manifest.wbNo
waybillNo.value = "${manifest.prefix}${manifest.no}"
waybillNum.value = manifest.totalPc.toString()
actualNum.value = manifest.pc.toString()
actualWeight.value = manifest.weight.toString()
@@ -365,8 +373,9 @@ class GjjManifestAddViewModel : BaseViewModel() {
return
}
val params = mapOf(
"mfId" to if (pageType.value == DetailsPageType.Modify) mfId else null,
val isModify = pageType.value == DetailsPageType.Modify
val paramsMap = mutableMapOf<String, Any?>(
"fid" to fid,
"wbNo" to waybillNo.value,
"agentCode" to agent.value,
@@ -376,19 +385,30 @@ class GjjManifestAddViewModel : BaseViewModel() {
"pc" to actualNum.value,
"weight" to actualWeight.value,
"cashWeight" to billingWeight.value,
"packageType" to packageType.value,
"origin" to departure.value,
"dest" to destination.value,
"goods" to goodsNameEn.value,
"goodsCn" to goodsNameCn.value,
"awbType" to waybillType.value,
"cargoType" to goodsType.value,
"unNumber" to unNumber.value,
"remark" to remark.value,
).toRequestBody(removeEmptyOrNull = true)
)
// 可选字段:非空时才传
if (!packageType.value.isNullOrEmpty()) paramsMap["packageType"] = packageType.value
if (!goodsType.value.isNullOrEmpty()) paramsMap["cargoType"] = goodsType.value
if (!unNumber.value.isNullOrEmpty()) paramsMap["unNumber"] = unNumber.value
if (!remark.value.isNullOrEmpty()) paramsMap["remark"] = remark.value
// 编辑模式:必须传 mfId、no、prefix不受空字符串过滤影响
if (isModify) {
paramsMap["mfId"] = mfId
paramsMap["no"] = no
paramsMap["prefix"] = prefix
}
val params = paramsMap.toRequestBody()
launchLoadingCollect({
if (pageType.value == DetailsPageType.Modify) {
if (isModify) {
NetApply.api.gjjManifestUpdate(params)
} else {
NetApply.api.gjjManifestInsert(params)
@@ -396,7 +416,7 @@ class GjjManifestAddViewModel : BaseViewModel() {
}) {
onSuccess = {
if (it.verifySuccess()) {
val successMsg = if (pageType.value == DetailsPageType.Modify) "修改成功" else "保存成功"
val successMsg = if (isModify) "修改成功" else "保存成功"
showToast(successMsg)
// 发送刷新事件

View File

@@ -22,6 +22,8 @@ class GjjManifestDetailsViewModel : BaseViewModel(), IGetData {
var id = ""
var fid = ""
var no = ""
var prefix = ""
var pic = ""
var originalPic = ""
@@ -103,6 +105,8 @@ class GjjManifestDetailsViewModel : BaseViewModel(), IGetData {
onSuccess = { result ->
result.data?.let { data ->
fid = data.fid.toString()
no = data.no
prefix = data.prefix
waybillNo.value = data.getWaybillCode().noNull()
waybillNum.value = data.awbpc.toString()
actualNum.value = data.pc.toString()
@@ -223,6 +227,8 @@ class GjjManifestDetailsViewModel : BaseViewModel(), IGetData {
mapOf(
"mfId" to id,
"fid" to fid,
"no" to no,
"prefix" to prefix,
"wbNo" to waybillNo.value,
"agent" to agent,
"spCode" to specialCode,

View File

@@ -65,6 +65,51 @@ class IntArrSupplementInfoViewModel : BaseViewModel() {
} else null
} ?: emptyList()
countryCodeList.value = keyValueList
// 全量加载完成后,按始发站过滤查询,若唯一则自动选中
autoMatchCountryCodeByFdep()
}
}
}
/**
* 根据始发站/目的站自动匹配国家代码
* - 始发站(fdep) → 匹配发货人国家代码
* - 目的站(fdest) → 匹配收货人国家代码
*/
private fun autoMatchCountryCodeByFdep() {
val manifest = manifestList.firstOrNull() ?: return
val bean = dataBean.value ?: return
// 始发站 → 发货人国家代码
if (manifest.fdep.isNotEmpty() && bean.consignorCountryCode.isEmpty()) {
launchCollect({ NetApply.api.getCountryCodeListByFdep(manifest.fdep) }) {
onSuccess = { result ->
val filtered = result.data?.mapNotNull { dictBean ->
if (dictBean.code != null && dictBean.name != null) KeyValue(dictBean.name, dictBean.code) else null
} ?: emptyList()
if (filtered.size == 1) {
dataBean.value?.let { current ->
dataBean.value = current.copy(consignorCountryCode = filtered.first().value)
}
}
}
}
}
// 目的站 → 收货人国家代码
if (manifest.fdest.isNotEmpty() && bean.consigneeCountryCode.isEmpty()) {
launchCollect({ NetApply.api.getCountryCodeListByFdep(manifest.fdest) }) {
onSuccess = { result ->
val filtered = result.data?.mapNotNull { dictBean ->
if (dictBean.code != null && dictBean.name != null) KeyValue(dictBean.name, dictBean.code) else null
} ?: emptyList()
if (filtered.size == 1) {
dataBean.value?.let { current ->
dataBean.value = current.copy(consigneeCountryCode = filtered.first().value)
}
}
}
}
}
}

View File

@@ -144,20 +144,19 @@ class IntImpAccidentVisaEditViewModel : BaseViewModel(), IOnItemClickListener {
onSuccess = {
dataBean.value = it.data ?: GjAccidentVisaEditBean()
// 渲染图片
// 渲染图片path 取原图 URL 确保预览清晰url 取缩略图用于提交
val bean = dataBean.value!!
val picList = bean.pic.split(",")
.filter { url -> url.isNotEmpty() }
.map { url -> FileBean(MediaUtil.fillUrl(url), url) }
val originalList = bean.originalPic.split(",")
.filter { url -> url.isNotEmpty() }
.map { url -> FileBean(MediaUtil.fillUrl(url)) }
for ((index, fileBean) in picList.withIndex()) {
if (index < originalList.size) {
picList[index].originalPic = originalList[index].path
}
val picList = bean.pic.split(",").filter { it.isNotEmpty() }
val originalList = bean.originalPic.split(",").filter { it.isNotEmpty() }
val images = picList.mapIndexed { index, picUrl ->
val originalUrl = originalList.getOrElse(index) { picUrl }
FileBean(
path = MediaUtil.fillUrl(originalUrl),
url = picUrl,
originalPic = originalUrl
)
}
rv?.commonAdapter()?.loadMore(picList)
rv?.commonAdapter()?.loadMore(images)
// 详情模式下无图片时显示占位提示
if (isDetailMode.value == true && picList.isEmpty()) {
@@ -278,8 +277,10 @@ class IntImpAccidentVisaEditViewModel : BaseViewModel(), IOnItemClickListener {
.filter { it.path.isNotEmpty() && it.url.isEmpty() }
.onEach {
val data = UploadUtil.upload(it.path).data
it.url = data?.newName ?: ""
it.originalPic = data?.zipFileName ?: ""
// UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
// FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
it.url = data?.zipFileName ?: ""
it.originalPic = data?.newName ?: ""
}
.flowOn(Dispatchers.IO)
.onStart { showLoading() }
@@ -292,8 +293,8 @@ class IntImpAccidentVisaEditViewModel : BaseViewModel(), IOnItemClickListener {
val list = (rv?.commonAdapter()?.items as List<FileBean>)
.filter { it.path.isNotEmpty() }
bean.picNumber = list.size.toString()
bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) }
bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) }
bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) }
bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) }
bean.idFlag = "1"
if (pageType.value == DetailsPageType.Add) {

View File

@@ -37,7 +37,6 @@ class IntImpAccidentVisaViewModel : BasePageViewModel() {
val wbNo = MutableLiveData("") // 运单号
// ========== 航班查询 ==========
private var fid: String = ""
private var lastQueriedFlight = ""
// ========== 统计信息 ==========
@@ -91,7 +90,6 @@ class IntImpAccidentVisaViewModel : BasePageViewModel() {
onSuccess = {
if (it.verifySuccess() && it.data != null) {
val flight = it.data!!
fid = flight.fid.noNull()
fdest.value = flight.fdest.noNull()
val list = mutableListOf(
@@ -103,7 +101,6 @@ class IntImpAccidentVisaViewModel : BasePageViewModel() {
fdepList.value = list
fdep.value = flight.fdep.noNull()
} else {
fid = ""
fdest.value = ""
fdepList.value = emptyList()
fdep.value = ""
@@ -112,7 +109,6 @@ class IntImpAccidentVisaViewModel : BasePageViewModel() {
}
onFailed = { _, _ ->
fid = ""
fdest.value = ""
fdepList.value = emptyList()
fdep.value = ""
@@ -177,17 +173,13 @@ class IntImpAccidentVisaViewModel : BasePageViewModel() {
}
override fun getData() {
val filterParams = mutableMapOf<String, Any?>(
val filterParams = mapOf<String, Any?>(
"fdate" to flightDate.value?.ifEmpty { null },
"fno" to flightNo.value?.ifEmpty { null },
"fdep" to fdep.value?.ifEmpty { null },
"fdest" to fdest.value?.ifEmpty { null },
"wbNo" to wbNo.value?.ifEmpty { null }
)
if (fid.isNotEmpty()) {
filterParams["fid"] = fid
} else {
filterParams["fdate"] = flightDate.value?.ifEmpty { null }
filterParams["fno"] = flightNo.value?.ifEmpty { null }
}
val listParams = (filterParams + mapOf(
"pageNum" to pageModel.page,

View File

@@ -36,7 +36,7 @@ class IntImpLoadingListEditViewModel : BaseViewModel() {
val bean = Gson().fromJson(jsonData, GjjManifest::class.java)
dataBean.value = bean
// 初始化可编辑字段
location.value = bean.location
location.value = bean.locationTally
totalPcStr.value = bean.totalPc.toString()
pcStr.value = bean.pc.toString()
weightStr.value = bean.weight.toString()
@@ -58,7 +58,6 @@ class IntImpLoadingListEditViewModel : BaseViewModel() {
val bean = dataBean.value ?: return
// 同步可编辑字段回 bean
bean.location = location.value ?: ""
bean.totalPc = totalPcStr.value?.toLongOrNull() ?: 0
bean.pc = pcStr.value?.toLongOrNull() ?: 0
bean.weight = weightStr.value?.toDoubleOrNull() ?: 0.0

View File

@@ -64,30 +64,40 @@ class IntImpManifestViewModel : BasePageViewModel() {
lastQueriedFlight = key
launchCollect({
NetApply.api.getGjFlightBean(
NetApply.api.searchFlightList(
mapOf(
"fdate" to fdate,
"fno" to fno,
"ieFlag" to "I",
"status" to "1",
).toRequestBody()
)
}) {
onSuccess = {
if (it.verifySuccess() && it.data != null) {
val flight = it.data!!
fid = flight.fid.noNull()
fdep = flight.fdep.noNull()
fdest.value = flight.fdest.noNull()
if (it.verifySuccess() && !it.data.isNullOrEmpty()) {
val dataList = it.data!!
if (dataList.size > 1) {
showToast("存在多个航班记录,请核实")
fid = ""
fdep = ""
fdest.value = ""
sendAddressList.value = emptyList()
sendAddress.value = ""
} else {
val flight = dataList[0]
fid = flight.fid.noNull()
fdep = flight.fdep.noNull()
fdest.value = flight.fdest.noNull()
// 构建始发站下拉列表fdep + jtz经停港
val list = mutableListOf(
KeyValue(flight.fdep.noNull(), flight.fdep.noNull()),
)
if (!flight.jtz.isNullOrEmpty()) {
list.add(KeyValue(flight.jtz.noNull(), flight.jtz.noNull()))
// 构建始发站下拉列表fdep + jtz经停港
val list = mutableListOf(
KeyValue(flight.fdep.noNull(), flight.fdep.noNull()),
)
if (!flight.jtz.isNullOrEmpty()) {
list.add(KeyValue(flight.jtz.noNull(), flight.jtz.noNull()))
}
sendAddressList.value = list
sendAddress.value = flight.fdep.noNull()
}
sendAddressList.value = list
sendAddress.value = flight.fdep.noNull()
} else {
fid = ""
fdep = ""

View File

@@ -230,9 +230,13 @@ class IntImpStorageUseViewModel : BasePageViewModel() {
return
}
val body = maWbListForInStorage.toRequestBody()
val body = mapOf(
"location" to locationName,
"locationId" to locationId.toLongOrNull(),
"warehouseList" to maWbListForInStorage
).toRequestBody()
launchLoadingCollect({ NetApply.api.inIntImpStorage(locationName, body) }) {
launchLoadingCollect({ NetApply.api.inIntImpStorage(body) }) {
onSuccess = {
showToast("入库成功")
viewModelScope.launch {

View File

@@ -43,7 +43,7 @@ class IntImpTallyViewModel : BasePageViewModel() {
val isAllChecked = MutableLiveData(false)
init {
// 监听全选状态,自动更新所有列表项(联动子列表
// 监听全选状态,自动更新所有列表项(主单和分单独立全选
isAllChecked.observeForever { checked ->
val list = pageModel.rv?.commonAdapter()?.items as? List<GjjImportTally> ?: return@observeForever
list.forEach {

View File

@@ -263,25 +263,14 @@
android:textColor="@color/text_gray"
android:textSize="14sp" />
<LinearLayout
android:id="@+id/ll_images"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_pic"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:minHeight="80dp"
android:orientation="horizontal">
<!-- 交接图片区域预留,后续对接图片数据 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂无图片"
android:textColor="@color/text_gray"
android:textSize="12sp" />
</LinearLayout>
android:orientation="horizontal" />
</LinearLayout>

View File

@@ -148,20 +148,10 @@
title='@{"品名(中)"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@{viewModel.dataBean.goodsCn}'
value='@{viewModel.dataBean.goods}'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
enable="@{false}"
title='@{"品名(英)"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@{viewModel.dataBean.goodsEn}'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
android:layout_weight="2" />
</LinearLayout>

View File

@@ -83,7 +83,7 @@
title='@{"运单件数"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@{viewModel.maWbData.get("pc") != null ? String.valueOf((int)Math.round(((Double)viewModel.maWbData.get("pc")))) : ``}' />
value='@{viewModel.maWbData.get("awbPc") != null ? String.valueOf((int)Math.round(((Double)viewModel.maWbData.get("awbPc")))) : ``}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
@@ -93,7 +93,7 @@
title='@{"运单重量"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@{viewModel.maWbData.get("weight") != null ? String.valueOf(viewModel.maWbData.get("weight")) : ``}' />
value='@{viewModel.maWbData.get("awbWeight") != null ? String.valueOf(viewModel.maWbData.get("awbWeight")) : ``}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"

View File

@@ -11,7 +11,6 @@
<ImageView
android:id="@+id/iv_thumbnail"
loadImage="@{bean.path}"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginEnd="8dp"

View File

@@ -104,7 +104,7 @@
android:gravity="center"
android:maxLines="1"
android:ellipsize="end"
android:text="@{bean.lastMftStatus ?? ``}"
android:text="@{bean.tallyStatus ?? ``}"
android:textColor="@color/colorPrimary"
android:textSize="14sp" />

View File

@@ -18,6 +18,7 @@ import com.lukouguoji.module_base.ktx.launchLoadingCollect
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.ktx.toRequestBody
import com.lukouguoji.module_base.ktx.verifyNullOrEmpty
import com.lukouguoji.module_base.util.MediaUtil
import com.lukouguoji.module_base.util.UploadUtil
import dev.utils.app.info.KeyValue
import kotlinx.coroutines.launch
@@ -71,9 +72,18 @@ class GnjYiKuEditViewModel : BaseViewModel(), IOnItemClickListener {
val bean = it.data ?: GnjYiKuBean()
dataBean.value = bean
// 处理图片列表
val images = bean.getImageList().map { url ->
FileBean(path = url)
// 处理图片列表pic 字段存缩略图文件名originalPic 字段存原图文件名
val picList = bean.pic.split(",").filter { it.isNotEmpty() }
val originalPicList = bean.originalPic.split(",").filter { it.isNotEmpty() }
val images = picList.mapIndexed { index, picUrl ->
val fb = FileBean(
path = MediaUtil.fillUrl(picUrl),
url = picUrl
)
if (index < originalPicList.size) {
fb.originalPic = MediaUtil.fillUrl(originalPicList[index])
}
fb
}.toMutableList()
// 如果是编辑模式添加一个空的FileBean用于添加新图片
@@ -108,38 +118,26 @@ class GnjYiKuEditViewModel : BaseViewModel(), IOnItemClickListener {
launchLoadingCollect({
// 1. 上传图片
val uploadedUrls = mutableListOf<String>()
images.forEach { fileBean ->
// 判断是否为已上传的图片在线URL
if (fileBean.path.startsWith("http")) {
uploadedUrls.add(fileBean.path)
if (fileBean.url.isNotEmpty()) {
// 已上传的图片,保持原有的 url 和 originalPic
} else {
// 本地图片需要上传
val result = UploadUtil.upload(fileBean.path)
if (result.verifySuccess()) {
uploadedUrls.add(result.data?.newName ?: "")
}
// 本地图片需要上传
// UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
// FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
val data = UploadUtil.upload(fileBean.path).data
fileBean.url = data?.zipFileName ?: ""
fileBean.originalPic = data?.newName ?: ""
}
}
// 2. 提交表单数据
val params = mapOf(
"id" to id,
"wbNo" to bean.wbNo,
"pc" to bean.pc,
"weight" to bean.weight,
"spCode" to bean.spCode,
"agentCode" to bean.agentCode,
"goods" to bean.goods,
"flight" to bean.flight,
"route" to bean.route,
"awbType" to bean.awbType,
"telegramNo" to bean.telegramNo,
"remark" to bean.remark,
"images" to uploadedUrls.joinToString(","),
).toRequestBody(removeEmptyOrNull = true)
// 2. 设置图片字段
bean.picNumber = images.size.toString()
bean.pic = images.joinToString(",") { MediaUtil.removeUrl(it.url) }
bean.originalPic = images.joinToString(",") { MediaUtil.removeUrl(it.originalPic) }
NetApply.api.saveGnjYiKu(params)
// 3. 提交表单数据
NetApply.api.saveGnjYiKu(bean.toRequestBody())
}) {
onSuccess = {
showToast(if (pageType.value == DetailsPageType.Add) "新增成功" else "保存成功")

View File

@@ -62,10 +62,23 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener {
val bean = it.data ?: GnjYiKuBean()
dataBean.value = bean
// 处理图片列表(拼接完整 URL
val images = bean.getImageList().map { url ->
FileBean(path = MediaUtil.fillUrl(url), url = url)
}.toMutableList()
// 处理图片列表(同时保留缩略图和原图信息,确保二次编辑时不丢失
val picList = bean.pic.split(",").filter { it.isNotEmpty() }
val originalPicList = bean.originalPic.split(",").filter { it.isNotEmpty() }
val images = if (picList.isNotEmpty()) {
picList.mapIndexed { index, picUrl ->
val originalUrl = originalPicList.getOrElse(index) { picUrl }
FileBean(
path = MediaUtil.fillUrl(originalUrl),
url = picUrl,
originalPic = originalUrl
)
}.toMutableList()
} else {
originalPicList.map { url ->
FileBean(path = MediaUtil.fillUrl(url), url = url, originalPic = url)
}.toMutableList()
}
// 编辑模式添加空 FileBean 用于显示"添加照片"按钮
if (pageType.value == DetailsPageType.Modify) {
@@ -75,7 +88,7 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener {
imageList.value = images
// 详情模式下无图片时显示占位提示
if (pageType.value == DetailsPageType.Details && bean.getImageList().isEmpty()) {
if (pageType.value == DetailsPageType.Details && images.isEmpty()) {
showNoImage.value = true
}
}
@@ -100,16 +113,16 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener {
launchLoadingCollect({
// 上传图片
val uploadedUrls = mutableListOf<String>()
images.forEach { fileBean ->
if (fileBean.url.isNotEmpty()) {
// 已上传的图片,直接用文件名
uploadedUrls.add(fileBean.url)
// 已上传的图片,保持原有的 url 和 originalPic
} else {
val result = UploadUtil.upload(fileBean.path)
if (result.verifySuccess()) {
uploadedUrls.add(result.data?.newName ?: "")
}
// 本地新图片需要上传
// UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
// FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
val data = UploadUtil.upload(fileBean.path).data
fileBean.url = data?.zipFileName ?: ""
fileBean.originalPic = data?.newName ?: ""
}
}
@@ -117,7 +130,9 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener {
val params = mapOf(
"mawbId" to mawbId,
"remark" to bean.remark,
"originalPic" to uploadedUrls.joinToString(","),
"picNumber" to images.size.toString(),
"pic" to images.joinToString(",") { MediaUtil.removeUrl(it.url) },
"originalPic" to images.joinToString(",") { MediaUtil.removeUrl(it.originalPic) },
).toRequestBody(removeEmptyOrNull = true)
NetApply.api.modifyGnjMoveStash(params)

View File

@@ -30,8 +30,8 @@ import java.text.SimpleDateFormat
import java.util.*
@Deprecated("旧版航班查询,使用 HbQueryListActivity 替代")
// @Route(path = ARouterConstants.ACTIVITY_URL_HANG_BAN_QUERY) // 已废弃,使用 HbQueryListActivity
@Deprecated("使用 HbQueryListActivity 替代")
class HangBanQueryActivity : BaseActivity(), View.OnClickListener {
private val currentTitleName = "航班查询"

View File

@@ -17,6 +17,7 @@ import com.lukouguoji.module_base.BaseActivity
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.router.ARouterConstants
@Deprecated("旧版航班查询详情,仅被废弃的 HangBanQueryActivity 调用,实际不可达")
@Route(path = ARouterConstants.ACTIVITY_URL_HANG_BAN_QUERY_INFO)
class HangBanQueryInfoActivity : BaseActivity(), View.OnClickListener {
private lateinit var viewModel: HangBanQueryInfoViewModel