Compare commits
107 Commits
0d9406af13
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a88904f79 | ||
|
|
e828abe32f | ||
|
|
daaf651c04 | ||
|
|
8d5c37039d | ||
|
|
6fb7161e80 | ||
|
|
542c35b22d | ||
|
|
a909f96d61 | ||
|
|
00f2dc98dd | ||
|
|
2ff052c6e2 | ||
|
|
1b6f14da09 | ||
|
|
42c0e26bde | ||
|
|
39b9669a87 | ||
|
|
1488547220 | ||
|
|
d738a4acfe | ||
|
|
a58845121b | ||
|
|
a4167c3cf3 | ||
|
|
9db20749e2 | ||
|
|
eb0fb12c8d | ||
|
|
2996e670e0 | ||
|
|
36d74c3f0d | ||
|
|
9e545ee0ec | ||
|
|
66c7c0f4f6 | ||
|
|
075b6bf1ea | ||
|
|
d1d995c698 | ||
|
|
fa6c3f473e | ||
|
|
2c8fa689a5 | ||
|
|
b22d55a380 | ||
|
|
7f1a413172 | ||
|
|
f41d4186ea | ||
|
|
d9dc927525 | ||
|
|
d5631930c4 | ||
|
|
5b217ab032 | ||
|
|
f1ff42f2bf | ||
|
|
388d605869 | ||
|
|
131eb031ba | ||
|
|
42647c2136 | ||
|
|
5e8fe7d69d | ||
|
|
6f813f9cff | ||
|
|
68e6c7c52b | ||
|
|
cca62aae29 | ||
|
|
1229ecc920 | ||
|
|
d2340d46fb | ||
|
|
a431c09bd0 | ||
|
|
b522efa6b9 | ||
|
|
40edbe7c67 | ||
|
|
415b685a85 | ||
|
|
b3b884504d | ||
|
|
f6b3842882 | ||
|
|
18e438550c | ||
| 871fa4c5ce | |||
| dda0ff873a | |||
| 9e66e892f2 | |||
| 9b820ba8ef | |||
| 74f1280896 | |||
| 2dc86bcae5 | |||
| aebf1b1090 | |||
| 698046c9fb | |||
| 94508f8c40 | |||
| d54e1e429d | |||
| b7a2be0f35 | |||
| 75bfe85f62 | |||
| f39127436a | |||
| 6b02f71e8a | |||
| 50275457a4 | |||
| b14649b228 | |||
| 52d1527121 | |||
| 74cf1346a6 | |||
| 8853405ffc | |||
| ee0606230f | |||
| 07ea8853f3 | |||
| 6d4908da1a | |||
| a1d078db7a | |||
| f6e5b37eaa | |||
| 2f4e723dd7 | |||
| 52584b9cfe | |||
| efe7a1d2a9 | |||
| 85fd66b005 | |||
| de64c8ddb6 | |||
| 2a83c1393e | |||
| 9dddb7c079 | |||
| 2ba70d1a09 | |||
| ea83361102 | |||
| 7a09bbbeab | |||
| 6b2006fdb7 | |||
| b368efa827 | |||
| fcb247c3f5 | |||
| 5edb1bea06 | |||
| 51c5ac6e96 | |||
| ea298ec322 | |||
| b57a3cd133 | |||
| 4e6e654558 | |||
| b65e61595b | |||
| 86f77353c0 | |||
| 39b5cc3689 | |||
| a1b55f656f | |||
| 13c9c6f5dc | |||
| de62b903d4 | |||
| 2d42ce9cc3 | |||
| 861ce8ca1e | |||
| f312ef866c | |||
| 6f27c15d9a | |||
| ded57554fa | |||
| 2f3ae2a814 | |||
| 19d02700b8 | |||
| aa6cc43bfa | |||
| dcb335ecdf | |||
| 567b76014b |
@@ -39,9 +39,6 @@
|
||||
"Bash(git -C /Users/kid/Development/Fusion/Projects/aerologic-app stash)",
|
||||
"WebFetch(domain:api.apifox.cn)",
|
||||
"Bash(sips:*)",
|
||||
"Bash(vfox list:*)",
|
||||
"Bash(vf list:*)",
|
||||
"Bash(vfox use:*)",
|
||||
"Bash(wait)",
|
||||
"Bash(while read file)",
|
||||
"Bash(do sed -i '' 's/MutableLiveData\\(DateUtils\\\\.getCurrentTime\\(\\)\\\\.formatDate\\(\\)\\)/MutableLiveData<String>\\(DateUtils.getCurrentTime\\(\\).formatDate\\(\\)\\)/g' \"$file\" echo \"Modified: $file\" done)",
|
||||
|
||||
@@ -183,14 +183,16 @@ if (DeviceUtil.isPhone()) viewModel.initPhoneExtras()
|
||||
手机端首页是 `module_p` 的 `PDAEnterActivity`,「国际」Tab 的菜单在
|
||||
`module_p/.../ui/enter/gj/GjViewModel.kt`(国内在 `gn/GnViewModel.kt`)。
|
||||
**菜单是手写清单,不是接口下发**;权限串只做过滤(`authList.contains(bean.key)`)。
|
||||
2026-10-21 起清单按 `documents/5.5寸首页新样式/` 设计稿分组(`PdaMenuGroup`),新入口加进对应分组。
|
||||
|
||||
适配完页面不回来加一行,手机上就**根本没有入口**——出库交接第一版就漏了这步,
|
||||
只能靠 adb 直启才能进去。加法:
|
||||
|
||||
```kotlin
|
||||
// 加在 GjViewModel.groups 的「国际出港」分组里
|
||||
ActionBean(
|
||||
"出港移库",
|
||||
R.drawable.gjc_yi_ku_icon, // 图标复用 Pad 同款,复制 PNG 到 module_p/res/drawable-xxhdpi/
|
||||
R.drawable.img_pda_menu_gjc_move, // 设计稿宫格图标(icons/ 源图转 PNG 放 drawable-nodpi,放 xxhdpi 会糊);没有新图标的沿用旧图标
|
||||
Constant.AuthName.GjcYiKuListActivity, // 权限串与 Pad 端菜单一致
|
||||
ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE // 填了 route 就走通用跳转,无需 when 分支
|
||||
),
|
||||
|
||||
@@ -65,6 +65,9 @@ curl -s -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
|
||||
- `IntExpMove`:出参无航班字段(缺口 #1)、无操作人姓名(缺口 #2);`opId`/`opdate`/`businessType`
|
||||
文档有定义但实测恒 null(缺口 #3);`pageQueryTotal` 正常(与 pageQuery 口径一致,`moveState` 过滤生效)
|
||||
- `typeCode/getSpecialCodeByParam`:`flag=1` 国际,`ieFlag` 必须传空串(传 `"E"` 返回空数组)
|
||||
- `typeCode/searchDateType`:**同一路径两个接口**——`GET ?code=xxx` 是通用字典(`Api.getDictList`),
|
||||
`POST` 无参是「发现时间类型」(`Api.getSearchDateTypeList`,出参 `code` 1~8 + `name` 中英文时机);
|
||||
国际事故签证 `seachDate` 与 Web 端一致存 **name**(历史数据另有 `2026-09-24` 日期值与带 `\r\n` 的 name)
|
||||
- `eqm/uld`(ULD信息,6 个端点):查询入参仅 `uld`/`status`/`uldSuffix`(uldSuffix 名义是
|
||||
"uld后两位",实际就是**按所属航司筛选**,配 `DictUtils.getIntCarrierList`);`status` 仅两态
|
||||
0 正常/1 故障(三态需求=缺口 #7);无「来源」字段(#8)、无 `checkInDate`/`ifNo` 筛选入参(#9)、
|
||||
@@ -158,3 +161,30 @@ curl -s -X POST "http://192.168.1.250:8093/IntExpOutHandover/pageQuery" \
|
||||
用它做「参数是否生效」的 A/B 对比(同条件只改一个参数看 total 变化),
|
||||
再回 App UI 走一遍确认端到端链路。UI 里的滚轮日期选择器可用
|
||||
`input swipe`(慢速 500ms、每次 3 格 ≈ 396px)驱动,滑完截图校准。
|
||||
|
||||
## 2026-10-09 后端反馈联调后的最新口径(与上文冲突时以本节为准)
|
||||
|
||||
后端在问题清单补「解决方案」后逐项内网实测的结论。**通用规则:后端对手机端新增筛选维度一律答复
|
||||
「与平板端查询参数保持一致」——手机筛选只放平板已用的入参;文档里定义了但平板没用的入参,
|
||||
多数实测也被忽略(agentCode/spCode/dest/opId 等),别再按文档加筛选。**
|
||||
|
||||
- **统一统计 DTO**:各 `*/pageQueryTotal` 出参现含 `uldNumber`/`storedNumber`/`departedNumber`/
|
||||
`notClearNumber`/`clearNumber`/`waitCheckInNumber`/`checkingNumber`/`flightNumber`(`ManifestTotalDto`
|
||||
已补齐)。分项统计一律取出参,**不要再按状态入参多调几次 total**
|
||||
- `IntExpOutHandover/pageQueryTotal` 已修复(与列表一致);Tab 角标取 `uldNumber`(板箱数);无交接人入参
|
||||
- `IntExpMove/pageQuery` 出参新增 `flight`("20260924/MU2222")/`businessName`/`opUserName`;已移库 `opId`/`opdate` 已回填
|
||||
- `eqm/uld`:状态维持两态;`source`(空运/陆运) 出参与保存入参支持、**查询入参不支持**;入库时间 = `inStoreTime`
|
||||
(date-time,**只传日期 400**)、所在港 = `nowPort`;批删 `deleteUldBatch`(Body 为字符串数组);
|
||||
⚠️ `pageQuery` 带 `uld` 即 500(#35);`saveUld` 缺 `maxVolume`/`maxWeight` 即 500(#36)
|
||||
- `IntExpCheckIn`:`listRecordByWh` 出参 `opUserName`/`trayNumber`/`trayTotalWeight`;
|
||||
`checked/pageQuery` 出参 `passageWay`;`splitCheckIn`/`completeCheckIn` 托盘入参 = `trayNumber` +
|
||||
`trayTotalWeight`(数量×自重);`arriveFlag` 已回填("0" 正常/"1" 提前运抵);
|
||||
⚠️ `pageQuery` 的 `checkIn` 过滤仍被忽略(#40,统计用分项出参);`checked/pageQuery` 的 `dest`/`opId` 被忽略
|
||||
- `IntExpArrive`:入参 `arrivalStatus`("0" 未运抵/"1" 已运抵)服务端过滤生效;`agentCode` 被忽略;补充信息页已删除
|
||||
- `IntExpSearch`:**`beginDate`/`endDate` 只有携带 `dateState`(0 入库/1 离港)才生效**;`outState` 生效
|
||||
- `IntExpStorageUse`:仅 `fdate`/`fno`/`wbNo`/`location`/`clearNormal` 有意义;⚠️ 列表 total 与统计 wbNumber 不一致(#38)
|
||||
- `IntImpStorage`:⚠️ `pageQuery` 当前**所有**过滤条件均失效(#37),`pageQueryTotal` 正常
|
||||
- `IntImpSearch/detail`:按平板展示 航程 `range`(无经停站)/`unNumber`/`lockState`/出库时间 `dlvTime`
|
||||
- `IntImpPickUpDlv`:运单号入参用 `wbNo`;出参新增 `refUserName`(出库人姓名,`dlvName` 是提货人);测试环境暂无数据
|
||||
- `DomImpMove`:`fdate`/`fno`/`spCode` 已生效;`searchMoved` 已回填 `opId`/`opDate`/`opUserName`;
|
||||
新增 `GET movedDetail?moveId=`(含 pic 三字段);⚠️ 已移库运单无 `mawbId`,`modify` 无法补传照片(#39)
|
||||
|
||||
@@ -1,49 +1,70 @@
|
||||
---
|
||||
name: sync-air-cargo
|
||||
description: 将 aerologic-app 项目中未提交的变更(已修改、新增、已删除、包括未跟踪的文件)以文件级 cp/rm 方式同步到同级目录下的 air-cargo 项目——两者是同一份源码的两个独立 git 仓库副本,分别推送到不同的远程地址。只做文件复制/删除,不对内容做任何二次编辑或转换。每当用户提到"同步到 air-cargo"、"同步到另一个仓库/项目"、"推送/复制到 cargo 项目"、"两个项目要保持一致",或者在 aerologic-app 里改完代码后问要不要同步/推送到另一个仓库时,应主动使用此 skill,而不是手动逐个 cp 文件。
|
||||
description: 将 aerologic-app 的变更以文件级 cp/rm 方式同步到项目根目录下的 air-cargo/ 仓库并在其本地提交(不 push)——两者是同一份源码的两个独立 git 仓库副本,分别推送到不同的远程地址。同步点记录在 air-cargo 提交信息的 Synced-From trailer 中,自动从上次同步点同步到 main 最新提交(覆盖已提交未 push、已 push 的变更),也支持只同步未提交的工作区变更。只做文件复制/删除,不对内容做任何二次编辑或转换。每当用户提到"同步到 air-cargo"、"同步到另一个仓库/项目"、"推送/复制到 cargo 项目"、"两个项目要保持一致"、"把某几个 commit 同步过去",或者在 aerologic-app 里改完代码后问要不要同步/推送到另一个仓库时,应主动使用此 skill,而不是手动逐个 cp 文件。
|
||||
---
|
||||
|
||||
# 同步变更到 air-cargo
|
||||
|
||||
## 背景
|
||||
|
||||
`aerologic-app` 和同级目录下的 `air-cargo` 是同一份项目源码的两个独立 git 仓库副本,目录结构完全一致,分别推送到不同的远程仓库地址。日常开发只在 `aerologic-app` 中进行,完成后需要把未提交的变更原样同步一份到 `air-cargo`,再由用户在 `air-cargo` 里自行提交、推送。
|
||||
`aerologic-app` 和 `air-cargo` 是同一份项目源码的两个独立 git 仓库副本,分别推送到不同的远程仓库地址。日常开发只在 `aerologic-app` 中进行,完成后把变更同步到 `air-cargo` 并在本地提交,**push 由用户自己做**。
|
||||
|
||||
同步的判定完全基于 `git status`:在 `aerologic-app` 里,工作区中存在的文件(无论是修改、新增还是未跟踪)就复制覆盖到 `air-cargo` 对应路径;工作区中已不存在的文件(即被删除的)就在 `air-cargo` 里删除对应路径。这样天然覆盖了修改/新增/删除/未跟踪/重命名(重命名会被当作"删除旧路径 + 新增新路径"处理)等所有情况,不需要逐个状态码去分支判断。
|
||||
- **位置**:`air-cargo` 在 aerologic-app 根目录下(`/Users/tim/Development/Projects/aerologic-app/air-cargo/`),已在 aerologic-app 的 `.gitignore` 中忽略
|
||||
- **分支映射**(`config` 文件):aerologic-app `main` → air-cargo `feature/hefei`。air-cargo 的 `develop` 是 2023 年的旧快照,不能往上同步;当前分支不是 `feature/hefei` 时脚本直接报错
|
||||
- **不参与同步**:`CLAUDE.md` / `CLAUDE.local.md`、`.claude/` 目录、根目录 `.claude.*`(如 `.claude.local.md`)与 `.mcp.json`(Claude Code 的配置/记忆),不复制也不删除。air-cargo 中不应存在任何 Claude / Agent 相关文件,其本地 `.git/info/exclude` 已屏蔽这些文件名,防止误提交
|
||||
- **air-cargo 只是提交通道**:不参与本项目开发,代码扫描、功能实现、查历史时都忽略它(见 CLAUDE.md「air-cargo 目录」一节)
|
||||
|
||||
**只同步工程项目本身的文件**:`CLAUDE.md` 以及 `.claude/` 目录(包括其下的 `skills/`、`commands/`、`settings.local.json`、`mcp.json` 等)属于 Claude Code 的工具配置/记忆,两个仓库各自独立维护,不属于"项目源码",因此始终排除在同步范围之外,不会被复制也不会被删除。脚本会在输出里单独列出"已忽略,不参与同步"的清单,方便核对。
|
||||
## 同步点:air-cargo 提交信息里的 Synced-From trailer
|
||||
|
||||
每次同步提交的信息只有一句带日期的通用语句加一个 trailer,**不写修改明细**:
|
||||
|
||||
```
|
||||
Update 20261012
|
||||
|
||||
Synced-From: aerologic-app@388d6058696c05cfd5892018f52120880a440524
|
||||
```
|
||||
|
||||
脚本以 air-cargo 当前分支上最近一个 `Synced-From` 作为基线,把 `基线..main` 的变更同步过去。记录跟着 air-cargo 的提交历史走,只有真正提交了才算数。
|
||||
|
||||
- **区间模式(默认)**:基线 = 最近的 `Synced-From`,目标 = aerologic-app `main` 最新提交。**文件内容从目标提交对象中读取**,与 aerologic-app 工作区无关,因此已 commit 未 push、已 push 的变更都用它。提交时写入新的 `Synced-From`
|
||||
- **工作区模式(`--worktree`)**:同步 aerologic-app 未提交的变更。提交信息**不写** `Synced-From`,因为这份内容不对应任何提交,不能推进同步点。之后这些改动在 aerologic-app 提交后,下次区间同步会再覆盖一次(内容相同,无副作用)
|
||||
- **兜底:按内容反推**:找不到 `Synced-From` 时(首次同步,或有人手工提交时丢了 trailer),在 main 的第一父链上找与 air-cargo HEAD 内容差异最少的提交作为候选基线。只用于预览,`apply` 必须显式传 `--base`
|
||||
- **基线校验**:每次预览都会比较 air-cargo HEAD 和基线的内容。正常情况下只剩 air-cargo 自己的 6 个元文件:`.gitignore`(air-cargo 额外忽略 `*.jks`)、`key密码`、`CONTRIBUTING.md`、`备忘录.md`、`国际出港进港权限对照表.xlsx`、`.docs/大写字母数字输入过滤器使用指南.md`。超过 50 个会提示基线或分支可能不对
|
||||
|
||||
## 使用方法
|
||||
|
||||
脚本位于 `scripts/sync.sh`,用法:
|
||||
|
||||
```bash
|
||||
bash scripts/sync.sh [SOURCE_DIR] [TARGET_DIR] [preview|apply]
|
||||
cd /Users/tim/Development/Projects/aerologic-app
|
||||
bash .claude/skills/sync-air-cargo/scripts/sync.sh [preview|apply] [--base <ref>] [--to <ref>] [--worktree] [--label <word>]
|
||||
```
|
||||
|
||||
- `SOURCE_DIR` 默认为当前目录(一般就是 aerologic-app 项目根目录)
|
||||
- `TARGET_DIR` 默认为 `SOURCE_DIR` 同级的 `air-cargo` 目录,一般不需要手动指定
|
||||
- 第三个参数默认 `preview`(只打印清单,不做任何改动),传 `apply` 才会真正执行复制/删除
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `preview` / `apply` | 默认 `preview`,只打印计划;`apply` 复制/删除并在 air-cargo 本地提交 |
|
||||
| `--base <ref>` | 指定基线:反推结果经确认后传入,或用来纠正错误的同步点 |
|
||||
| `--to <ref>` | 指定目标提交,默认 `main`(只同步到某个较早的提交时使用) |
|
||||
| `--worktree` | 工作区模式,不能和 `--base`/`--to` 同时用 |
|
||||
| `--label <word>` | 提交信息的通用词,默认 `Update`。用户要求时可换成 `Fixed`、`Optimized` 等 |
|
||||
|
||||
### 步骤
|
||||
|
||||
1. **先预览**,让用户看到将要发生什么:
|
||||
```bash
|
||||
bash /Users/kid/Development/Fusion/Projects/aerologic-app/.claude/skills/sync-air-cargo/scripts/sync.sh
|
||||
```
|
||||
输出会分别列出"待复制/覆盖"和"待删除"的文件清单。
|
||||
1. **预览**:先确定模式——用户要同步已提交的代码用默认区间模式;明确说的是"没提交的改动"才加 `--worktree`。然后执行 `sync.sh`(区间模式不加参数)。
|
||||
- 输出「无需同步」:直接告诉用户,结束
|
||||
- 基线来源是「按内容反推」:把候选基线连同基线校验结果一起拿给用户确认(反推可能取到比预期更早的提交,例如 air-cargo 从未同步过的环境配置提交),确认后加 `--base <sha>` 再预览一次
|
||||
2. **用 `AskUserQuestion` 确认**,不要只在文字里问一句。问题描述里要包含:基线 → 目标、区间提交数、待复制/待删除数量(及主要模块分布)、**「⚠ 将覆盖 air-cargo 侧独立修改」清单(逐个列出)**、提交信息。选项至少包含「确认同步并提交」和「取消」。覆盖清单不为空时要特别说明这些文件是 air-cargo 自己改过的(如 `.gitignore` 里的 `*.jks` 忽略),由用户决定。
|
||||
3. **执行**:把预览时的参数原样带上,`preview` 换成 `apply`。脚本会复制/删除文件、精确暂存这些路径并提交,最后输出提交号和变更统计。
|
||||
4. **不要 push**。向用户报告提交号,并说明 air-cargo 比 `origin/feature/hefei` 超前几个提交,push 由用户自己做。
|
||||
|
||||
2. **用 `AskUserQuestion` 工具向用户确认**,不要只是在文字里问一句让用户打字回复。这是一个会覆盖 `air-cargo` 中已有文件、且删除操作不可逆的操作,必须先把预览清单(待复制、待删除的文件数量和列表)放进问题描述里,让用户能一眼看到影响范围,选项至少包含"确认同步"和"取消"。得到确认后才能继续执行 `apply`(这遵循整体规则:覆盖性/破坏性操作需要用户确认,不要因为 skill 本身能直接跑就跳过确认)。
|
||||
### 脚本的保护措施
|
||||
|
||||
3. **用户确认后执行同步**:
|
||||
```bash
|
||||
bash /Users/kid/Development/Fusion/Projects/aerologic-app/.claude/skills/sync-air-cargo/scripts/sync.sh . /Users/kid/Development/Fusion/Projects/air-cargo apply
|
||||
```
|
||||
执行完会输出"复制/更新 N 个文件,删除 M 个文件"的汇总。
|
||||
|
||||
4. **同步之后不要自动 commit/push**。脚本只做文件复制,`air-cargo` 那边的 `git add` / `commit` / `push` 由用户自行决定和操作——两个仓库指向不同的远程地址,提交推送这类会影响远程共享状态的操作应始终由用户主导确认。
|
||||
- air-cargo 当前分支不是 `config` 里的 `TARGET_BRANCH` → 报错
|
||||
- air-cargo 工作区有本次同步范围**以外**的未提交改动 → 报错;范围内的改动(例如上次同步还没提交)允许,重跑会覆盖成相同内容
|
||||
- 基线不是目标的祖先(aerologic-app 被 rebase / force-push 过)→ 报错,用 `--base` 重新指定
|
||||
- 区间内只有不参与同步的文件变化 → 仍然提交一个空提交,推进同步点
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 脚本只识别 git 能感知到的变更(`git status` 看得到的),`.gitignore` 忽略的文件(如构建产物 `build/`、`.gradle/`)不会被同步,这也是符合预期的——不应该把本地构建缓存同步过去。
|
||||
- 如果 `air-cargo` 里对应路径本身也有未提交的本地修改,会被直接覆盖覆盖掉,预览阶段要留意这一点并提醒用户。
|
||||
- 只同步 git 跟踪的内容,`.gitignore` 忽略的构建产物(`build/`、`.gradle/`)不会同步,`air-cargo/` 本身也不在变更清单里
|
||||
- 不要在 air-cargo 里手动提交同步内容。如果确实手动提交了,trailer 格式必须完全一致(`Synced-From: aerologic-app@<完整 sha>`),否则下次只能走反推
|
||||
- 首个同步点:2026-10-12,air-cargo `3dd792f9`,对应 aerologic-app `388d605`(基线选 `871fa4c`)。跳过的 `dda0ff8`(vfox→mise)随后在 air-cargo 单独提交 `3c76385b` 补上:删除 `.tool-versions` / `.vfox.toml` / `.vfox/`,新增与 aerologic-app 相同的 `mise.toml`。这个提交不带 trailer,同步点不变
|
||||
- 两个仓库都用 mise 管理 JDK(`mise.toml`:`java = "17"`)。air-cargo 位于 aerologic-app 之下,mise 会逐级读取配置,所以 air-cargo 里不能再出现 `.tool-versions`,否则会覆盖成未安装的旧版本
|
||||
|
||||
8
.claude/skills/sync-air-cargo/config
Normal file
@@ -0,0 +1,8 @@
|
||||
# sync-air-cargo 配置,由 scripts/sync.sh 以 shell 变量形式读入
|
||||
|
||||
# aerologic-app 的哪个分支同步到 air-cargo 的哪个分支(一对一)
|
||||
SOURCE_BRANCH=main
|
||||
TARGET_BRANCH=feature/hefei
|
||||
|
||||
# air-cargo 仓库相对 aerologic-app 根目录的位置(已在 aerologic-app 的 .gitignore 中忽略)
|
||||
TARGET_SUBDIR=air-cargo
|
||||
@@ -1,98 +1,276 @@
|
||||
#!/usr/bin/env bash
|
||||
# 将 SOURCE_DIR 中未提交的变更(修改/新增/删除)以 cp/rm 的方式同步到 TARGET_DIR。
|
||||
# 将 aerologic-app 的变更以文件级 cp/rm 同步到 air-cargo,并在 air-cargo 本地提交(不 push)。
|
||||
# 两个目录是同一份项目源码的独立 git 仓库副本,只做文件级复制/删除,不做任何内容编辑。
|
||||
#
|
||||
# 同步点记录在 air-cargo 提交信息的 trailer「Synced-From: aerologic-app@<sha>」中:
|
||||
# - 区间模式(默认):基线 = air-cargo 当前分支最近一个 Synced-From,目标 = 源仓库 SOURCE_BRANCH 最新提交。
|
||||
# 文件内容取自目标提交对象,与源仓库工作区无关;提交时写入新的 Synced-From,推进同步点
|
||||
# - 工作区模式(--worktree):同步源仓库未提交的变更;提交时不写 Synced-From,不推进同步点
|
||||
# 没有同步记录时按内容反推基线(目标第一父链上与 air-cargo HEAD 差异最少的提交),仅供确认,apply 须显式 --base
|
||||
set -euo pipefail
|
||||
export GIT_LITERAL_PATHSPECS=1
|
||||
|
||||
SOURCE_DIR="${1:-$(pwd)}"
|
||||
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
|
||||
TARGET_DIR="${2:-$(dirname "$SOURCE_DIR")/air-cargo}"
|
||||
MODE="${3:-preview}" # preview | apply
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: sync.sh [preview|apply] [选项]
|
||||
preview 只打印同步计划,不做任何改动(默认)
|
||||
apply 复制/删除文件并在 air-cargo 本地提交(不 push)
|
||||
选项:
|
||||
--base <ref> 指定基线:没有 Synced-From 记录(首次同步)或需要纠正时使用
|
||||
--to <ref> 指定目标提交,默认 config 中的 SOURCE_BRANCH
|
||||
--worktree 工作区模式:同步源仓库未提交的变更,不推进同步点
|
||||
--label <word> 提交信息的通用词,默认 Update(可用 Fixed、Optimized 等)
|
||||
EOF
|
||||
}
|
||||
|
||||
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
||||
echo "错误:$SOURCE_DIR 不是一个 git 仓库根目录" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -d "$TARGET_DIR" ]; then
|
||||
echo "错误:目标目录不存在:$TARGET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=../config
|
||||
. "$SCRIPT_DIR/../config"
|
||||
SOURCE_DIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
TARGET_DIR="$SOURCE_DIR/$TARGET_SUBDIR"
|
||||
SOURCE_NAME="$(basename "$SOURCE_DIR")"
|
||||
|
||||
# 只同步工程项目本身的文件,Claude Code 相关的配置/记忆文件(CLAUDE.md、.claude/ 目录,
|
||||
MODE=preview BASE_ARG="" TO_ARG="" WORKTREE=0 LABEL=Update
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
preview|apply) MODE="$1" ;;
|
||||
--base) BASE_ARG="${2:?--base 缺少参数}"; shift ;;
|
||||
--to) TO_ARG="${2:?--to 缺少参数}"; shift ;;
|
||||
--worktree) WORKTREE=1 ;;
|
||||
--label) LABEL="${2:?--label 缺少参数}"; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "未知参数:$1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
die() { echo "错误:$*" >&2; exit 1; }
|
||||
S() { git -C "$SOURCE_DIR" "$@"; }
|
||||
T() { git -C "$TARGET_DIR" "$@"; }
|
||||
short() { S rev-parse --short "$1"; }
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# 只同步工程项目本身的文件,Claude Code 相关的配置/记忆文件(CLAUDE.md、CLAUDE.local.md、.claude/ 目录、根目录 .claude.* 与 .mcp.json,
|
||||
# 包括其中的 skills/commands 等)两个仓库各自独立维护,不参与同步。
|
||||
is_excluded() {
|
||||
local path="$1"
|
||||
case "$path" in
|
||||
.claude/*|.claude) return 0 ;;
|
||||
CLAUDE.md|*/CLAUDE.md) return 0 ;;
|
||||
case "$1" in
|
||||
.claude|.claude/*|.claude.*|.mcp.json) return 0 ;;
|
||||
CLAUDE.md|*/CLAUDE.md|CLAUDE.local.md|*/CLAUDE.local.md) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# --no-renames:把 rename 拆成"删除旧路径 + 新增新路径"两条记录,避免解析组合格式
|
||||
# --untracked-files=all:新增的整个目录也会逐个文件列出,而不是折叠成一行目录
|
||||
# -z:以 NUL 分隔,规避文件名转义/含空格的问题
|
||||
copy_list=()
|
||||
delete_list=()
|
||||
excluded_list=()
|
||||
# 列出提交中参与同步的文件「路径<TAB>blob」(已排序),用于按内容比对两个仓库
|
||||
filtered_tree() {
|
||||
git -C "$1" -c core.quotepath=false ls-tree -r "$2" | awk -F'\t' '
|
||||
$2 ~ /^\.claude($|\/|\.)/ || $2 == ".mcp.json" || $2 ~ /(^|\/)CLAUDE(\.local)?\.md$/ { next }
|
||||
{ split($1, m, " "); print $2 "\t" m[3] }' | LC_ALL=C sort
|
||||
}
|
||||
|
||||
while IFS= read -r -d '' entry; do
|
||||
status="${entry:0:2}"
|
||||
path="${entry:3}"
|
||||
[ -z "$path" ] && continue
|
||||
if is_excluded "$path"; then
|
||||
excluded_list+=("$status|$path")
|
||||
continue
|
||||
fi
|
||||
if [ -e "$SOURCE_DIR/$path" ]; then
|
||||
copy_list+=("$status|$path")
|
||||
# 两份 filtered_tree 输出中内容不同的路径(去重)
|
||||
diff_paths() {
|
||||
LC_ALL=C comm -3 "$1" "$2" | sed $'s/^\t//' | cut -f1 | LC_ALL=C sort -u
|
||||
}
|
||||
|
||||
blob_at() { S rev-parse -q --verify "$1:$2" 2>/dev/null || true; }
|
||||
|
||||
# ---------- 前置检查 ----------
|
||||
[ -d "$SOURCE_DIR/.git" ] || die "$SOURCE_DIR 不是 git 仓库根目录"
|
||||
[ -d "$TARGET_DIR/.git" ] || die "目标仓库不存在:$TARGET_DIR"
|
||||
cur_branch="$(T branch --show-current)"
|
||||
[ "$cur_branch" = "$TARGET_BRANCH" ] ||
|
||||
die "air-cargo 当前分支为「${cur_branch:-游离 HEAD}」,应为「${TARGET_BRANCH}」(见 config),请先与用户确认"
|
||||
|
||||
# ---------- 确定基线与目标 ----------
|
||||
BASE="" BASE_FROM="" TO="" INFERRED=0
|
||||
if [ "$WORKTREE" -eq 1 ]; then
|
||||
[ -z "$BASE_ARG$TO_ARG" ] || die "--worktree 不能与 --base / --to 同时使用"
|
||||
BASE="$(S rev-parse HEAD)"
|
||||
BASE_FROM="源仓库 HEAD(工作区模式)"
|
||||
else
|
||||
TO="$(S rev-parse -q --verify "${TO_ARG:-$SOURCE_BRANCH}^{commit}")" || die "源仓库找不到 ${TO_ARG:-$SOURCE_BRANCH}"
|
||||
if [ -n "$BASE_ARG" ]; then
|
||||
BASE="$(S rev-parse -q --verify "$BASE_ARG^{commit}")" || die "源仓库找不到基线 $BASE_ARG"
|
||||
BASE_FROM="--base 参数"
|
||||
else
|
||||
delete_list+=("$status|$path")
|
||||
record="$(T log -1 --grep='^Synced-From: ' --format='%(trailers:key=Synced-From,valueonly)' | sed -n '1p')"
|
||||
if [ -n "$record" ]; then
|
||||
BASE="$(S rev-parse -q --verify "${record##*@}^{commit}")" ||
|
||||
die "air-cargo 记录的同步点 ${record##*@} 在源仓库中不存在(源仓库被 rebase / force-push 过?),请用 --base 指定"
|
||||
BASE_FROM="air-cargo 提交记录中的 Synced-From"
|
||||
fi
|
||||
fi
|
||||
done < <(git -C "$SOURCE_DIR" status --porcelain=v1 -z --no-renames --untracked-files=all)
|
||||
|
||||
echo "源目录: $SOURCE_DIR"
|
||||
echo "目标目录: $TARGET_DIR"
|
||||
echo ""
|
||||
echo "=== 待复制/覆盖(${#copy_list[@]})==="
|
||||
for item in "${copy_list[@]+"${copy_list[@]}"}"; do
|
||||
echo " [${item%%|*}] ${item#*|}"
|
||||
filtered_tree "$TARGET_DIR" HEAD > "$TMP/target.tree"
|
||||
if [ -z "$BASE" ]; then
|
||||
# 兜底:按内容反推,在目标的第一父链上找与 air-cargo HEAD 差异最少的提交
|
||||
best_n=-1
|
||||
for c in $(S rev-list --first-parent --max-count=300 "$TO"); do
|
||||
filtered_tree "$SOURCE_DIR" "$c" > "$TMP/cand.tree"
|
||||
n="$(diff_paths "$TMP/cand.tree" "$TMP/target.tree" | wc -l | tr -d ' ')"
|
||||
if [ "$best_n" -lt 0 ] || [ "$n" -lt "$best_n" ]; then BASE="$c"; best_n="$n"; fi
|
||||
[ "$n" -eq 0 ] && break
|
||||
done
|
||||
[ -n "$BASE" ] || die "无法反推基线,请用 --base 指定"
|
||||
INFERRED=1
|
||||
BASE_FROM="按内容反推(air-cargo 无 Synced-From 记录),需确认后以 --base 传入"
|
||||
fi
|
||||
S merge-base --is-ancestor "$BASE" "$TO" || die "基线 $(short "$BASE") 不是目标 $(short "$TO") 的祖先"
|
||||
fi
|
||||
|
||||
# ---------- 计算变更清单 ----------
|
||||
copy_list=() delete_list=() excluded_list=()
|
||||
classify() { # $1=状态 $2=路径 $3=同步后文件是否应存在(1/0)
|
||||
[ -z "$2" ] && return
|
||||
if is_excluded "$2"; then excluded_list+=("$1|$2")
|
||||
elif [ "$3" -eq 1 ]; then copy_list+=("$1|$2")
|
||||
else delete_list+=("$1|$2")
|
||||
fi
|
||||
}
|
||||
if [ "$WORKTREE" -eq 1 ]; then
|
||||
# --no-renames:rename 拆成「删除旧路径 + 新增新路径」;--untracked-files=all:新目录逐个文件列出
|
||||
while IFS= read -r -d '' entry; do
|
||||
p="${entry:3}"
|
||||
if [ -e "$SOURCE_DIR/$p" ] || [ -L "$SOURCE_DIR/$p" ]; then classify "${entry:0:2}" "$p" 1; else classify "${entry:0:2}" "$p" 0; fi
|
||||
done < <(S status --porcelain=v1 -z --no-renames --untracked-files=all)
|
||||
else
|
||||
# git diff -z 的 name-status 输出为「状态\0路径\0」交替
|
||||
while IFS= read -r -d '' st && IFS= read -r -d '' p; do
|
||||
if [ "$st" = D ]; then classify "$st" "$p" 0; else classify "$st" "$p" 1; fi
|
||||
done < <(S diff --name-status --no-renames -z "$BASE" "$TO")
|
||||
fi
|
||||
|
||||
new_blob() { # 同步后该路径应有的内容
|
||||
if [ "$WORKTREE" -eq 1 ]; then S hash-object -- "$1"; else blob_at "$TO" "$1"; fi
|
||||
}
|
||||
|
||||
# 三方校验:air-cargo 现有内容既不等于基线、也不等于同步后的内容,说明 air-cargo 侧有独立修改,会被覆盖
|
||||
conflict_list=()
|
||||
for item in ${copy_list[@]+"${copy_list[@]}"} ${delete_list[@]+"${delete_list[@]}"}; do
|
||||
st="${item%%|*}" p="${item#*|}"
|
||||
[ -f "$TARGET_DIR/$p" ] || continue
|
||||
tb="$(T hash-object -- "$p")"
|
||||
[ "$tb" = "$(blob_at "$BASE" "$p")" ] && continue
|
||||
[ "$st" != D ] && [ "$tb" = "$(new_blob "$p")" ] && continue
|
||||
conflict_list+=("$p")
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 待删除(${#delete_list[@]})==="
|
||||
for item in "${delete_list[@]+"${delete_list[@]}"}"; do
|
||||
echo " [${item%%|*}] ${item#*|}"
|
||||
done
|
||||
# air-cargo 工作区只允许存在本次同步范围内的改动(如上次同步未提交,重跑会覆盖为相同内容)
|
||||
for item in ${copy_list[@]+"${copy_list[@]}"} ${delete_list[@]+"${delete_list[@]}"}; do
|
||||
printf '%s\n' "${item#*|}"
|
||||
done | LC_ALL=C sort -u > "$TMP/synced"
|
||||
T status --porcelain=v1 -z --no-renames --untracked-files=all | tr '\0' '\n' | cut -c4- | LC_ALL=C sort -u > "$TMP/dirty"
|
||||
unrelated="$(LC_ALL=C comm -23 "$TMP/dirty" "$TMP/synced")"
|
||||
|
||||
echo ""
|
||||
echo "=== 已忽略,不参与同步(${#excluded_list[@]})==="
|
||||
for item in "${excluded_list[@]+"${excluded_list[@]}"}"; do
|
||||
echo " [${item%%|*}] ${item#*|}"
|
||||
done
|
||||
# ---------- 提交信息 ----------
|
||||
printf '%s %s\n' "$LABEL" "$(date +%Y%m%d)" > "$TMP/msg"
|
||||
[ "$WORKTREE" -eq 1 ] || printf '\nSynced-From: %s@%s\n' "$SOURCE_NAME" "$TO" >> "$TMP/msg"
|
||||
|
||||
if [ "$MODE" != "apply" ]; then
|
||||
# ---------- 打印计划 ----------
|
||||
print_list() { # $1=标题 其余=「状态|路径」
|
||||
local title="$1"; shift
|
||||
echo ""
|
||||
echo "(预览模式,未执行任何操作。确认无误后加 apply 参数执行同步)"
|
||||
echo "=== ${title}($#)==="
|
||||
for item in "$@"; do echo " [${item%%|*}] ${item#*|}"; done
|
||||
}
|
||||
echo "源仓库: $SOURCE_DIR"
|
||||
echo "目标仓库: ${TARGET_DIR}(分支 ${TARGET_BRANCH})"
|
||||
if [ "$WORKTREE" -eq 1 ]; then
|
||||
echo "模式: 工作区模式(同步未提交变更,不推进同步点)"
|
||||
else
|
||||
echo "模式: 区间模式"
|
||||
echo "基线: $(short "$BASE") $(S log -1 --format=%s "$BASE")"
|
||||
echo " 来源:$BASE_FROM"
|
||||
echo "目标: $(short "$TO") $(S log -1 --format=%s "$TO")(${TO_ARG:-$SOURCE_BRANCH})"
|
||||
echo "区间提交数: $(S rev-list --count "$BASE..$TO")"
|
||||
[ "$(S rev-parse HEAD)" = "$TO" ] || echo "提示: 源仓库 HEAD 不在目标提交上,同步内容以目标提交为准"
|
||||
|
||||
# 内容校验:同步点记录正确时,air-cargo HEAD 与基线应只有少量元文件差异
|
||||
filtered_tree "$SOURCE_DIR" "$BASE" > "$TMP/base.tree"
|
||||
diff_paths "$TMP/base.tree" "$TMP/target.tree" > "$TMP/verify"
|
||||
vn="$(wc -l < "$TMP/verify" | tr -d ' ')"
|
||||
echo ""
|
||||
echo "=== 基线校验:air-cargo HEAD 与基线有 $vn 个文件内容不同 ==="
|
||||
sed -n '1,20s/^/ /p' "$TMP/verify"
|
||||
[ "$vn" -le 20 ] || echo " ……(其余 $((vn - 20)) 个省略)"
|
||||
[ "$vn" -le 50 ] || echo " ⚠ 差异较大:基线可能不对,或 air-cargo 分支落后/不对,请先核实"
|
||||
fi
|
||||
|
||||
if [ "$WORKTREE" -eq 0 ] && [ "$BASE" = "$TO" ]; then
|
||||
echo ""
|
||||
echo "air-cargo 已同步到 $(short "$TO"),无需同步。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
copied=0
|
||||
deleted=0
|
||||
print_list "待复制/覆盖" ${copy_list[@]+"${copy_list[@]}"}
|
||||
print_list "待删除" ${delete_list[@]+"${delete_list[@]}"}
|
||||
print_list "已忽略,不参与同步" ${excluded_list[@]+"${excluded_list[@]}"}
|
||||
echo ""
|
||||
echo "=== ⚠ 将覆盖 air-cargo 侧独立修改(${#conflict_list[@]})==="
|
||||
for p in ${conflict_list[@]+"${conflict_list[@]}"}; do echo " $p"; done
|
||||
if [ -n "$unrelated" ]; then
|
||||
echo ""
|
||||
echo "=== ✖ air-cargo 工作区存在与本次同步无关的未提交改动 ==="
|
||||
printf '%s\n' "$unrelated" | sed 's/^/ /'
|
||||
fi
|
||||
echo ""
|
||||
echo "=== 提交信息 ==="
|
||||
sed 's/^/ /' "$TMP/msg"
|
||||
|
||||
for item in "${copy_list[@]+"${copy_list[@]}"}"; do
|
||||
path="${item#*|}"
|
||||
mkdir -p "$(dirname "$TARGET_DIR/$path")"
|
||||
cp -f "$SOURCE_DIR/$path" "$TARGET_DIR/$path"
|
||||
copied=$((copied+1))
|
||||
done
|
||||
if [ "$MODE" != apply ]; then
|
||||
echo ""
|
||||
echo "(预览模式,未执行任何操作)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for item in "${delete_list[@]+"${delete_list[@]}"}"; do
|
||||
path="${item#*|}"
|
||||
if [ -e "$TARGET_DIR/$path" ]; then
|
||||
rm -f "$TARGET_DIR/$path"
|
||||
deleted=$((deleted+1))
|
||||
# ---------- 执行 ----------
|
||||
[ "$INFERRED" -eq 0 ] || die "基线为反推结果,确认无误后请用 --base $(short "$BASE") 重新执行"
|
||||
[ -z "$unrelated" ] || die "air-cargo 工作区存在与本次同步无关的未提交改动,请先处理"
|
||||
|
||||
copied=0 deleted=0
|
||||
stage_list=()
|
||||
for item in ${copy_list[@]+"${copy_list[@]}"}; do
|
||||
p="${item#*|}" dst="$TARGET_DIR/${item#*|}"
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
rm -f "$dst"
|
||||
if [ "$WORKTREE" -eq 1 ]; then
|
||||
cp -f "$SOURCE_DIR/$p" "$dst"
|
||||
else
|
||||
mode="$(S ls-tree "$TO" -- "$p" | cut -d' ' -f1)"
|
||||
case "$mode" in
|
||||
120000) ln -s "$(S cat-file blob "$TO:$p")" "$dst" ;;
|
||||
160000) echo "跳过子模块:$p" >&2; continue ;;
|
||||
*) S cat-file blob "$TO:$p" > "$dst"; [ "$mode" != 100755 ] || chmod +x "$dst" ;;
|
||||
esac
|
||||
fi
|
||||
stage_list+=("$p")
|
||||
copied=$((copied + 1))
|
||||
done
|
||||
for item in ${delete_list[@]+"${delete_list[@]}"}; do
|
||||
p="${item#*|}"
|
||||
if [ -e "$TARGET_DIR/$p" ] || [ -L "$TARGET_DIR/$p" ]; then
|
||||
rm -f "$TARGET_DIR/$p"
|
||||
deleted=$((deleted + 1))
|
||||
fi
|
||||
# 只暂存 air-cargo 中已跟踪的路径,否则 git add 会因 pathspec 不匹配而失败
|
||||
! T ls-files --error-unmatch -- "$p" > /dev/null 2>&1 || stage_list+=("$p")
|
||||
done
|
||||
|
||||
[ ${#stage_list[@]} -eq 0 ] || printf '%s\0' "${stage_list[@]}" | T add -A --pathspec-from-file=- --pathspec-file-nul
|
||||
|
||||
echo ""
|
||||
echo "同步完成:复制/更新 $copied 个文件,删除 $deleted 个文件。"
|
||||
if T diff --cached --quiet; then
|
||||
if [ "$WORKTREE" -eq 1 ]; then
|
||||
echo "air-cargo 内容已一致,无需提交。"
|
||||
exit 0
|
||||
fi
|
||||
# 区间内只有不参与同步的文件变化时,仍提交一个空提交推进同步点
|
||||
T commit -q --allow-empty -F "$TMP/msg"
|
||||
else
|
||||
T commit -q -F "$TMP/msg"
|
||||
fi
|
||||
echo "已在 air-cargo 本地提交(未 push):$(T log -1 --format='%h %s')"
|
||||
T show --stat --format= HEAD | tail -1
|
||||
|
||||
83
.claude/skills/zentao-bug/SKILL.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: zentao-bug
|
||||
description: 读取禅道(njlkairport.chandao.net,开源版 18.8)中的 Bug 单——我的 Bug 列表、单个 Bug 的标题/描述/截图/评论/修改历史/附件——并按 AirLogistics 项目规范定位代码完成修复,也可在禅道上解决、指派 Bug(如转给后端并附问题说明)。禅道 18.8 没有 API 2.0,官方 zentao-cli / MCP 不可用,必须用本 skill 的 zentao.py。当用户提到"禅道"、"bug 单"、"我的 bug"、"修 bug"、"处理 bug"、给出 Bug 编号(如 "#632"、"bug 632")或禅道链接(bug-view-xxx.html / my-work-bug.html)时使用。
|
||||
---
|
||||
|
||||
# 禅道 Bug 读取与修复
|
||||
|
||||
## 工具
|
||||
|
||||
```bash
|
||||
Z=.claude/skills/zentao-bug/zentao.py
|
||||
python3 $Z whoami # 当前账号 + 会话是否有效
|
||||
python3 $Z list # 指派给我的 Bug(= 网页「地盘 → 我的Bug」)
|
||||
python3 $Z list --status active # 只看激活的
|
||||
python3 $Z list --type resolvedBy # 由我解决 / openedBy / closedBy
|
||||
python3 $Z list --json # 结构化输出
|
||||
python3 $Z view 632 # Bug 详情(Markdown)
|
||||
```
|
||||
|
||||
`view` 输出并保存到 `~/.cache/zentao-lite/bugs/<id>/bug.md`:基本信息、重现步骤、附件、历史记录与评论(评论以 `>` 引用块呈现)。
|
||||
描述和评论中的截图下载到 `images/`,文中以绝对路径 `` 引用。
|
||||
|
||||
**截图是需求的主要载体**(团队习惯把原型/标注贴在描述和评论里,几乎不用附件),**必须逐张用 Read 查看**,不能只看文字。
|
||||
|
||||
## 登录(只能由用户本人执行)
|
||||
|
||||
退出码 2 表示未登录或会话过期。禅道会话默认空闲 24 分钟失效。让用户在自己的终端执行:
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/zentao-bug/zentao.py login --account <账号>
|
||||
```
|
||||
|
||||
必须在**独立的终端窗口**里执行——Claude Code 的 `! <命令>` 没有 TTY,密码提示会直接 EOFError。
|
||||
密码交互输入、不回显;默认存入 macOS 钥匙串,此后会话过期会自动重新登录(`--no-keychain` 则不保存)。
|
||||
**不要向用户索要密码,也不要替用户在命令行里带密码登录。**
|
||||
|
||||
## 修复流程
|
||||
|
||||
1. **读单**:`view <id>`,Read 全部截图;评论里常有后续补充或改口,以最新评论为准。
|
||||
2. **定位页面**:Bug 标题/描述通常写的是菜单名(如"出港移库""进港仓库")。按 CLAUDE.md「页面定位规则」,
|
||||
以首页菜单实际跳转的 Activity 为准(平板 `app/.../HomeFragment.kt`,手机 `module_p` 的 `GnViewModel`/`GjViewModel`)。
|
||||
3. **判断端**:截图是手机竖屏(如 5.5 寸设计稿样式)还是平板横屏,决定改 `res/layout/` 还是 `res/layout-sw600dp/`;
|
||||
业务逻辑改动两端都要检查(CLAUDE.md「业务以平板为准」)。截图提到"原型"时对照 `documents/5.5寸手持端设计文件/`。
|
||||
4. **修改并验证**:遵守 CLAUDE.md 的 UI 规范;`./gradlew assembleDebug` 编译通过;能在模拟器验证的就验证并截图比对。
|
||||
5. **汇报**:按 Bug 逐条说明改了什么、改在哪里、如何验证、有无未覆盖项。多个 Bug 时每个 Bug 单独提交,
|
||||
提交信息带 `Bug #<id>`。不确定截图意图时先问用户,禁止臆测。
|
||||
|
||||
## 写回禅道
|
||||
|
||||
```bash
|
||||
python3 $Z resolve 621 622 --comment 已解决 --dry-run # 先预览
|
||||
python3 $Z resolve 621 622 --comment 已解决 # 提交
|
||||
```
|
||||
|
||||
等同网页「解决」表单,默认:解决方案 `fixed`(已解决)、解决版本 `trunk`(主干);**解决日期与指派给取网页表单渲染出的默认值**
|
||||
(服务器当前时间 / 创建人,可用 `--date` / `--assign-to` 覆盖),其余字段不动;备注默认留空,团队习惯只写「已解决」,不写业务细节。
|
||||
只处理激活状态的单子,提交后回读校验状态。
|
||||
|
||||
- **写操作会改动线上状态,只在用户明确要求时执行**;只解决确实修好、且没有待确认疑问的单子。
|
||||
无缺陷 / 需后端修复 / 有疑问的保持激活,并向用户列出原因(无缺陷应选「设计如此」等其他方案,不要标「已解决」)
|
||||
- 接口提交**不会**自动补表单默认值,所以先 GET 解决表单 HTML 读出 `resolvedDate` 与 `assignedTo` 的选中项再原样提交;
|
||||
解决日期不能用本机时间:本机可能在美西时区,禅道服务器是北京时间(#621 曾因此写成本机时间)
|
||||
|
||||
### 指派(转给后端等)
|
||||
|
||||
```bash
|
||||
python3 $Z assign 627 --to 陶雨佳 --comment-file /path/c627.txt --dry-run # 先预览
|
||||
python3 $Z assign 627 --to 陶雨佳 --comment-file /path/c627.txt # 提交
|
||||
```
|
||||
|
||||
等同网页「指派」表单:`--to` 接受账号(如 `TYJ`)或姓名,状态保持激活,提交后回读校验指派人。
|
||||
需后端修复的单子转给后端时,备注照搬 `documents/后端接口对接问题清单.xlsx` 对应行
|
||||
(接口地址 / 问题类型 / 问题描述 / 期望调整 / 前端现状),并注明清单序号;多行备注用 `--comment-file`,换行会转成 `<br />`。
|
||||
|
||||
## 实现说明
|
||||
|
||||
- 认证:`POST /api.php/v1/tokens` 换 Token;数据走页面 JSON 模式(`.html` → `.json`),Cookie `zentaosid=<Token>`。
|
||||
- 接口:`/my-work-bug-<type>-0-id_desc-0-<perPage>-<page>.json`、`/bug-view-<id>.json`、`/file-read-<fileID>.<ext>`;
|
||||
解决:GET `/bug-resolve-<id>.json` 取版本/用户选项,POST `/bug-resolve-<id>.html?onlybody=yes` 表单
|
||||
(resolution/resolvedBuild/resolvedDate/assignedTo/status=resolved/comment);
|
||||
指派:GET `/bug-assignTo-<id>.json` 取用户,POST `/bug-assignTo-<id>.html?onlybody=yes` 表单(assignedTo/status/mailto[]/comment)。
|
||||
- REST v1 的 `/products/<id>/bugs` 只返回激活中的 Bug,不能替代"我的 Bug"页面。
|
||||
- 凭据:Token 在 `~/.config/zentao-lite/config.json`(0600),密码只在钥匙串(服务名 `zentao-lite`)。`logout` 一并清除。
|
||||
678
.claude/skills/zentao-bug/zentao.py
Normal file
@@ -0,0 +1,678 @@
|
||||
#!/usr/bin/env python3
|
||||
"""禅道 18.x Bug 读取工具(仅依赖 Python 标准库)。
|
||||
|
||||
禅道 18.8 没有 RESTful API 2.0,官方 zentao-cli 无法使用;本脚本组合两条 18.x 可用的通道:
|
||||
- 认证:POST /api.php/v1/tokens 换取 Token(即会话 ID)
|
||||
- 数据:页面 JSON 模式(把页面地址 .html 换成 .json),Cookie 带 zentaosid=<Token>
|
||||
- 我的 Bug:/my-work-bug-<type>-0-id_desc-0-<perPage>-<page>.json
|
||||
- Bug 详情:/bug-view-<id>.json(含描述、修改记录、评论)
|
||||
- 图片:/file-read-<fileID>.<ext>
|
||||
|
||||
凭据:Token 存 ~/.config/zentao-lite/config.json(0600);密码只存 macOS 钥匙串,
|
||||
用于会话过期(禅道默认 24 分钟空闲失效)后自动重新登录,任何文件里都不落明文密码。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import getpass
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_DIR = Path(os.environ.get("ZENTAO_LITE_HOME", Path.home() / ".config" / "zentao-lite"))
|
||||
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||
DEFAULT_OUT = Path.home() / ".cache" / "zentao-lite" / "bugs"
|
||||
KEYCHAIN_SERVICE = "zentao-lite"
|
||||
DEFAULT_URL = "https://njlkairport.chandao.net"
|
||||
|
||||
STATUS = {"active": "激活", "resolved": "已解决", "closed": "已关闭"}
|
||||
TYPES = {
|
||||
"codeerror": "代码错误", "config": "配置相关", "install": "安装部署", "security": "安全相关",
|
||||
"performance": "性能问题", "standard": "标准规范", "automation": "测试脚本",
|
||||
"designdefect": "设计缺陷", "others": "其他",
|
||||
}
|
||||
RESOLUTIONS = {
|
||||
"fixed": "已解决", "bydesign": "设计如此", "duplicate": "重复Bug", "external": "外部原因",
|
||||
"notrepro": "无法重现", "postponed": "延期处理", "willnotfix": "不予解决", "tostory": "转为需求",
|
||||
}
|
||||
ACTIONS = {
|
||||
"opened": "创建", "edited": "编辑", "commented": "备注", "resolved": "解决", "activated": "激活",
|
||||
"closed": "关闭", "assigned": "指派", "confirmed": "确认", "bugconfirmed": "确认",
|
||||
"tostory": "转需求", "totask": "转任务", "linked2build": "关联版本", "linked2release": "关联发布",
|
||||
}
|
||||
FIELDS = {
|
||||
"steps": "重现步骤", "title": "标题", "assignedTo": "指派给", "status": "状态", "severity": "严重程度",
|
||||
"pri": "优先级", "type": "类型", "resolution": "解决方案", "resolvedBuild": "解决版本",
|
||||
"module": "模块", "deadline": "截止日期", "keywords": "关键词", "openedBuild": "影响版本",
|
||||
"resolvedBy": "解决者", "resolvedDate": "解决日期", "closedBy": "关闭者", "closedDate": "关闭日期",
|
||||
"confirmed": "是否确认", "mailto": "抄送给", "linkBug": "相关Bug", "activatedCount": "激活次数",
|
||||
}
|
||||
USER_FIELDS = {"assignedTo", "resolvedBy", "closedBy", "openedBy"}
|
||||
BUG_TYPES = ("assignedTo", "openedBy", "resolvedBy", "closedBy")
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def die(msg, code=1):
|
||||
print(f"错误:{msg}", file=sys.stderr)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 配置与凭据
|
||||
|
||||
def load_config():
|
||||
if not CONFIG_FILE.exists():
|
||||
die("尚未登录,请先执行:python3 .claude/skills/zentao-bug/zentao.py login", 2)
|
||||
return json.loads(CONFIG_FILE.read_text())
|
||||
|
||||
|
||||
def save_config(cfg):
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
tmp = CONFIG_FILE.with_suffix(".tmp")
|
||||
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, CONFIG_FILE)
|
||||
|
||||
|
||||
def keychain_get(account):
|
||||
if sys.platform != "darwin":
|
||||
return None
|
||||
r = subprocess.run(
|
||||
["security", "find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
return r.stdout.rstrip("\n") if r.returncode == 0 else None
|
||||
|
||||
|
||||
def keychain_set(account, password):
|
||||
if sys.platform != "darwin":
|
||||
return False
|
||||
r = subprocess.run(
|
||||
["security", "add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", account, "-w", password],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
return r.returncode == 0
|
||||
|
||||
|
||||
def keychain_delete(account):
|
||||
if sys.platform == "darwin":
|
||||
subprocess.run(["security", "delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account],
|
||||
capture_output=True)
|
||||
|
||||
|
||||
def request_token(url, account, password):
|
||||
body = json.dumps({"account": account, "password": password}).encode()
|
||||
req = urllib.request.Request(url + "/api.php/v1/tokens", data=body,
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = json.load(resp)
|
||||
except urllib.error.HTTPError as e:
|
||||
try:
|
||||
data = json.load(e)
|
||||
except Exception:
|
||||
data = {"error": f"HTTP {e.code}"}
|
||||
if not data.get("token"):
|
||||
raise AuthError(data.get("error") or "登录失败")
|
||||
return data["token"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 请求
|
||||
|
||||
class Client:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.url = cfg["url"].rstrip("/")
|
||||
self.relogged = False
|
||||
|
||||
def _relogin(self):
|
||||
if self.relogged:
|
||||
raise AuthError("重新登录后仍无权限")
|
||||
password = keychain_get(self.cfg["account"])
|
||||
if not password:
|
||||
raise AuthError("会话已过期,且钥匙串中没有保存密码,请重新执行 login")
|
||||
self.cfg["token"] = request_token(self.url, self.cfg["account"], password)
|
||||
save_config(self.cfg)
|
||||
self.relogged = True
|
||||
|
||||
def _open(self, path):
|
||||
req = urllib.request.Request(self.url + path, headers={
|
||||
"Cookie": f"zentaosid={self.cfg['token']}", "Token": self.cfg["token"],
|
||||
})
|
||||
try:
|
||||
return urllib.request.urlopen(req, timeout=60)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
raise AuthError("Token 无效")
|
||||
raise
|
||||
|
||||
def page(self, path):
|
||||
"""读取页面 JSON;会话失效时自动用钥匙串密码重登一次。"""
|
||||
for _ in range(2):
|
||||
try:
|
||||
with self._open(path) as resp:
|
||||
outer = json.load(resp)
|
||||
data = outer.get("data")
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
if outer.get("status") != "success" or (isinstance(data, dict) and "user-login" in str(data.get("locate", ""))):
|
||||
raise AuthError("会话已过期")
|
||||
return data
|
||||
except AuthError:
|
||||
self._relogin()
|
||||
raise AuthError("无法获取数据")
|
||||
|
||||
def post_form(self, path, fields):
|
||||
"""提交页面表单(与网页点击「保存」相同);返回响应 HTML(禅道用 <script>alert/parent 跳转回执)。"""
|
||||
body = urllib.parse.urlencode(fields).encode()
|
||||
for _ in range(2):
|
||||
req = urllib.request.Request(self.url + path, data=body, headers={
|
||||
"Cookie": f"zentaosid={self.cfg['token']}", "Token": self.cfg["token"],
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Referer": self.url + path,
|
||||
})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
text = resp.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 401:
|
||||
raise
|
||||
text = "user-login"
|
||||
if "user-login" not in text:
|
||||
return text
|
||||
self._relogin()
|
||||
raise AuthError("无法提交表单")
|
||||
|
||||
def download(self, path, dest):
|
||||
for _ in range(2):
|
||||
try:
|
||||
with self._open(path) as resp:
|
||||
ctype = resp.headers.get("Content-Type", "")
|
||||
content = resp.read()
|
||||
if "text/html" in ctype or "application/json" in ctype:
|
||||
if b"user-login" in content:
|
||||
raise AuthError("会话已过期")
|
||||
raise RuntimeError(f"返回的不是文件内容({ctype})")
|
||||
dest.write_bytes(content)
|
||||
return
|
||||
except AuthError:
|
||||
self._relogin()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- HTML → Markdown
|
||||
|
||||
class Html2Md(HTMLParser):
|
||||
BLOCK = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "tr", "table", "blockquote", "pre"}
|
||||
|
||||
def __init__(self, on_image):
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.out = []
|
||||
self.lists = []
|
||||
self.on_image = on_image
|
||||
self.href = None
|
||||
self.skip = 0
|
||||
|
||||
def _nl(self, n=1):
|
||||
text = "".join(self.out)
|
||||
have = len(text) - len(text.rstrip("\n"))
|
||||
if text.strip():
|
||||
self.out.append("\n" * max(0, n - have))
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
a = dict(attrs)
|
||||
if tag in ("style", "script"):
|
||||
self.skip += 1
|
||||
elif tag in self.BLOCK:
|
||||
self._nl(2 if tag != "tr" else 1)
|
||||
if tag[0] == "h" and tag[1:].isdigit():
|
||||
self.out.append("**") # 标题降级为加粗,避免与外层 Markdown 标题层级混淆
|
||||
elif tag == "br":
|
||||
self.out.append("\n")
|
||||
elif tag in ("ul", "ol"):
|
||||
self._nl(1)
|
||||
self.lists.append([tag, 0])
|
||||
elif tag == "li":
|
||||
self._nl(1)
|
||||
indent = " " * max(0, len(self.lists) - 1)
|
||||
if self.lists and self.lists[-1][0] == "ol":
|
||||
self.lists[-1][1] += 1
|
||||
self.out.append(f"{indent}{self.lists[-1][1]}. ")
|
||||
else:
|
||||
self.out.append(f"{indent}- ")
|
||||
elif tag in ("strong", "b"):
|
||||
self.out.append("**")
|
||||
elif tag == "td" or tag == "th":
|
||||
self.out.append("| ")
|
||||
elif tag == "a":
|
||||
self.href = a.get("href")
|
||||
self.out.append("[")
|
||||
elif tag == "img":
|
||||
src = a.get("src") or a.get("data-src") or ""
|
||||
if src:
|
||||
self.out.append(self.on_image(src))
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag in ("style", "script"):
|
||||
self.skip = max(0, self.skip - 1)
|
||||
elif tag in self.BLOCK:
|
||||
if tag[0] == "h" and tag[1:].isdigit():
|
||||
self.out.append("**")
|
||||
self._nl(2 if tag != "tr" else 1)
|
||||
elif tag in ("ul", "ol"):
|
||||
if self.lists:
|
||||
self.lists.pop()
|
||||
self._nl(2)
|
||||
elif tag in ("strong", "b"):
|
||||
self.out.append("**")
|
||||
elif tag in ("td", "th"):
|
||||
self.out.append(" ")
|
||||
elif tag == "a":
|
||||
self.out.append(f"]({self.href})" if self.href else "]")
|
||||
self.href = None
|
||||
|
||||
def handle_data(self, data):
|
||||
if not self.skip:
|
||||
self.out.append(re.sub(r"[ \t\r\n]+", " ", data) if not data.isspace() else (" " if self.out and not self.out[-1].endswith(("\n", " ")) else ""))
|
||||
|
||||
def markdown(self):
|
||||
text = "".join(self.out).replace("****", "")
|
||||
text = "\n".join(line.rstrip() for line in text.splitlines())
|
||||
return re.sub(r"\n{3,}", "\n\n", text).strip()
|
||||
|
||||
|
||||
class ImageFetcher:
|
||||
"""把富文本中的图片下载到本地,返回 Markdown 图片引用(绝对路径,可直接用 Read 查看)。"""
|
||||
|
||||
def __init__(self, client, img_dir):
|
||||
self.client = client
|
||||
self.dir = img_dir
|
||||
self.files = []
|
||||
self.errors = []
|
||||
|
||||
def __call__(self, src):
|
||||
src = html.unescape(src.strip())
|
||||
m = re.fullmatch(r"\{(\d+)\.(\w+)\}", src) # 数据库原始占位符 {1154.jpeg}
|
||||
if m:
|
||||
src = f"/file-read-{m.group(1)}.{m.group(2)}"
|
||||
try:
|
||||
dest = self._fetch(src)
|
||||
except Exception as e: # 单张图片失败不影响整体
|
||||
self.errors.append(f"{src}:{e}")
|
||||
return f""
|
||||
if dest not in self.files:
|
||||
self.files.append(dest)
|
||||
return f""
|
||||
|
||||
def _fetch(self, src):
|
||||
self.dir.mkdir(parents=True, exist_ok=True)
|
||||
if src.startswith("data:"):
|
||||
head, b64 = src.split(",", 1)
|
||||
ext = re.search(r"image/(\w+)", head)
|
||||
dest = self.dir / f"inline-{len(self.files) + 1}.{ext.group(1) if ext else 'png'}"
|
||||
dest.write_bytes(base64.b64decode(b64))
|
||||
return dest
|
||||
parsed = urllib.parse.urlparse(src)
|
||||
if parsed.netloc and parsed.netloc not in self.client.url:
|
||||
dest = self.dir / Path(parsed.path).name
|
||||
if not dest.exists():
|
||||
urllib.request.urlretrieve(src, dest)
|
||||
return dest
|
||||
path = parsed.path + (f"?{parsed.query}" if parsed.query else "")
|
||||
m = re.search(r"file-read-(\d+)(?:\.(\w+))?", path)
|
||||
name = f"{m.group(1)}.{m.group(2) or 'bin'}" if m else Path(parsed.path).name
|
||||
dest = self.dir / name
|
||||
if not dest.exists() or dest.stat().st_size == 0:
|
||||
self.client.download(path, dest)
|
||||
return dest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 命令
|
||||
|
||||
def cmd_login(args):
|
||||
url = (args.url or os.environ.get("ZENTAO_URL") or DEFAULT_URL).rstrip("/")
|
||||
account = args.account or os.environ.get("ZENTAO_ACCOUNT") or input("禅道账号:").strip()
|
||||
password = os.environ.get("ZENTAO_PASSWORD") or getpass.getpass("禅道密码(输入不回显):")
|
||||
try:
|
||||
token = request_token(url, account, password)
|
||||
except AuthError as e:
|
||||
die(f"登录失败:{e}")
|
||||
cfg = {"url": url, "account": account, "token": token}
|
||||
save_config(cfg)
|
||||
saved = False if args.no_keychain else keychain_set(account, password)
|
||||
print(f"登录成功:{account} @ {url}")
|
||||
print(f"Token 已保存到 {CONFIG_FILE}(权限 0600)")
|
||||
print("密码已存入 macOS 钥匙串,会话过期时自动重新登录" if saved
|
||||
else "未保存密码:会话过期(默认 24 分钟空闲)后需重新 login")
|
||||
|
||||
|
||||
def cmd_logout(args):
|
||||
if CONFIG_FILE.exists():
|
||||
keychain_delete(json.loads(CONFIG_FILE.read_text()).get("account", ""))
|
||||
CONFIG_FILE.unlink()
|
||||
print("已退出并清除本地 Token 与钥匙串密码")
|
||||
|
||||
|
||||
def cmd_whoami(args):
|
||||
client = Client(load_config())
|
||||
data = client.page("/my-work-bug-assignedTo-0-id_desc-0-1-1.json") # 顺带验证会话可用
|
||||
users = data.get("users", {})
|
||||
account = client.cfg["account"]
|
||||
print(f"{account}({users.get(account, '')}) @ {client.url},会话有效")
|
||||
|
||||
|
||||
def fetch_bug_list(client, bug_type, per_page=100):
|
||||
bugs, page, users = [], 1, {}
|
||||
while True:
|
||||
data = client.page(f"/my-work-bug-{bug_type}-0-id_desc-0-{per_page}-{page}.json")
|
||||
users.update(data.get("users") or {})
|
||||
bugs.extend(data.get("bugs") or [])
|
||||
pager = data.get("pager") or {}
|
||||
if page >= int(pager.get("pageTotal") or 1):
|
||||
return bugs, users
|
||||
page += 1
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
client = Client(load_config())
|
||||
bugs, users = fetch_bug_list(client, args.type)
|
||||
if args.status != "all":
|
||||
bugs = [b for b in bugs if b.get("status") == args.status]
|
||||
if args.json:
|
||||
print(json.dumps([{
|
||||
"id": int(b["id"]), "title": html.unescape(b["title"]), "status": b["status"],
|
||||
"severity": int(b["severity"]), "pri": int(b["pri"]), "type": b["type"],
|
||||
"product": b.get("productName"), "openedBy": b["openedBy"], "openedDate": b["openedDate"],
|
||||
"assignedTo": b["assignedTo"], "deadline": b.get("deadline"),
|
||||
} for b in bugs], ensure_ascii=False, indent=2))
|
||||
return
|
||||
title = {"assignedTo": "指派给我", "openedBy": "由我创建", "resolvedBy": "由我解决", "closedBy": "由我关闭"}[args.type]
|
||||
print(f"# 我的 Bug({title},状态:{args.status})共 {len(bugs)} 条\n")
|
||||
print("| ID | 严重 | 优先级 | 状态 | 类型 | 标题 | 创建人 | 创建时间 |")
|
||||
print("|---|---|---|---|---|---|---|---|")
|
||||
for b in bugs:
|
||||
t = html.unescape(b["title"]).replace("|", "\\|")
|
||||
print(f"| {b['id']} | {b['severity']} | {b['pri']} | {STATUS.get(b['status'], b['status'])} | "
|
||||
f"{TYPES.get(b['type'], b['type'])} | {t} | {users.get(b['openedBy'], b['openedBy'])} | {b['openedDate'][:16]} |")
|
||||
|
||||
|
||||
def cmd_view(args):
|
||||
client = Client(load_config())
|
||||
data = client.page(f"/bug-view-{args.id}.json")
|
||||
bug = data.get("bug")
|
||||
if not bug:
|
||||
die(f"Bug #{args.id} 不存在或无权查看")
|
||||
users = data.get("users") or {}
|
||||
who = lambda a: f"{users.get(a, a)}({a})" if a and users.get(a) and users.get(a) != a else (a or "-")
|
||||
|
||||
def show(field, value):
|
||||
if not value:
|
||||
return "空"
|
||||
if field in USER_FIELDS:
|
||||
return who(value)
|
||||
if field == "mailto":
|
||||
return "、".join(who(v) for v in value.split(",") if v) or "空"
|
||||
if field == "confirmed":
|
||||
return "是" if value == "1" else "否"
|
||||
return {"status": STATUS, "resolution": RESOLUTIONS, "type": TYPES}.get(field, {}).get(value, value)
|
||||
|
||||
out_dir = Path(args.out or DEFAULT_OUT) / str(args.id)
|
||||
fetcher = ImageFetcher(client, out_dir / "images")
|
||||
|
||||
def md(html_text):
|
||||
p = Html2Md(fetcher)
|
||||
p.feed(html_text or "")
|
||||
p.close()
|
||||
return p.markdown()
|
||||
|
||||
module = " / ".join(m.get("name", "") for m in (data.get("modulePath") or [])) or "/"
|
||||
product = (data.get("product") or {}).get("name") or (data.get("products") or {}).get(bug.get("product"), "")
|
||||
lines = [
|
||||
f"# Bug #{bug['id']} {html.unescape(bug['title'])}", "",
|
||||
f"- 链接:{client.url}/bug-view-{bug['id']}.html",
|
||||
f"- 产品 / 模块:{product} / {module}",
|
||||
f"- 状态:{STATUS.get(bug['status'], bug['status'])}"
|
||||
+ (f"({RESOLUTIONS.get(bug['resolution'], bug['resolution'])})" if bug.get("resolution") else "")
|
||||
+ f" 确认:{'是' if bug.get('confirmed') == '1' else '否'}",
|
||||
f"- 严重程度:{bug['severity']} 优先级:{bug['pri']} 类型:{TYPES.get(bug['type'], bug['type'])}",
|
||||
f"- 创建:{who(bug['openedBy'])} {bug['openedDate']} 影响版本:{bug.get('openedBuild', '')}",
|
||||
f"- 指派给:{who(bug['assignedTo'])} {bug.get('assignedDate', '')}",
|
||||
]
|
||||
if bug.get("resolvedBy"):
|
||||
lines.append(f"- 解决:{who(bug['resolvedBy'])} {bug.get('resolvedDate', '')} 解决版本:{bug.get('resolvedBuild', '')}")
|
||||
if bug.get("deadline") and not bug["deadline"].startswith("0000"):
|
||||
lines.append(f"- 截止日期:{bug['deadline']}")
|
||||
if bug.get("keywords"):
|
||||
lines.append(f"- 关键词:{bug['keywords']}")
|
||||
lines += ["", "## 重现步骤", "", md(bug.get("steps")) or "(空)"]
|
||||
|
||||
files = list((bug.get("files") or {}).values()) if isinstance(bug.get("files"), dict) else (bug.get("files") or [])
|
||||
if files:
|
||||
lines += ["", "## 附件", ""]
|
||||
att_dir = out_dir / "attachments"
|
||||
att_dir.mkdir(parents=True, exist_ok=True)
|
||||
for f in files:
|
||||
name = f"{f['id']}-{f.get('title', 'file')}"
|
||||
if not name.lower().endswith("." + f.get("extension", "").lower()):
|
||||
name += "." + f.get("extension", "bin")
|
||||
dest = att_dir / re.sub(r"[/\\]", "_", name)
|
||||
try:
|
||||
if not dest.exists():
|
||||
client.download(f"/file-read-{f['id']}.{f.get('extension', '')}", dest)
|
||||
lines.append(f"- [{f.get('title')}]({dest})({f.get('size', '?')} 字节)")
|
||||
except Exception as e:
|
||||
lines.append(f"- {f.get('title')}:下载失败({e})")
|
||||
|
||||
actions = sorted((data.get("actions") or {}).values(), key=lambda a: (a.get("date", ""), int(a.get("id", 0))))
|
||||
comment_count = sum(1 for a in actions if (a.get("comment") or "").strip())
|
||||
lines += ["", f"## 历史记录与评论({len(actions)} 条,含评论 {comment_count} 条)", ""]
|
||||
for a in actions:
|
||||
name = ACTIONS.get(a.get("action", ""), a.get("action", ""))
|
||||
extra = a.get("extra") or ""
|
||||
if a.get("action") == "resolved" and extra:
|
||||
extra = RESOLUTIONS.get(extra.split(",")[0], extra)
|
||||
elif a.get("action") == "assigned" and extra:
|
||||
extra = f"→ {who(extra)}"
|
||||
lines.append(f"### {a.get('date', '')} {who(a.get('actor'))} {name}{(' ' + extra) if extra else ''}")
|
||||
for h in a.get("history") or []:
|
||||
field = FIELDS.get(h.get("field"), h.get("field"))
|
||||
if h.get("field") == "steps":
|
||||
lines.append(f"- 修改了 {field}(以上方当前内容为准)")
|
||||
else:
|
||||
lines.append(f"- 修改了 {field}:{show(h.get('field'), h.get('old'))} → {show(h.get('field'), h.get('new'))}")
|
||||
if (a.get("comment") or "").strip():
|
||||
lines += ["", "**评论:**", ""] + [f"> {l}" if l else ">" for l in md(a["comment"]).splitlines()]
|
||||
lines.append("")
|
||||
|
||||
if fetcher.files:
|
||||
lines += ["## 本地图片", ""] + [f"- 图{i + 1}:{p}" for i, p in enumerate(fetcher.files)] + [""]
|
||||
if fetcher.errors:
|
||||
lines += ["## 图片下载失败", ""] + [f"- {e}" for e in fetcher.errors] + [""]
|
||||
|
||||
text = "\n".join(lines).rstrip() + "\n"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
(out_dir / "bug.md").write_text(text)
|
||||
if args.json:
|
||||
bug["_images"] = [str(p) for p in fetcher.files]
|
||||
bug["_actions"] = actions
|
||||
print(json.dumps(bug, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(text)
|
||||
print(f"(已保存到 {out_dir / 'bug.md'},图片 {len(fetcher.files)} 张)")
|
||||
|
||||
|
||||
def form_defaults(client, bug_id):
|
||||
"""读取网页「解决」表单渲染出的默认值:解决日期(服务器时间,北京时间)与指派给(禅道默认选中创建人)。
|
||||
接口提交不会自动补默认值,必须先取表单再原样带上;不能用本机时间,本机可能在其他时区。"""
|
||||
with client._open(f"/bug-resolve-{bug_id}.html?onlybody=yes") as resp:
|
||||
page = resp.read().decode("utf-8", "replace")
|
||||
date = re.search(r"name='resolvedDate'[^>]*value='([^']+)'", page)
|
||||
select = re.search(r"<select name='assignedTo'.*?</select>", page, re.S)
|
||||
assigned = select and re.search(r"<option value='([^']*)' selected", select.group(0))
|
||||
if not date or not assigned or not assigned.group(1):
|
||||
die(f"#{bug_id} 未能从解决表单读取默认的解决日期 / 指派给,可用 --date / --assign-to 指定")
|
||||
return date.group(1), assigned.group(1)
|
||||
|
||||
|
||||
def cmd_resolve(args):
|
||||
"""解决 Bug:等同网页「解决」表单。默认解决方案=已解决、解决版本=主干(trunk)、
|
||||
解决日期与指派给取网页表单默认值(服务器时间 / 创建人);只处理激活状态的单子,提交后回读校验。"""
|
||||
client = Client(load_config())
|
||||
failed = 0
|
||||
for bug_id in args.ids:
|
||||
form = client.page(f"/bug-resolve-{bug_id}.json")
|
||||
bug = (form or {}).get("bug")
|
||||
if not bug:
|
||||
print(f"#{bug_id} 跳过:不存在或无权查看")
|
||||
failed += 1
|
||||
continue
|
||||
title = html.unescape(bug["title"])
|
||||
if bug["status"] != "active":
|
||||
print(f"#{bug_id} 跳过:当前状态为「{STATUS.get(bug['status'], bug['status'])}」,只解决激活的 Bug({title})")
|
||||
continue
|
||||
builds = form.get("builds") or {}
|
||||
if args.build not in builds:
|
||||
die(f"解决版本 {args.build} 不在可选列表中:{', '.join(k for k in builds if k)}")
|
||||
default_date, default_assigned = form_defaults(client, bug_id)
|
||||
assigned = args.assign_to or default_assigned
|
||||
fields = {
|
||||
"resolution": args.resolution, "duplicateBug": "", "buildExecution": "",
|
||||
"resolvedBuild": args.build, "buildName": "",
|
||||
"resolvedDate": args.date or default_date,
|
||||
"assignedTo": assigned, "status": "resolved", "comment": args.comment or "",
|
||||
}
|
||||
desc = (f"#{bug_id} {title} → {RESOLUTIONS.get(args.resolution, args.resolution)},"
|
||||
f"版本 {builds[args.build]},指派给 {(form.get('users') or {}).get(assigned, assigned)}")
|
||||
if args.dry_run:
|
||||
print(f"[dry-run] {desc}")
|
||||
continue
|
||||
resp = client.post_form(f"/bug-resolve-{bug_id}.html?onlybody=yes", fields)
|
||||
after = client.page(f"/bug-view-{bug_id}.json").get("bug") or {}
|
||||
if after.get("status") == "resolved":
|
||||
print(f"已解决 {desc}")
|
||||
else:
|
||||
alerts = re.findall(r"alert\((['\"])(.*?)\1\)", resp)
|
||||
print(f"#{bug_id} 失败:提交后状态仍为 {after.get('status')}"
|
||||
+ (f",禅道提示:{'; '.join(html.unescape(a[1]) for a in alerts)}" if alerts else ""))
|
||||
failed += 1
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def resolve_user(users, who):
|
||||
"""按账号或姓名定位用户:先精确匹配账号,再按「姓名」匹配(禅道显示名形如 T:陶雨佳)。"""
|
||||
if who in users:
|
||||
return who
|
||||
hits = [k for k, v in users.items() if k and v.split(":", 1)[-1] == who]
|
||||
if len(hits) == 1:
|
||||
return hits[0]
|
||||
die(f"找不到唯一的用户「{who}」" + (f",候选:{', '.join(hits)}" if hits else ""))
|
||||
|
||||
|
||||
def comment_html(text):
|
||||
"""备注是富文本,纯文本换行在网页上会被合并,逐行转义后用 <br /> 连接。"""
|
||||
return "<br />".join(html.escape(line) for line in text.splitlines())
|
||||
|
||||
|
||||
def cmd_assign(args):
|
||||
"""指派 Bug:等同网页「指派」表单(指派给 + 备注),状态保持激活;提交后回读校验指派人。"""
|
||||
client = Client(load_config())
|
||||
comment = args.comment
|
||||
if args.comment_file:
|
||||
comment = Path(args.comment_file).read_text(encoding="utf-8").strip()
|
||||
failed = 0
|
||||
for bug_id in args.ids:
|
||||
form = client.page(f"/bug-assignTo-{bug_id}.json")
|
||||
bug = (form or {}).get("bug")
|
||||
if not bug:
|
||||
print(f"#{bug_id} 跳过:不存在或无权查看")
|
||||
failed += 1
|
||||
continue
|
||||
users = form.get("users") or {}
|
||||
account = resolve_user(users, args.to)
|
||||
title = html.unescape(bug["title"])
|
||||
desc = f"#{bug_id} {title} → 指派给 {users.get(account, account)}"
|
||||
if args.dry_run:
|
||||
print(f"[dry-run] {desc}")
|
||||
if comment:
|
||||
print(" 备注:\n " + comment.replace("\n", "\n "))
|
||||
continue
|
||||
fields = {"assignedTo": account, "status": bug["status"], "mailto[]": "",
|
||||
"comment": comment_html(comment) if comment else ""}
|
||||
resp = client.post_form(f"/bug-assignTo-{bug_id}.html?onlybody=yes", fields)
|
||||
after = client.page(f"/bug-view-{bug_id}.json").get("bug") or {}
|
||||
if after.get("assignedTo") == account:
|
||||
print(f"已指派 {desc}")
|
||||
else:
|
||||
alerts = re.findall(r"alert\((['\"])(.*?)\1\)", resp)
|
||||
print(f"#{bug_id} 失败:提交后指派人仍为 {after.get('assignedTo')}"
|
||||
+ (f",禅道提示:{'; '.join(html.unescape(a[1]) for a in alerts)}" if alerts else ""))
|
||||
failed += 1
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="禅道 18.x Bug 读取工具")
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p = sub.add_parser("login", help="登录并保存 Token(密码存 macOS 钥匙串)")
|
||||
p.add_argument("--url", help=f"禅道地址,默认 {DEFAULT_URL}")
|
||||
p.add_argument("--account", help="账号")
|
||||
p.add_argument("--no-keychain", action="store_true", help="不在钥匙串保存密码")
|
||||
p.set_defaults(func=cmd_login)
|
||||
|
||||
sub.add_parser("logout", help="清除 Token 与钥匙串密码").set_defaults(func=cmd_logout)
|
||||
sub.add_parser("whoami", help="查看当前账号并验证会话").set_defaults(func=cmd_whoami)
|
||||
|
||||
p = sub.add_parser("list", help="我的 Bug 列表")
|
||||
p.add_argument("--type", choices=BUG_TYPES, default="assignedTo", help="默认 assignedTo(指派给我)")
|
||||
p.add_argument("--status", choices=("active", "resolved", "closed", "all"), default="all")
|
||||
p.add_argument("--json", action="store_true")
|
||||
p.set_defaults(func=cmd_list)
|
||||
|
||||
p = sub.add_parser("view", help="Bug 详情(描述、图片、历史、评论、附件)")
|
||||
p.add_argument("id", type=int)
|
||||
p.add_argument("--out", help=f"输出目录,默认 {DEFAULT_OUT}")
|
||||
p.add_argument("--json", action="store_true")
|
||||
p.set_defaults(func=cmd_view)
|
||||
|
||||
p = sub.add_parser("resolve", help="解决 Bug(写操作,会改动禅道线上状态)")
|
||||
p.add_argument("ids", type=int, nargs="+")
|
||||
p.add_argument("--resolution", choices=tuple(RESOLUTIONS), default="fixed", help="默认 fixed(已解决)")
|
||||
p.add_argument("--build", default="trunk", help="解决版本,默认 trunk(主干)")
|
||||
p.add_argument("--assign-to", help="指派给(账号),默认取表单默认值(即创建人)")
|
||||
p.add_argument("--date", help="解决日期 YYYY-MM-DD HH:MM:SS,默认取服务器当前时间")
|
||||
p.add_argument("--comment", default="", help="备注,默认留空")
|
||||
p.add_argument("--dry-run", action="store_true", help="只打印将要做的修改,不提交")
|
||||
p.set_defaults(func=cmd_resolve)
|
||||
|
||||
p = sub.add_parser("assign", help="指派 Bug 并附备注(写操作,会改动禅道线上状态)")
|
||||
p.add_argument("ids", type=int, nargs="+")
|
||||
p.add_argument("--to", required=True, help="指派给:账号(如 TYJ)或姓名(如 陶雨佳)")
|
||||
p.add_argument("--comment", default="", help="备注,多行用换行分隔")
|
||||
p.add_argument("--comment-file", help="从文件读取备注(多行内容推荐用这个)")
|
||||
p.add_argument("--dry-run", action="store_true", help="只打印将要做的修改,不提交")
|
||||
p.set_defaults(func=cmd_assign)
|
||||
|
||||
args = ap.parse_args()
|
||||
try:
|
||||
args.func(args)
|
||||
except AuthError as e:
|
||||
die(f"{e}。请执行:python3 .claude/skills/zentao-bug/zentao.py login", 2)
|
||||
except urllib.error.URLError as e:
|
||||
die(f"无法访问禅道:{e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/androidstudio,gradle,java,kotlin
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,gradle,java,kotlin
|
||||
|
||||
air-cargo/
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
43
CHANGELOG.md
@@ -8,6 +8,43 @@
|
||||
## [Unreleased]
|
||||
|
||||
### 变更 (Changed)
|
||||
- 【5.5 寸首页改版 · 2026-10-21】手持端首页 `PDAEnterActivity` 四个 Tab 按 `documents/5.5寸首页新样式/` 设计稿
|
||||
1:1 还原(仅 module_p,平板不经过该页,零影响):
|
||||
① 国内/国际 Tab 改为「分组标题栏 + 三列宫格」(新增 `PdaMenuGroup` / `PdaMenuRenderer`),分组与顺序按设计稿,
|
||||
20 个入口换设计稿新图标(源 GIF/PNG 统一转 PNG 放 drawable-nodpi——放 xxhdpi 实测解码后再放大发糊),
|
||||
设计稿未画的入口追加到组尾沿用旧图标;
|
||||
权限过滤 `authList.contains(key)` 与改版前逐字一致——ADMIN 账号改版前后入口集合 diff 为空(国内 9、国际 12),
|
||||
21 个入口逐个点击跳转目标与原映射一致。
|
||||
② 首页航班动态改为卡片 + 国内/国际、进港/出港分段 + 日期卡片(‹ › 前后翻日)+ 浅色表格 + 右下设置悬浮钮;
|
||||
默认方向按设计稿改为「进港」;日期改为补零格式(原选择器会写出 `2026-10-5`),选择器从当前选中日期打开。
|
||||
③ 底部 Tab 栏、消息铃铛与角标、状态栏(#3B71E8)按设计稿重做(Tab 图标 16dp 并与文字水平居中,
|
||||
不照搬设计稿偏左上的 16px 图形),「我的」顶栏同步新导航栏样式;分段控件 padding 计入 1dp 描边以上下居中;消息铃铛四个 Tab 统一按国内/国际设计稿位置(首页设计稿略有偏差),切 Tab 不再移动;
|
||||
首页 AutoSize 基准由 400 改为设计稿画布 390。删除 module_p 中不再引用的旧菜单/Tab 图标副本 19 个。
|
||||
- 【手机适配分支合入主干 · 2026-10-11】`feature/phone-ui-adaptation` 合并入 `main`,此后平板与 5.5 寸手机只在
|
||||
`main` 上同时开发。合并以平板业务为准:
|
||||
① 主干 58 个平板提交(运抵海关放行/运抵状态筛选/海关回执、出库交接装机状态/允许装机、出港移库详情与侧滑编辑、
|
||||
开始计重托盘信息与运抵重量自动计算、计重明细重量/托盘自重联动、出入港仓库清仓备注与入/出库件数重量、
|
||||
进港查询出库状态筛选、ULD 来源/入库时间/所在港/位置等)全部同步到手机布局;冲突的平板布局三方合并进
|
||||
`layout-sw600dp/`,ViewModel/Bean 冲突取平板业务逻辑。
|
||||
② 新增手机变体:出库/入库/清仓/配运/删除申报弹框、出港移库详情编辑页(`IntExpMoveEditActivity`,替代手机专属
|
||||
`IntExpMoveDetailActivity`)、共用海关回执页 `CustomsResponseActivity`;`PhoneBottomBar` 增次操作按钮。
|
||||
③ 手机端对齐平板:移除平板没有的查询入参(`likeNo`、进港移库 fdate/fno/spCode、出港移库 spCode)与改写的默认值
|
||||
(计重/计重记录/提取出库默认日期),补齐平板表单与筛选项(运抵分单号、进港查询出库状态、计重记录航班号、
|
||||
进港移库承运人、开始计重运单号/地磅重量/平板车/航班可编辑)与操作(运抵删除申报、计重记录运抵申报、计重新增);
|
||||
ULD 表单移除平板没有的进港航班号与额外必填校验。
|
||||
- 【后端反馈联调 · 2026-10-09】后端在《问题清单》补「解决方案/备注」后,逐项(#1~#34)api-doc 核对 +
|
||||
内网直调 A/B + AeroLogic_Phone 模拟器端到端验证,并按后端意见调整前端:
|
||||
① 后端已补字段/接口 → 前端对接:出港移库 flight/businessName/opUserName;出库交接 uldNumber 角标;
|
||||
ULD source/inStoreTime/nowPort + `deleteUldBatch` 一次批删;出港计重 waitCheckInNumber/checkingNumber
|
||||
统计、passageWay、opUserName、trayNumber/trayTotalWeight;运抵 arrivalStatus 服务端 Tab;
|
||||
出港/进港查询 storedNumber/departedNumber;出港/进港仓库 notClearNumber/clearNumber(统计请求 3→1);
|
||||
提取出库 refUserName;国内进港移库 opUserName + `movedDetail` 照片回填。
|
||||
`ManifestTotalDto` 补齐后端统一统计 DTO 分项字段。
|
||||
② 后端「按平板端调整/设计如此」→ 前端对齐:出库交接/ULD/计重/记录/出港仓库/进港仓库/提取出库/运抵
|
||||
的手机筛选全部收敛为平板同款查询参数;ULD 状态回归两态;删除运抵「补充信息」页;出港详情移除
|
||||
锁定状态/计费重量;进港详情按平板改 航程/出库时间(无经停站);提取出库移除无出参占位。
|
||||
③ 联调新发现记入问题清单 #35~#41(ULD uld 搜索 500、saveUld 缺 maxVolume/maxWeight 500、
|
||||
IntImpStorage/pageQuery 过滤全失效、出港仓库列表与统计口径不一致、已移库无 mawbId 无法补传照片等)。
|
||||
- 【进港移库归属更正 · 2026-08-03】设计稿 11「进港移库」经确认归属**国内进港移库
|
||||
(DomImpMove,后端 8 接口齐全,Pad 首页「国内进港→进港移库」既有页面)**,此前误判为
|
||||
国际进港并按 IntExpMove 约定预接不存在的 IntImpMove 模块(原问题清单 #31 作废)。处置:
|
||||
@@ -29,6 +66,12 @@
|
||||
`fdate`/`fno`/`spCode` 被后端静默忽略(新缺口 #32)、`searchMoved`/`detail` 出参
|
||||
`opId`/`opDate` 恒 null 且无姓名字段(#33)、`searchMoved` 无 pic 三字段(#34,前端已规避)
|
||||
|
||||
### 修复 (Fixed)
|
||||
- 出港计重「修改记录」托盘自重输入框:Double 回写导致光标跳回开头(输入 18 存成 81),改为保留输入原文
|
||||
- ULD 新增:入库时间日期行仅产出 yyyy-MM-dd(后端 400)→ 保存时补 00:00:00;补充最大装载容积/最大载重
|
||||
可编辑必填(缺失时 saveUld 500)
|
||||
- 提取出库运单号入参 no → wbNo(与主干平板端修复一致)
|
||||
|
||||
### 新增 (Added)
|
||||
- 「进港查询」5.5 寸手机端适配(设计稿 10_进港查询,06 出港查询的进港镜像;详情页与运单追踪
|
||||
复用 08/06 已适配页零新增):列表 `IntImpQueryActivity`(module_gjj,首页菜单实际跳转页;
|
||||
|
||||
157
CLAUDE.md
@@ -5,8 +5,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## 项目概况
|
||||
|
||||
**项目名称**: AirLogistics - 航空物流信息管理系统
|
||||
**架构模式**: MVVM + 组件化 | **语言**: Kotlin 1.6.21 + Java | **版本**: 1.8.4 (versionCode 84)
|
||||
**SDK**: minSdk 24 / targetSdk 30 / compileSdk 31 | **Gradle**: 7.3.3 | **JDK**: 1.8
|
||||
**架构模式**: MVVM + 组件化 | **语言**: Kotlin 1.6.21 + Java | **版本**: 1.8.7 (versionCode 87)
|
||||
**SDK**: minSdk 24 / targetSdk 30 / compileSdk 31 | **Gradle**: 7.3.3 | **JDK**: 17(`mise.toml`,运行 Gradle 用);编译目标 Java 1.8(`sourceCompatibility` / `jvmTarget`)
|
||||
|
||||
### 核心架构
|
||||
|
||||
@@ -51,6 +51,15 @@ aerologic-app/
|
||||
└── 其他业务模块...
|
||||
```
|
||||
|
||||
### ⛔ `air-cargo/` 目录:一律忽略
|
||||
|
||||
根目录下的 `air-cargo/` 是同一份源码的另一个独立 git 仓库(远程地址不同,工作分支 `feature/hefei`),**只用于把本项目代码提交到另一个仓库**,不参与本项目任何开发;它已在 `.gitignore` 中忽略。
|
||||
|
||||
- 代码扫描、搜索、实现功能、排查问题、查 git 历史时**一律跳过该目录**,不读取、不修改、不当作参考实现(其中文件是同步过去的副本,可能落后于主干)
|
||||
- `find` / `grep -r` 这类不认 `.gitignore` 的命令要显式排除(如 `find . -path ./air-cargo -prune -o …`、`grep -r --exclude-dir=air-cargo`);`git grep`、`rg` 默认已跳过
|
||||
- 唯一允许操作它的场景是 `sync-air-cargo` skill(同步并在其本地提交,不 push)
|
||||
- 其中不应存在任何 Claude / Agent 相关文件(`CLAUDE.md`、`.claude/`、`.mcp.json` 等),sync 脚本和 air-cargo 本地的 `.git/info/exclude` 都已屏蔽
|
||||
|
||||
---
|
||||
|
||||
## 6 种典型页面类型
|
||||
@@ -383,7 +392,7 @@ fun toggleAllExpand() {
|
||||
}
|
||||
```
|
||||
|
||||
4. **子列表项 checkbox 样式**(必须使用 `_style` 系列,禁止使用 `_gray` 系列):
|
||||
4. **子列表项 checkbox 样式**(必须复用底部“全选”的 `img_check_all_*` 资源,高度 20dp):
|
||||
```xml
|
||||
<!-- 子列表项 item_xxx_sub.xml 中的 iv_checkbox -->
|
||||
<ImageView
|
||||
@@ -392,54 +401,61 @@ fun toggleAllExpand() {
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="0.5"
|
||||
loadImage="@{bean.checked.get() ? @drawable/radiobtn_checked_style : @drawable/radiobtn_unchecked_style}"
|
||||
android:src="@drawable/radiobtn_unchecked_style" />
|
||||
loadImage="@{bean.checked.get() ? @drawable/img_check_all_checked : @drawable/img_check_all_unchecked}"
|
||||
android:src="@drawable/img_check_all_unchecked" />
|
||||
```
|
||||
|
||||
| 资源 | 含义 | 外观 |
|
||||
|------|------|------|
|
||||
| `radiobtn_checked_style` | 选中 | colorPrimary 蓝色实心圆 + 白色内环 |
|
||||
| `radiobtn_unchecked_style` | 未选中 | 透明 + 黑色边框圆 |
|
||||
| `img_check_all_checked` | 选中 | `#1C8CF5` 蓝色实心圆 + 白色对勾(与底部“全选”相同)|
|
||||
| `img_check_all_unchecked` | 未选中 | `#707070` 灰色细圆环(与底部“全选”相同)|
|
||||
| ~~`radiobtn_checked_style` / `radiobtn_unchecked_style`~~ | ❌ 已废弃 | 旧版分单复选框,不再使用 |
|
||||
| ~~`radiobtn_checked_gray`~~ | ❌ 禁用 | 灰色实心圆(错误样式) |
|
||||
|
||||
#### 含子列表的列表项 UI 规范(以 `item_int_exp_storage_use.xml` 为基准)
|
||||
#### 列表项 UI 规范(以 `item_int_exp_arrive / assemble / load / tally.xml` 四个基准页为准)
|
||||
|
||||
> 应用主题未覆盖 `textViewStyle`,未显式写 `textSize`/`textColor` 的 TextView 会落到系统默认(14sp、偏灰的次级文字色)。因此列表项里**每个** Key/Value TextView 都必须显式写 `textSize` 和 `textColor`,不能依赖“默认继承”。
|
||||
|
||||
**主列表项卡片**:
|
||||
|
||||
| 部位 | 属性 | 标准值 |
|
||||
|------|------|--------|
|
||||
| 外层容器 | marginHorizontal / marginTop | 10dp / 10dp |
|
||||
| 卡片背景 | background | `@drawable/bg_white_radius_8` |
|
||||
| 内容区 | padding | **10dp** |
|
||||
| 选中图标 | 尺寸 / marginEnd / marginTop | 40×40dp / 10dp / **0.5px**(像素级对齐)|
|
||||
| 外层容器 | marginHorizontal / marginTop | 10dp / 10dp(禁止旧写法 `marginHorizontal=15dp` + `marginVertical=5dp`)|
|
||||
| 卡片背景 | background | `@drawable/bg_white_radius_8`(`bg_item` 已废弃,不再引用)|
|
||||
| 内容区 | paddingHorizontal / paddingVertical / gravity | **10dp / 20dp** / `center_vertical` |
|
||||
| 选中图标 | 尺寸 / marginTop | 40×40dp / **0.5px**(像素级对齐),不设 marginEnd |
|
||||
| 内容列(图标右侧的竖向 LinearLayout)| layout_marginLeft | **15dp** |
|
||||
| 选中图标 | 切换资源 | `img_plane_s`(选中)/ `img_plane`(未选中)|
|
||||
| KV 文字 | textSize | **15sp**(Key 和 Value 均需显式设置)|
|
||||
| 首要字段值(运单号)| textColor | `@color/colorPrimary` |
|
||||
| 其他字段 | textColor | 无需设置(继承默认 text_normal)|
|
||||
| KV 文字 | textSize / textColor | **15sp** / **`@color/text_normal`**(Key 和 Value 均需显式设置)|
|
||||
| 首要字段值(运单号/ULD编码)| textColor / textStyle | `@color/colorPrimary` / **`bold`** |
|
||||
| 状态类字段值(带颜色表达式)| textSize | **14sp** + 颜色表达式(如 `text_green / colorPrimary / text_gray`)|
|
||||
| 两行间距 | layout_marginTop | 10dp |
|
||||
| 右侧箭头(有详情页时)| `img_pda_right` | 30×30dp,`layout_marginLeft=10dp` |
|
||||
|
||||
**展开/折叠按钮(`iv_show`)**:
|
||||
|
||||
| 属性 | 标准值 |
|
||||
|------|--------|
|
||||
| layout_width | `match_parent` |
|
||||
| layout_height | **10dp** |
|
||||
| layout_marginTop | **-10dp**(向上收紧,紧贴卡片底边)|
|
||||
| scaleType | **centerInside**(保证箭头不被压扁/截断)|
|
||||
| layout_height | **20dp** |
|
||||
| layout_marginTop | **-20dp**(向上收紧,紧贴卡片底边)|
|
||||
| scaleType | **center** |
|
||||
| src | `@mipmap/img_down` |
|
||||
| 显示控制 | `visible="@{bean.subList != null && !bean.subList.empty}"` |
|
||||
| 不设 padding,不设 layout_marginBottom | — |
|
||||
|
||||
> 旧版本(18dp + padding=4dp + marginBottom=5dp)已统一替换为上述新标准,参考 `item_int_exp_arrive.xml`。组装类列表若需展开后翻转箭头,附加 `android:rotation="@{bean.showMore.get() ? 180f : 0f}"`(参考 `item_int_exp_assemble.xml`)。
|
||||
> 旧版本(18dp + padding=4dp + marginBottom=5dp,以及 10dp/-10dp/centerInside)已统一替换为上述新标准,参考 `item_int_exp_arrive.xml`。组装类列表若需展开后翻转箭头,附加 `android:rotation="@{bean.showMore.get() ? 180f : 0f}"`(参考 `item_int_exp_assemble.xml`)。
|
||||
|
||||
**子列表区域**:
|
||||
|
||||
- 容器:`layout_marginTop="5dp"`,`background="#e3f6e0"`
|
||||
- 表头行:`layout_marginVertical="10dp"`,`paddingHorizontal="10dp"`
|
||||
- 容器:`layout_marginTop="10dp"`,`background="@drawable/bg_white_radius_8"`(旧的 `#e3f6e0` 绿色容器已废弃)
|
||||
- 空提示:`textSize="14sp"`,`textColor="@color/text_gray"`,`paddingVertical="15dp"`
|
||||
- 表头行:`background="@drawable/bg_arrive_sub_header"`,`paddingHorizontal="10dp"`,`paddingVertical="10dp"`
|
||||
- 表头文字:`textSize="14sp"`,`textColor="@color/text_normal"`,`textStyle="bold"`,`gravity="center"`
|
||||
- 表头下方分隔线:`MaterialDivider` 高度 1px,`background="@color/c999999"`
|
||||
- 子列表项 padding:`paddingHorizontal="10dp"`,`paddingVertical="8dp"`
|
||||
- 子列表文字:`textSize="14sp"`,`textColor="@color/text_normal"`,`gravity="center"`,`layout_gravity="center_vertical"`
|
||||
- 表头下方分隔线:`MaterialDivider` 高度 1px,`dividerColor="@color/sub_list_divider"`
|
||||
- 子列表项:`layout_marginVertical="8dp"`,`paddingHorizontal="10dp"`
|
||||
- 子列表文字:`textSize="14sp"`,`textColor="@color/text_normal"`,`gravity="center"`,`layout_gravity="center_vertical"`;首要列(运单号/分单号)`@color/colorPrimary` + `bold`;状态列 `13sp` + 颜色表达式
|
||||
- 详情页里的表格式列表(如查询详情的库位/仓库 Tab)同样 14sp + `text_normal`,禁止 `@android:color/black`
|
||||
|
||||
**子列表复选框(关键)**:
|
||||
|
||||
@@ -449,11 +465,11 @@ fun toggleAllExpand() {
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="0.5"
|
||||
loadImage="@{bean.checked.get() ? @drawable/radiobtn_checked_style : @drawable/radiobtn_unchecked_style}"
|
||||
android:src="@drawable/radiobtn_unchecked_style" />
|
||||
loadImage="@{bean.checked.get() ? @drawable/img_check_all_checked : @drawable/img_check_all_unchecked}"
|
||||
android:src="@drawable/img_check_all_unchecked" />
|
||||
```
|
||||
|
||||
> `loadImage` 和 `android:src` **均须**使用 `_style` 系列,**禁止**使用 `_gray` 系列(参见上方复选框样式表)
|
||||
> `loadImage` 和 `android:src` **均须**使用 `img_check_all_checked`/`img_check_all_unchecked`(与底部“全选”同源),高度固定 **20dp**;**禁止**再使用 `radiobtn_*_style`/`radiobtn_*_gray`(参见上方复选框样式表)
|
||||
|
||||
**参考文件**: `module_gjc/.../IntExpStorageUseActivity.kt`、`IntExpStorageUseViewModel.kt`
|
||||
|
||||
@@ -843,6 +859,11 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
|
||||
|
||||
项目需同时支持 **10 寸平板横屏** 与 **5.5 寸手机竖屏**。核心原则:**业务逻辑层完全复用,只重写布局**。
|
||||
|
||||
> **分支约定(2026-10-11 起)**:手机适配分支 `feature/phone-ui-adaptation` 已合并入 `main`,此后平板与手机**只在 `main` 上同时开发**,不再区分分支。合并时的状态、验证覆盖与待决事项见 `documents/双端合入主干交接文档.md`。
|
||||
> **业务以平板为准**:主流程交互、表单/筛选项、接口入参一律与平板一致,手机端只做 UI 适配——平板底部批量按钮在手机上可改为卡片级操作或 `PhoneBottomBar` 次按钮(`subAction1Text/subAction2Text`),
|
||||
> 但不得新增平板没有的查询入参(如 `likeNo`)、不得改写平板的默认查询值(如默认当天日期)、不得少平板的表单项。
|
||||
> 平板新增/修改功能时,必须同步检查对应手机布局与该页全部弹框的手机变体。
|
||||
|
||||
### 核心机制:布局按资源限定符自动切换
|
||||
|
||||
```
|
||||
@@ -991,9 +1012,11 @@ Manifest 写死 `userLandscape` 时,即使 `BaseActivity` 在 `super.onCreate`
|
||||
| 国内 | `GnViewModel` | `module_p/.../ui/enter/gn/` |
|
||||
| 国际 | `GjViewModel` | `module_p/.../ui/enter/gj/` |
|
||||
|
||||
**菜单项是手写清单,不是接口下发**。权限机制只做过滤:登录返回的权限串存在
|
||||
`SharedPreferenceUtil.getString(Constant.Share.authList)`(一个逗号分隔的长字符串),
|
||||
菜单项按 `authList.contains(bean.key)` 决定显不显示。
|
||||
**菜单项是手写清单,不是接口下发**。登录接口只下发「分组 → 权限码」(`auths`,形如
|
||||
`{"IntExp":["AppIntExpCheckWeighing",…],"Supervision":[…]}`,存在
|
||||
`SharedPreferenceUtil.getString(Constant.Share.authList)`),菜单名称/图标/跳转在平板和手机上都是前端写死的映射。
|
||||
手机端菜单项按 `authList.contains(bean.key)` 决定显不显示(`PdaMenuGroup.kt` 的 `filterByAuth()`,
|
||||
**2026-10-21 首页改版时刻意保持与改版前逐字一致**,保证同一账号的入口集合不变)。
|
||||
|
||||
所以「适配完页面但手机首页看不到入口」的原因通常**不是没权限,而是清单里没加**。
|
||||
排查顺序:先确认权限串里有对应 key,再去看清单。
|
||||
@@ -1004,25 +1027,43 @@ adb -s <serial> 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 分支
|
||||
**2026-10-21 起首页按 `documents/5.5寸首页新样式/` 设计稿改版**:清单按设计稿分组
|
||||
(`PdaMenuGroup(标题, 入口列表)`,如国际 Tab =「国际出港 / 国际进港 / 综合管理」),
|
||||
由 `PdaMenuRenderer` 渲染成「分组标题栏 + 三列宫格」,过滤后为空的分组整组隐藏。
|
||||
**分组口径以设计稿为准,不是接口的 auths 分组**(例如单证交接/转运记录在接口里属 Supervision,
|
||||
设计稿放在「国内出港」)。
|
||||
|
||||
加一项就是在对应分组里加一行 —— `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 就走通用跳转
|
||||
PdaMenuGroup(
|
||||
"国际出港", listOf(
|
||||
ActionBean(
|
||||
"出港移库",
|
||||
R.drawable.img_pda_menu_gjc_move, // 设计稿新版宫格图标
|
||||
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 个图标就是这么放的,同名不同密度桶可共存)
|
||||
- **图标**:设计稿图标源文件在 `documents/5.5寸首页新样式/icons/`(约 167px 方图,自带「国际/国内」角标),
|
||||
统一转 PNG(源文件多为单帧 GIF,GIF 只有 256 色 + 1 位透明,不进资源)后放
|
||||
`module_p/src/main/res/drawable-nodpi/img_pda_menu_*.png`,按 59.7dp 宫格居中裁剪 + 圆角显示。
|
||||
**必须放 nodpi,不能放 xxhdpi**:167px 放 xxhdpi 会在 ~295dpi 设备上解码时先缩到 ≈103px,再被拉伸到
|
||||
宫格的 110px,实测明显发糊;nodpi 原尺寸解码、由固定尺寸 ImageView 一次缩小,与浏览器渲染设计稿一致
|
||||
**设计稿没有的入口追加到所属分组末尾、沿用旧图标**(如「板箱过磅」`gjc_fu_bang_icon`)
|
||||
- **改动清单后核对入口集合**:登录后分别在「国内 / 国际」Tab 用 `ui_probe.sh texts`(需滚动)导出入口名,
|
||||
与改动前 `LC_ALL=C sort` 后 diff(CJK 文本不加 `LC_ALL=C` 排序不稳定,会 diff 出假差异)。
|
||||
ADMIN 测试账号基线:国内 9 个、国际 12 个
|
||||
|
||||
**示例:状态 Tab 零 ViewModel 改动**(`IntExpMoveActivity` 已实现)
|
||||
|
||||
@@ -1111,7 +1152,8 @@ override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) =
|
||||
|
||||
- ✅ **已完成**:适配框架 + 手机端公共资源 + 7 个手机版公共组件
|
||||
- ✅ **已完成页面**:
|
||||
- 「国际出港移库」`IntExpMoveActivity` + 「运单详情」`IntExpMoveDetailActivity`(框架参考页)
|
||||
- 「国际出港移库」`IntExpMoveActivity`(框架参考页);详情/编辑与平板共用 `IntExpMoveEditActivity`
|
||||
(手机布局为设计稿「运单详情」样式,卡片「查看详情」/「编辑」分别对应平板点击卡片/侧滑编辑)
|
||||
- 「国际出港出库交接」`IntExpOutHandoverActivity` + 「板箱详情」`IntExpOutHandoverDetailActivity`
|
||||
(公共组件首个完整落地页,双端实机验证通过)
|
||||
- 「ULD管理」`UldListActivity` + `UldEditActivity`(app 模块首个适配页;一套编辑布局覆盖
|
||||
@@ -1120,14 +1162,14 @@ override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) =
|
||||
FAB 进记录)、`GjcWeighingStartActivity`(开始计重,托盘数量/自重按设计稿手输)、
|
||||
`GjcWeighingRecordListActivity`(计重记录)、`GjcWeighingRecordDetailsActivity`(计重明细,
|
||||
isEditMode 复用为手机全屏「修改记录」覆层)+ 手机专属新页 `GjcWeighingWbDetailActivity`(运单详情)。
|
||||
搜索分流规则(实测):含字母→fno、11 位数字→wbNo、其余→likeNo(likeNo 按 8 位 no 匹配并非模糊);
|
||||
合并搜索框只分流到平板同名入参:含字母→fno、其余→wbNo;
|
||||
checkIn 过滤等 7 项后端缺口见问题清单 #12~#18
|
||||
- 「出港运抵」主列表 + 3 个手机专属新页:`IntExpArriveActivity`(待运抵/已运抵 Tab,客户端按
|
||||
`declareStatus` 是否为空拆分数据集,卡片长按浮层:待运抵→补充、已运抵→重置+回执)、
|
||||
`declareStatus` 是否为空拆分数据集,卡片长按浮层仅已运抵:重置 / 删除申报 / 回执)、
|
||||
`IntExpArriveSubOrderActivity`(分单明细,数据直接复用主列表已加载的 `haWbList`,零新增请求)、
|
||||
`IntExpArriveReceiptActivity`(回执内容只读页,复用 `haWbList[].response`)、
|
||||
`IntExpArriveSupplementActivity`(补充信息,参照 Pad 端国际进港「收发货人信息」批量应用模式,
|
||||
后端接口不存在,实机 404 已确认,见问题清单 #19)。
|
||||
`IntExpArriveReceiptActivity`(分单回执只读页,复用 `haWbList[].response`;主单「回执」与平板侧滑同走
|
||||
`getResponse` → 共用 `CustomsResponseActivity`)。
|
||||
(原手机专属「补充信息」页 `IntExpArriveSupplementActivity` 因后端无接口(#19)已于 2026-10-09 删除。)
|
||||
Tab 过滤两个坑:① **RecyclerView 里 `View.GONE` 不会让 item 塌陷为 0 高度**(不同于普通
|
||||
ViewGroup),必须在数据层整体 `refresh()` 过滤后的子集给 adapter,不能给 item 根节点加
|
||||
visibility 绑定(实机截图验证过,见「常见编译错误速查」);② 通过 `Intent.putExtra(Serializable)`
|
||||
@@ -1192,10 +1234,12 @@ override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) =
|
||||
- ✅ **设计稿 11 个入口(01~11)已于 2026-07-31 全部适配完成**
|
||||
- ⏳ **待办**:
|
||||
- `PhoneSearchBar` 接入 AutoQuery 联想(`AutoQueryManager` 目前只对接了 Pad 系组件)
|
||||
- 进港移库筛选三项(航班日期/航班号/特码)待后端实装 `fdate`/`fno`/`spCode` 入参(缺口 #32)、
|
||||
移库人/移库时间待后端回填 `opId`/`opDate` 并补姓名出参(缺口 #33)后自动生效
|
||||
- 出库交接 Tab 角标恒 0:`IntExpOutHandover/pageQueryTotal` 出参恒 0(后端缺陷,问题清单 #6),
|
||||
后端修复后角标自动恢复(`handoverState` 过滤本身已于 2026-07-29 实机验证通过)生效
|
||||
- 联调新发现的后端缺口 #35~#41(ULD uld 搜索 500、IntImpStorage 列表过滤全失效、已移库无 mawbId
|
||||
无法补传照片等)待后端处理,详见问题清单
|
||||
- ✅ **2026-10-09 后端反馈联调完成**:问题清单 #1~#34 逐项内网直调 + 模拟器端到端验证,结论写入清单 P 列。
|
||||
**后端口径:手机筛选一律与平板端查询参数保持一致**(不为手机新增入参);分项统计统一取
|
||||
`pageQueryTotal` 出参(`ManifestTotalDto` 已补齐分项字段),不要再按状态多调 total。
|
||||
最新字段语义见 `.claude/skills/phone-adapt/references/api-doc.md` 末节
|
||||
- 📋 **接口对接**:适配中发现的后端接口缺口统一记录在 `documents/后端接口对接问题清单.xlsx`
|
||||
(按模块/页面/接口定义/接口地址索引,与后端对接的正式文档,每页适配完必须维护);
|
||||
接口入参/出参一律以 api-doc MCP 为准核对,流程见
|
||||
@@ -1288,6 +1332,9 @@ adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.activity.IntEx
|
||||
| 通过 `Intent.putExtra(Serializable)` 传递的 Bean,切换其 `checked`/`showMore` 等状态时崩溃 `NullPointerException: ... on a null object reference` | 这些字段标了 `@Transient`(= JVM `transient`,本意是避免 Gson 网络请求序列化 `ObservableBoolean`),但 Java 对象序列化(Intent 传递走的机制)会跳过 `transient` 字段的恢复,反序列化后是 null | 接收方 `initOnCreated` 里对列表做 `.map { it.copy() }`——`.copy()` 会重新走一遍类体初始化逻辑,用全新 `ObservableBoolean(false)` 补上 |
|
||||
| 布局构建报 `Error: The string "--" is not permitted within comments` | XML 注释中出现连续双横线(如写"兜底 \"--\""),XML 规范禁止 | 注释里改用文字描述(如「兜底双横线」),代码绑定表达式中的 `"--"` 不受影响 |
|
||||
| 多选页手机版点「全选」后底部「已选 N 项」不更新(单卡勾选正常) | 批量勾选在 `checkAllClick` 里直接 set,不走 FlowBus 的单卡 EVENT_CHECK_CHANGED 事件,`selectedCountText` 只在事件回调里更新 | `checkAllClick` 末尾自行同步 `selectedCountText`(平板布局不绑定该字段,无影响) |
|
||||
| 手机版数字输入框(`PhoneFormRow` 双向绑 Double 的 `xxxStr`)输入 "18" 变成 "81" | getter 由 Double 反推字符串("1" → "1.0"),与输入框文本不等触发 `setText`,光标回到开头 | Bean 里保留输入原文(`@Transient` 私有 String),getter 优先返回原文(参考 `GjcCheckInRecord.trayTotalWeightStr`) |
|
||||
| 固定 dp 尺寸显示的位图图标在手机上发糊(设计稿里很清晰) | 位图放进了比「源像素 ÷ 显示 dp」更高的密度桶(如 167px 的宫格图放 xxhdpi,在 ~295dpi 设备上解码先缩到 103px,再拉伸到 110px 显示) | 只按固定 dp 尺寸显示的设计稿位图放 `drawable-nodpi`(原尺寸解码,View 一次缩小);排查:截图放大对比设计稿,算 `源px × 设备dpi / 桶dpi` 是否小于显示 px |
|
||||
| 分段/胶囊控件里选中块上下不居中(上窄下宽) | 设计稿 CSS 的 `border` 计入盒子尺寸,而 Android shape 的 `stroke` 画在 background 上、不占 padding——照抄 CSS 的 `padding: 2px` 少了 1dp 描边 | 容器 padding = CSS padding + border 宽度(如 2 + 1 = 3dp),且容器高度用 `wrap_content`——写死 28dp 时 28/3/22dp 各自取整成 px,内框比选中块少 1px,选中块底边被截;用像素列取色(`magick … txt:-`)量上下间隙验证 |
|
||||
| XML 里 `bean.a ?? bean.b` 兜底不生效,字段明明有值却显示占位符 | 网络层转换器把 JSON null 归一成空串 `""`,DataBinding 的 `??` 只判 null 不判空串,永远取到左侧的空串 | 在 Bean 上加计算属性做 `isNullOrEmpty` 双兜底(如 `agentDisplay`),XML 绑计算属性(进港查询/出港查询代理人已踩过) |
|
||||
|
||||
---
|
||||
@@ -1533,14 +1580,16 @@ Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail)
|
||||
### 双端验证命令
|
||||
|
||||
```bash
|
||||
~/Library/Android/sdk/emulator/emulator -list-avds # 可用:Aerologic_Phone_5.5_API31(手机,首选)、Aerologic_Tablet(平板)、Medium_Phone_API_36.1
|
||||
/opt/homebrew/share/android-commandlinetools/emulator/emulator -list-avds # 可用:AeroLogic_Phone(手机)、AeroLogic_Tablet(平板)
|
||||
adb -s <设备> shell wm size && adb -s <设备> shell wm density # 确认屏幕参数
|
||||
adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果 PHONE(sw=384dp) / TABLET(sw=800dp)
|
||||
adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果 PHONE(sw=394dp) / TABLET(sw=800dp)
|
||||
adb -s <设备> exec-out screencap -p > /tmp/shot.png # 截图比对
|
||||
```
|
||||
|
||||
> 手机端验证**首选 `Aerologic_Phone_5.5_API31`**:按真实交付手持机 1:1 定制——720×1440、5.51 寸(300dpi,sw=384dp)、
|
||||
> Android 12 / API 31(与 targetSdk 一致)、arm64-v8a,与实机的差异最小。`Medium_Phone_API_36.1`(1080×2400、API 36)
|
||||
> 仅作兼容性参考,分辨率与系统版本都和交付机不符。
|
||||
> 手机端用 **`AeroLogic_Phone`**:按真实交付手持机 1:1 定制——720×1440、5.51 寸(292dpi,sw=394dp)、
|
||||
> Android 12 / API 31、arm64-v8a,与实机的差异最小。
|
||||
> 平板端用 **`AeroLogic_Tablet`**:1280×800、160dpi(sw=800dp)、横屏、API 31。平板模拟器没有物理按键,
|
||||
> `adb shell monkey` 拉起会失败,改用 `am start -n com.lukouguoji.aerologic/com.lukouguoji.module_base.LoginActivity`。
|
||||
> 模拟器可执行文件在 Homebrew 的 android-commandlinetools 下(见上方命令),不在 `~/Library/Android/sdk`。
|
||||
|
||||
> 改动 `BaseActivity` / `BaseBindingActivity` / 公共组件后,**必须在平板上回归**,确认现有平板版行为不变。
|
||||
|
||||
@@ -26,8 +26,8 @@ android {
|
||||
applicationId "com.lukouguoji.aerologic"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
versionCode 88
|
||||
versionName "1.8.8"
|
||||
versionCode 87
|
||||
versionName "1.8.7"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -20,11 +20,6 @@
|
||||
android:exported="true"
|
||||
tools:replace="android:exported" />
|
||||
|
||||
<!-- 出港移库-运单详情(手机版适配验证页) -->
|
||||
<activity
|
||||
android:name="com.lukouguoji.gjc.page.move.IntExpMoveDetailActivity"
|
||||
android:exported="true"
|
||||
tools:replace="android:exported" />
|
||||
|
||||
<!-- 出库交接(手机版适配验证页) -->
|
||||
<activity
|
||||
@@ -98,12 +93,6 @@
|
||||
android:exported="true"
|
||||
tools:replace="android:exported" />
|
||||
|
||||
<!-- 出港运抵-补充信息(手机版适配验证页) -->
|
||||
<activity
|
||||
android:name="com.lukouguoji.gjc.activity.IntExpArriveSupplementActivity"
|
||||
android:exported="true"
|
||||
tools:replace="android:exported" />
|
||||
|
||||
<!-- 出港查询(手机版适配验证页) -->
|
||||
<activity
|
||||
android:name="com.lukouguoji.gjc.activity.GjcQueryActivity"
|
||||
|
||||
@@ -157,12 +157,6 @@
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:screenOrientation="unspecified" />
|
||||
<!-- 国际出港运抵-补充信息(5.5寸手机版专属页面) -->
|
||||
<activity
|
||||
android:name="com.lukouguoji.gjc.activity.IntExpArriveSupplementActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:screenOrientation="unspecified" />
|
||||
<!-- 国际出港仓库 -->
|
||||
<activity
|
||||
android:name="com.lukouguoji.gjc.activity.IntExpStorageUseActivity"
|
||||
@@ -252,9 +246,9 @@
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:screenOrientation="unspecified" />
|
||||
<!-- 国际出港-出港移库-运单详情(手机版专属) -->
|
||||
<!-- 国际出港-出港移库编辑/详情 -->
|
||||
<activity
|
||||
android:name="com.lukouguoji.gjc.page.move.IntExpMoveDetailActivity"
|
||||
android:name="com.lukouguoji.gjc.page.move.IntExpMoveEditActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:screenOrientation="unspecified" />
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.lukouguoji.module_base.bean.FlightBean
|
||||
import com.lukouguoji.module_base.common.Constant
|
||||
import com.lukouguoji.module_base.http.net.NetApply
|
||||
import com.lukouguoji.module_base.ktx.launchLoadingCollect
|
||||
import com.lukouguoji.module_base.util.DictUtils
|
||||
import dev.utils.app.info.KeyValue
|
||||
|
||||
class FlightQueryDetailsViewModel : BaseViewModel() {
|
||||
|
||||
@@ -14,9 +16,24 @@ class FlightQueryDetailsViewModel : BaseViewModel() {
|
||||
|
||||
var dataBean = MutableLiveData<FlightBean>()
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// 枚举字段展示文案:接口已返回名称时原样显示,只返回值(code)时按字典反查名称
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
val countryTypeText = MutableLiveData("")
|
||||
val serviceTypeText = MutableLiveData("")
|
||||
val portCodeText = MutableLiveData("")
|
||||
val flightStatusText = MutableLiveData("")
|
||||
|
||||
private var countryTypeDict = emptyList<KeyValue>()
|
||||
private var serviceTypeDict = emptyList<KeyValue>()
|
||||
private var portCodeDict = emptyList<KeyValue>()
|
||||
private var flightStatusDict = emptyList<KeyValue>()
|
||||
|
||||
fun initOnCreated(intent: Intent) {
|
||||
id = intent.getStringExtra(Constant.Key.ID) ?: ""
|
||||
|
||||
loadDictLists()
|
||||
getData()
|
||||
}
|
||||
|
||||
@@ -27,8 +44,52 @@ class FlightQueryDetailsViewModel : BaseViewModel() {
|
||||
onSuccess = {
|
||||
it.data?.let { data ->
|
||||
dataBean.value = data
|
||||
refreshEnumText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载四个枚举字典(与新增/编辑页同源),用于把 code 反查成 label
|
||||
*/
|
||||
private fun loadDictLists() {
|
||||
DictUtils.getCountryTypeList(addAll = false) {
|
||||
countryTypeDict = it
|
||||
refreshEnumText()
|
||||
}
|
||||
DictUtils.getFlightServiceTypeList(addAll = false) {
|
||||
serviceTypeDict = it
|
||||
refreshEnumText()
|
||||
}
|
||||
DictUtils.getFlightPortCodeList(addAll = false) {
|
||||
portCodeDict = it
|
||||
refreshEnumText()
|
||||
}
|
||||
DictUtils.getFlightStatusList(addAll = false) {
|
||||
flightStatusDict = it
|
||||
refreshEnumText()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据或字典任一就绪都重算一次,避免两者返回先后顺序影响展示
|
||||
*/
|
||||
private fun refreshEnumText() {
|
||||
val bean = dataBean.value ?: return
|
||||
countryTypeText.value = toLabel(countryTypeDict, bean.countryType)
|
||||
serviceTypeText.value = toLabel(serviceTypeDict, bean.serviceType)
|
||||
portCodeText.value = toLabel(portCodeDict, bean.portCode)
|
||||
flightStatusText.value = toLabel(flightStatusDict, bean.flightStatus)
|
||||
}
|
||||
|
||||
/**
|
||||
* 值转 label:先按 code 反查,命中不了再判断是否本身就是 label,仍不命中则原样返回
|
||||
*/
|
||||
private fun toLabel(dict: List<KeyValue>, raw: String?): String {
|
||||
if (raw.isNullOrEmpty()) return ""
|
||||
dict.firstOrNull { it.value == raw }?.let { return it.key }
|
||||
dict.firstOrNull { it.key == raw }?.let { return it.key }
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class GnjMoveStashDetailActivity :
|
||||
/** 「异常照片」加号槽位 → 拍照上传页 */
|
||||
fun openPhoto() {
|
||||
viewModel.dataBean.value?.let {
|
||||
GnjMoveStashPhotoActivity.start(this, it.mawbId, it.wbNo)
|
||||
GnjMoveStashPhotoActivity.start(this, it.mawbId, it.wbNo, it.moveId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +63,7 @@ class GnjMoveStashListActivity :
|
||||
|
||||
/** 筛选:重置(只清空弹层内条件,不影响 Tab 与顶部搜索框) */
|
||||
fun resetFilter() {
|
||||
viewModel.flightDate.value = ""
|
||||
viewModel.flightNo.value = ""
|
||||
viewModel.spCode.value = ""
|
||||
viewModel.carrier.value = ""
|
||||
viewModel.businessType.value = ""
|
||||
}
|
||||
|
||||
@@ -90,7 +88,7 @@ class GnjMoveStashListActivity :
|
||||
/** 浮层「拍照」→ 拍照上传页 */
|
||||
fun openPhoto(bean: MoveStashBean) {
|
||||
bean.actionMenuVisible.set(false)
|
||||
GnjMoveStashPhotoActivity.start(this, bean.mawbId, bean.wbNo)
|
||||
GnjMoveStashPhotoActivity.start(this, bean.mawbId, bean.wbNo, bean.moveId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -54,12 +54,6 @@ class GnjMoveStashListViewModel : BasePageViewModel() {
|
||||
/** 移库状态 Tab:"0" 待移库(DomImpMove/search)/ "1" 已移库(DomImpMove/searchMoved) */
|
||||
val moveState = MutableLiveData("0")
|
||||
|
||||
/** 手机筛选弹层:航班日期 / 航班号 / 特码(默认空 → 不进请求) */
|
||||
val flightDate = MutableLiveData("")
|
||||
val flightNo = MutableLiveData("")
|
||||
val spCode = MutableLiveData("")
|
||||
val spCodeList = MutableLiveData<List<KeyValue>>(emptyList())
|
||||
|
||||
val moveStateTabs = MutableLiveData(
|
||||
listOf(KeyValue("待移库", "0"), KeyValue("已移库", "1"))
|
||||
)
|
||||
@@ -73,15 +67,11 @@ class GnjMoveStashListViewModel : BasePageViewModel() {
|
||||
/** 全选状态(手机 PhoneBottomBar 勾选图标) */
|
||||
val isAllChecked = MutableLiveData(false)
|
||||
|
||||
/** 手机专属的额外请求(Tab 角标、特码字典)开关,仅手机形态在 Activity 中开启 */
|
||||
/** 手机专属的额外请求(Tab 角标)开关,仅手机形态在 Activity 中开启 */
|
||||
private var phoneExtrasEnabled = false
|
||||
|
||||
fun initPhoneExtras() {
|
||||
phoneExtrasEnabled = true
|
||||
// 特码字典:flag=0 国内、ieFlag=I 进港(是否生效待实测,见问题清单)
|
||||
DictUtils.getSpecialCodeList(addAll = false, flag = 0, ieFlag = "I") { list ->
|
||||
spCodeList.postValue(listOf(KeyValue("全部", "")) + list)
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -135,10 +125,6 @@ class GnjMoveStashListViewModel : BasePageViewModel() {
|
||||
"awbType" to businessType.value!!.ifEmpty { null },
|
||||
"wbNo" to waybill.value!!.ifEmpty { null },
|
||||
"carrier" to carrier.value!!.ifEmpty { null },
|
||||
// 以下为手机筛选弹层字段,平板恒为空串 → 恒传 null,行为不变
|
||||
"fdate" to flightDate.value?.ifEmpty { null },
|
||||
"fno" to flightNo.value?.ifEmpty { null },
|
||||
"spCode" to spCode.value?.ifEmpty { null },
|
||||
)
|
||||
|
||||
override fun getData() {
|
||||
|
||||
@@ -33,12 +33,16 @@ class GnjMoveStashPhotoActivity :
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** 已移库运单的 moveId(movedDetail 回填照片用) */
|
||||
const val EXTRA_MOVE_ID = "moveId"
|
||||
|
||||
@JvmStatic
|
||||
fun start(context: Context, mawbId: String, wbNo: String) {
|
||||
fun start(context: Context, mawbId: String, wbNo: String, moveId: String = "") {
|
||||
context.startActivity(
|
||||
Intent(context, GnjMoveStashPhotoActivity::class.java)
|
||||
.putExtra(Constant.Key.ID, mawbId)
|
||||
.putExtra(Constant.Key.CODE, wbNo)
|
||||
.putExtra(EXTRA_MOVE_ID, moveId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,17 @@ import kotlinx.coroutines.launch
|
||||
* 国内进港移库「拍照上传」ViewModel(5.5 寸手持端专属页面)
|
||||
*
|
||||
* 图片选择/预览/删除复用 module_base 的 ImageSelectNewViewHolder(末位加号槽位)。
|
||||
* 已有照片经 DomImpMove/detail 回填;提交走 DomImpMove/modify 最小体
|
||||
* 已有照片回填:待移库经 DomImpMove/detail(mawbId),已移库经 DomImpMove/movedDetail(moveId,
|
||||
* searchMoved 出参不含 mawbId)。提交走 DomImpMove/modify 最小体
|
||||
* (mawbId/remark/picNumber/pic/originalPic),与平板移交编辑页同一链路。
|
||||
*/
|
||||
class GnjMoveStashPhotoViewModel : BaseViewModel(), IOnItemClickListener {
|
||||
|
||||
var mawbId = ""
|
||||
|
||||
/** 已移库运单的移库记录 ID(searchMoved 出参),用于 movedDetail 回填 */
|
||||
var moveId = ""
|
||||
|
||||
/** 运单号(副标题展示,由列表带入) */
|
||||
val wbNo = MutableLiveData("")
|
||||
|
||||
@@ -59,9 +63,14 @@ class GnjMoveStashPhotoViewModel : BaseViewModel(), IOnItemClickListener {
|
||||
fun initOnCreated(intent: Intent) {
|
||||
mawbId = intent.getStringExtra(Constant.Key.ID).orEmpty()
|
||||
wbNo.value = intent.getStringExtra(Constant.Key.CODE).orEmpty()
|
||||
moveId = intent.getStringExtra(GnjMoveStashPhotoActivity.EXTRA_MOVE_ID).orEmpty()
|
||||
|
||||
// 已有照片回填(pic 缩略图 + originalPic 原图),末位补加号槽位
|
||||
launchCollect({ NetApply.api.getGnjMoveStashDetail(mawbId) }) {
|
||||
// 已有照片回填(pic 缩略图 + originalPic 原图),末位补加号槽位;
|
||||
// 已移库运单无 mawbId,改按 moveId 走 movedDetail
|
||||
launchCollect({
|
||||
if (mawbId.isEmpty() && moveId.isNotEmpty()) NetApply.api.getGnjMoveStashMovedDetail(moveId)
|
||||
else NetApply.api.getGnjMoveStashDetail(mawbId)
|
||||
}) {
|
||||
onSuccess = {
|
||||
detailBean = it.data ?: GnjYiKuBean()
|
||||
val picList = detailBean.pic.split(",").filter { p -> p.isNotEmpty() }
|
||||
@@ -88,6 +97,11 @@ class GnjMoveStashPhotoViewModel : BaseViewModel(), IOnItemClickListener {
|
||||
* 确认提交:先补传未上传的本地图片,再按三字段规范提交 modify
|
||||
*/
|
||||
fun submitClick() {
|
||||
// modify 以 mawbId 定位运单;已移库运单 searchMoved/movedDetail 均不返回 mawbId(后端缺口),无法提交
|
||||
if (mawbId.isEmpty()) {
|
||||
showToast("已移库运单缺少运单ID,暂不支持提交照片")
|
||||
return
|
||||
}
|
||||
(rv?.commonAdapter()?.items ?: emptyList())
|
||||
.asFlow()
|
||||
.map { it as FileBean }
|
||||
|
||||
@@ -19,7 +19,7 @@ class GnjUnloadAddActivity :
|
||||
override fun viewModelClass() = GnjUnloadAddViewModel::class.java
|
||||
|
||||
override fun initOnCreate(savedInstanceState: Bundle?) {
|
||||
setBackArrow("国内进港开始卸机")
|
||||
setBackArrow("进港开始卸机")
|
||||
|
||||
binding.viewModel = viewModel
|
||||
viewModel.rv = binding.rv
|
||||
|
||||
@@ -24,7 +24,7 @@ class GnjUnloadListActivity :
|
||||
}
|
||||
|
||||
override fun initOnCreate(savedInstanceState: Bundle?) {
|
||||
setBackArrow("国内进港卸机")
|
||||
setBackArrow("进港卸机")
|
||||
binding.viewModel = viewModel
|
||||
viewModel.pageModel.bindSmartRefreshLayout(
|
||||
binding.srl,
|
||||
|
||||
@@ -31,22 +31,22 @@ class UldEditViewModel : BaseViewModel() {
|
||||
)
|
||||
)
|
||||
|
||||
// 所属航司下拉列表(数据源:POST /typeCode/intCarrier)
|
||||
val carrierList = MutableLiveData<List<KeyValue>>(emptyList())
|
||||
|
||||
// ========== 手机版(5.5 寸手持端)专用 ==========
|
||||
|
||||
/** 来源选项(本地枚举;后端字段暂不支持,需求已登记 documents/后端接口对接问题清单.xlsx) */
|
||||
val sourceEditList = MutableLiveData<List<KeyValue>>(
|
||||
// 来源下拉列表
|
||||
val sourceList = MutableLiveData<List<KeyValue>>(
|
||||
listOf(
|
||||
KeyValue("空运", "空运"),
|
||||
KeyValue("陆运", "陆运"),
|
||||
)
|
||||
)
|
||||
|
||||
// 所属航司下拉列表(数据源:POST /typeCode/intCarrier)
|
||||
val carrierList = MutableLiveData<List<KeyValue>>(emptyList())
|
||||
|
||||
// ========== 手机版(5.5 寸手持端)专用 ==========
|
||||
|
||||
/**
|
||||
* 手机版状态选项。设计稿为三态(可用/占用/维修),后端枚举当前仅 0/1,
|
||||
* 按语义映射 可用=0 / 维修=1;「占用」待后端扩展枚举后放开(需求已登记清单)。
|
||||
* 手机版状态选项。后端枚举维持两态(0 正常 / 1 故障,不新增「占用」),
|
||||
* 按设计稿文案映射 可用=0 / 维修=1。
|
||||
*/
|
||||
val statusPhoneList = listOf(
|
||||
KeyValue("可用", "0"),
|
||||
@@ -60,12 +60,9 @@ class UldEditViewModel : BaseViewModel() {
|
||||
else if (modify) DetailsPageType.Modify else DetailsPageType.Details
|
||||
|
||||
if (id.isEmpty()) {
|
||||
// 新增:直接加载航司列表
|
||||
// 新增:所在港默认 HFE,直接加载航司列表
|
||||
uldBean.value = ULDBean(nowPort = "HFE")
|
||||
loadCarrierList(null)
|
||||
// 手机版新增默认所在港 HFE(设计稿约定);平板无此预填,行为不变
|
||||
if (DeviceUtil.isPhone()) {
|
||||
uldBean.value = uldBean.value?.copy(airport = "HFE")
|
||||
}
|
||||
} else {
|
||||
getData()
|
||||
}
|
||||
@@ -101,16 +98,9 @@ class UldEditViewModel : BaseViewModel() {
|
||||
showToast("ULD编号不能为空")
|
||||
return
|
||||
}
|
||||
// 手机版按设计稿校验必填项(自重 / 入库时间);平板表单无入库时间项,保持原校验行为
|
||||
if (DeviceUtil.isPhone()) {
|
||||
if (bean.uldWeight.isBlank()) {
|
||||
showToast("自重不能为空")
|
||||
return
|
||||
}
|
||||
if (bean.checkInDate.isBlank()) {
|
||||
showToast("入库时间不能为空")
|
||||
return
|
||||
}
|
||||
// 手机版日期行只产出 yyyy-MM-dd,而 inStoreTime 为 date-time(仅日期后端返回 400),补齐时分秒
|
||||
if (DeviceUtil.isPhone() && bean.inStoreTime.length == 10) {
|
||||
bean.inStoreTime = "${bean.inStoreTime} 00:00:00"
|
||||
}
|
||||
val isAdd = pageType.value == DetailsPageType.Add
|
||||
launchLoadingCollect({
|
||||
@@ -119,12 +109,7 @@ class UldEditViewModel : BaseViewModel() {
|
||||
}) {
|
||||
onSuccess = {
|
||||
showToast(it.msg.noNull(if (isAdd) "新增成功" else "修改成功"))
|
||||
if (isAdd) {
|
||||
// 清空表单继续录入;手机版保持所在港默认 HFE(与进入页面时一致)
|
||||
uldBean.value = if (DeviceUtil.isPhone()) ULDBean(airport = "HFE") else ULDBean()
|
||||
} else {
|
||||
getData()
|
||||
}
|
||||
if (isAdd) uldBean.value = ULDBean(nowPort = "HFE") else getData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +70,11 @@ class UldListActivity : BaseBindingActivity<ActivityUldListBinding, UldListViewM
|
||||
/**
|
||||
* 手机版筛选:重置
|
||||
*
|
||||
* 只清空弹层内的四个条件(入库时间 / 进港航班号 / 所属航司 / 来源),不影响顶部搜索框。
|
||||
* 只清空弹层内的两个条件(状态 / 所属航司),不影响顶部搜索框。
|
||||
*/
|
||||
fun resetFilter() {
|
||||
viewModel.checkInDateFilter.value = ""
|
||||
viewModel.ifNoFilter.value = ""
|
||||
viewModel.status.value = ""
|
||||
viewModel.uldSuffix.value = ""
|
||||
viewModel.sourceFilter.value = ""
|
||||
}
|
||||
|
||||
/** 手机版筛选:确认 */
|
||||
@@ -85,7 +83,7 @@ class UldListActivity : BaseBindingActivity<ActivityUldListBinding, UldListViewM
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
/** 手机版批量删除(二次确认后逐条调用单条删除接口) */
|
||||
/** 手机版批量删除(二次确认后调用批量删除接口) */
|
||||
fun showDeleteConfirmDialog() {
|
||||
val selected = viewModel.getSelectedItems()
|
||||
if (selected.isEmpty()) {
|
||||
|
||||
@@ -27,20 +27,15 @@ class UldListViewModel : BasePageViewModel() {
|
||||
val uldSuffix = MutableLiveData("")
|
||||
val uldNo = MutableLiveData("")
|
||||
|
||||
// ========== 手机版(5.5 寸手持端)专用筛选条件 ==========
|
||||
// 平板布局无对应控件,恒为空 → 不进请求参数,平板查询行为与改造前完全一致。
|
||||
// 三个入参后端暂不支持(api-doc 已核对),已登记 documents/后端接口对接问题清单.xlsx,
|
||||
// 按「以设计为准」的约定照传(后端静默忽略无副作用),后端支持后自动生效。
|
||||
val checkInDateFilter = MutableLiveData("") // 入库时间
|
||||
val ifNoFilter = MutableLiveData("") // 进港航班号
|
||||
val sourceFilter = MutableLiveData("") // 来源:空运 / 陆运
|
||||
// ========== 手机版(5.5 寸手持端)筛选 ==========
|
||||
// 后端确认查询入参与平板端保持一致(uld / status / uldSuffix),手机筛选弹层只放 状态 + 所属航司。
|
||||
|
||||
/** 来源下拉(本地枚举,后端字段需求已登记清单) */
|
||||
val sourceList = MutableLiveData<List<KeyValue>>(
|
||||
/** 手机版状态下拉:文案与卡片标签一致(可用=0 / 维修=1) */
|
||||
val statusPhoneFilterList = MutableLiveData<List<KeyValue>>(
|
||||
listOf(
|
||||
KeyValue("全部", ""),
|
||||
KeyValue("空运", "空运"),
|
||||
KeyValue("陆运", "陆运"),
|
||||
KeyValue("可用", "0"),
|
||||
KeyValue("维修", "1"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -82,10 +77,6 @@ class UldListViewModel : BasePageViewModel() {
|
||||
"status" to status.value?.ifEmpty { null },
|
||||
"uldSuffix" to uldSuffix.value?.ifEmpty { null },
|
||||
"uld" to uldNo.value?.ifEmpty { null },
|
||||
// 手机版筛选维度(平板恒空不参与);后端暂不支持这三个入参,已登记清单
|
||||
"checkInDate" to checkInDateFilter.value?.ifEmpty { null },
|
||||
"ifNo" to ifNoFilter.value?.ifEmpty { null },
|
||||
"source" to sourceFilter.value?.ifEmpty { null },
|
||||
).toRequestBody()
|
||||
)
|
||||
}) {
|
||||
@@ -122,10 +113,7 @@ class UldListViewModel : BasePageViewModel() {
|
||||
?.filter { it.checked.get() } ?: emptyList()
|
||||
|
||||
/**
|
||||
* 批量删除选中的 ULD。
|
||||
*
|
||||
* 后端仅有单条删除接口(/eqm/uld/deleteUld,批量接口需求已登记清单),
|
||||
* 此处按队列逐条串行调用,全部完成后统一刷新;中途失败即停并提示。
|
||||
* 批量删除选中的 ULD(/eqm/uld/deleteUldBatch,请求体为 ULD 编号数组,一次调用)。
|
||||
*/
|
||||
fun deleteSelected() {
|
||||
val ulds = getSelectedItems().map { it.uld }
|
||||
@@ -133,19 +121,9 @@ class UldListViewModel : BasePageViewModel() {
|
||||
showToast("请选择要删除的ULD")
|
||||
return
|
||||
}
|
||||
deleteQueue(ulds, 0)
|
||||
}
|
||||
|
||||
private fun deleteQueue(ulds: List<String>, index: Int) {
|
||||
if (index >= ulds.size) {
|
||||
showToast("已删除 ${ulds.size} 条")
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
launchLoadingCollect({ NetApply.api.deleteUld(ulds[index]) }) {
|
||||
onSuccess = { deleteQueue(ulds, index + 1) }
|
||||
onFailed = { _, msg ->
|
||||
showToast("删除 ${ulds[index]} 失败:${msg.noNull("未知错误")}")
|
||||
launchLoadingCollect({ NetApply.api.deleteUldBatch(ulds.toRequestBody()) }) {
|
||||
onSuccess = {
|
||||
showToast("已删除 ${ulds.size} 条")
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ class HomeFragment : Fragment() {
|
||||
.navigation()
|
||||
}
|
||||
|
||||
// 理货报告
|
||||
// 进港理货
|
||||
Constant.AuthName.IntImpTally -> {
|
||||
ARouter.getInstance().build(ARouterConstants.ACTIVITY_URL_INT_IMP_TALLY)
|
||||
.navigation()
|
||||
@@ -847,7 +847,7 @@ class HomeFragment : Fragment() {
|
||||
RightMenu(
|
||||
Constant.AuthName.IntImpTally,
|
||||
R.mipmap.gjj_li_huo_bao_gao_icon,
|
||||
"理货报告"
|
||||
"进港理货"
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
|
||||
@@ -125,6 +125,49 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Row 3: 来源 / 入库时间 / 所在港 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请选择来源"}'
|
||||
list="@{viewModel.sourceList}"
|
||||
title='@{"来源"}'
|
||||
titleLength="@{7}"
|
||||
type="@{DataLayoutType.SPINNER}"
|
||||
value='@={viewModel.uldBean.source}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请选择入库时间"}'
|
||||
title='@{"入库时间"}'
|
||||
titleLength="@{5}"
|
||||
type="@{DataLayoutType.DATETIME}"
|
||||
value='@={viewModel.uldBean.inStoreTime}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请输入所在港"}'
|
||||
title='@{"所在港"}'
|
||||
titleLength="@{5}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@={viewModel.uldBean.nowPort}' />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 备注(全行,多行输入) -->
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.7"
|
||||
android:layout_weight="0.8"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.5"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
@@ -109,7 +109,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="自重:"
|
||||
android:textSize="15sp"
|
||||
completeSpace="@{3}" />
|
||||
completeSpace="@{4}" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -165,7 +165,7 @@
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<!-- 第二行:最大载重 | 备注 -->
|
||||
<!-- 第二行:最大载重 | 来源 | 所在港 | 位置 | 入库时间 -->
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -175,7 +175,7 @@
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.5"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
@@ -193,24 +193,90 @@
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<!-- 备注 -->
|
||||
<!-- 来源 -->
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="3.6"
|
||||
android:layout_weight="0.8"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="备注:"
|
||||
android:text="来源:"
|
||||
android:textSize="15sp"
|
||||
completeSpace="@{3}" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{bean.remark}"
|
||||
android:text="@{bean.source}"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<!-- 所在港 -->
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.8"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="所在港:"
|
||||
android:textSize="15sp"
|
||||
completeSpace="@{4}" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{bean.nowPort}"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<!-- 位置 -->
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.8"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="位置:"
|
||||
android:textSize="15sp"
|
||||
completeSpace="@{5}" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{bean.nowPosition}"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<!-- 入库时间 -->
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.2"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="入库时间:"
|
||||
android:textSize="15sp"
|
||||
completeSpace="@{7}" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{bean.inStoreTime}"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="15dp">
|
||||
|
||||
<!-- 第1行:航班日期、航班号、航程 -->
|
||||
<!-- 第1行:航班日期、航班号、机型 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -68,14 +68,53 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"航程"}'
|
||||
title='@{"机型"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.range}' />
|
||||
value='@{viewModel.dataBean.aircraftCode}' />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 第2行:地区类型、服务类型、经停站 -->
|
||||
<!-- 第2行:始发港、经停港、目的港 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"始发港"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.fdep}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"经停港"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.jtz}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"目的港"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.fdest}' />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 第3行:地区类型、服务类型 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -90,7 +129,7 @@
|
||||
title='@{"地区类型"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.countryType}' />
|
||||
value='@{viewModel.countryTypeText}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
@@ -100,21 +139,55 @@
|
||||
title='@{"服务类型"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.serviceType}' />
|
||||
value='@{viewModel.serviceTypeText}' />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 第4行:飞行状态、航班状态、延误原因 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"经停站"}'
|
||||
title='@{"飞行状态"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.jtz}' />
|
||||
value='@{viewModel.portCodeText}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"航班状态"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.flightStatusText}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"延误原因"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.delayFreeText}' />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 第3行:计划起飞、预计起飞、实际起飞 -->
|
||||
<!-- 第5行:计划起飞、预计起飞、实际起飞 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -153,7 +226,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 第4行:计划降落、预计降落、实际降落 -->
|
||||
<!-- 第6行:计划降落、预计降落、实际降落 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -192,72 +265,13 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 第5行:飞行状态、航班状态、延误原因 -->
|
||||
<!-- 第7行:机位、机号 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"飞行状态"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.portCode}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"航班状态"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.flightStatus}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"延误原因"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.delayFreeText}' />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 第6行:机号、机型、机位 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"机号"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.registration}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"机型"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.aircraftCode}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -268,6 +282,21 @@
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.standId}' />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
enable="@{false}"
|
||||
title='@{"机号"}'
|
||||
titleLength="@{4}"
|
||||
type="@{DataLayoutType.INPUT}"
|
||||
value='@{viewModel.dataBean.registration}' />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
title='@{"移库人"}'
|
||||
value="@{viewModel.dataBean.opId}" />
|
||||
value="@{viewModel.dataBean.opUserDisplay}" />
|
||||
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -121,33 +121,14 @@
|
||||
setOnResetClick="@{(v)-> activity.resetFilter()}"
|
||||
title='@{"筛选条件"}'>
|
||||
|
||||
<!-- 航班日期(入参 fdate) -->
|
||||
<!-- 承运人(入参 carrier,与平板搜索栏一致) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
hint='@{"请选择航班日期"}'
|
||||
title='@{"航班日期"}'
|
||||
type="@{PhoneDataLayoutType.DATE}"
|
||||
value="@={viewModel.flightDate}" />
|
||||
|
||||
<!-- 航班号(入参 fno) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
hint='@{"请输入航班号"}'
|
||||
title='@{"航班号"}'
|
||||
hint='@{"请输入承运人"}'
|
||||
title='@{"承运人"}'
|
||||
type="@{PhoneDataLayoutType.INPUT}"
|
||||
value="@={viewModel.flightNo}" />
|
||||
|
||||
<!-- 特码(入参 spCode) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
hint='@{"请选择特码"}'
|
||||
list="@{viewModel.spCodeList}"
|
||||
title='@{"特码"}'
|
||||
type="@{PhoneDataLayoutType.SPINNER}"
|
||||
value="@={viewModel.spCode}" />
|
||||
value="@={viewModel.carrier}" />
|
||||
|
||||
<!-- 运单类型(入参 awbType,复用平板既有 businessType 字段与字典) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
· ULD修改(Modify):编号只读,其余可编辑 + 底部保存
|
||||
· ULD详情(Details):全只读右对齐 + 顶栏右侧铅笔进入修改 + 额外展示最大装载容积/最大载重
|
||||
|
||||
「来源」「状态三态(占用)」为后端暂不支持的设计稿要求,
|
||||
已登记 documents/后端接口对接问题清单.xlsx,UI 先行、后端补齐后自动生效。
|
||||
字段与平板端一致:来源 source(空运/陆运)、入库时间 inStoreTime、所在港 nowPort;
|
||||
状态维持两态(0 可用 / 1 维修,后端不新增「占用」)。
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
@@ -119,11 +119,11 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="22dp"
|
||||
android:background="@{viewModel.uldBean.status.empty ? null : (viewModel.uldBean.isStatusAvailable ? @drawable/bg_phone_tag_green : (viewModel.uldBean.isStatusOccupied ? @drawable/bg_phone_tag_orange : @drawable/bg_phone_tag_red_light))}"
|
||||
android:background="@{viewModel.uldBean.status.empty ? null : (viewModel.uldBean.isStatusAvailable ? @drawable/bg_phone_tag_green : @drawable/bg_phone_tag_red_light)}"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:text='@{viewModel.uldBean.status.empty ? "请选择状态" : viewModel.uldBean.statusPhoneName}'
|
||||
android:textColor="@{viewModel.uldBean.status.empty ? @color/phone_text_hint : (viewModel.uldBean.isStatusAvailable ? @color/phone_tag_green_text : (viewModel.uldBean.isStatusOccupied ? @color/phone_tag_orange_text : @color/phone_danger_text))}"
|
||||
android:textColor="@{viewModel.uldBean.status.empty ? @color/phone_text_hint : (viewModel.uldBean.isStatusAvailable ? @color/phone_tag_green_text : @color/phone_danger_text)}"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<!-- 编辑态右箭头 -->
|
||||
@@ -167,40 +167,34 @@
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请输入自重"}'
|
||||
numeric="@{true}"
|
||||
required="@{viewModel.pageType == DetailsPageType.Add}"
|
||||
title='@{"自重(kg)"}'
|
||||
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
|
||||
value="@={viewModel.uldBean.uldWeight}" />
|
||||
|
||||
<!-- 最大装载容积(仅详情展示,与设计稿一致) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
title='@{"最大装载容积(m³)"}'
|
||||
type="@{PhoneFormRowType.TEXT}"
|
||||
value="@{viewModel.uldBean.maxVolume}"
|
||||
android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.VISIBLE : View.GONE}" />
|
||||
|
||||
<!-- 最大载重(仅详情展示,与设计稿一致) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
title='@{"最大载重(kg)"}'
|
||||
type="@{PhoneFormRowType.TEXT}"
|
||||
value="@{viewModel.uldBean.maxWeight}"
|
||||
android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.VISIBLE : View.GONE}" />
|
||||
|
||||
<!-- 进港航班号(绑 ifNo,语义待后端确认,已登记清单) -->
|
||||
<!-- 最大装载容积 -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请输入进港航班号"}'
|
||||
title='@{"进港航班号"}'
|
||||
hint='@{"请输入最大装载容积"}'
|
||||
numeric="@{true}"
|
||||
title='@{"最大装载容积(m³)"}'
|
||||
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
|
||||
value="@={viewModel.uldBean.ifNo}" />
|
||||
value="@={viewModel.uldBean.maxVolume}" />
|
||||
|
||||
<!-- 所在港(新增默认 HFE,由 ViewModel 手机开关预填) -->
|
||||
<!-- 最大载重 -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请输入最大载重"}'
|
||||
numeric="@{true}"
|
||||
title='@{"最大载重(kg)"}'
|
||||
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
|
||||
value="@={viewModel.uldBean.maxWeight}" />
|
||||
|
||||
|
||||
<!-- 所在港(nowPort,新增默认 HFE) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -208,29 +202,28 @@
|
||||
hint='@{"请输入所在港"}'
|
||||
title='@{"所在港"}'
|
||||
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
|
||||
value="@={viewModel.uldBean.airport}" />
|
||||
value="@={viewModel.uldBean.nowPort}" />
|
||||
|
||||
<!-- 来源(设计稿顺序位于所在港之后;后端字段暂不支持,已登记清单,提交后端忽略) -->
|
||||
<!-- 来源(source:空运/陆运,设计稿顺序位于所在港之后) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请选择来源"}'
|
||||
list="@{viewModel.sourceEditList}"
|
||||
list="@{viewModel.sourceList}"
|
||||
title='@{"来源"}'
|
||||
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.SELECT}"
|
||||
value="@={viewModel.uldBean.source}" />
|
||||
|
||||
<!-- 入库时间(绑 checkInDate,筛选/格式语义待后端确认,已登记清单) -->
|
||||
<!-- 入库时间(inStoreTime,与平板端一致) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||||
hint='@{"请输入入库时间"}'
|
||||
required="@{viewModel.pageType == DetailsPageType.Add}"
|
||||
title='@{"入库时间"}'
|
||||
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.DATE}"
|
||||
value="@={viewModel.uldBean.checkInDate}" />
|
||||
value="@={viewModel.uldBean.inStoreTime}" />
|
||||
|
||||
<!-- 备注 -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
|
||||
|
||||
@@ -130,23 +130,15 @@
|
||||
setOnResetClick="@{(v)-> activity.resetFilter()}"
|
||||
title='@{"筛选条件"}'>
|
||||
|
||||
<!-- 入库时间(后端筛选入参暂不支持,已登记清单) -->
|
||||
<!-- 状态(后端入参 status,与平板端查询参数一致) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
hint='@{"请输入入库时间"}'
|
||||
title='@{"入库时间"}'
|
||||
type="@{PhoneDataLayoutType.DATE}"
|
||||
value="@={viewModel.checkInDateFilter}" />
|
||||
|
||||
<!-- 进港航班号(后端筛选入参暂不支持,已登记清单) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
hint='@{"请输入进港航班号"}'
|
||||
title='@{"进港航班号"}'
|
||||
type="@{PhoneDataLayoutType.INPUT}"
|
||||
value="@={viewModel.ifNoFilter}" />
|
||||
hint='@{"请选择状态"}'
|
||||
list="@{viewModel.statusPhoneFilterList}"
|
||||
title='@{"状态"}'
|
||||
type="@{PhoneDataLayoutType.SPINNER}"
|
||||
value="@={viewModel.status}" />
|
||||
|
||||
<!-- 所属航司(后端入参 uldSuffix,真实生效) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
@@ -158,16 +150,6 @@
|
||||
type="@{PhoneDataLayoutType.SPINNER}"
|
||||
value="@={viewModel.uldSuffix}" />
|
||||
|
||||
<!-- 来源(后端字段暂不支持,已登记清单) -->
|
||||
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
hint='@{"请选择来源"}'
|
||||
list="@{viewModel.sourceList}"
|
||||
title='@{"来源"}'
|
||||
type="@{PhoneDataLayoutType.SPINNER}"
|
||||
value="@={viewModel.sourceFilter}" />
|
||||
|
||||
</com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
· 平板独有 id(tv_modify / tv_delete / iv_icon)与手机独有 id(iv_check)
|
||||
在 Binding 基类中自动为 @Nullable,ViewHolder 判空使用
|
||||
|
||||
状态标签三色(设计稿):可用=浅绿 / 占用=浅橙 / 维修=浅红。
|
||||
后端当前仅两态(0 可用 / 1 维修),「占用」为后端扩展枚举后的预留,见 ULDBean.statusPhoneName。
|
||||
状态标签:可用=浅绿 / 维修=浅红(后端枚举维持两态 0/1,不含「占用」),见 ULDBean.statusPhoneName。
|
||||
「来源」字段后端暂无(恒空显示 "-"),已登记 documents/后端接口对接问题清单.xlsx。
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
@@ -61,23 +60,24 @@
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- 状态标签:可用(绿) / 占用(橙) / 维修(红,浅红底红字) -->
|
||||
<!-- 状态标签:可用(绿) / 维修(红,浅红底红字) -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@{bean.isStatusAvailable ? @drawable/bg_phone_tag_green : (bean.isStatusOccupied ? @drawable/bg_phone_tag_orange : @drawable/bg_phone_tag_red_light)}"
|
||||
android:background="@{bean.isStatusAvailable ? @drawable/bg_phone_tag_green : @drawable/bg_phone_tag_red_light}"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:text="@{bean.statusPhoneName}"
|
||||
android:textColor="@{bean.isStatusAvailable ? @color/phone_tag_green_text : (bean.isStatusOccupied ? @color/phone_tag_orange_text : @color/phone_danger_text)}"
|
||||
android:textColor="@{bean.isStatusAvailable ? @color/phone_tag_green_text : @color/phone_danger_text}"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:src="@drawable/ic_phone_chevron_right" />
|
||||
android:src="@drawable/ic_phone_chevron_right"
|
||||
android:tint="@color/phone_text_body" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:text='@{"入库时间:" + (bean.checkInDate.empty ? "-" : bean.checkInDate)}'
|
||||
android:text='@{"入库时间:" + (bean.inStoreTime.empty ? "-" : bean.inStoreTime)}'
|
||||
android:textColor="@color/phone_text_body"
|
||||
android:textSize="13sp" />
|
||||
|
||||
|
||||
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164513_257_1.jpg
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164514_258_1.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164515_259_1.jpg
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164515_260_1.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164516_261_1.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164517_262_1.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164535_263_1.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164714_265_1.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164715_266_1.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164716_267_1.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
documents/5.5寸旧页面颜色调整/微信图片_20261022164716_268_1.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
881
documents/5.5寸首页新样式/5.5设计图html/dashboard.html
Normal file
@@ -0,0 +1,881 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="w-full h-full overflow-hidden" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
|
||||
<title>首页</title>
|
||||
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
|
||||
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F0F3F7;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC",
|
||||
"HarmonyOS Sans SC", "MiSans", "Noto Sans SC", "Source Han Sans SC",
|
||||
"Microsoft YaHei", ui-sans-serif, system-ui, sans-serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.app-frame,
|
||||
.app-frame *,
|
||||
.app-frame *::before,
|
||||
.app-frame *::after {
|
||||
font-family: inherit;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.app-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: min(393px, 100vw);
|
||||
max-height: min(852px, 100vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #F5F7FA;
|
||||
position: relative;
|
||||
--nav-height: 52px;
|
||||
--tab-height: 68px;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: var(--nav-height);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: clamp(16px, 4.8vw, 18px);
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
letter-spacing: clamp(0px, 0.16vw, 0.3px);
|
||||
}
|
||||
|
||||
.nav-bell {
|
||||
width: clamp(30px, 9.4vw, 34px);
|
||||
height: clamp(30px, 9.4vw, 34px);
|
||||
}
|
||||
|
||||
.nav-bell iconify-icon {
|
||||
width: clamp(18px, 5.6vw, 20px);
|
||||
height: clamp(18px, 5.6vw, 20px);
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
min-width: clamp(21px, 6.7vw, 24px);
|
||||
font-size: clamp(10px, 2.9vw, 11px);
|
||||
line-height: clamp(14px, 4.2vw, 15px);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
height: var(--tab-height);
|
||||
}
|
||||
|
||||
.tab-bar iconify-icon {
|
||||
width: clamp(22px, 6.7vw, 24px);
|
||||
height: clamp(22px, 6.7vw, 24px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack {
|
||||
width: clamp(25px, 7.5vw, 27px);
|
||||
height: clamp(22px, 6.7vw, 24px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack > iconify-icon:first-child {
|
||||
width: clamp(20px, 6.1vw, 22px);
|
||||
height: clamp(20px, 6.1vw, 22px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack > iconify-icon:last-child {
|
||||
width: clamp(15px, 4.7vw, 17px);
|
||||
height: clamp(15px, 4.7vw, 17px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-label {
|
||||
font-size: 11.5px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.tab-bar .tab-label.is-active {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: clamp(26px, 8.3vw, 30px);
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
overscroll-behavior-y: contain;
|
||||
/* 为右下角悬浮的设置按钮留出避让空间,滚到底时列表不被遮挡 */
|
||||
padding-bottom: calc(52px + var(--tab-height) + 16px + 20px);
|
||||
}
|
||||
|
||||
.main-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* 航班动态设置:右下角悬浮按钮,抬升到 tab 栏之上 */
|
||||
.settings-fab {
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
right: 16px;
|
||||
bottom: calc(var(--tab-height) + 16px);
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: #E8EFFD;
|
||||
color: #3B71E8;
|
||||
transform: translateZ(0);
|
||||
box-shadow: 0 4px 14px rgba(31, 42, 51, 0.14);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
transition: transform 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.settings-fab:active {
|
||||
transform: translateZ(0) scale(0.96);
|
||||
box-shadow: 0 2px 8px rgba(31, 42, 51, 0.18);
|
||||
}
|
||||
|
||||
.settings-fab:focus-visible {
|
||||
outline: 2px solid rgba(59, 113, 232, 0.55);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.settings-fab .settings-fab-icon {
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 主卡片:白卡 + 1px 描边 + 轻阴影,与两页统一 */
|
||||
.flight-card {
|
||||
margin: clamp(10px, 3vw, 14px);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #EDF0F4;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 10px clamp(10px, 3.4vw, 14px);
|
||||
border-bottom: 1px solid #EEF1F5;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex: none;
|
||||
font-size: clamp(13px, 3.7vw, 14px);
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.2px;
|
||||
color: #16202A;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scope-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex: none;
|
||||
padding: 2px;
|
||||
border-radius: 999px;
|
||||
background: #F4F6F9;
|
||||
border: 1px solid #EDF0F4;
|
||||
/* 作为筛选行中的一个平级项,宽度按内容自适应,不拉伸 */
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scope-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
padding: 0 clamp(5px, 1.6vw, 8px);
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #6B7A8D;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
.scope-option.is-active {
|
||||
background: #3B71E8;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 筛选行:国内/国际、进港/出港、日期控件为三个平级项。
|
||||
控件本体按内容自适应,不拉伸;两段间隙为 flex 弹性间隔,
|
||||
各占剩余空间的一半,因此首项贴左内边距、末项贴右内边距 */
|
||||
.card-head > .scope-group,
|
||||
.card-head > .date-group {
|
||||
flex: 0 0 auto;
|
||||
max-width: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-head > .gap-flex {
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.date-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
flex: none;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1px;
|
||||
width: 30px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid #EDF0F4;
|
||||
border-radius: 7px;
|
||||
background: #FFFFFF;
|
||||
color: #3B71E8;
|
||||
font-size: 8px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.2px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
transition: border-color 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.date-picker:focus-visible,
|
||||
.date-picker.is-open {
|
||||
border-color: #3B71E8;
|
||||
box-shadow: 0 0 0 2px rgba(59, 113, 232, 0.14), 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.date-picker .date-month {
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
color: #98A4B3;
|
||||
}
|
||||
|
||||
.date-picker .date-day {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
color: #3B71E8;
|
||||
}
|
||||
|
||||
.date-step {
|
||||
flex: none;
|
||||
width: 22px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #16202A;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.date-picker + .date-step,
|
||||
.date-step + .date-picker {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.date-step.is-next {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.date-step iconify-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dir-tab {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0 clamp(5px, 1.6vw, 8px);
|
||||
height: 24px;
|
||||
margin: 0;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6B7A8D;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
.dir-tab.is-active {
|
||||
background: #3B71E8;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dir-tab + .dir-tab {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* 导航栏右侧图标组:仅铃铛 */
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.nav-icon-btn {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: clamp(32px, 10vw, 34px);
|
||||
height: clamp(32px, 10vw, 34px);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease, opacity 140ms ease;
|
||||
}
|
||||
|
||||
.nav-icon-btn:active {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.nav-icon-btn:focus-visible {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.7);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.nav-icon-btn iconify-icon {
|
||||
width: clamp(17px, 5.4vw, 19px);
|
||||
height: clamp(17px, 5.4vw, 19px);
|
||||
}
|
||||
|
||||
.table-head {
|
||||
display: grid;
|
||||
grid-template-columns: 0.95fr 1fr 1.05fr 1fr;
|
||||
align-items: center;
|
||||
background: #F4F6F9;
|
||||
border-bottom: 1px solid #EEF1F5;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.table-head span {
|
||||
padding: 9px 4px;
|
||||
text-align: center;
|
||||
color: #6B7A8D;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 120px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.flight-row {
|
||||
display: grid;
|
||||
grid-template-columns: 0.95fr 1fr 1.05fr 1fr;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
border-bottom: 1px solid #EEF1F5;
|
||||
}
|
||||
|
||||
.flight-row span {
|
||||
padding: 9px 4px;
|
||||
text-align: center;
|
||||
color: #16202A;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #98A4B3;
|
||||
cursor: pointer;
|
||||
transition: color 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.tab-button:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.tab-button.is-active {
|
||||
color: #3B71E8;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 80px;
|
||||
z-index: 60;
|
||||
max-width: calc(100% - 44px);
|
||||
width: max-content;
|
||||
padding: 9px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(29, 41, 57, 0.9);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 8px);
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
box-shadow: 0 8px 24px rgba(18, 33, 49, 0.18);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.toast.is-visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
/* 5.51 寸小屏(窄屏或矮屏)收紧纵向节奏 */
|
||||
@media (max-width: 374px), (max-height: 760px) {
|
||||
.app-frame {
|
||||
--nav-height: 46px;
|
||||
--tab-height: 58px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tab-bar .tab-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.flight-card {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
gap: 0;
|
||||
padding: 8px 9px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.scope-group {
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.scope-option,
|
||||
.dir-tab {
|
||||
font-size: 11.5px;
|
||||
height: 22px;
|
||||
padding: 0 clamp(4px, 1.3vw, 7px);
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
width: 27px;
|
||||
height: 31px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.settings-fab {
|
||||
right: 12px;
|
||||
bottom: calc(var(--tab-height) + 12px);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.settings-fab .settings-fab-icon {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.date-picker .date-month {
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
.date-picker .date-day {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.date-step {
|
||||
width: 19px;
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.date-step iconify-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.dir-tab {
|
||||
height: 22px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-head span {
|
||||
padding: 8px 2px;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.flight-row span {
|
||||
padding: 8px 2px;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
bottom: 72px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tab-button,
|
||||
.nav-icon-btn,
|
||||
.settings-fab,
|
||||
.toast {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 359px) {
|
||||
.table-head,
|
||||
.flight-row {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.table-head span,
|
||||
.flight-row span {
|
||||
padding: 8px 1px;
|
||||
font-size: 10.5px;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-frame">
|
||||
<!-- 顶部系统状态栏 -->
|
||||
<header class="flex-none bg-[#3B71E8] text-white">
|
||||
<div class="status-bar flex items-center justify-between px-3">
|
||||
<div class="flex items-center gap-[7px]">
|
||||
<span class="text-[11.5px] font-semibold tracking-[0.2px]">16:48</span>
|
||||
</div>
|
||||
<div aria-label="Wi-Fi" class="flex min-w-0 items-center gap-[5px]">
|
||||
<iconify-icon class="opacity-95" height="14" icon="mdi:wifi" width="14"></iconify-icon>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- 蓝色导航栏 -->
|
||||
<nav aria-label="首页导航栏" class="nav-bar relative z-20 flex flex-none items-center justify-center bg-[#3B71E8] text-white">
|
||||
<h1 class="nav-title">首页</h1>
|
||||
<div class="nav-actions absolute right-[8px] top-1/2 -translate-y-1/2">
|
||||
<button aria-label="通知,507 条消息" class="nav-icon-btn text-white" onclick="showNotificationToast()" type="button">
|
||||
<iconify-icon class="drop-shadow-[0_1px_1px_rgba(0,0,0,0.12)]" icon="mdi:bell-outline"></iconify-icon>
|
||||
<span class="nav-badge absolute -right-[2px] top-[0px] rounded-full bg-[#e1192d] px-[4px] text-center font-semibold text-white shadow-[0_1px_2px_rgba(145,16,31,0.35)]">507</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- 主内容滚动区 -->
|
||||
<main class="main-scroll min-h-0 flex flex-1 flex-col overflow-y-auto overflow-x-hidden bg-[#F5F7FA]" id="mainContent">
|
||||
<section aria-label="航班动态" class="flight-card">
|
||||
<!-- 首行:国内/国际 + 进港/出港分段 + 图标式日期 -->
|
||||
<div class="card-head">
|
||||
<div aria-label="国内国际" class="scope-group" role="radiogroup">
|
||||
<button aria-checked="true" class="scope-option is-active" data-scope="国内" onclick="selectScope(this)" role="radio" type="button">国内</button>
|
||||
<button aria-checked="false" class="scope-option" data-scope="国际" onclick="selectScope(this)" role="radio" type="button">国际</button>
|
||||
</div>
|
||||
<span aria-hidden="true" class="gap-flex"></span>
|
||||
<div aria-label="进港出港方向" class="scope-group" role="radiogroup">
|
||||
<button aria-checked="true" class="dir-tab is-active" data-dir="进港" onclick="selectDirection(this)" role="radio" type="button">进港</button>
|
||||
<button aria-checked="false" class="dir-tab" data-dir="出港" onclick="selectDirection(this)" role="radio" type="button">出港</button>
|
||||
</div>
|
||||
<span aria-hidden="true" class="gap-flex"></span>
|
||||
<div class="date-group">
|
||||
<button aria-label="前一天" class="date-step date-step-prev" data-date-step="-1" onclick="stepDate(-1)" type="button">
|
||||
<iconify-icon height="18" icon="mdi:chevron-left" width="18"></iconify-icon>
|
||||
</button>
|
||||
<button aria-label="选择日期" class="date-picker" id="datePicker" type="button">
|
||||
<span class="date-month" id="dateMonth">10月</span>
|
||||
<span class="date-day" id="dateDay">20</span>
|
||||
<input aria-label="日期" class="date-input" id="dateInput" type="date" value="2026-10-20"/>
|
||||
</button>
|
||||
<button aria-label="后一天" class="date-step is-next" data-date-step="1" onclick="stepDate(1)" type="button">
|
||||
<iconify-icon height="18" icon="mdi:chevron-right" width="18"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表头 -->
|
||||
<div class="table-head" role="row">
|
||||
<span role="columnheader">航班号</span>
|
||||
<span role="columnheader">航程</span>
|
||||
<span role="columnheader">地区类型</span>
|
||||
<span role="columnheader">计划起飞</span>
|
||||
</div>
|
||||
<!-- 数据区 -->
|
||||
<div aria-label="航班动态列表" class="table-body">
|
||||
<div class="flight-row" role="row">
|
||||
<span role="cell">MU2222</span>
|
||||
<span role="cell">HFE-PEK</span>
|
||||
<span role="cell">国内</span>
|
||||
<span role="cell">12:20</span>
|
||||
</div>
|
||||
<div class="flight-row" role="row">
|
||||
<span role="cell">MU6895</span>
|
||||
<span role="cell">HFE-LAX</span>
|
||||
<span role="cell">国际</span>
|
||||
<span role="cell">02:26</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- 右下角悬浮:航班动态设置 -->
|
||||
<button aria-label="航班动态设置" class="settings-fab" onclick="showToast('航班动态设置')" type="button">
|
||||
<svg aria-hidden="true" class="settings-fab-icon" focusable="false" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" fill="#3B71E8"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 底部 Tab 栏 -->
|
||||
<nav aria-label="底部导航" class="tab-bar relative z-30 flex flex-none items-stretch border-t border-[#EDF0F4] bg-white pb-[4px] shadow-[0_-1px_2px_rgba(16,24,40,0.04)]">
|
||||
<button aria-current="page" aria-label="首页,当前页面" class="tab-button is-active" onclick="switchTab('首页')" type="button">
|
||||
<iconify-icon icon="mdi:home"></iconify-icon>
|
||||
<span class="tab-label is-active">首页</span>
|
||||
</button>
|
||||
<button aria-label="国内" class="tab-button" onclick="switchTab('国内')" type="button">
|
||||
<iconify-icon icon="mdi:earth"></iconify-icon>
|
||||
<span class="tab-label">国内</span>
|
||||
</button>
|
||||
<button aria-label="国际" class="tab-button" onclick="switchTab('国际')" type="button">
|
||||
<span class="tab-icon-stack relative inline-flex items-center justify-center">
|
||||
<iconify-icon class="absolute bottom-0 left-0" icon="mdi:earth"></iconify-icon>
|
||||
<iconify-icon class="absolute right-0 top-0 -rotate-[14deg]" icon="mdi:airplane"></iconify-icon>
|
||||
</span>
|
||||
<span class="tab-label">国际</span>
|
||||
</button>
|
||||
<button aria-label="我的" class="tab-button" onclick="switchTab('我的')" type="button">
|
||||
<iconify-icon icon="mdi:account-circle-outline"></iconify-icon>
|
||||
<span class="tab-label">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div aria-live="polite" class="toast" id="toast" role="status"></div>
|
||||
</div>
|
||||
<script>
|
||||
let toastTimer = null;
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.getElementById("toast");
|
||||
if (!toast) return;
|
||||
|
||||
toast.textContent = message;
|
||||
toast.classList.add("is-visible");
|
||||
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer);
|
||||
}
|
||||
|
||||
toastTimer = setTimeout(function () {
|
||||
toast.classList.remove("is-visible");
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
function showNotificationToast() {
|
||||
showToast("您有 507 条待处理消息");
|
||||
}
|
||||
|
||||
function selectScope(target) {
|
||||
const options = document.querySelectorAll(".scope-option");
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const isTarget = options[i] === target;
|
||||
options[i].classList.toggle("is-active", isTarget);
|
||||
options[i].setAttribute("aria-checked", isTarget ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
function selectDirection(target) {
|
||||
const tabs = document.querySelectorAll(".dir-tab");
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
const isTarget = tabs[i] === target;
|
||||
tabs[i].classList.toggle("is-active", isTarget);
|
||||
tabs[i].setAttribute("aria-checked", isTarget ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(name) {
|
||||
if (name === "国内") {
|
||||
window.location.href = "domestic.html";
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "国际") {
|
||||
window.location.href = "home.html";
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "首页") {
|
||||
const main = document.getElementById("mainContent");
|
||||
if (main) {
|
||||
main.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
showToast("当前已在首页");
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(name + "模块暂未接入");
|
||||
}
|
||||
|
||||
(function () {
|
||||
const datePicker = document.getElementById("datePicker");
|
||||
const dateInput = document.getElementById("dateInput");
|
||||
const dateMonth = document.getElementById("dateMonth");
|
||||
const dateDay = document.getElementById("dateDay");
|
||||
if (!datePicker || !dateInput || !dateMonth || !dateDay) return;
|
||||
|
||||
function renderDate(value) {
|
||||
if (!value) return;
|
||||
const parts = value.split("-");
|
||||
const month = parseInt(parts[1], 10);
|
||||
const day = parseInt(parts[2], 10);
|
||||
if (!month || !day) return;
|
||||
dateMonth.textContent = month + "月";
|
||||
dateDay.textContent = String(day);
|
||||
datePicker.setAttribute("aria-label", "选择日期,当前 " + month + "月" + day + "日");
|
||||
}
|
||||
|
||||
window.stepDate = function (offset) {
|
||||
const parts = dateInput.value.split("-");
|
||||
const current = new Date(
|
||||
parseInt(parts[0], 10) || 2026,
|
||||
(parseInt(parts[1], 10) || 1) - 1,
|
||||
parseInt(parts[2], 10) || 1
|
||||
);
|
||||
current.setDate(current.getDate() + offset);
|
||||
const y = current.getFullYear();
|
||||
const m = String(current.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(current.getDate()).padStart(2, "0");
|
||||
dateInput.value = y + "-" + m + "-" + d;
|
||||
renderDate(dateInput.value);
|
||||
};
|
||||
|
||||
renderDate(dateInput.value);
|
||||
|
||||
datePicker.addEventListener("click", function (event) {
|
||||
if (event.target === dateInput) return;
|
||||
if (typeof dateInput.showPicker === "function") {
|
||||
try {
|
||||
dateInput.showPicker();
|
||||
} catch (error) {
|
||||
dateInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dateInput.addEventListener("focus", function () {
|
||||
datePicker.classList.add("is-open");
|
||||
});
|
||||
|
||||
dateInput.addEventListener("blur", function () {
|
||||
datePicker.classList.remove("is-open");
|
||||
});
|
||||
|
||||
dateInput.addEventListener("change", function () {
|
||||
if (dateInput.value) {
|
||||
renderDate(dateInput.value);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
535
documents/5.5寸首页新样式/5.5设计图html/domestic.html
Normal file
@@ -0,0 +1,535 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="w-full h-full overflow-hidden" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
|
||||
<title>国内业务</title>
|
||||
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
|
||||
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F0F3F7;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC",
|
||||
"HarmonyOS Sans SC", "MiSans", "Noto Sans SC", "Source Han Sans SC",
|
||||
"Microsoft YaHei", ui-sans-serif, system-ui, sans-serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 全页字体统一:覆盖 Tailwind 默认字体族,数字统一等宽 */
|
||||
.app-frame,
|
||||
.app-frame *,
|
||||
.app-frame *::before,
|
||||
.app-frame *::after {
|
||||
font-family: inherit;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.app-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: min(393px, 100vw);
|
||||
max-height: min(852px, 100vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #F5F7FA;
|
||||
position: relative;
|
||||
/* 以 360×640(5.51 寸 16:9)为小屏基准,宫格尺寸随视口宽度等比缩放 */
|
||||
--tile-size: clamp(48px, 15.3vw, 60px);
|
||||
--icon-size: calc(var(--tile-size) * 0.55);
|
||||
--nav-height: 52px;
|
||||
--tab-height: 68px;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: var(--nav-height);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: clamp(16px, 4.8vw, 18px);
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
letter-spacing: clamp(0px, 0.16vw, 0.3px);
|
||||
}
|
||||
|
||||
.nav-bell {
|
||||
width: clamp(30px, 9.4vw, 34px);
|
||||
height: clamp(30px, 9.4vw, 34px);
|
||||
}
|
||||
|
||||
.nav-bell iconify-icon {
|
||||
width: clamp(22px, 6.9vw, 25px);
|
||||
height: clamp(22px, 6.9vw, 25px);
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
min-width: clamp(21px, 6.7vw, 24px);
|
||||
font-size: clamp(10px, 2.9vw, 11px);
|
||||
line-height: clamp(14px, 4.2vw, 15px);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
height: var(--tab-height);
|
||||
}
|
||||
|
||||
.tab-bar iconify-icon {
|
||||
width: clamp(22px, 6.7vw, 24px);
|
||||
height: clamp(22px, 6.7vw, 24px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack {
|
||||
width: clamp(25px, 7.5vw, 27px);
|
||||
height: clamp(22px, 6.7vw, 24px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack > iconify-icon:first-child {
|
||||
width: clamp(20px, 6.1vw, 22px);
|
||||
height: clamp(20px, 6.1vw, 22px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack > iconify-icon:last-child {
|
||||
width: clamp(15px, 4.7vw, 17px);
|
||||
height: clamp(15px, 4.7vw, 17px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-label {
|
||||
font-size: 11.5px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.tab-bar .tab-label.is-active {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: clamp(26px, 8.3vw, 30px);
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.main-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.group-bar {
|
||||
height: 41px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 24px;
|
||||
background: #F4F6F9;
|
||||
border-top: 1px solid #FFFFFF;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: clamp(13px, 3.7vw, 14px);
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.2px;
|
||||
color: #16202A;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
column-gap: 0;
|
||||
row-gap: clamp(12px, 4.6vw, 18px);
|
||||
background: #ffffff;
|
||||
padding: clamp(14px, 4.6vw, 18px) clamp(12px, 5vw, 18px) clamp(18px, 6vw, 24px);
|
||||
}
|
||||
|
||||
.group-arrow {
|
||||
display: block;
|
||||
flex: none;
|
||||
width: auto;
|
||||
height: clamp(12px, 3.6vw, 15px);
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 1px 1px rgba(37, 99, 235, 0.28));
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: transform 140ms ease;
|
||||
}
|
||||
|
||||
.feature-card:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.icon-tile {
|
||||
width: var(--tile-size);
|
||||
height: var(--tile-size);
|
||||
flex: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: calc(var(--tile-size) * 0.267);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.92),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.45),
|
||||
0 6px 12px -7px rgba(54, 80, 99, 0.34);
|
||||
}
|
||||
|
||||
.icon-tile.is-image {
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: 0 6px 12px -7px rgba(54, 80, 99, 0.26);
|
||||
}
|
||||
|
||||
.icon-tile.is-image::before,
|
||||
.icon-tile.is-image::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.icon-tile.is-image > .tile-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.feature-label {
|
||||
margin-top: clamp(6px, 2.2vw, 10px);
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #2d3640;
|
||||
font-size: clamp(12px, 3.4vw, 13px);
|
||||
line-height: 1.3;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #98A4B3;
|
||||
cursor: pointer;
|
||||
transition: color 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.tab-button:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.tab-button.is-active {
|
||||
color: #3B71E8;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 80px;
|
||||
z-index: 60;
|
||||
max-width: calc(100% - 44px);
|
||||
width: max-content;
|
||||
padding: 9px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(29, 41, 57, 0.9);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 8px);
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
box-shadow: 0 8px 24px rgba(18, 33, 49, 0.18);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.toast.is-visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
/* 5.51 寸小屏(窄屏或矮屏)收紧纵向节奏,保证首屏完整 */
|
||||
@media (max-width: 374px), (max-height: 760px) {
|
||||
.app-frame {
|
||||
--nav-height: 46px;
|
||||
--tab-height: 58px;
|
||||
}
|
||||
|
||||
.group-bar {
|
||||
height: 35px;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.feature-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tab-bar .tab-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.group-arrow {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
row-gap: 12px;
|
||||
padding: 14px 12px 18px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
bottom: 72px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.feature-card,
|
||||
.tab-button,
|
||||
.toast {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-frame">
|
||||
<!-- 顶部系统状态栏 -->
|
||||
<header class="flex-none bg-[#3B71E8] text-white">
|
||||
<div class="status-bar flex items-center justify-between px-3">
|
||||
<div class="flex items-center gap-[7px]">
|
||||
<span class="text-[11.5px] font-semibold tracking-[0.2px]">16:48</span>
|
||||
</div>
|
||||
<div aria-label="Wi-Fi" class="flex min-w-0 items-center gap-[5px]">
|
||||
<iconify-icon class="opacity-95" height="14" icon="mdi:wifi" width="14"></iconify-icon>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- 蓝色导航栏 -->
|
||||
<nav aria-label="国内业务导航栏" class="nav-bar relative z-20 flex flex-none items-center justify-center bg-[#3B71E8] text-white">
|
||||
<h1 class="nav-title">国内业务</h1>
|
||||
<button aria-label="通知,341 条消息" class="nav-bell absolute right-[12px] top-1/2 flex -translate-y-1/2 items-center justify-center rounded-full active:bg-white/10" onclick="showNotificationToast()" type="button">
|
||||
<iconify-icon class="drop-shadow-[0_1px_1px_rgba(0,0,0,0.12)]" icon="mdi:bell-outline"></iconify-icon>
|
||||
<span class="nav-badge absolute -right-[2px] top-[0px] rounded-full bg-[#e1192d] px-[4px] text-center font-semibold text-white shadow-[0_1px_2px_rgba(145,16,31,0.35)]">341</span>
|
||||
</button>
|
||||
</nav>
|
||||
<!-- 主内容滚动区 -->
|
||||
<main class="main-scroll min-h-0 flex-1 overflow-y-auto overflow-x-hidden bg-[#F5F7FA]" id="mainContent">
|
||||
<!-- 国内出港 -->
|
||||
<section aria-labelledby="dom-outbound-title">
|
||||
<div class="group-bar">
|
||||
<img alt="" aria-hidden="true" class="group-arrow" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/f48cf8269922458abee299bf1f4bdfae.png"/>
|
||||
<h2 class="group-title" id="dom-outbound-title">国内出港</h2>
|
||||
</div>
|
||||
<div class="feature-grid">
|
||||
<button aria-label="出港收运" class="feature-card" onclick="openFeature('出港收运')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港收运图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/4dfe2842e40947c9916d27c6da2829a9.png"/>
|
||||
</span>
|
||||
<span class="feature-label">出港收运</span>
|
||||
</button>
|
||||
<button aria-label="出港复磅" class="feature-card" onclick="openFeature('出港复磅')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港复磅图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/d209b04e82874625ae8977733789486a.png"/>
|
||||
</span>
|
||||
<span class="feature-label">出港复磅</span>
|
||||
</button>
|
||||
<button aria-label="出港存放" class="feature-card" onclick="openFeature('出港存放')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港存放图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/3655ce17a68b4090870f929d7a510c64.png"/>
|
||||
</span>
|
||||
<span class="feature-label">出港存放</span>
|
||||
</button>
|
||||
<button aria-label="单证交接" class="feature-card" onclick="openFeature('单证交接')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="单证交接图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/934528d010e74d1690dce7b20cee545c.png"/>
|
||||
</span>
|
||||
<span class="feature-label">单证交接</span>
|
||||
</button>
|
||||
<button aria-label="货物转运" class="feature-card" onclick="openFeature('货物转运')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="货物转运图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/9c2d255f57d6416ab18713b02431023e.png"/>
|
||||
</span>
|
||||
<span class="feature-label">货物转运</span>
|
||||
</button>
|
||||
<button aria-label="转运记录" class="feature-card" onclick="openFeature('转运记录')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="转运记录图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/50e01da375de4c1bb4574401ba60500c.png"/>
|
||||
</span>
|
||||
<span class="feature-label">转运记录</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 国内进港 -->
|
||||
<section aria-labelledby="dom-inbound-title">
|
||||
<div class="group-bar">
|
||||
<img alt="" aria-hidden="true" class="group-arrow" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/f48cf8269922458abee299bf1f4bdfae.png"/>
|
||||
<h2 class="group-title" id="dom-inbound-title">国内进港</h2>
|
||||
</div>
|
||||
<div class="feature-grid">
|
||||
<button aria-label="进港卸机" class="feature-card" onclick="openFeature('进港卸机')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="进港卸机图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/153ca414db114dd487852b22727fb38f.png"/>
|
||||
</span>
|
||||
<span class="feature-label">进港卸机</span>
|
||||
</button>
|
||||
<button aria-label="进港移库" class="feature-card" onclick="openFeature('进港移库')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="进港移库图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/2dc4ad4cc1884f4692da574db217e91e.png"/>
|
||||
</span>
|
||||
<span class="feature-label">进港移库</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 综合管理 -->
|
||||
<section aria-labelledby="dom-manage-title" class="pb-2">
|
||||
<div class="group-bar">
|
||||
<img alt="" aria-hidden="true" class="group-arrow" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/f48cf8269922458abee299bf1f4bdfae.png"/>
|
||||
<h2 class="group-title" id="dom-manage-title">综合管理</h2>
|
||||
</div>
|
||||
<div class="feature-grid" style="padding-bottom: clamp(16px, 5.6vw, 22px);">
|
||||
<button aria-label="事故签证" class="feature-card" onclick="openFeature('事故签证')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="事故签证图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/2d2e97049ef44cfabbf1ed051702d567.png"/>
|
||||
</span>
|
||||
<span class="feature-label">事故签证</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- 底部 Tab 栏 -->
|
||||
<nav aria-label="底部导航" class="tab-bar relative z-30 flex flex-none items-stretch border-t border-[#EDF0F4] bg-white pb-[4px] shadow-[0_-1px_2px_rgba(16,24,40,0.04)]">
|
||||
<button aria-label="首页" class="tab-button" onclick="switchTab('首页')" type="button">
|
||||
<iconify-icon icon="mdi:home-outline"></iconify-icon>
|
||||
<span class="tab-label">首页</span>
|
||||
</button>
|
||||
<button aria-current="page" aria-label="国内,当前页面" class="tab-button is-active" onclick="switchTab('国内')" type="button">
|
||||
<iconify-icon icon="mdi:earth"></iconify-icon>
|
||||
<span class="tab-label is-active">国内</span>
|
||||
</button>
|
||||
<button aria-label="国际" class="tab-button" onclick="switchTab('国际')" type="button">
|
||||
<span class="tab-icon-stack relative inline-flex items-center justify-center">
|
||||
<iconify-icon class="absolute bottom-0 left-0" icon="mdi:earth"></iconify-icon>
|
||||
<iconify-icon class="absolute right-0 top-0 -rotate-[14deg]" icon="mdi:airplane"></iconify-icon>
|
||||
</span>
|
||||
<span class="tab-label">国际</span>
|
||||
</button>
|
||||
<button aria-label="我的" class="tab-button" onclick="switchTab('我的')" type="button">
|
||||
<iconify-icon icon="mdi:account-circle-outline"></iconify-icon>
|
||||
<span class="tab-label">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div aria-live="polite" class="toast" id="toast" role="status"></div>
|
||||
</div>
|
||||
<script>
|
||||
let toastTimer = null;
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.getElementById("toast");
|
||||
if (!toast) return;
|
||||
|
||||
toast.textContent = message;
|
||||
toast.classList.add("is-visible");
|
||||
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer);
|
||||
}
|
||||
|
||||
toastTimer = setTimeout(function () {
|
||||
toast.classList.remove("is-visible");
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
function openFeature(name) {
|
||||
showToast(name + " · 功能即将开放");
|
||||
}
|
||||
|
||||
function showNotificationToast() {
|
||||
showToast("您有 341 条待处理消息");
|
||||
}
|
||||
|
||||
function switchTab(name) {
|
||||
if (name === "首页") {
|
||||
window.location.href = "dashboard.html";
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "国际") {
|
||||
window.location.href = "home.html";
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "国内") {
|
||||
const main = document.getElementById("mainContent");
|
||||
if (main) {
|
||||
main.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
showToast("当前已在国内业务");
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(name + "模块暂未接入");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
694
documents/5.5寸首页新样式/5.5设计图html/home.html
Normal file
@@ -0,0 +1,694 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="w-full h-full overflow-hidden" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
|
||||
<title>国际业务</title>
|
||||
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
|
||||
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F0F3F7;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC",
|
||||
"HarmonyOS Sans SC", "MiSans", "Noto Sans SC", "Source Han Sans SC",
|
||||
"Microsoft YaHei", ui-sans-serif, system-ui, sans-serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 全页字体统一:覆盖 Tailwind 默认字体族,数字统一等宽 */
|
||||
.app-frame,
|
||||
.app-frame *,
|
||||
.app-frame *::before,
|
||||
.app-frame *::after {
|
||||
font-family: inherit;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.app-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: min(393px, 100vw);
|
||||
max-height: min(852px, 100vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #F5F7FA;
|
||||
position: relative;
|
||||
/* 以 360×640(5.51 寸 16:9)为小屏基准,宫格尺寸随视口宽度等比缩放 */
|
||||
--tile-size: clamp(48px, 15.3vw, 60px);
|
||||
--icon-size: calc(var(--tile-size) * 0.55);
|
||||
--tag-size: clamp(28px, 8.9vw, 35px);
|
||||
--tag-offset: calc(var(--tag-size) * 0.2);
|
||||
--tag-line: calc(var(--tag-size) * 0.4);
|
||||
--nav-height: 52px;
|
||||
--tab-height: 68px;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: var(--nav-height);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: clamp(16px, 4.8vw, 18px);
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
letter-spacing: clamp(0px, 0.16vw, 0.3px);
|
||||
}
|
||||
|
||||
.nav-bell {
|
||||
width: clamp(30px, 9.4vw, 34px);
|
||||
height: clamp(30px, 9.4vw, 34px);
|
||||
}
|
||||
|
||||
.nav-bell iconify-icon {
|
||||
width: clamp(22px, 6.9vw, 25px);
|
||||
height: clamp(22px, 6.9vw, 25px);
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
min-width: clamp(21px, 6.7vw, 24px);
|
||||
font-size: clamp(10px, 2.9vw, 11px);
|
||||
line-height: clamp(14px, 4.2vw, 15px);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
height: var(--tab-height);
|
||||
}
|
||||
|
||||
.tab-bar iconify-icon {
|
||||
width: clamp(22px, 6.7vw, 24px);
|
||||
height: clamp(22px, 6.7vw, 24px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack {
|
||||
width: clamp(25px, 7.5vw, 27px);
|
||||
height: clamp(22px, 6.7vw, 24px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack > iconify-icon:first-child {
|
||||
width: clamp(20px, 6.1vw, 22px);
|
||||
height: clamp(20px, 6.1vw, 22px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-icon-stack > iconify-icon:last-child {
|
||||
width: clamp(15px, 4.7vw, 17px);
|
||||
height: clamp(15px, 4.7vw, 17px);
|
||||
}
|
||||
|
||||
.tab-bar .tab-label {
|
||||
font-size: 11.5px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.tab-bar .tab-label.is-active {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: clamp(26px, 8.3vw, 30px);
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.main-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.group-bar {
|
||||
height: 41px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 24px;
|
||||
background: #F4F6F9;
|
||||
border-top: 1px solid #FFFFFF;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: clamp(13px, 3.7vw, 14px);
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.2px;
|
||||
color: #16202A;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
column-gap: 0;
|
||||
row-gap: clamp(12px, 4.6vw, 18px);
|
||||
background: #ffffff;
|
||||
padding: clamp(14px, 4.6vw, 18px) clamp(12px, 5vw, 18px) clamp(18px, 6vw, 24px);
|
||||
}
|
||||
|
||||
.group-arrow {
|
||||
display: block;
|
||||
flex: none;
|
||||
width: auto;
|
||||
height: clamp(12px, 3.6vw, 15px);
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 1px 1px rgba(37, 99, 235, 0.28));
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: transform 140ms ease;
|
||||
}
|
||||
|
||||
.feature-card:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.icon-tile {
|
||||
width: var(--tile-size);
|
||||
height: var(--tile-size);
|
||||
flex: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: calc(var(--tile-size) * 0.267);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.92),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.45),
|
||||
0 6px 12px -7px rgba(54, 80, 99, 0.34);
|
||||
}
|
||||
|
||||
.icon-tile::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: calc(var(--tile-size) * 0.1);
|
||||
left: calc(var(--tile-size) * 0.167);
|
||||
width: calc(var(--tile-size) * 0.3);
|
||||
height: calc(var(--tile-size) * 0.133);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.46);
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.icon-tile::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.corner-tag {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: var(--tag-size);
|
||||
height: var(--tag-size);
|
||||
overflow: hidden;
|
||||
border-top-left-radius: inherit;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.corner-tag span {
|
||||
position: absolute;
|
||||
left: calc(var(--tag-size) * -0.429);
|
||||
top: var(--tag-offset);
|
||||
width: calc(var(--tag-size) * 1.714);
|
||||
transform: rotate(-45deg);
|
||||
background: linear-gradient(90deg, #4e97ff 0%, #2f7df6 53%, #2a73eb 100%);
|
||||
color: #ffffff;
|
||||
font-size: clamp(7px, 2.1vw, 8px);
|
||||
line-height: var(--tag-line);
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.32);
|
||||
}
|
||||
|
||||
.icon-main {
|
||||
filter: drop-shadow(0 2px 0 rgba(255, 255, 255, 0.22))
|
||||
drop-shadow(0 3px 3px rgba(37, 56, 76, 0.18));
|
||||
}
|
||||
|
||||
.icon-stage {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
|
||||
.icon-stage > iconify-icon {
|
||||
width: calc(var(--icon-size) * 0.94);
|
||||
height: calc(var(--icon-size) * 0.94);
|
||||
}
|
||||
|
||||
.icon-stage > .icon-sub {
|
||||
width: calc(var(--icon-size) * 0.62);
|
||||
height: calc(var(--icon-size) * 0.62);
|
||||
}
|
||||
|
||||
.icon-stage > .icon-mini {
|
||||
width: calc(var(--icon-size) * 0.46);
|
||||
height: calc(var(--icon-size) * 0.46);
|
||||
}
|
||||
|
||||
.icon-tile.is-image {
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: 0 6px 12px -7px rgba(54, 80, 99, 0.26);
|
||||
}
|
||||
|
||||
.icon-tile.is-image::before,
|
||||
.icon-tile.is-image::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.icon-tile.is-image > .tile-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mini-accent {
|
||||
position: absolute;
|
||||
right: calc(var(--tile-size) * -0.13);
|
||||
bottom: calc(var(--tile-size) * -0.115);
|
||||
width: calc(var(--tile-size) * 0.283);
|
||||
height: calc(var(--tile-size) * 0.283);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 2px 5px rgba(31, 48, 67, 0.12);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.mini-accent iconify-icon {
|
||||
width: calc(var(--tile-size) * 0.19);
|
||||
height: calc(var(--tile-size) * 0.19);
|
||||
}
|
||||
|
||||
.feature-label {
|
||||
margin-top: clamp(6px, 2.2vw, 10px);
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #2d3640;
|
||||
font-size: clamp(12px, 3.4vw, 13px);
|
||||
line-height: 1.3;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tile-purple {
|
||||
color: #8b66f4;
|
||||
background:
|
||||
radial-gradient(circle at 29% 22%, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0) 42%),
|
||||
linear-gradient(145deg, #f7f2ff 0%, #ece3ff 60%, #e4d8ff 100%);
|
||||
}
|
||||
|
||||
.tile-mint {
|
||||
color: #67c8bb;
|
||||
background:
|
||||
radial-gradient(circle at 29% 22%, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0) 42%),
|
||||
linear-gradient(145deg, #effcf9 0%, #ddf8f2 58%, #d4f4ed 100%);
|
||||
}
|
||||
|
||||
.tile-blue {
|
||||
color: #4b8df5;
|
||||
background:
|
||||
radial-gradient(circle at 29% 22%, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0) 42%),
|
||||
linear-gradient(145deg, #f1f6ff 0%, #e6efff 60%, #dce8ff 100%);
|
||||
}
|
||||
|
||||
.tile-pink {
|
||||
color: #ee6f7d;
|
||||
background:
|
||||
radial-gradient(circle at 29% 22%, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0) 42%),
|
||||
linear-gradient(145deg, #fff3f4 0%, #ffe6e9 58%, #ffdde2 100%);
|
||||
}
|
||||
|
||||
.tile-peach {
|
||||
color: #f2a34b;
|
||||
background:
|
||||
radial-gradient(circle at 29% 22%, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0) 42%),
|
||||
linear-gradient(145deg, #fff8ee 0%, #ffefdd 60%, #ffe8cf 100%);
|
||||
}
|
||||
|
||||
.tile-green {
|
||||
color: #35ae8b;
|
||||
background:
|
||||
radial-gradient(circle at 29% 22%, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0) 42%),
|
||||
linear-gradient(145deg, #f0fcf8 0%, #e0f8f1 58%, #d7f5ec 100%);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #98A4B3;
|
||||
cursor: pointer;
|
||||
transition: color 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.tab-button:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.tab-button.is-active {
|
||||
color: #3B71E8;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 80px;
|
||||
z-index: 60;
|
||||
max-width: calc(100% - 44px);
|
||||
width: max-content;
|
||||
padding: 9px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(29, 41, 57, 0.9);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 8px);
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
box-shadow: 0 8px 24px rgba(18, 33, 49, 0.18);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.toast.is-visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
/* 5.51 寸小屏(窄屏或矮屏)收紧纵向节奏,保证首屏完整 */
|
||||
@media (max-width: 374px), (max-height: 760px) {
|
||||
.app-frame {
|
||||
--nav-height: 46px;
|
||||
--tab-height: 58px;
|
||||
--tag-offset: 5.4px;
|
||||
--tag-line: 13px;
|
||||
}
|
||||
|
||||
.group-bar {
|
||||
height: 35px;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.feature-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tab-bar .tab-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.group-arrow {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
row-gap: 12px;
|
||||
padding: 14px 12px 18px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
bottom: 72px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.feature-card,
|
||||
.tab-button,
|
||||
.toast {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-frame">
|
||||
<!-- 顶部系统状态栏 -->
|
||||
<header class="flex-none bg-[#3B71E8] text-white">
|
||||
<div class="status-bar flex items-center justify-between px-3">
|
||||
<div class="flex items-center gap-[7px]">
|
||||
<span class="text-[11.5px] font-semibold tracking-[0.2px]">16:48</span>
|
||||
</div>
|
||||
<div aria-label="Wi-Fi" class="flex min-w-0 items-center gap-[5px]">
|
||||
<iconify-icon class="opacity-95" height="14" icon="mdi:wifi" width="14"></iconify-icon>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- 蓝色导航栏 -->
|
||||
<nav aria-label="国际业务导航栏" class="nav-bar relative z-20 flex flex-none items-center justify-center bg-[#3B71E8] text-white">
|
||||
<h1 class="nav-title">国际业务</h1>
|
||||
<button aria-label="通知,341 条消息" class="nav-bell absolute right-[12px] top-1/2 flex -translate-y-1/2 items-center justify-center rounded-full active:bg-white/10" onclick="showNotificationToast()" type="button">
|
||||
<iconify-icon class="drop-shadow-[0_1px_1px_rgba(0,0,0,0.12)]" icon="mdi:bell-outline"></iconify-icon>
|
||||
<span class="nav-badge absolute -right-[2px] top-[0px] rounded-full bg-[#e1192d] px-[4px] text-center font-semibold text-white shadow-[0_1px_2px_rgba(145,16,31,0.35)]">341</span>
|
||||
</button>
|
||||
</nav>
|
||||
<!-- 主内容滚动区 -->
|
||||
<main class="main-scroll min-h-0 flex-1 overflow-y-auto overflow-x-hidden bg-[#F5F7FA]" id="mainContent">
|
||||
<!-- 国际出港 -->
|
||||
<section aria-labelledby="outbound-title">
|
||||
<div class="group-bar">
|
||||
<img alt="" aria-hidden="true" class="group-arrow" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/f48cf8269922458abee299bf1f4bdfae.png"/>
|
||||
<h2 class="group-title" id="outbound-title">国际出港</h2>
|
||||
</div>
|
||||
<div class="feature-grid">
|
||||
<button aria-label="出港计重" class="feature-card" onclick="openFeature('出港计重')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港计重图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/1e9507a32e7c45dea0362c3cc87b98c2.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">出港计重</span>
|
||||
</button>
|
||||
<button aria-label="出港抵运" class="feature-card" onclick="openFeature('出港抵运')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港抵运图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/1c4971c9a2d9459da4ee19b5b5431bf1.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">出港抵运</span>
|
||||
</button>
|
||||
<button aria-label="出港交接" class="feature-card" onclick="openFeature('出港交接')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港交接图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/460f6760462b4690823a98613894ce6e.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">出港交接</span>
|
||||
</button>
|
||||
<button aria-label="出港移库" class="feature-card" onclick="openFeature('出港移库')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港移库图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/0bc7fe8fff684d748b0b27f2a120c6ed.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">出港移库</span>
|
||||
</button>
|
||||
<button aria-label="出港仓库" class="feature-card" onclick="openFeature('出港仓库')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港仓库图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/18258a4775944c13b120d486ea13e925.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">出港仓库</span>
|
||||
</button>
|
||||
<button aria-label="出港查询" class="feature-card" onclick="openFeature('出港查询')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="出港查询图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/6c6fa3ed9a1449f1a704dbb6415814a1.png"/>
|
||||
</span>
|
||||
<span class="feature-label">出港查询</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 国际进港 -->
|
||||
<section aria-labelledby="inbound-title">
|
||||
<div class="group-bar">
|
||||
<img alt="" aria-hidden="true" class="group-arrow" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/f48cf8269922458abee299bf1f4bdfae.png"/>
|
||||
<h2 class="group-title" id="inbound-title">国际进港</h2>
|
||||
</div>
|
||||
<div class="feature-grid">
|
||||
<button aria-label="进港理货" class="feature-card" onclick="openFeature('进港理货')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="进港理货图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/cc9965b937344aca99bdf329fea23e91.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">进港理货</span>
|
||||
</button>
|
||||
<button aria-label="提取出库" class="feature-card" onclick="openFeature('提取出库')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="提取出库图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/7bcf76ae718e4da9a784a7003c3884a7.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">提取出库</span>
|
||||
</button>
|
||||
<button aria-label="进港仓库" class="feature-card" onclick="openFeature('进港仓库')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="进港仓库图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/2d325743d7004623a87c3982190fe1d9.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">进港仓库</span>
|
||||
</button>
|
||||
<button aria-label="进港查询" class="feature-card" onclick="openFeature('进港查询')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="进港查询图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/0fd94c77b8c644b8bb37c1b5d471fcaf.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">进港查询</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 综合管理 -->
|
||||
<section aria-labelledby="manage-title" class="pb-2">
|
||||
<div class="group-bar">
|
||||
<img alt="" aria-hidden="true" class="group-arrow" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/f48cf8269922458abee299bf1f4bdfae.png"/>
|
||||
<h2 class="group-title" id="manage-title">综合管理</h2>
|
||||
</div>
|
||||
<div class="feature-grid" style="padding-bottom: clamp(16px, 5.6vw, 22px);">
|
||||
<button aria-label="ULD管理" class="feature-card" onclick="openFeature('ULD管理')" type="button">
|
||||
<span class="icon-tile is-image">
|
||||
<img alt="ULD管理图标" class="tile-img" draggable="false" src="https://modao.cc/agent-py/media/generated_images/2026-10-20/7b2321b0f277416abad0b61252c5730a.gif"/>
|
||||
</span>
|
||||
<span class="feature-label">ULD管理</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- 底部 Tab 栏 -->
|
||||
<nav aria-label="底部导航" class="tab-bar relative z-30 flex flex-none items-stretch border-t border-[#EDF0F4] bg-white pb-[4px] shadow-[0_-1px_2px_rgba(16,24,40,0.04)]">
|
||||
<button aria-label="首页" class="tab-button" onclick="switchTab('首页')" type="button">
|
||||
<iconify-icon icon="mdi:home-outline"></iconify-icon>
|
||||
<span class="tab-label">首页</span>
|
||||
</button>
|
||||
<button aria-label="国内" class="tab-button" onclick="switchTab('国内')" type="button">
|
||||
<iconify-icon icon="mdi:earth"></iconify-icon>
|
||||
<span class="tab-label">国内</span>
|
||||
</button>
|
||||
<button aria-current="page" aria-label="国际,当前页面" class="tab-button is-active" onclick="switchTab('国际')" type="button">
|
||||
<span class="tab-icon-stack relative inline-flex items-center justify-center">
|
||||
<iconify-icon class="absolute bottom-0 left-0" icon="mdi:earth"></iconify-icon>
|
||||
<iconify-icon class="absolute right-0 top-0 -rotate-[14deg]" icon="mdi:airplane"></iconify-icon>
|
||||
</span>
|
||||
<span class="tab-label is-active">国际</span>
|
||||
</button>
|
||||
<button aria-label="我的" class="tab-button" onclick="switchTab('我的')" type="button">
|
||||
<iconify-icon icon="mdi:account-circle-outline"></iconify-icon>
|
||||
<span class="tab-label">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div aria-live="polite" class="toast" id="toast" role="status"></div>
|
||||
</div>
|
||||
<script>
|
||||
let toastTimer = null;
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.getElementById("toast");
|
||||
if (!toast) return;
|
||||
|
||||
toast.textContent = message;
|
||||
toast.classList.add("is-visible");
|
||||
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer);
|
||||
}
|
||||
|
||||
toastTimer = setTimeout(function () {
|
||||
toast.classList.remove("is-visible");
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
function openFeature(name) {
|
||||
showToast(name + " · 功能即将开放");
|
||||
}
|
||||
|
||||
function showNotificationToast() {
|
||||
showToast("您有 341 条待处理消息");
|
||||
}
|
||||
|
||||
function switchTab(name) {
|
||||
if (name === "首页") {
|
||||
window.location.href = "dashboard.html";
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "国内") {
|
||||
window.location.href = "domestic.html";
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "国际") {
|
||||
const main = document.getElementById("mainContent");
|
||||
if (main) {
|
||||
main.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
showToast("当前已在国际业务");
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(name + "模块暂未接入");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
documents/5.5寸首页新样式/icons v2/国内业务/事故签证.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/出港复磅.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/出港存放.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/出港收运.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/单证交接.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/货物转运.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/转运记录.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/进港卸机.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国内业务/进港移库.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国际业务/ULD管理.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国际业务/出港移库.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国际业务/进港仓库.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
BIN
documents/5.5寸首页新样式/icons v2/国际业务/进港理货.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_03.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_05.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_07.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_13.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_15.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_17.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_22.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_23.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
documents/5.5寸首页新样式/icons/国内/guonei_26.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_03.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_07.gif
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_09.gif
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_11.gif
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_16.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_17.gif
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_19.gif
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_24.gif
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_26.gif
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_28.gif
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_33.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
documents/5.5寸首页新样式/icons/国际/guoji_35.gif
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
219
documents/双端合入主干交接文档.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 平板 + 5.5 寸手机合入主干后的交接
|
||||
|
||||
> 日期:2026-10-11 | 基线提交:`origin/main @ 131eb03` | 在线版:https://claude.ai/artifact/N8cWdTcMAzk9N86NSPevTC
|
||||
>
|
||||
> 本文描述合并当天的状态。分支、版本号等信息如有变化,以仓库为准。
|
||||
|
||||
## 接手前先知道这几件事
|
||||
|
||||
1. **只有一条开发分支:`main`。** 手机适配分支 `feature/phone-ui-adaptation` 已于 2026-10-11 合入,平板和 5.5 寸手机以后都在 `main` 上一起开发。
|
||||
2. **业务以平板为准,手机只做界面。** 两端共用 ViewModel、接口、表单项、筛选入参和默认值;差异只能出现在布局(View)层。
|
||||
3. **同一个页面有两份同名布局**:`res/layout-sw600dp/` 是平板,`res/layout/` 是手机。改平板页面时要同时检查手机布局和这个页面的所有弹框。
|
||||
4. **有两件事等人拍板**:ULD 保存可能报 500(问题清单 #36),以及手机独有的几个界面功能留不留(见第 6 节)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 仓库与分支
|
||||
|
||||
| 分支 / 位置 | 提交 | 状态 | 建议 |
|
||||
|---|---|---|---|
|
||||
| `origin/main` | 131eb03 | 唯一开发分支 | 以后所有开发从这里拉分支、合回这里。 |
|
||||
| 本地 `main`(主 checkout `~/Development/Projects/aerologic-app`) | 871fa4c | 落后 30 个提交 | 合并是在另一个 worktree 里推送的,主 checkout 还没同步。到那个目录执行 `git pull --ff-only`。 |
|
||||
| `feature/phone-ui-adaptation`(本地 + 远端) | 42647c2 | 已完整合入 | 不要再往上提交。确认无人使用后可以删除(删除远端分支前先跟团队确认)。 |
|
||||
| `worktree/phone-adaptation`(仅本地) | 42647c2 | 已完整合入 | 可删除。 |
|
||||
| `merge/phone-into-main`(仅本地,worktree `~/.herdr/worktrees/aerologic-app/worktree-phone-adaptation` 当前检出) | 131eb03 | 与 main 相同 | 合并时用的临时分支。可以 `git worktree remove` 这个 worktree 后删除分支。 |
|
||||
|
||||
合并提交 `131eb03` 的第一父提交是原 main(`871fa4c`),第二父提交是手机分支(`42647c2`),所以 `git log --first-parent main` 仍然是平板的历史。两条线的分叉点是 `9c4588e`:分叉后 main 有 58 个平板提交,手机分支有 29 个提交,合并时共 45 个文件冲突。
|
||||
|
||||
## 2. 平板与手机的开发状态
|
||||
|
||||
**平板(10 寸横屏)**
|
||||
|
||||
- 功能完整,即合并前 main 上的全部功能。
|
||||
- 合并没有改平板的业务逻辑。冲突的平板布局用三方合并放进了 `layout-sw600dp/`,内容就是 main 最新版。
|
||||
- 已有手机变体的页面:20 个 Activity 布局 + 11 个弹框布局在 `layout-sw600dp/`。
|
||||
- 其余平板页面(舱单、理货、组装、航班管理等)只有一份布局,放在 `res/layout/`,没有手机版。
|
||||
|
||||
**手机(5.5 寸竖屏)**
|
||||
|
||||
- 设计稿 01~11 的 11 个入口全部完成,并已按 main 的新平板功能补齐(见第 3 节)。
|
||||
- 手机首页是 `module_p` 的 `PDAEnterActivity`,菜单是手写清单(`GjViewModel` / `GnViewModel`),按登录权限串过滤。
|
||||
- 设计参考:`documents/5.5寸手持端设计文件/`,技术方案 `documents/5.5寸手持端适配技术方案.md`。
|
||||
|
||||
### 手机已适配页面
|
||||
|
||||
| 首页入口 | 页面(Activity) | 模块 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 国际 · 出港移库 | `IntExpMoveActivity`、`IntExpMoveEditActivity` | gjc | 详情 / 编辑与平板共用同一个 Activity,手机布局为「运单详情」样式。 |
|
||||
| 国际 · 出库交接 | `IntExpOutHandoverActivity`、`IntExpOutHandoverDetailActivity` | gjc | 底部栏带「允许装机」「配运」次按钮。 |
|
||||
| 国际 · 出港计重 | `GjcWeighingListActivity`、`GjcWeighingStartActivity`、`GjcWeighingRecordListActivity`、`GjcWeighingRecordDetailsActivity` | gjc | 另有手机专属 `GjcWeighingWbDetailActivity`(运单详情)。 |
|
||||
| 国际 · 出港运抵 | `IntExpArriveActivity` | gjc | 手机专属 `IntExpArriveSubOrderActivity`(分单明细)、`IntExpArriveReceiptActivity`(分单回执);主单回执用两端共用的 `CustomsResponseActivity`。 |
|
||||
| 国际 · 出港查询 | `GjcQueryActivity`、`GjcQueryDetailsActivity`、`LogDetailActivity`(运单追踪) | gjc / app | |
|
||||
| 国际 · 出港仓库 | `IntExpStorageUseActivity` | gjc | 平板的「勾选 + 底部批量」在手机上改为卡片四按钮:清仓 / 修改库位 / 出库 / 入库。 |
|
||||
| 国际 · 进港仓库 | `IntImpStorageUseActivity`、`IntImpQueryDetailsActivity` | gjj | 接口前缀是 `IntImpStorage`,不是 `IntImpStorageUse`。 |
|
||||
| 国际 · 进港查询 | `IntImpQueryActivity` | gjj | |
|
||||
| 国际 · 提取出库 | `IntImpPickUpDLVActivity` | gjj | 手机专属 `IntImpPickUpDetailActivity`。 |
|
||||
| 国际 · ULD 管理 | `UldListActivity`、`UldEditActivity` | app | 一套编辑布局覆盖新增 / 修改 / 详情。 |
|
||||
| 国内 · 进港移库 | `GnjMoveStashListActivity` | app | 业务归属国内进港移库(DomImpMove)。手机专属 `GnjMoveStashDetailActivity`、`GnjMoveStashPhotoActivity`。 |
|
||||
|
||||
手机首页还有一批旧的 PDA 竖屏入口(国际:普货收运、板箱组装、板箱过磅、进港理货;国内:出港收运、出港复磅、出港存放、进港卸机、货物转运、转运记录、事故签证、单证交接)。它们是 `module_p` 原有页面,不在这次设计稿范围内,合并没有改动,也没有验证。
|
||||
|
||||
## 3. 这次合并做了什么
|
||||
|
||||
### 冲突处理原则
|
||||
|
||||
- **布局**:平板布局取 main 最新版,三方合并进 `layout-sw600dp/`,没有冲突残留。手机布局保留原样,再逐页补上 main 新增的业务字段和操作。
|
||||
- **Bean / ViewModel**:一律取平板逻辑。手机只保留它额外需要的展示字段(如 `opUserDisplay`、统计字段)。
|
||||
- **Manifest**:取手机分支版本(屏幕方向统一为 `unspecified`),再补上 main 新增的 Activity。
|
||||
|
||||
### main 的平板功能同步到手机
|
||||
|
||||
| 页面 | 同步到手机的内容 |
|
||||
|---|---|
|
||||
| 出港运抵 | 卡片显示海关放行、运单类型;筛选加分单号;回执改走平板同一接口和回执页;长按菜单加「删除申报」(手机底部弹框)。 |
|
||||
| 出库交接 | 卡片显示装机状态;底部栏加「允许装机」「配运」;配运弹框做了手机变体。 |
|
||||
| 出港移库 | 详情和编辑改用平板的 `IntExpMoveEditActivity`,删除手机专属 `IntExpMoveDetailActivity`;卡片加「编辑」;筛选加卸货站。 |
|
||||
| 出港 / 进港仓库 | 清仓弹框加备注,卡片显示清仓备注;入库、出库弹框可填件数和重量(出库弹框新做了手机变体);进港卡片件数取 `pcLabel`。 |
|
||||
| 开始计重 | ViewModel 整体取平板版本:加运单号、地磅重量、平板车、托盘信息,运抵重量按平板公式自动计算;航班日期 / 航班号 / 航程可编辑。 |
|
||||
| 计重 / 计重记录 | 计重列表加「新增」按钮;计重记录加航班号筛选,卡片加「运抵申报」入口。 |
|
||||
| 计重明细 | 重量与托盘自重按平板逻辑联动(和恒定),同时保留手机的输入原文处理(避免光标跳动)。 |
|
||||
| 进港查询 | 筛选加出库状态。 |
|
||||
| ULD | 采用平板的来源列表与 HFE 默认值;卡片显示所在港 / 位置。 |
|
||||
|
||||
### 手机按平板收敛的地方(使用上能感觉到)
|
||||
|
||||
- 删除了平板不传的查询入参:`likeNo`(8 位运单号模糊查询)、进港移库的航班日期 / 航班号 / 特码、出港移库的特码。
|
||||
- 恢复平板默认查询值:计重、计重记录、提取出库等页面默认查当天,首次打开常常是空列表,属于预期行为。
|
||||
- 补齐平板有而手机缺的筛选项:运抵分单号、进港查询出库状态、计重记录航班号、进港移库承运人、出港移库卸货站。
|
||||
- ULD 编辑去掉平板没有的「进港航班号」和手机额外加的必填校验(影响见 #36)。
|
||||
|
||||
## 4. 以后怎么开发
|
||||
|
||||
项目根目录的 `CLAUDE.md`「5.5 寸手持端双形态适配规范」一节是完整规范,`.claude/skills/phone-adapt/` 是手机适配流程。下面是最常用的几条。
|
||||
|
||||
### 布局双变体的硬性约束
|
||||
|
||||
- 两个变体**文件同名**:平板 `layout-sw600dp/xxx.xml`,手机 `layout/xxx.xml`。禁止用 `xxx_phone.xml` 加代码判断,那样会生成另一个 Binding 类,进页面就 `ClassCastException`。
|
||||
- 两个变体的 `<variable>` 名称和类型必须一致。
|
||||
- Kotlin 里引用的控件 id 两边都要有;只有一边有的 id 在 Binding 里是可空的,代码里要写 `binding.xxx?.`。
|
||||
- 弹框同样做双变体,并在构造参数里切换位置:`BaseDialogModel(if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER)`。
|
||||
- Manifest 新 Activity 的 `screenOrientation` 写 `unspecified`,由 `BaseActivity` 按设备锁方向。
|
||||
|
||||
### 平板改了一个已适配页面,标准步骤
|
||||
|
||||
1. 改平板布局(`layout-sw600dp/`)和 ViewModel。
|
||||
2. 打开同名手机布局(`layout/`),补上同样的字段、筛选项和操作入口。平板的批量按钮在手机上可以改成卡片按钮或 `PhoneBottomBar` 的次按钮(`subAction1Text` / `subAction2Text`)。
|
||||
3. 检查这个页面用到的每一个 DialogModel,新弹框要做手机变体。
|
||||
4. 手机 Tab 角标、计数这类只有手机用的额外请求,放在 `phoneExtrasEnabled` 一类的开关后面,不能改变平板的请求。
|
||||
5. 两台模拟器各走一遍,再编译 `./gradlew assembleDebug`。
|
||||
|
||||
### 手机端不能做的事
|
||||
|
||||
- 不能新增平板没有的查询入参(后端口径:手机筛选一律与平板一致,后端不认识的参数会被静默忽略)。
|
||||
- 不能改写平板的默认查询值(如默认当天)。
|
||||
- 不能少平板的表单项或校验,也不能多加平板没有的校验。
|
||||
|
||||
### 新做一个手机页面
|
||||
|
||||
按 `CLAUDE.md`「手机版页面改造步骤」:`git mv` 平板布局到 `layout-sw600dp/` → 写同名手机布局 → 优先复用 `module_base/.../ui/weight/phone/` 下的组件(`PhoneSearchBar`、`PhoneFormRow`、`PhoneBottomBar` 等)→ 到 `GjViewModel` / `GnViewModel` 加首页菜单入口(最容易忘,忘了在手机上就进不去)。
|
||||
|
||||
## 5. 已验证与未验证
|
||||
|
||||
验证环境:手机模拟器 `AeroLogic_Phone`(720×1440,API 31,sw=394dp),平板模拟器 `AeroLogic_Tablet`(1280×800,sw=800dp),连内网测试服务。
|
||||
|
||||
| 项目 | 结果 | 说明 |
|
||||
|---|---|---|
|
||||
| Debug 编译 | ✅ 通过 | 合并后 `assembleDebug` 成功。 |
|
||||
| 手机 11 个入口主流程 | ✅ 已验证 | 列表、Tab、筛选、详情、弹框打开,无崩溃。 |
|
||||
| 平板回归(已适配的页面) | ✅ 已验证 | 主要页面打开、筛选、弹框,无崩溃,布局为 main 最新版。 |
|
||||
| 开始计重自动计算 | ✅ 已验证 | 地磅 500、托盘 2 个 × 18 → 运抵重量 464,与平板公式一致。新增模式头部不再显示 "null kg"。 |
|
||||
| 手机新弹框(出库 / 入库 / 清仓 / 配运 / 删除申报) | ⚠️ 只打开过 | 样式和字段都确认了,没有点提交,以免改动测试数据。 |
|
||||
| 所有写操作(申报、出入库、清仓、保存、移库、拍照上传) | ⚠️ 未提交验证 | 合并后没有真实提交过。建议用测试数据逐个点一次提交,平板和手机都要。 |
|
||||
| 海关回执页有内容时的显示 | ❌ 未验证 | 测试数据里没有带回执的运单。 |
|
||||
| ULD 管理、国内进港移库 | ❌ 只编译过 | 合并后的改动没在模拟器上点过。 |
|
||||
| 手机首页旧 PDA 入口 | ❌ 未验证 | 不在本次范围。 |
|
||||
| Release 包、真机(交付手持机 / 平板) | ❌ 未验证 | 只在模拟器上跑过 Debug 包。 |
|
||||
|
||||
## 6. 已知问题与待决事项
|
||||
|
||||
### 需要决定
|
||||
|
||||
- **ULD 保存可能报 500(问题清单 #36)。** 后端 `saveUld` 在缺少最大装载容积 / 最大载重时返回 500。之前只有手机做了必填拦截;按「以平板为准」去掉之后,两端都可能报错。可选做法:让后端修,或者在平板加同样的必填校验(再同步到手机)。
|
||||
- **手机独有的界面功能目前保留。** 它们只在手机上发请求,不影响平板,所以合并时留下了:
|
||||
- 各列表的「待 / 已」Tab,以及 Tab 角标额外发的统计请求;
|
||||
- 进港移库「已移库」Tab(调用平板不用的 `searchMoved` 接口);
|
||||
- 出港运抵分单明细页、分单回执页;ULD 状态显示为「可用 / 维修」;ULD 入库时间保存时补 `00:00:00`。
|
||||
|
||||
如果要求两端严格一致,需要逐项删除。
|
||||
|
||||
### 对用户的行为变化(属于预期)
|
||||
|
||||
- 手机列表默认查当天,首次打开常常为空,要在筛选里清掉日期。
|
||||
- 手机输入 8 位运单号不再能模糊查到(去掉了 `likeNo`),和平板一致。
|
||||
|
||||
### 后端缺口(问题清单 #35~#41,待后端处理)
|
||||
|
||||
- ULD 列表按 uld 搜索返回 500;`saveUld` 缺 maxVolume / maxWeight 返回 500(#36)。
|
||||
- `IntImpStorage/pageQuery` 列表过滤全部不生效。
|
||||
- 出港仓库列表与统计的口径不一致。
|
||||
- 已移库数据没有 `mawbId`,无法补传照片。
|
||||
|
||||
完整内容和每一项的后端答复在 `documents/后端接口对接问题清单.xlsx`。这份表是和后端对接的正式文档,新发现的缺口要继续记在里面。
|
||||
|
||||
### 其他待办
|
||||
|
||||
- `PhoneSearchBar` 还没接 AutoQuery 联想(`AutoQueryManager` 只接了平板组件)。
|
||||
|
||||
## 7. 容易踩的坑
|
||||
|
||||
1. **API 文档的 token 提交进了仓库。** `.mcp.json` 里写着 api-doc MCP 的 `Authorization: Bearer …`,并且已被 git 跟踪。建议轮换这个 token,改为从环境变量或本地未跟踪的配置读取。
|
||||
2. **换行符混用,没有 `.gitattributes`。** main 原有文件多为 CRLF,手机分支新建的文件是 LF。编辑时保持文件原有换行符,否则会出现整文件 diff,合并和 review 都会很难看。
|
||||
3. **没有手机变体的平板页面,在手机上会直接显示平板布局。** 给手机首页加入口前,必须先做手机布局。
|
||||
4. **后端会静默忽略不认识的参数。** 猜错参数名会得出「后端不支持」的误判。入参出参一律用 api-doc MCP 核对,流程见 `.claude/skills/phone-adapt/references/api-doc.md`。
|
||||
5. **XML 里 `??` 兜底不生效。** 网络层会把 JSON null 转成空串,`??` 只判断 null。要在 Bean 上写计算属性(如 `agentDisplay`、`opUserDisplay`)。
|
||||
6. **RecyclerView 里给 item 设 `GONE` 不会收起高度。** Tab 过滤要在数据层过滤后整体 `refresh`。
|
||||
7. **Intent 传递的 Bean 里 `@Transient` 字段会变成 null**(`checked`、`showMore` 等)。接收方要 `.map { it.copy() }`。
|
||||
8. **同页多个手机复合组件会串状态。** 新写同类组件要覆写 `dispatchSaveInstanceState` / `dispatchRestoreInstanceState`(见 CLAUDE.md)。
|
||||
|
||||
第 5~8 条在 CLAUDE.md「常见编译错误速查」里都有详细说明和修复方式。
|
||||
|
||||
## 8. 环境与工具
|
||||
|
||||
- **工具链**:Gradle 用 Java 17 运行(`mise.toml`),编译目标是 Java 1.8;当前版本 1.8.7(versionCode 87)。
|
||||
- **内网**:测试服务器在内网,模拟器要在内网环境下才能拉到数据,否则提示「接口访问失败」。测试账号向项目负责人索取(不写在文档里)。
|
||||
- **登录不保留**:冷启动后要重新登录。
|
||||
- **Debug 直达页面**:`app/src/debug/AndroidManifest.xml` 把适配页设为 exported,可以直接拉起(只在 Debug 包生效)。
|
||||
- **平板模拟器没有物理按键**,`adb shell monkey` 拉起会失败,改用 `am start`。
|
||||
- **手机模拟器**上软键盘弹出时按返回键,有时会被吞掉并退出到桌面,不是崩溃。
|
||||
- **mobile-mcp** 偶尔识别不到 `emulator-5554`,可以直接用 adb。
|
||||
|
||||
```bash
|
||||
# 启动模拟器(模拟器可执行文件在 Homebrew 的 android-commandlinetools 下)
|
||||
/opt/homebrew/share/android-commandlinetools/emulator/emulator -avd AeroLogic_Phone &
|
||||
/opt/homebrew/share/android-commandlinetools/emulator/emulator -avd AeroLogic_Tablet &
|
||||
|
||||
# 编译安装
|
||||
./gradlew assembleDebug
|
||||
adb -s emulator-5554 install -r app/build/outputs/apk/debug/*.apk
|
||||
|
||||
# 拉起登录页(平板没有物理键时用这个)
|
||||
adb -s emulator-5556 shell am start -n com.lukouguoji.aerologic/com.lukouguoji.module_base.LoginActivity
|
||||
|
||||
# 确认设备形态识别
|
||||
adb -s emulator-5554 logcat -d -s BaseActivity:D # PHONE(sw=394dp) / TABLET(sw=800dp)
|
||||
```
|
||||
|
||||
## 9. 关键文件索引
|
||||
|
||||
| 文件 | 用途 |
|
||||
|---|---|
|
||||
| `CLAUDE.md` | 项目规范总入口,包括双形态适配规范、分支约定、常见错误速查。 |
|
||||
| `CHANGELOG.md` | [Unreleased] 下「手机适配分支合入主干 · 2026-10-11」是本次合并的完整记录。 |
|
||||
| `documents/后端接口对接问题清单.xlsx` | 与后端对接的正式问题清单(#1~#41)。 |
|
||||
| `documents/5.5寸手持端设计文件/` | 手机设计稿(HTML)。 |
|
||||
| `.claude/skills/phone-adapt/` | 手机适配流程,含 api-doc、组件、平板安全、验证等参考。 |
|
||||
| `module_base/.../util/DeviceUtil.kt` | 手机 / 平板判定(sw ≥ 600dp 为平板)。 |
|
||||
| `module_base/.../ui/weight/phone/` | 手机公共组件,适配器在 `PhoneWidgetKtx.kt`。 |
|
||||
| `module_base/.../MyApplication.kt` | AutoSize 设计基准(手机竖屏 390×844,平板横屏 1152×720)。 |
|
||||
| `module_p/.../ui/enter/gj/GjViewModel.kt`、`gn/GnViewModel.kt` | 手机首页国际 / 国内菜单清单。 |
|
||||
| `app/.../HomeFragment.kt` | 平板首页菜单,手机入口的权限串和路由要以它为准。 |
|
||||
@@ -149,7 +149,7 @@ dependencies {
|
||||
api 'com.github.li-xiaojun:XPopup:2.9.19'
|
||||
|
||||
// 屏幕适配
|
||||
api 'me.jessyan:autosize:1.2.+'
|
||||
api 'me.jessyan:autosize:1.2.1'
|
||||
|
||||
// 佳博打印SDK
|
||||
api(name: 'sdk2-2.0.4', ext: 'aar') {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.lukouguoji.module_base.adapter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import androidx.databinding.BindingAdapter
|
||||
import com.lukouguoji.module_base.interfaces.IOnFocusChangeListener
|
||||
|
||||
@@ -27,6 +28,18 @@ fun visible(view: View, visible: Any?) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态设置 LinearLayout 子 View 的 layout_weight(如按状态切换列宽)
|
||||
*/
|
||||
@BindingAdapter("layoutWeight")
|
||||
fun setLayoutWeight(view: View, weight: Float) {
|
||||
val lp = view.layoutParams as? LinearLayout.LayoutParams ?: return
|
||||
if (lp.weight != weight) {
|
||||
lp.weight = weight
|
||||
view.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("setFocusChangedListener")
|
||||
fun setOnFocusChangeListener(et: EditText, listener: IOnFocusChangeListener) {
|
||||
et.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
|
||||
|
||||
@@ -19,6 +19,27 @@ class AssembleWaybillBean {
|
||||
var originalPieces: String = "" // 原始运单件数
|
||||
var originalWeight: String = "" // 原始运单重量
|
||||
|
||||
// ========== 剩余/总数显示(开始组装页首次装货成功后切换) ==========
|
||||
var checkInPieces: Long = 0 // 已组装件数(接口 checkInPc)
|
||||
var checkInWeight: Double = 0.0 // 已组装重量(接口 checkInWeight)
|
||||
var showRemaining: Boolean = false // 是否以“剩余/总数”形式显示
|
||||
|
||||
/** 件数列显示:首次装货成功前为总件数,之后为“剩余件数/总件数”(剩余 = pc − checkInPc) */
|
||||
val piecesDisplay: String
|
||||
get() {
|
||||
if (!showRemaining) return pieces
|
||||
val total = pieces.toLongOrNull() ?: return pieces
|
||||
return "${total - checkInPieces}/$pieces"
|
||||
}
|
||||
|
||||
/** 重量列显示:首次装货成功前为总重量,之后为“剩余重量/总重量”(剩余 = weight − checkInWeight) */
|
||||
val weightDisplay: String
|
||||
get() {
|
||||
if (!showRemaining) return weight
|
||||
val total = weight.toDoubleOrNull() ?: return weight
|
||||
return String.format("%.1f", total - checkInWeight) + "/" + weight
|
||||
}
|
||||
|
||||
val fLightInfo: String
|
||||
get() = "$fno/${fdate.replace("-", "")}"
|
||||
|
||||
|
||||
@@ -234,7 +234,10 @@ class FlightBean : ICheck {
|
||||
return when (countryType) {
|
||||
"0" -> "国内"
|
||||
"1" -> "国际"
|
||||
else -> ""
|
||||
"2" -> "地区"
|
||||
"3" -> "混合"
|
||||
// 部分接口直接返回中文名,原样显示
|
||||
else -> countryType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.lukouguoji.module_base.bean
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.library.baseAdapters.BR
|
||||
import com.lukouguoji.module_base.ktx.showToast
|
||||
|
||||
/**
|
||||
* 国际出港计重记录明细Bean
|
||||
@@ -22,9 +23,16 @@ data class GjcCheckInRecord(
|
||||
var weight: Double = 0.0, // 运抵重量
|
||||
var volume: Double = 0.0, // 运抵体积
|
||||
var whId: Long = 0, // GJC_WAREHOUSE.ID
|
||||
var carWeight: String = "" // 托盘自重
|
||||
var carWeight: String = "", // 托盘车重
|
||||
var trayNumber: Int = 0, // 托盘数量
|
||||
var trayTotalWeight: Double = 0.0, // 托盘重量(托盘自重)
|
||||
var opUserName: String = "" // 计重人姓名(opId 关联查询)
|
||||
) : BaseObservable() {
|
||||
|
||||
/** 计重人:优先姓名,为空回退操作员编码 */
|
||||
val opUserDisplay: String
|
||||
get() = opUserName.ifEmpty { opId }
|
||||
|
||||
// 数据变化回调
|
||||
var onDataChanged: (() -> Unit)? = null
|
||||
|
||||
@@ -41,28 +49,76 @@ data class GjcCheckInRecord(
|
||||
}
|
||||
}
|
||||
|
||||
// 重量 / 托盘自重编辑过程中的原始输入串(保持回显与输入一致,避免光标跳位)
|
||||
// 不作为构造参数,data class copy 备份/恢复时自动丢弃,回退到数值显示
|
||||
// @Transient:仅界面内部状态,不参与 Gson 序列化提交
|
||||
@Transient
|
||||
private var weightInput: String? = null
|
||||
|
||||
@Transient
|
||||
private var trayTotalWeightInput: String? = null
|
||||
|
||||
// 重量的字符串表示(用于双向绑定)
|
||||
// 业务规则:重量 + 托盘自重 恒定(等于接口返回初始值之和),修改一方另一方反向同步变动
|
||||
// 校验:任一方不能小于 0,非法输入直接拒绝并回退显示
|
||||
@get:Bindable
|
||||
var weightStr: String
|
||||
get() = if (weight == 0.0) "" else weight.toString()
|
||||
get() = weightInput ?: if (weight == 0.0) "" else weight.toString()
|
||||
set(value) {
|
||||
val newWeight = value.toDoubleOrNull() ?: 0.0
|
||||
if (weight != newWeight) {
|
||||
weight = newWeight
|
||||
notifyPropertyChanged(BR.weightStr)
|
||||
onDataChanged?.invoke()
|
||||
val newTrayWeight = round2(weight + trayTotalWeight - newWeight)
|
||||
when {
|
||||
newWeight < 0 -> {
|
||||
showToast("重量不能小于 0")
|
||||
notifyPropertyChanged(BR.weightStr)
|
||||
}
|
||||
newTrayWeight < 0 -> {
|
||||
showToast("重量不能超过 ${round2(weight + trayTotalWeight)},否则托盘自重将小于 0")
|
||||
notifyPropertyChanged(BR.weightStr)
|
||||
}
|
||||
else -> {
|
||||
weightInput = value
|
||||
if (weight != newWeight) {
|
||||
weight = newWeight
|
||||
trayTotalWeight = newTrayWeight
|
||||
trayTotalWeightInput = null
|
||||
notifyPropertyChanged(BR.weightStr)
|
||||
notifyPropertyChanged(BR.trayTotalWeightStr)
|
||||
onDataChanged?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 托盘自重的字符串表示(用于双向绑定,5.5寸手机版「修改记录」覆层)
|
||||
// 托盘自重的字符串表示(用于双向绑定),联动规则同上
|
||||
@get:Bindable
|
||||
var carWeightStr: String
|
||||
get() = carWeight
|
||||
var trayTotalWeightStr: String
|
||||
get() = trayTotalWeightInput ?: if (trayTotalWeight == 0.0) "" else trayTotalWeight.toString()
|
||||
set(value) {
|
||||
if (carWeight != value) {
|
||||
carWeight = value
|
||||
notifyPropertyChanged(BR.carWeightStr)
|
||||
onDataChanged?.invoke()
|
||||
val newTrayWeight = value.toDoubleOrNull() ?: 0.0
|
||||
val newWeight = round2(weight + trayTotalWeight - newTrayWeight)
|
||||
when {
|
||||
newTrayWeight < 0 -> {
|
||||
showToast("托盘自重不能小于 0")
|
||||
notifyPropertyChanged(BR.trayTotalWeightStr)
|
||||
}
|
||||
newWeight < 0 -> {
|
||||
showToast("托盘自重不能超过 ${round2(weight + trayTotalWeight)},否则重量将小于 0")
|
||||
notifyPropertyChanged(BR.trayTotalWeightStr)
|
||||
}
|
||||
else -> {
|
||||
trayTotalWeightInput = value
|
||||
if (trayTotalWeight != newTrayWeight) {
|
||||
trayTotalWeight = newTrayWeight
|
||||
weight = newWeight
|
||||
weightInput = null
|
||||
notifyPropertyChanged(BR.trayTotalWeightStr)
|
||||
notifyPropertyChanged(BR.weightStr)
|
||||
onDataChanged?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun round2(v: Double): Double = Math.round(v * 100) / 100.0
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.lukouguoji.module_base.bean
|
||||
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import com.lukouguoji.module_base.ktx.toPlainText
|
||||
import java.io.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@@ -45,6 +46,7 @@ data class GjcMaWb(
|
||||
|
||||
// ==================== 货物信息 ====================
|
||||
var pc: Long? = null, // 预配件数
|
||||
var pcLabel: String? = null, // 件数展示文本(国际进港仓库分页返回,如 0/8、44)
|
||||
var weight: Double? = null, // 预配重量(KG)
|
||||
var volume: Double? = null, // 预配体积(CBM)
|
||||
var goods: String? = null, // 品名(英)
|
||||
@@ -58,6 +60,8 @@ data class GjcMaWb(
|
||||
var arriveWeight: Double? = null, // 运抵重量
|
||||
var arriveVolume: Double? = null, // 运抵体积
|
||||
var arriveFlag: String? = null, // 运抵状态(0:正常运抵,1:提前运抵)
|
||||
var trayNumber: Int? = null, // 托盘数量
|
||||
var trayTotalWeight: Double? = null, // 托盘重量(托盘数量 × 托盘自重)
|
||||
|
||||
// ==================== 特码与收货人 ====================
|
||||
var spCode: String? = null, // 特码
|
||||
@@ -76,15 +80,16 @@ data class GjcMaWb(
|
||||
var carNumber: String? = null, // 车牌号
|
||||
var passageWay: String? = null, // 通道号
|
||||
var passageWayId: String? = null,
|
||||
var palletNumber: String? = null, // 托盘数量(5.5寸手机版开始计重页,后端暂无该入参,见接口问题清单)
|
||||
var carWeight: String? = null, // 托盘自重(5.5寸手机版手输,后端暂无该入参,见接口问题清单)
|
||||
|
||||
// ==================== 状态信息 ====================
|
||||
var checkIn: String? = null, // 收运状态。0:待收运,1:已收运,2:收运中
|
||||
var declareStatus: String? = null, // 申报状态
|
||||
var cmdStatus: String? = null, // 海关放行(命令状态)
|
||||
var reviewStatus: String? = null, // 审核状态(0:未审核;1:通过;2:退回)
|
||||
var tranFlag: String? = null, // 转运标志
|
||||
var clearNormal: String? = null, // 清仓正常(0:否,1:是)
|
||||
var clearRemark: String? = null, // 清仓备注
|
||||
var allTally: String? = null, // 运单理货申报全部通过(0:否;1:是),为 1 时才允许全选该主单下的分单
|
||||
|
||||
// ==================== 操作信息 ====================
|
||||
var opDate: String? = null, // 操作时间(入库时间)
|
||||
@@ -94,6 +99,9 @@ data class GjcMaWb(
|
||||
|
||||
// ==================== 备注与其他 ====================
|
||||
var remark: String? = null, // 备注
|
||||
var pic: String? = null, // 交接图片缩略图路径(逗号分隔)
|
||||
var originalPic: String? = null, // 交接图片原图路径(逗号分隔)
|
||||
var picNumber: String? = null, // 交接图片数量
|
||||
var ffmMemo: String? = null, // ffm备注
|
||||
var mftMemo: String? = null, // mft备注
|
||||
var unNumber: String? = null, // UN编号
|
||||
@@ -132,11 +140,11 @@ data class GjcMaWb(
|
||||
set(value) = checked.set(value)
|
||||
|
||||
/**
|
||||
* 5.5寸手机版:是否处于「待运抵」Tab(申报状态为空)。
|
||||
* 接口无按申报状态区分的查询参数,Tab 由客户端对已加载数据拆分。
|
||||
* 是否允许全选该主单下的分单
|
||||
* 主单存在分批申报(未全部申报完毕)时接口返回 allTally=0,此时不允许全选其分单
|
||||
*/
|
||||
val isPending: Boolean
|
||||
get() = declareStatus.isNullOrEmpty()
|
||||
val canCheckAllHaWb: Boolean
|
||||
get() = allTally == "1"
|
||||
|
||||
/**
|
||||
* 5.5寸手机版:申报状态三色标签的着色分类(01=绿/W=靛蓝/其余非空=琥珀)。
|
||||
@@ -169,6 +177,13 @@ data class GjcMaWb(
|
||||
val agentDisplay: String?
|
||||
get() = if (agentName.isNullOrEmpty()) agentCode else agentName
|
||||
|
||||
/**
|
||||
* 5.5寸手机版-进港仓库:始发站展示值。IntImpStorage/pageQuery 不返回 dep/fdep,
|
||||
* 始发站在 origin(货源地)中(Bug #635 实测)。
|
||||
*/
|
||||
val depDisplay: String?
|
||||
get() = if (dep.isNullOrEmpty()) origin else dep
|
||||
|
||||
/**
|
||||
* 5.5寸手机版-出港仓库:卡片状态标签(已清仓=绿 / 未清仓=橙,clearNormal 为空视作未清仓)。
|
||||
* 已清仓时卡片「清仓」按钮置灰不可点。
|
||||
@@ -375,12 +390,21 @@ data class GjcStorageUse(
|
||||
var outDate: String? = null, // 出库时间
|
||||
var outOpId: String? = null, // 出库人
|
||||
var outId: String? = null, // 出库人(兼容字段)
|
||||
var cargoStatus: String? = null // 货物状态
|
||||
var cargoStatus: String? = null, // 货物状态
|
||||
var inPc: Long? = null, // 入库件数
|
||||
var inWeight: Double? = null, // 入库重量
|
||||
var outPc: Long? = null, // 出库件数
|
||||
var outWeight: Double? = null // 出库重量
|
||||
) {
|
||||
// ==================== UI扩展字段 ====================
|
||||
@Transient
|
||||
val checked: ObservableBoolean = ObservableBoolean(false) // 选中状态
|
||||
|
||||
val inPcText: String get() = inPc?.toString() ?: ""
|
||||
val inWeightText: String get() = inWeight?.toPlainText() ?: ""
|
||||
val outPcText: String get() = outPc?.toString() ?: ""
|
||||
val outWeightText: String get() = outWeight?.toPlainText() ?: ""
|
||||
|
||||
// 兼容现有API的isSelected属性
|
||||
var isSelected: Boolean
|
||||
get() = checked.get()
|
||||
|
||||
@@ -44,6 +44,9 @@ data class GjcMove(
|
||||
var likeNo: String = "", // 部分运单号no(模糊查询)
|
||||
|
||||
var range: String = "",
|
||||
var flight: String = "", // 航班信息(航班日期/航班号,如 20260924/MU2222)
|
||||
var businessName: String = "", // 业务类型名称(中)
|
||||
var opUserName: String = "", // 操作人(移库人)姓名
|
||||
|
||||
// UI扩展字段 - 使用ObservableBoolean实现自动UI更新
|
||||
val checked: ObservableBoolean = ObservableBoolean(false)
|
||||
@@ -67,6 +70,14 @@ data class GjcMove(
|
||||
val goodsName: String
|
||||
get() = goodsCn.ifEmpty { goods }
|
||||
|
||||
/** 业务类型:优先中文名称,为空回退编码 */
|
||||
val businessDisplay: String
|
||||
get() = businessName.ifEmpty { businessType }
|
||||
|
||||
/** 移库人:优先姓名,为空回退操作人ID */
|
||||
val opUserDisplay: String
|
||||
get() = opUserName.ifEmpty { opId }
|
||||
|
||||
/**
|
||||
* 详情页字段统一空值占位。
|
||||
* 接口对 businessType / opdate / opId 等字段常返回 null,
|
||||
|
||||