Compare commits

33 Commits

Author SHA1 Message Date
YANG JIANKUAN
8a88904f79 feat(phone): 手持端首页菜单图标更新为 V2 版本
按 documents/5.5寸首页新样式/icons v2 一一替换 drawable-nodpi 下 21 个
img_pda_menu_* 资源(国际 12 含分组箭头、国内 9),资源名不变、代码零改动。
设计稿文件名「出港抵运」对应菜单「出港运抵」(gjc_arrive)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:47:40 +08:00
YANG JIANKUAN
e828abe32f feat: modify old 5.5 pages color 2026-10-26 11:59:43 +08:00
YANG JIANKUAN
daaf651c04 fix(phone): 旧版手持端页面内容区蓝色统一为标题栏标准色 phone_primary
- module_p 旧页面布局(列表/新增/详情/弹框/列表项)中 colorPrimary(#1C8CF5)、
  @color/blue 全部改为 phone_primary(#2563EB):数量标签、底部按钮、表单标签、Tab、悬浮按钮等
- 单色图标(数量三角、收运记录入口、打印、加货物、搜索、上传加号、勾选框)改用 tint 着色,
  无需新图;勾选框新增着色包装 img_pda_check_n/s
- 开始收运底部按钮新增 bg_pda_btn_bottom 覆盖 tv_bottom_btn 背景
- 原 colorPrimary / bg_btn_bottom / 位图资源均与平板共用,未改动,平板不受影响

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:53:52 +08:00
YANG JIANKUAN
8d5c37039d fix(phone): 移除手机端国际 Tab「进港理货」入口
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:07:51 +08:00
YANG JIANKUAN
6fb7161e80 fix(phone): 首页四个 Tab 顶栏统一为业务页标题栏样式,移除国际板箱过磅入口
- 顶栏改为 48dp、phone_primary、18sp 加粗,与 title_tool_bar 一致;状态栏同色
- 「我的」页退出图标缩小(padding 14dp),铃铛垂直居中随之微调
- 手机端国际 Tab 去掉「板箱过磅」菜单入口及对应跳转分支

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:06:16 +08:00
YANG JIANKUAN
542c35b22d feat(phone): 5.5 寸手持端首页按新设计稿改版
按 documents/5.5寸首页新样式/ 设计稿还原 PDAEnterActivity 四个 Tab(仅 module_p,平板不经过该页):

- 国内/国际 Tab:分组标题栏 + 三列宫格(PdaMenuGroup / PdaMenuRenderer),分组与顺序按设计稿;
  20 个入口换新图标(源 GIF/PNG 转 PNG 放 drawable-nodpi,放 xxhdpi 会先缩后放发糊),
  设计稿未覆盖的入口追加到组尾沿用旧图标
- 权限过滤 authList.contains(key) 与改版前逐字一致:ADMIN 账号入口集合改版前后 diff 为空
  (国内 9、国际 12),21 个入口逐个点击跳转目标不变
- 首页航班动态:卡片 + 国内/国际、进港/出港分段 + 日期卡片(‹ › 翻日)+ 浅色表格 + 设置悬浮钮;
  默认方向改为进港,日期统一补零,日期选择器从当前选中日期打开
- 底部 Tab 栏(16dp 图标与文字居中)、消息铃铛与角标(四个 Tab 统一位置、切换不再移动)、
  状态栏 #3B71E8;「我的」顶栏同步新导航栏样式;首页 AutoSize 基准 400 → 390
- 删除 module_p 中不再引用的旧菜单/Tab 图标副本
- CLAUDE.md / phone-adapt skill 更新首页菜单入口说明,补图标发糊、分段控件描边与取整两个坑

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:37:04 +08:00
YANG JIANKUAN
a909f96d61 fix(gjc): 出港移库编辑页手机卡片改为与出港货物运单信息一致
- 四张卡片统一 bg_phone_card + 左侧蓝色短竖条标题 + 分割线,
  间距/内边距对齐 fragment_gjc_query_waybill(页面 12dp、卡片 14dp)
- 字段改用 PhoneKvItem 两列网格;运单号/移交状态并入基本信息首行,
  运单类型/品名改为整行字段
- 备注沿用无下划线输入,字号与 PhoneKvItem 一致,详情态空值占位与字段同色
- 删除上次为本页单独新增的 move_edit_ 色值与 drawable,及主卡 clipToOutline 设置

Refs: 禅道 Bug #632

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:10:34 +08:00
YANG JIANKUAN
00f2dc98dd feat(skill): zentao-bug 新增 assign 命令,支持指派 Bug 并附备注
等同网页「指派」表单:--to 接受账号或姓名,状态保持激活,提交后回读校验指派人;
多行备注用 --comment-file,换行转 <br /> 以便网页分行显示。
SKILL.md 补充用法,约定转给后端时备注照搬《后端接口对接问题清单》对应行。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:44:31 +08:00
YANG JIANKUAN
2ff052c6e2 fix(phone): 卡片右上角箭头改为原型灰色
ULD 管理、出港查询、出港仓库、进港查询、进港仓库卡片右上角箭头改为 gray-500
(phone_text_body),出港计重改为 gray-400(phone_text_hint),与各页原型一致。
仅在各卡片布局上加 tint,公共图标 ic_phone_chevron_right 不变;
「查看详情 ›」的蓝色箭头保持原样。

Refs: 禅道 Bug #620

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:35:25 +08:00
YANG JIANKUAN
1b6f14da09 fix(gjc): 出港运抵已运抵卡片同样去掉海关放行与运单类型
此前只在「待运抵」隐藏该行,现两个 Tab 都不显示,直接从手机卡片布局移除。

Refs: 禅道 Bug #626

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:35:25 +08:00
YANG JIANKUAN
42c0e26bde fix(gjc): 出港运抵卡片无分单也显示「查看分单 0」,分单明细补空态
- 手机卡片「查看分单」按钮始终显示,分单数为空时按 0 显示
- 无分单的运单可进入分单明细页,补「暂无分单」空态,避免整页空白

Refs: 禅道 Bug #631

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:29:24 +08:00
YANG JIANKUAN
39b9669a87 fix(gjc): 出港移库编辑页手机卡片按原型重做样式
- 主信息卡去掉蓝色表头,改为白底 + 左侧 3dp 蓝色装饰条(clipToOutline 随圆角裁切)
- 移库信息 / 交接图片 / 补充信息三张分区卡:白底深色标题 + 分割线,替代彩色表头
- 字号、字重、颜色、圆角、描边按「移库编辑」HTML 原型逐项对齐
- 备注行改为本页自有输入行,去掉下划线
- 卡片阴影改用原型的蓝灰半透明色,容器 clipToPadding=false,避免阴影发脏与被截断

只改本页手机布局:页面专用色值与 drawable 以 move_edit_ 前缀放在 module_gjc,
公共颜色、组件与平板布局均未改动。

Refs: 禅道 Bug #632

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:29:24 +08:00
YANG JIANKUAN
1488547220 build: autosize 依赖固定为 1.2.1,消除每次构建的动态版本查询
`me.jessyan:autosize:1.2.+` 让 Gradle 每次构建都去所有仓库列版本,
其中私服 118.31.6.84:8081 连接后挂起再返回空响应,失败结果不缓存,
导致无改动构建也要约 7 分钟。固定为本地一直在用的 1.2.1 后,
联网的无改动构建降到 1~2 秒,运行行为不变。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:29:24 +08:00
YANG JIANKUAN
d738a4acfe feat(skill): zentao-bug 新增 resolve 命令,支持在禅道解决 Bug
- resolve <id...>:等同网页「解决」表单,默认解决方案 fixed、解决版本 trunk(主干),
  支持 --dry-run 预览;只处理激活状态,提交后回读校验
- 接口提交不会自动补默认值:解决日期与指派给先从解决表单 HTML 读取默认值
  (服务器北京时间 / 创建人)再原样提交,避免用本机时区时间
- SKILL.md 补写回说明,并注明 login 需在独立终端执行(! 前缀无 TTY)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:29:08 -07:00
YANG JIANKUAN
a58845121b docs: 后端接口对接问题清单新增 #42~#43(禅道 Bug 排查发现)
- #42 国际出港运抵 arrivalStatus 归类错误:已申报(01/W/E)运单仍在待运抵返回(禅道 #627)
- #43 国际进港提取出库出库后任何条件都查不到记录,outState=1 不生效(禅道 #628)
均经 2026-10-14 内网直调确认;「使用说明」补更新记录。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:22:10 -07:00
YANG JIANKUAN
a4167c3cf3 fix(gjj): 进港仓库补齐始发站与库位件重,运单信息去掉出库时间
- 运单信息 Tab 去掉「出库时间」
- 始发站显示 "-":详情 warehouseList 返回 fdep 而非 dep,列表 IntImpStorage/pageQuery
  不返回 dep/fdep、始发站在 origin。GjjWarehouse 新增 fdep 与 depDisplay
  (fdep → dep → origin),GjcMaWb 新增 depDisplay(dep → origin),仓库卡片与列表卡片改绑
- 库位卡片:GjjStorageUse 补 inPc/inWeight/outPc/outWeight,增加出入库件数与重量两行

Refs: 禅道 Bug #635

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:22:01 -07:00
YANG JIANKUAN
9db20749e2 fix(gjc): 出库交接卡片装机状态标签移到查看详情行最左侧
「允许/不允许装机」原在卡片头部右侧,与 IMP 代码标签抢同一位置;
头部右侧只保留 IMP 代码,查看详情点击区域仍为标签右侧整段。

Refs: 禅道 Bug #634

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:22:00 -07:00
YANG JIANKUAN
eb0fb12c8d fix(gjc): 出港货物详情调整字段顺序,库位卡片增加出入库件数与重量
- 运单信息:「包装类型 | 业务类型」同行,「入库时间」单独一行放在备注下面
- 库位信息:IntExpSearch/detail 的 storageUseList 已返回 inPc/inWeight/outPc/outWeight,
  卡片增加「入库件数 | 入库重量」「出库件数 | 出库重量」两行

修改库位「原库位」应列出全部已分配库位:模拟器实测已列出全部 4 个,未复现,待确认。

Refs: 禅道 Bug #633

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:22:00 -07:00
YANG JIANKUAN
2996e670e0 fix(gjc): 出港移库编辑拆分交接图片与补充信息卡片,补上图片删除
- 交接图片单独成卡:橙色表头 + 三列虚线加号槽位,与国内进港移库「异常照片」一致;
  其下新增「补充信息」卡片(表头底色 #F3F4F6)承载备注录入
- 删除不了图片:选图组件长按回调 onItemClick(rl) 以显示删除角标,ViewModel 只处理了
  点角标删除,角标永远出不来;补上长按切换(详情态不可删,平板共用一并生效)
- 公共选图格子 item_image_select_new 拆双变体:平板原样移到 layout-sw600dp/,
  手机版改为灰色虚线框 + 灰色加号(国内移库拍照页同步统一)

表头文字颜色暂用深色(Bug 单写白字,白字在 #F3F4F6 上不可读),待确认。

Refs: 禅道 Bug #632

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:50 -07:00
YANG JIANKUAN
36d74c3f0d fix(phone): 出港/进港仓库卡片备注行前补图标
备注行原为纯文字,与上方带 14dp 图标的字段行不一致;改为 ic_phone_clipboard 图标 + 文字,
进港仓库卡片同步(#630 排查确认进港备注行本身正常,仅清仓时填了备注才显示)。

Refs: 禅道 Bug #629, #630

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:34 -07:00
YANG JIANKUAN
9e545ee0ec fix(gjc): 出港运抵待运抵卡片不显示海关放行与运单类型
该行改为仅在「已运抵」Tab 显示。

Refs: 禅道 Bug #626

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:10 -07:00
YANG JIANKUAN
66c7c0f4f6 fix(app): ULD 列表手机卡片去掉「所在港 / 位置」一行
与原型保持一致;「来源」「入库时间」为数据本身为空(入库时间取 inStoreTime)。

Refs: 禅道 Bug #625

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:10 -07:00
YANG JIANKUAN
075b6bf1ea fix(gjc): 出港待计重手机端去掉「新增计重」浮动按钮
原型只有「计重记录」一个浮动按钮;平板搜索栏的「+」不变。

Refs: 禅道 Bug #624

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:10 -07:00
YANG JIANKUAN
d1d995c698 fix(phone): PhoneDataLayout 下拉改为底部弹出列表
筛选面板与弹框里的 PhoneDataLayout 原用系统 Spinner,点开是贴在输入框下的下拉菜单。
SPINNER 形态改为与 PhoneFormRow 同款 XPopup 底部列表(标题取 hint),保留原取值语义:
无 hint 默认选中第一项、有 hint 为空值,筛选「重置」逻辑不变。19 个手机布局随之生效。

Refs: 禅道 Bug #623

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:04 -07:00
YANG JIANKUAN
fa6c3f473e fix(gjc): 计重记录运单详情补齐航班日期、航班号、航程与业务类型
IntExpCheckIn/checked/pageQuery 出参中 fdate/fno/range/businessType/remark 恒为空,
详情页只展示列表传入的数据,因此全部显示 "-"。进入后按 11 位运单号调用
IntExpCheckIn/queryWbByNo 补齐空值,业务类型代码按国际出港字典(IO)转中文名。

Refs: 禅道 Bug #622

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:04 -07:00
YANG JIANKUAN
2c8fa689a5 fix(gjc): 开始计重手机端去掉地磅重量,平板车自重左对齐并自动带出
- 手机布局删除「地磅重量」;手机端不再按「地磅重量 − 托盘总重」自动计算运抵重量
  (地磅为 0 时会被写成 "0"),改为手输、默认空白;平板逻辑不变
- 「平板车自重」改用不可编辑的 INPUT 形态,与其他行一样左对齐
- 运单加载后若已带平板车号,直接回查自重(平板共用 ViewModel,一并生效)
- PhoneFormRow 的 INPUT 形态补上输入完成回调(失焦 / 键盘完成、下一项),
  原先从不触发,手输平板车号、运单号后不会回查

Refs: 禅道 Bug #621

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:20:57 -07:00
YANG JIANKUAN
b22d55a380 fix(phone): 三格统计卡左侧色条改为随圆角弯曲的整条左边框
原实现是卡片内上下各缩进 12dp 的独立短竖条,与原型 rounded-lg border-l-[4px] 不符。
新增 bg_phone_stat_blue/orange/green(白底 12dp 圆角 + 左侧 4dp 色边),替换出港待计重、
计重记录、出港查询、出港仓库、进港查询、进港仓库 6 个页面共 18 处统计卡。

Refs: 禅道 Bug #620

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:20:50 -07:00
YANG JIANKUAN
7f1a413172 feat(skill): 新增 zentao-bug skill,读取禅道 18.x Bug 单并按项目规范修复
禅道 18.8 无 RESTful API 2.0,官方 zentao-cli/MCP 不可用。zentao.py 以 v1 tokens 换会话,
走页面 JSON 模式读取「我的 Bug」列表与 Bug 详情(描述/截图/评论/修改历史/附件),
截图下载到本地供 Agent 查看;Token 存 ~/.config(0600),密码仅存 macOS 钥匙串。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:20:40 -07:00
YANG JIANKUAN
f41d4186ea style(gjc): 开始组装页运单列表 5 列间距均分,表头两种状态各用一套列宽
- 新增 AssembleWaybillColumns:「件数/重量」与「剩余件数/总件数」两种表头状态
  各一套列宽,表头按 hasLoadedOnce、列表项按 showRemaining 切换,保证列对齐
- 件数与重量之间加弹性间隔;剩余态重量列放宽到 140dp,大重量不换行
- module_base 新增通用 layoutWeight 绑定适配器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 02:29:33 -07:00
YANG JIANKUAN
d9dc927525 fix(gjj): 国际事故签证「发现时间」改为下拉选择,数据源 POST typeCode/searchDateType
- Api 新增 getSearchDateTypeList(POST 无参,返回 code 1~8 + 中英文 name),
  与同路径的 GET 通用字典 getDictList 区分
- 编辑页 DATE → SPINNER,与 Web 端一致提交字典 name
- 历史数据(日期值、带尾随换行的 name)不在字典内时补进下拉列表,
  避免编辑/详情回显为空、保存时被清掉
- phone-adapt api-doc 记录该接口 GET/POST 语义

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 02:29:33 -07:00
YANG JIANKUAN
d5631930c4 chore: 清理 settings.local.json 中 vfox 迁移遗留的权限项
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:20:55 -07:00
YANG JIANKUAN
5b217ab032 feat(sync-air-cargo): 以 Synced-From trailer 记录同步点,同步后自动在 air-cargo 本地提交
- 默认区间模式:基线取 air-cargo 最近的 Synced-From,目标为 main,内容取自提交对象
- 无记录时按内容反推基线(仅预览,apply 须 --base 确认),每次预览做基线校验
- 新增三方校验,列出将覆盖的 air-cargo 侧独立修改;分支/工作区/祖先关系保护
- --worktree 同步未提交变更且不推进同步点;提交信息仅「<Label> <日期>」+ trailer,不 push
- 新增 config(main → feature/hefei,air-cargo 位于项目根目录)
- 排除清单补充 .mcp.json、.claude.*、CLAUDE.local.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:20:55 -07:00
YANG JIANKUAN
f1ff42f2bf chore: 忽略根目录 air-cargo/,CLAUDE.md 声明其仅作另一仓库的提交通道
- .gitignore 新增 air-cargo/(同一份源码的独立 git 仓库,远程不同)
- CLAUDE.md 新增「air-cargo 目录:一律忽略」:扫描、开发、查历史均跳过,
  仅 sync-air-cargo skill 可操作

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:20:55 -07:00
219 changed files with 5247 additions and 1210 deletions

View File

@@ -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)",

View File

@@ -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 分支
),

View File

@@ -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)、

View File

@@ -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`,否则会覆盖成未安装的旧版本

View 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

View File

@@ -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

View 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/`,文中以绝对路径 `![图N](/Users/.../images/1154.jpeg)` 引用。
**截图是需求的主要载体**(团队习惯把原型/标注贴在描述和评论里,几乎不用附件),**必须逐张用 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` 一并清除。

View 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"![图片下载失败]({src})"
if dest not in self.files:
self.files.append(dest)
return f"![图{self.files.index(dest) + 1}]({dest})"
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
View File

@@ -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

View File

@@ -8,6 +8,18 @@
## [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 个平板提交(运抵海关放行/运抵状态筛选/海关回执、出库交接装机状态/允许装机、出港移库详情与侧滑编辑、

View File

@@ -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 种典型页面类型
@@ -1003,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,再去看清单。
@@ -1016,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` 已实现)
@@ -1304,6 +1333,8 @@ adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.activity.IntEx
| 布局构建报 `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 绑计算属性(进港查询/出港查询代理人已踩过) |
---

View File

@@ -76,7 +76,8 @@
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>
@@ -118,34 +119,6 @@
</LinearLayout>
<!-- ==================== 所在港 / 位置(与平板列表一致) ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:text='@{"所在港:" + (bean.nowPort.empty ? "-" : bean.nowPort)}'
android:textColor="@color/phone_text_body"
android:textSize="13sp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:text='@{"位置:" + (bean.nowPosition.empty ? "-" : bean.nowPosition)}'
android:textColor="@color/phone_text_body"
android:textSize="13sp" />
</LinearLayout>
<!-- ==================== 第三行:入库时间 ==================== -->
<LinearLayout
android:layout_width="match_parent"

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -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') {

View File

@@ -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 ->

View File

@@ -177,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 为空视作未清仓)。
* 已清仓时卡片「清仓」按钮置灰不可点。

View File

@@ -54,9 +54,14 @@ class GjjWarehouse(
var splitFlag: String? = "",
var inDate: String? = "",
var clearNormal: String? = "",
var range: String? = ""
var range: String? = "",
var fdep: String? = "" // 始发站(详情接口 warehouseList 返回 fdep,dep 恒为空)
) {
/** 始发站展示:优先 fdep,兜底 dep / origin(Bug #635) */
val depDisplay: String
get() = listOf(fdep, dep, origin).firstOrNull { !it.isNullOrEmpty() } ?: ""
fun getWaybillCode() = "${prefix}${no}"
fun getFlightDesc() = "${fdate}/${fno}"

View File

@@ -1,5 +1,6 @@
package com.lukouguoji.module_base.bean
import com.lukouguoji.module_base.ktx.toPlainText
import java.io.Serializable
/**
@@ -19,5 +20,14 @@ data class GjjStorageUse(
var outDate: String? = null, // 出库时间
var outOpId: String? = null, // 出库人ID
var outOpName: String? = null, // 出库人姓名
var cargoStatus: String? = null // 货物状态
) : Serializable
var cargoStatus: String? = null, // 货物状态
var inPc: Long? = null, // 入库件数
var inWeight: Double? = null, // 入库重量
var outPc: Long? = null, // 出库件数
var outWeight: Double? = null // 出库重量
) : Serializable {
val inPcText: String get() = inPc?.toString() ?: ""
val inWeightText: String get() = inWeight?.let { "${it.toPlainText()} kg" } ?: ""
val outPcText: String get() = outPc?.toString() ?: ""
val outWeightText: String get() = outWeight?.let { "${it.toPlainText()} kg" } ?: ""
}

View File

@@ -2168,6 +2168,12 @@ interface Api {
@POST("typeCode/unusualType")
suspend fun getUnusualTypeList(): DictListBean
/**
* 字典-发现时间类型(接收时 / 分拣理货时 / 提货时 …)
*/
@POST("typeCode/searchDateType")
suspend fun getSearchDateTypeList(): DictListBean
///////////////////////////////////////////////////////////////////////////
// ULD管理
///////////////////////////////////////////////////////////////////////////

View File

@@ -13,12 +13,10 @@ import android.widget.TextView
import androidx.core.widget.doOnTextChanged
import androidx.databinding.InverseBindingListener
import com.lukouguoji.module_base.R
import com.lukouguoji.module_base.adapter.bindAdapter
import com.lukouguoji.module_base.adapter.bindOnSelected
import com.lukouguoji.module_base.interfaces.IOnSpinnerSelected
import com.lukouguoji.module_base.ktx.formatDate
import com.lukouguoji.module_base.ktx.getActivity
import com.lukouguoji.module_base.util.Common
import com.lxj.xpopup.XPopup
import dev.utils.app.info.KeyValue
import java.util.Calendar
@@ -32,7 +30,7 @@ import java.util.Calendar
* |-----------|-----------|----------|
* | `INPUT` | EditText | 无 |
* | `DATE` | TextView(点击弹日历)| 日历 |
* | `SPINNER` | Spinner | 下箭头 |
* | `SPINNER` | TextView(点击弹底部列表,与 PhoneFormRow SELECT 同款)| 下箭头 |
*
* XML 用法:
* ```xml
@@ -58,6 +56,7 @@ class PhoneDataLayout @JvmOverloads constructor(
val llContent: LinearLayout
val et: EditText
val tv: TextView
/** 保留以兼容布局 id;Bug #623 起 SPINNER 形态改为底部弹出列表,原生 Spinner 恒隐藏 */
val spinner: Spinner
val iv: ImageView
@@ -85,7 +84,7 @@ class PhoneDataLayout @JvmOverloads constructor(
field = v
et.hint = v
tv.hint = v
bindAdapter(spinner, list, v, R.layout.item_spinner_list_14sp)
applyDefaultSelection()
}
var value: String = ""
@@ -99,7 +98,7 @@ class PhoneDataLayout @JvmOverloads constructor(
var list: List<KeyValue> = emptyList()
set(v) {
field = v
bindAdapter(spinner, v, hint, R.layout.item_spinner_list_14sp)
applyDefaultSelection()
applyValue()
}
@@ -114,7 +113,6 @@ class PhoneDataLayout @JvmOverloads constructor(
field = v
llContent.isEnabled = v
et.isEnabled = v
spinner.isEnabled = v
}
init {
@@ -136,13 +134,6 @@ class PhoneDataLayout @JvmOverloads constructor(
}
}
bindOnSelected(spinner, object : IOnSpinnerSelected {
override fun onSelected(position: Int) {
value = list.getOrNull(position)?.value ?: ""
refreshCallBack?.invoke()
}
})
applyType()
}
@@ -157,6 +148,28 @@ class PhoneDataLayout @JvmOverloads constructor(
}
}
/** SPINNER:底部弹出列表(标题取 hint,无 hint 时取 title),选中后回写 value 并回调 */
private val spinnerClick: (v: View) -> Unit = {
if (enable && list.isNotEmpty()) {
XPopup.Builder(context)
.asBottomList(hint.ifEmpty { title }, list.map { it.key }.toTypedArray()) { position, _ ->
value = list.getOrNull(position)?.value ?: ""
refreshCallBack?.invoke()
}
.show()
}
}
/**
* 保持原生 Spinner 的默认取值语义:无 hint 时自动选中第一项(value 为空才补),
* 有 hint 时保持空值显示占位文字。
*/
private fun applyDefaultSelection() {
if (type == PhoneDataLayoutType.SPINNER && hint.isEmpty() && value.isEmpty() && list.isNotEmpty()) {
value = list[0].value
}
}
private fun applyType() {
when (type) {
PhoneDataLayoutType.INPUT -> {
@@ -179,12 +192,12 @@ class PhoneDataLayout @JvmOverloads constructor(
PhoneDataLayoutType.SPINNER -> {
et.visibility = GONE
tv.visibility = GONE
spinner.visibility = VISIBLE
tv.visibility = VISIBLE
spinner.visibility = GONE
iv.visibility = VISIBLE
iv.setImageResource(R.drawable.ic_phone_chevron_down)
llContent.setOnClickListener(null)
llContent.isClickable = false
llContent.setOnClickListener(spinnerClick)
applyDefaultSelection()
}
}
}
@@ -200,10 +213,8 @@ class PhoneDataLayout @JvmOverloads constructor(
}
PhoneDataLayoutType.SPINNER -> {
if (value.isNotEmpty()) {
val position = list.indexOfFirst { it.value == value }
if (position >= 0) spinner.setSelection(position)
}
// 显示选中项文案;列表里找不到就按原值显示(如后端返回的历史值),空值显示 hint
tv.text = list.firstOrNull { it.value == value }?.key ?: value
}
}
}
@@ -223,10 +234,10 @@ class PhoneDataLayout @JvmOverloads constructor(
/** 重置为空值(筛选弹层「重置」按钮使用) */
fun reset() {
value = ""
if (type == PhoneDataLayoutType.SPINNER && list.isNotEmpty()) {
spinner.setSelection(if (hint.isEmpty()) list.size - 1 else list.size)
}
// 与原 Spinner 版本一致:有 hint 回到占位(空值);无 hint 选中最后一项
value = if (type == PhoneDataLayoutType.SPINNER && hint.isEmpty() && list.isNotEmpty()) {
list.last().value
} else ""
}
}

View File

@@ -7,6 +7,7 @@ import android.util.AttributeSet
import android.util.SparseArray
import android.view.Gravity
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
@@ -136,6 +137,20 @@ class PhoneFormRow @JvmOverloads constructor(
}
}
// INPUT 输入完成回调(与平板 PadDataLayoutNew 一致:失焦即视为输入完成;另加键盘「完成/下一项」)。
// 原先 INPUT 形态从不触发 refreshCallBack,平板车号/运单号手输后不会回查(Bug #621)
et.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus && type == PhoneFormRowType.INPUT) refreshCallBack?.invoke()
}
et.setOnEditorActionListener { _, actionId, _ ->
if (type == PhoneFormRowType.INPUT &&
(actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_NEXT)
) {
refreshCallBack?.invoke()
}
false
}
applyType()
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版详情卡片表头:#F3F4F6 浅灰底 + 顶部 12dp 圆角(出港移库编辑-补充信息卡片) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_bg" />
<corners
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版三格统计卡背景:白底 12dp 圆角 + 左侧 4dp 色边(随圆角弯曲),
对应设计稿 rounded-lg border-l-[4px](Bug #620,替代原先卡片内悬空的短竖条)
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/phone_primary" />
<corners android:radius="12dp" />
</shape>
</item>
<item android:left="4dp">
<shape android:shape="rectangle">
<solid android:color="@color/white" />
<corners
android:bottomLeftRadius="8dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="8dp"
android:topRightRadius="12dp" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版三格统计卡背景:白底 12dp 圆角 + 左侧 4dp 色边(随圆角弯曲),
对应设计稿 rounded-lg border-l-[4px](Bug #620,替代原先卡片内悬空的短竖条)
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/phone_tag_green_text" />
<corners android:radius="12dp" />
</shape>
</item>
<item android:left="4dp">
<shape android:shape="rectangle">
<solid android:color="@color/white" />
<corners
android:bottomLeftRadius="8dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="8dp"
android:topRightRadius="12dp" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版三格统计卡背景:白底 12dp 圆角 + 左侧 4dp 色边(随圆角弯曲),
对应设计稿 rounded-lg border-l-[4px](Bug #620,替代原先卡片内悬空的短竖条)
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/phone_tag_orange_text" />
<corners android:radius="12dp" />
</shape>
</item>
<item android:left="4dp">
<shape android:shape="rectangle">
<solid android:color="@color/white" />
<corners
android:bottomLeftRadius="8dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="8dp"
android:topRightRadius="12dp" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="bean"
type="com.lukouguoji.module_base.bean.FileBean" />
</data>
<RelativeLayout
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="5dp"
android:background="@{bean.path.length() == 0 ? @color/color_f2 : @color/white}">
<ImageView
visible="@{bean.path.length() == 0}"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:src="@drawable/img_add" />
<ImageView
android:id="@+id/iv"
loadImage="@{bean.path}"
visible="@{bean.path}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<View
visible="@{bean.canDelete}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_tran10" />
<ImageView
android:id="@+id/iv_delete"
visible="@{bean.canDelete}"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerInParent="true"
android:src="@drawable/img_delete_red" />
</RelativeLayout>
</layout>

View File

@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版选图格子(平板版在 layout-sw600dp/,两者变量与 id 一致,共用 ImageSelectNewViewHolder)。
加号槽位与进港移库「异常照片」一致:灰色虚线框 + 灰色加号;已选图片长按出删除角标。
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
@@ -12,15 +16,17 @@
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="5dp"
android:background="@{bean.path.length() == 0 ? @color/color_f2 : @color/white}">
android:layout_margin="6dp"
android:background="@drawable/bg_phone_photo_add">
<ImageView
<TextView
visible="@{bean.path.length() == 0}"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/img_add" />
android:text="+"
android:textColor="@color/phone_text_hint"
android:textSize="24sp" />
<ImageView
android:id="@+id/iv"

View File

@@ -21,7 +21,8 @@
android:layout_height="50dp"
android:padding="10dp"
android:layout_centerInParent="true"
android:src="@drawable/img_add" />
android:src="@drawable/img_add"
android:tint="@color/phone_primary" />
<ImageView
android:id="@+id/iv"

View File

@@ -0,0 +1,27 @@
package com.lukouguoji.gjc.page.assemble
/**
* 开始组装页「运单列表」列宽权重,表头与 item_assemble_waybill 共用以保证列对齐
*
* 首次装货成功前后,件数/重量列表头文字长短差异很大(「件数」↔「剩余件数/总件数」),
* 一套列宽无法让两种状态下 5 列文字间距都均匀,故按状态各取一套。
* 数值即平板(AutoSize 1152dp 基准)上各列的 dp 宽度;「选择」列固定 64dp 不在其中。
*/
object AssembleWaybillColumns {
const val WB_NO = 0
const val PIECES = 1
const val GAP = 2 // 件数与重量之间的间隔
const val WEIGHT = 3
const val FLIGHT = 4
/** 表头「件数 / 重量」 */
private val TOTAL = floatArrayOf(237f, 72f, 76f, 72f, 250f)
/** 表头「剩余件数/总件数 / 剩余重量/总重量」;重量列放宽到 140,容纳"120856.0/123456.0"不换行 */
private val REMAINING = floatArrayOf(199f, 120f, 57f, 140f, 192f)
@JvmStatic
fun weight(column: Int, showRemaining: Boolean): Float =
(if (showRemaining) REMAINING else TOTAL)[column]
}

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.lukouguoji.module_base.base.BaseViewModel
import com.lukouguoji.module_base.util.DeviceUtil
import com.lukouguoji.module_base.bean.GjcMaWb
import com.lukouguoji.module_base.bean.GjcWeighingBean
import com.lukouguoji.module_base.common.Constant
@@ -120,6 +121,8 @@ class GjcWeighingStartViewModel : BaseViewModel() {
* 自动计算运抵重量:地磅称重 - (托盘数量 × 托盘自重)
*/
private fun recalculateArriveWeight() {
// 手机端无地磅重量输入(Bug #621),运抵重量手输;否则会被 0 - 托盘总重 覆盖成 "0"
if (DeviceUtil.isPhone()) return
val gross = diBangWeight.value?.toDoubleOrNull() ?: 0.0
val trayTotal = getTrayTotalWeight()
val net = gross - trayTotal
@@ -198,6 +201,8 @@ class GjcWeighingStartViewModel : BaseViewModel() {
onSuccess = { result ->
maWbBean.value = result.data ?: GjcMaWb()
pageRemark.value = maWbBean.value?.remark
// 运单已带平板车号时直接回查自重(原先只在手输车号完成后查询,Bug #621)
onCarIdInputComplete()
// 更新航班日期字符串
flightDate.value = result.data?.fdate?.formatDate() ?: ""
}

View File

@@ -5,14 +5,18 @@ import androidx.lifecycle.MutableLiveData
import com.lukouguoji.module_base.base.BaseViewModel
import com.lukouguoji.module_base.bean.GjcWeighingRecordBean
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.http.net.NetApply
import com.lukouguoji.module_base.ktx.formatDate
import com.lukouguoji.module_base.ktx.launchCollect
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.util.DictUtils
/**
* 国际出港计重-运单详情 ViewModel(5.5寸手机版专属页面)
*
* 只读展示计重记录列表传入的运单信息,无接口请求。
* 注:设计稿中的「通道号」字段在 /IntExpCheckIn/checked/pageQuery 出参中不存在(后端缺口,
* 见接口问题清单),布局侧空值兜底显示 "-"。
* 先展示计重记录列表传入的运单信息;/IntExpCheckIn/checked/pageQuery 出参中
* 航班日期/航班号/航程/业务类型/备注恒为空(Bug #622),再按 11 位运单号调
* /IntExpCheckIn/queryWbByNo 补齐这些字段(只补空值,不覆盖列表已有数据)。
*/
class GjcWeighingWbDetailViewModel : BaseViewModel() {
@@ -23,8 +27,37 @@ class GjcWeighingWbDetailViewModel : BaseViewModel() {
val data = intent.getSerializableExtra(Constant.Key.BEAN) as? GjcWeighingRecordBean
if (data != null) {
bean.value = data
loadWaybillInfo(data)
} else {
showToast("参数错误")
}
}
private fun loadWaybillInfo(data: GjcWeighingRecordBean) {
if (data.wbNo.isEmpty()) return
launchCollect({ NetApply.api.getIntExpCheckInWbByNo(data.wbNo) }) {
onSuccess = { result ->
result.data?.let { wb ->
if (data.fdate.isEmpty()) data.fdate = wb.fdate.formatDate()
if (data.fno.isEmpty()) data.fno = wb.fno.orEmpty()
if (data.range.isEmpty()) data.range = wb.range.orEmpty()
if (data.businessType.isEmpty()) data.businessType = wb.businessType.orEmpty()
if (data.businessName.isEmpty()) data.businessName = wb.businessName.orEmpty()
if (data.remark.isEmpty()) data.remark = wb.remark.orEmpty()
bean.value = data
fillBusinessName(data)
}
}
}
}
/** queryWbByNo 只返回业务类型代码(如 ANR),按国际出港业务类型字典转成中文名(与开始计重页同源) */
private fun fillBusinessName(data: GjcWeighingRecordBean) {
if (data.businessName.isNotEmpty() || data.businessType.isEmpty()) return
DictUtils.getBusinessTypeList(type = "IO", addAll = false) { list ->
val name = list.firstOrNull { it.value == data.businessType }?.key ?: return@getBusinessTypeList
data.businessName = name
bean.postValue(data)
}
}
}

View File

@@ -178,11 +178,18 @@ class IntExpMoveEditViewModel : BaseViewModel(), IOnItemClickListener {
* 处理图片删除点击事件
*/
override fun onItemClick(position: Int, type: Int) {
if (type == R.id.iv_delete) {
val adapter = rvImages?.commonAdapter() ?: return
if (position < adapter.items.size) {
adapter.removeItem(position)
val adapter = rvImages?.commonAdapter() ?: return
if (position >= adapter.items.size) return
when (type) {
// 长按已选图片:切换删除角标(Bug #632:原先未处理长按,角标出不来导致无法删除)
R.id.rl -> {
val bean = adapter.getItem(position) as? FileBean ?: return
if (bean.path.isNotEmpty() && pageType.value != DetailsPageType.Details) {
bean.canDelete.set(!bean.canDelete.get())
}
}
R.id.iv_delete -> adapter.removeItem(position)
}
}
}

View File

@@ -78,15 +78,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_blue"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_primary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -121,15 +115,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_orange"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_orange_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -164,15 +152,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_green"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_green_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -80,15 +80,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_blue"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_primary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -123,15 +117,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_orange"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_orange_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -166,15 +154,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_green"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_green_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -230,22 +212,7 @@
</LinearLayout>
<!-- ==================== 右下 FAB:新增计重(与平板搜索栏「+」一致,空运单进入开始计重) ==================== -->
<TextView
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="20dp"
android:layout_marginBottom="84dp"
android:background="@drawable/bg_phone_fab"
android:contentDescription="新增计重"
android:elevation="6dp"
android:gravity="center"
android:includeFontPadding="false"
android:onClick="@{()-> viewModel.addClick()}"
android:text="+"
android:textColor="@color/white"
android:textSize="32sp" />
<!-- Bug #624:手机端去掉「新增计重」FAB(平板搜索栏「+」不变) -->
<!-- ==================== 右下 FAB:计重记录 ==================== -->
<ImageView

View File

@@ -74,15 +74,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_blue"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_primary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -118,15 +112,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_orange"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_orange_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -162,15 +150,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_green"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_green_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -176,15 +176,7 @@
type="@{PhoneFormRowType.INPUT}"
value="@={viewModel.arrivePc}" />
<!-- 地磅重量(与平板右上角地磅称重一致):运抵重量 = 地磅重量 - 托盘数量 × 托盘自重,由 ViewModel 自动计算 -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
hint='@{"请输入地磅重量"}'
numeric="@{true}"
title='@{"地磅重量"}'
type="@{PhoneFormRowType.INPUT}"
value="@={viewModel.diBangWeight}" />
<!-- Bug #621:手机端去掉「地磅重量」,运抵重量手输(不再按地磅重量自动计算) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
@@ -267,12 +259,14 @@
type="@{PhoneFormRowType.INPUT}"
value="@={viewModel.maWbBean.carId}" />
<!-- 平板车自重:平板车号输入完成后回查,只读 -->
<!-- 平板车自重:平板车号输入完成 / 运单自带车号加载后回查,只读;
Bug #621:用不可编辑的 INPUT 形态,与其他行一样左对齐(TEXT 形态为右对齐) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{false}"
title='@{"平板车自重"}'
type="@{PhoneFormRowType.TEXT}"
type="@{PhoneFormRowType.INPUT}"
value="@{viewModel.carWeight}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow

View File

@@ -18,6 +18,8 @@
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.lukouguoji.gjc.viewModel.IntExpArriveSubOrderViewModel" />
@@ -60,21 +62,52 @@
</LinearLayout>
<!-- ==================== 列表 ==================== -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
<!-- ==================== 列表 + 空态 ==================== -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingTop="4dp"
android:paddingBottom="12dp"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_int_exp_arrive_sub_order" />
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingTop="4dp"
android:paddingBottom="12dp"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_int_exp_arrive_sub_order" />
<!-- 空态(Bug #631:无分单的运单也可进入本页) -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:visibility="@{viewModel.displayList.empty ? View.VISIBLE : View.GONE}">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_phone_clipboard"
android:tint="@color/phone_text_hint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="暂无分单"
android:textColor="@color/phone_text_hint"
android:textSize="14sp" />
</LinearLayout>
</FrameLayout>
<!-- ==================== 底部操作条 ==================== -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneBottomBar

View File

@@ -11,6 +11,8 @@
<import type="com.lukouguoji.module_base.ui.weight.search.layout.SearchLayoutType" />
<import type="com.lukouguoji.gjc.page.assemble.AssembleWaybillColumns" />
<variable
name="viewModel"
type="com.lukouguoji.gjc.viewModel.IntExpAssembleStartViewModel" />
@@ -179,7 +181,7 @@
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.WB_NO, viewModel.hasLoadedOnce)}"
android:gravity="center"
android:text="运单号"
android:textColor="@color/text_normal"
@@ -187,17 +189,25 @@
<!-- 件数 -->
<TextView
android:layout_width="120dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.PIECES, viewModel.hasLoadedOnce)}"
android:gravity="center"
android:text='@{viewModel.hasLoadedOnce ? "剩余件数/总件数" : "件数"}'
android:textColor="@color/text_normal"
android:textSize="13sp" />
<!-- 件数与重量列间距(列宽见 AssembleWaybillColumns) -->
<Space
android:layout_width="0dp"
android:layout_height="1dp"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.GAP, viewModel.hasLoadedOnce)}" />
<!-- 重量 -->
<TextView
android:layout_width="120dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.WEIGHT, viewModel.hasLoadedOnce)}"
android:gravity="center"
android:text='@{viewModel.hasLoadedOnce ? "剩余重量/总重量" : "重量"}'
android:textColor="@color/text_normal"
@@ -207,7 +217,7 @@
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.2"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.FLIGHT, viewModel.hasLoadedOnce)}"
android:gravity="center"
android:text="配载航班"
android:textColor="@color/text_normal"

View File

@@ -5,7 +5,8 @@
详情(DetailsPageType.Details):列表卡片「查看详情」进入,全部只读;
编辑(DetailsPageType.Modify):列表卡片「编辑」进入(对应平板侧滑「编辑」),仅备注与交接图片可改。
样式沿用设计稿 05_出港移库「运单详情」:蓝色表头卡片 + 两列字段网格;移库人/移库时间取列表数据(listBean)。
卡片样式与「出港货物」详情的运单信息 Tab(fragment_gjc_query_waybill.xml)一致:
bg_phone_card + 左侧蓝色短竖条标题 + 分割线 + PhoneKvItem 两列字段网格;移库人/移库时间取列表数据(listBean)。
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
@@ -14,8 +15,6 @@
<import type="com.lukouguoji.module_base.common.DetailsPageType" />
<import type="com.lukouguoji.module_base.ui.weight.phone.PhoneFormRowType" />
<import type="android.view.View" />
<variable
@@ -42,180 +41,202 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:padding="12dp">
<!-- ==================== 卡片一:运单基本信息 ==================== -->
<!-- ==================== 卡片一:基本信息 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card"
android:orientation="vertical">
android:orientation="vertical"
android:paddingHorizontal="14dp"
android:paddingBottom="14dp">
<!-- 蓝色表头:运单号 + 移交状态 -->
<!-- 卡片头 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card_header_blue"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<View
android:layout_width="4dp"
android:layout_height="16dp"
android:background="@color/phone_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="基本信息"
android:textColor="@color/phone_text_title"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginBottom="12dp"
android:background="@color/phone_divider" />
<!-- 运单号 | 移交状态 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{viewModel.dataBean.wbNo}"
android:textColor="@color/white"
android:textSize="17sp"
android:textStyle="bold" />
title='@{"运单号"}'
value="@{viewModel.dataBean.wbNo}" />
<View
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginEnd="6dp"
android:background="@{viewModel.dataBean.moveState == 1 ? @drawable/bg_phone_dot_green : @drawable/bg_phone_dot_gray}" />
<TextView
android:layout_width="wrap_content"
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.dataBean.moveStateText}"
android:textColor="@color/white"
android:textSize="14sp" />
android:layout_weight="1"
title='@{"移交状态"}'
value="@{viewModel.dataBean.moveStateText}" />
</LinearLayout>
<!-- 件数 | 重量 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:layout_marginTop="12dp"
android:orientation="horizontal">
<!-- 运单类型(红标签) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="22dp"
android:background="@drawable/bg_phone_tag_red"
android:gravity="center"
android:paddingHorizontal="8dp"
android:text='@{viewModel.dataBean.awbTypeName == null || viewModel.dataBean.awbTypeName.empty ? "-" : viewModel.dataBean.awbTypeName}'
android:textColor="@color/phone_tag_red_text"
android:textSize="12sp"
android:textStyle="bold" />
<!-- 品名(主字段大字) -->
<TextView
android:layout_width="wrap_content"
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:text='@{viewModel.dataBean.goods == null || viewModel.dataBean.goods.empty ? "-" : viewModel.dataBean.goods}'
android:textColor="@color/phone_text_strong"
android:textSize="22sp"
android:textStyle="bold" />
android:layout_weight="1"
title='@{"件数"}'
value='@{viewModel.dataBean.pc == null ? "" : String.valueOf(viewModel.dataBean.pc)}' />
<!-- 第 1 行:件数 / 重量 -->
<LinearLayout
android:layout_width="match_parent"
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"件数"}'
value='@{viewModel.dataBean.pc == null ? "" : String.valueOf(viewModel.dataBean.pc)}' />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_weight="1"
title='@{"重量"}'
value='@{viewModel.dataBean.weight == null ? "" : String.valueOf(viewModel.dataBean.weight) + " kg"}' />
</LinearLayout>
<!-- 第 2 行:特码 / 代理人 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"特码"}'
value="@{viewModel.dataBean.spCode}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_weight="1"
title='@{"代理人"}'
value="@{viewModel.dataBean.agentCode}" />
</LinearLayout>
<!-- 第 3 行:出运路径 / 承运人 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"出运路径"}'
value="@{viewModel.dataBean.range}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_weight="1"
title='@{"承运人"}'
value="@{viewModel.dataBean.by}" />
</LinearLayout>
android:layout_weight="1"
title='@{"重量"}'
value='@{viewModel.dataBean.weight == null ? "" : String.valueOf(viewModel.dataBean.weight) + " kg"}' />
</LinearLayout>
<!-- 特码 | 代理人 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"特码"}'
value="@{viewModel.dataBean.spCode}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"代理人"}'
value="@{viewModel.dataBean.agentCode}" />
</LinearLayout>
<!-- 出运路径 | 承运人 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"出运路径"}'
value="@{viewModel.dataBean.range}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"承运人"}'
value="@{viewModel.dataBean.by}" />
</LinearLayout>
<!-- 运单类型(整行) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
title='@{"运单类型"}'
value="@{viewModel.dataBean.awbTypeName}" />
<!-- 品名(整行) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
title='@{"品名"}'
value="@{viewModel.dataBean.goods}" />
</LinearLayout>
<!-- ==================== 卡片二:移库信息(列表数据,仅已移库显示) ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="12dp"
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:paddingHorizontal="14dp"
android:paddingBottom="14dp"
android:visibility="@{viewModel.listBean != null &amp;&amp; viewModel.listBean.isMoved ? View.VISIBLE : View.GONE}">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card_header_green"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp"
android:text="移库信息"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<View
android:layout_width="4dp"
android:layout_height="16dp"
android:background="@color/phone_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="移库信息"
android:textColor="@color/phone_text_title"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginBottom="12dp"
android:background="@color/phone_divider" />
<!-- 移库人 | 移库时间 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -226,7 +247,6 @@
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_weight="1"
title='@{"移库时间"}'
value="@{viewModel.listBean.opdate}" />
@@ -235,43 +255,56 @@
</LinearLayout>
<!-- ==================== 卡片三:备注 + 交接图片(编辑态可改) ==================== -->
<!-- ==================== 卡片三:交接图片(三列加号槽位,长按图片出删除角标) ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="12dp"
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingBottom="12dp">
android:paddingHorizontal="14dp"
android:paddingBottom="14dp">
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入备注"}'
title='@{"备注"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.remark}" />
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="交接图片"
android:textColor="@color/phone_text_label"
android:textSize="15sp" />
<View
android:layout_width="4dp"
android:layout_height="16dp"
android:background="@color/phone_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="交接图片"
android:textColor="@color/phone_text_title"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginBottom="6dp"
android:background="@color/phone_divider" />
<!-- 格子自带 6dp 外边距,左右各收回 6dp,让图片与上方字段左边缘对齐 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_images"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="-6dp"
android:overScrollMode="never"
itemLayoutId="@{viewModel.imageItemLayoutId}"
viewHolder="@{viewModel.imageItemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="4" />
app:spanCount="3" />
<!-- 详情模式下无图片时的占位提示 -->
<TextView
@@ -286,6 +319,72 @@
</LinearLayout>
<!-- ==================== 卡片四:补充信息(Bug #632:备注录入与保存,无下划线) ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:paddingHorizontal="14dp"
android:paddingBottom="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="12dp">
<View
android:layout_width="4dp"
android:layout_height="16dp"
android:background="@color/phone_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="补充信息"
android:textColor="@color/phone_text_title"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginBottom="12dp"
android:background="@color/phone_divider" />
<!-- 备注:标签/数值字号与 PhoneKvItem 一致(12sp 灰标签 + 15sp 数值),编辑态可直接输入 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="备注"
android:textColor="@color/phone_text_hint"
android:textSize="12sp" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:background="@null"
android:enabled="@{viewModel.pageType != DetailsPageType.Details}"
android:hint='@{viewModel.pageType == DetailsPageType.Details ? "-" : "请输入备注"}'
android:importantForAutofill="no"
android:inputType="text"
android:maxLines="1"
android:minHeight="24dp"
android:padding="0dp"
android:text="@={viewModel.remark}"
android:textColor="@color/phone_text_title"
android:textColorHint="@{viewModel.pageType == DetailsPageType.Details ? @color/phone_text_title : @color/phone_text_hint}"
android:textSize="15sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -83,15 +83,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_blue"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_primary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -126,15 +120,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_orange"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_orange_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -169,15 +157,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_card"
android:background="@drawable/bg_phone_stat_green"
android:orientation="horizontal">
<View
android:layout_width="4dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:background="@color/phone_tag_green_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -167,7 +167,7 @@
</LinearLayout>
<!-- 包装类型(半宽) -->
<!-- 包装类型 | 业务类型(Bug #633:业务类型紧跟包装类型) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -181,20 +181,6 @@
title='@{"包装类型"}'
value='@{viewModel.str(viewModel.maWbData, "packageType")}' />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<!-- 业务类型 | 入库时间 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -202,13 +188,6 @@
title='@{"业务类型"}'
value='@{viewModel.str(viewModel.maWbData, "businessName")}' />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"入库时间"}'
value='@{viewModel.str(viewModel.maWbData, "opDate")}' />
</LinearLayout>
<!-- 运单类型(整行) -->
@@ -243,6 +222,14 @@
title='@{"备注"}'
value='@{viewModel.str(viewModel.maWbData, "remark")}' />
<!-- 入库时间(整行,Bug #633:单独一行放在备注下面) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
title='@{"入库时间"}'
value='@{viewModel.str(viewModel.maWbData, "opDate")}' />
</LinearLayout>
<!-- ==================== 卡片2:件重信息 ==================== -->

View File

@@ -6,6 +6,8 @@
<import type="android.view.View" />
<import type="com.lukouguoji.gjc.page.assemble.AssembleWaybillColumns" />
<variable
name="bean"
type="com.lukouguoji.module_base.bean.AssembleWaybillBean" />
@@ -44,7 +46,7 @@
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.WB_NO, bean.showRemaining)}"
android:gravity="center"
android:text="@{bean.waybillNo}"
android:textColor="@{bean.isMarked ? @color/text_red : @color/text_normal}"
@@ -52,17 +54,25 @@
<!-- 件数 -->
<TextView
android:layout_width="120dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.PIECES, bean.showRemaining)}"
android:gravity="center"
android:text="@{bean.piecesDisplay}"
android:textColor="@{bean.isMarked ? @color/text_red : @color/text_normal}"
android:textSize="14sp" />
<!-- 件数与重量列间距(列宽见 AssembleWaybillColumns) -->
<Space
android:layout_width="0dp"
android:layout_height="1dp"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.GAP, bean.showRemaining)}" />
<!-- 重量 -->
<TextView
android:layout_width="120dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.WEIGHT, bean.showRemaining)}"
android:gravity="center"
android:text="@{bean.weightDisplay}"
android:textColor="@{bean.isMarked ? @color/text_red : @color/text_normal}"
@@ -72,7 +82,7 @@
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.2"
layoutWeight="@{AssembleWaybillColumns.weight(AssembleWaybillColumns.FLIGHT, bean.showRemaining)}"
android:gravity="center"
android:text="@{bean.fLightInfo}"
android:textColor="@{bean.isMarked ? @color/text_red : @color/text_normal}"

View File

@@ -104,7 +104,8 @@
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>

View File

@@ -86,6 +86,29 @@
</LinearLayout>
<!-- 入库件数 | 入库重量(Bug #633) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"入库件数"}'
value="@{bean.inPcText}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"入库重量"}'
value="@{bean.inWeightText.empty ? `` : bean.inWeightText + ` kg`}" />
</LinearLayout>
<!-- 出库人 | 出库时间 -->
<LinearLayout
android:layout_width="match_parent"
@@ -109,5 +132,28 @@
</LinearLayout>
<!-- 出库件数 | 出库重量(Bug #633) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"出库件数"}'
value="@{bean.outPcText}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
title='@{"出库重量"}'
value="@{bean.outWeightText.empty ? `` : bean.outWeightText + ` kg`}" />
</LinearLayout>
</LinearLayout>
</layout>

Some files were not shown because too many files have changed in this diff Show More