Compare commits

64 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
YANG JIANKUAN
388d605869 docs: 新增双端合入主干交接文档,修正 CLAUDE.md 过期信息
- documents/双端合入主干交接文档.md:分支状态、平板/手机开发状态、合并内容、
  开发规则、验证覆盖、待决事项与注意事项
- CLAUDE.md:版本 1.8.7 (87);JDK 17 运行 / Java 1.8 编译目标;模拟器路径与
  AVD 名(AeroLogic_Phone sw=394dp / AeroLogic_Tablet);运抵长按浮层与已删除的
  补充信息页;分支约定处链接交接文档

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:10:45 -07:00
YANG JIANKUAN
131eb031ba merge: 手机适配分支 feature/phone-ui-adaptation 合入 main,平板与 5.5 寸手机统一在主干开发
合并原则:业务层以 main 平板为准,手机端只做 UI 适配。
- 冲突布局:平板改动三方合并进 layout-sw600dp/,手机布局保留并补齐主干新增业务
- Bean/ViewModel 冲突取平板业务逻辑(计重托盘联动、开始计重运抵重量自动计算、
  出港查询 dateState、仓库入/出库件数重量、清仓备注等),手机仅保留展示字段
- 主干新功能同步到手机:运抵海关放行/分单号筛选/回执走 getResponse、删除申报;
  出库交接装机状态与允许装机/配运;出港移库详情与编辑共用 IntExpMoveEditActivity;
  出入港仓库清仓备注、出库弹框件数重量、进港仓库 pcLabel;ULD 所在港/位置
- 新增手机变体:出库/入库/清仓/配运/删除申报弹框、移库详情编辑页、海关回执页;
  PhoneBottomBar 增次操作按钮
- 手机对齐平板:去掉 likeNo 与平板没有的筛选入参、恢复平板默认查询日期,
  补齐平板表单/筛选项与操作入口;移除手机专属 IntExpMoveDetailActivity
- CLAUDE.md 记录单分支约定与「业务以平板为准」规则,CHANGELOG 补合并记录

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:20:03 -07:00
YANG JIANKUAN
42647c2136 docs: 后端反馈联调结论入库——问题清单 P 列结论 + 新增 #35~#41,更新 api-doc 口径与 CHANGELOG
- documents/后端接口对接问题清单.xlsx 基于后端返回版本(保留其解决方案列与截图),
  新增「前端联调结论(2026-10-09)」列逐条记录 #1~#34 结论,追加联调新发现 #35~#41
- api-doc.md 追加 2026-10-09 最新字段口径;CLAUDE.md 更新待办与新坑(数字输入框光标跳位)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:44:57 -07:00
YANG JIANKUAN
5e8fe7d69d fix(ULD管理): 新增/修改补充最大装载容积、最大载重为可编辑必填(问题清单 #36)
- 联调发现 saveUld 缺 maxVolume 或 maxWeight 时后端直接 500,手机新增表单(原仅详情展示两项)必然失败
- 按平板表单口径改为新增/修改可编辑(数字键盘,新增必填)并前端校验
- 内网实测:PMC12349JG 新增成功,source/inStoreTime(补 00:00:00)/nowPort/maxVolume/maxWeight 均落库,测试数据已清理

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:44:57 -07:00
YANG JIANKUAN
6f813f9cff feat(进港移库): 移库人取 opUserName,已移库照片经 movedDetail 回填(问题清单 #32~#34)
- #32 fdate/fno/spCode 后端已实装(search/searchMoved),手机端原有传参零改动生效
- #33 searchMoved 已回填 opId/opDate 并新增 opUserName:详情「移库人」改绑姓名(回退 opId)
- #34 新增 GET /DomImpMove/movedDetail:已移库运单(列表无 mawbId)拍照页按 moveId 回填照片
- 已移库运单 searchMoved/movedDetail 均无 mawbId,而 modify 以 mawbId 定位(实测返回「未能获取到运单id」),
  提交前给出明确提示,不再发出必失败请求;缺口已登记问题清单
- 内网实测:fno=MU2222 待移库 0 / 已移库 2 与直调一致;详情移库人「张三」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:34:59 -07:00
YANG JIANKUAN
68e6c7c52b feat(进港查询): 三格统计改取 pageQueryTotal 分项 storedNumber/departedNumber(问题清单 #30)
- 已入库/已出库由「已加载数据客户端计数」改为服务端全量口径,删除 phoneLoadedList 累计逻辑
- 内网实测:全量 123/119/4,outState=0/1 分别 119/0 与 4/4 口径自洽;当日 1/1/0 与 App 一致

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:33:43 -07:00
YANG JIANKUAN
cca62aae29 feat(提取出库): 筛选对齐平板参数,出库人改绑 refUserName,移除无出参占位(问题清单 #28/#29)
- #28 后端不新增入参/出参:筛选弹层改为平板同款 缴费日期起止(beginDate/endDate)/代理人/特码,
  移除航班日期/航班号;卡片移除「提货车辆」,详情移除「目的港」「业务类型」占位
- 运单号入参 no → wbNo(与主干平板端修复一致,no 为 8 位编号)
- #29 Tab 继续按 outState 过滤(后端确认生效);「出库人」改绑新增出参 refUserName(dlvName 是提货人)
- 内网:请求体已确认携带 wbNo/outState;测试环境该接口暂无数据(total=0),卡片展示待有数据后复核

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:33:27 -07:00
YANG JIANKUAN
1229ecc920 feat(进港仓库): 筛选对齐平板、统计取 notClearNumber/clearNumber、详情按平板字段(问题清单 #25~#27)
- #25 后端不新增入参:筛选弹层改为平板同款 航班日期/航班号/库位/清仓结果,
  移除始发站/代理/特码/运单类型/业务类型/品名
- #26 未清仓/已清仓取 pageQueryTotal 出参 notClearNumber/clearNumber,统计请求 3 次 → 1 次
- #27 进港详情按平板:航程 range 替代 始发站/中转站/目的站(无经停站),入库时间(opDate 恒空)
  改为平板同款 出库时间 dlvTime
- 内网实测:48/42/5;详情 航程 LAX-HFE / UN编号 / 锁定状态 正常显示
- 注意:IntImpStorage/pageQuery 当前对所有过滤条件(含平板自用的 wbNo/fno/fdate/location/clearNormal)
  均不生效,属后端缺陷,已登记问题清单

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:32:47 -07:00
YANG JIANKUAN
d2340d46fb feat(出港查询): 三格统计取服务端分项,筛选补日期类型,详情移除锁定状态/计费重量(问题清单 #21/#22)
- #21 已入库/已离港改取 pageQueryTotal 出参 storedNumber/departedNumber(全量口径),删除客户端计数
- #21「起始时间参数无意义」:内网实测 beginDate/endDate 仅在携带 dateState 时生效,
  手机筛选新增「日期类型」(入库/离港,默认入库,与主干平板一致),有起止日期时才携带 dateState
- #22 后端确认无需展示:运单信息移除「锁定状态」「计费重量」,件重区改两列对齐
- 内网实测:当日入库 2/2/0;切换离港 0/0/0,与直调一致

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:31:04 -07:00
YANG JIANKUAN
a431c09bd0 feat(出港仓库): 筛选对齐平板查询参数,三格统计改取 notClearNumber/clearNumber(问题清单 #23/#24)
- #23 后端不新增入参:筛选弹层改为平板同款 航班日期/航班号/库位/清仓结果,
  移除代理/特码/目的站/运单类型/业务类型/品名(内网实测 agentCode/spCode/dest/awbType/goodsCn 均被忽略)
- #24 未清仓/已清仓直接取 pageQueryTotal 出参 notClearNumber/clearNumber,
  手机端统计请求由 3 次降为 1 次(与平板相同)
- 内网实测:全量 7/7/0;fno=MU2222 时 3/3/0 且列表同步过滤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:47:41 -07:00
YANG JIANKUAN
b522efa6b9 feat(出港运抵): Tab 改服务端 arrivalStatus 过滤,移除补充信息页(问题清单 #19/#20)
- #20 待运抵/已运抵 Tab 改为入参 arrivalStatus=0/1 服务端过滤(与平板运抵状态筛选一致),
  角标为同条件 pageQueryTotal 全量票数;删除客户端拆分逻辑与 GjcMaWb.isPending
  (口径变化:申报状态 E 等异常态归入「待运抵」,以后端 arrivalStatus 为准)
- #19 后端确认目前无需「补充信息」页:删除 IntExpArriveSupplementActivity/ViewModel/布局与清单注册,
  待运抵卡片不再弹长按浮层(已运抵保留 重置/回执)
- 筛选移除「代理」:内网实测 IntExpArrive/pageQuery 忽略 agentCode,与平板查询参数对齐
- 内网实测:全量 待运抵 6 / 已运抵 2,与 pageQuery total 一致

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:45:27 -07:00
YANG JIANKUAN
40edbe7c67 feat(出港计重): 对接后端统计分项/计重人/通道号/托盘字段,筛选对齐平板(问题清单 #12~#18)
- #12 三格统计改取 pageQueryTotal 出参 waitCheckInNumber/checkingNumber(一次请求),
  不再依赖 checkIn 入参过滤(内网实测 pageQuery 的 checkIn 过滤仍被忽略,统计出参正确)
- #13 待计重筛选对齐平板:航班日期/代理人/特码(移除通道号/承运人/运单类型)
- #14 计重记录筛选对齐平板:航班日期/代理人/特码(移除入库日期起止;dest/opId 实测被忽略一并移除)
- #15 运单详情「通道号」绑 checked/pageQuery 出参 passageWay
- #16 计重明细/修改记录「计重人」绑 listRecordByWh 出参 opUserName(回退 opId)
- #17 arriveFlag 已回填,「已提前运抵」置灰态生效(无需改代码,实机确认)
- #18 托盘改用平板同款 trayNumber + trayTotalWeight(= 数量 × 自重,自重默认 18kg);
  记录卡片/修改记录托盘自重绑 trayTotalWeight;修复托盘输入框 Double 回写致光标跳位(输 18 存成 81)
- 内网实测:三格 6/5/1;分托计重 trayNumber=2/trayTotalWeight=36 落库;修改记录保存 18.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:45:18 -07:00
YANG JIANKUAN
415b685a85 feat(ULD管理): 对接后端调整——两态状态/来源/inStoreTime/批量删除接口(问题清单 #7~#11)
- #7 状态维持两态:移除「占用」预留映射与橙色标签
- #8 来源 source(空运/陆运)出参与保存入参已支持;查询入参不支持 source,筛选弹层移除来源
- #9 查询参数与平板一致(uld/status/uldSuffix):筛选弹层改为 状态 + 所属航司,移除入库时间/进港航班号
- #10 批量删除改为一次调用 /eqm/uld/deleteUldBatch(请求体 ULD 编号数组)
- #11 入库时间改绑 inStoreTime;所在港改绑 nowPort(与平板端一致);手机日期行只产出 yyyy-MM-dd,
  保存时补齐 00:00:00(仅日期后端返回 400,内网实测)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:14:04 -07:00
YANG JIANKUAN
b3b884504d feat(出库交接): 筛选对齐平板查询参数,Tab 角标改用 uldNumber(问题清单 #4/#6)
- 后端不提供交接人查询入参:手机筛选弹层「交接人」替换为平板同款「目的站」(fdest),移除 hoId 与交接人下拉
- pageQueryTotal 已修复且新增 uldNumber(板箱数),Tab 角标改取 uldNumber
- ManifestTotalDto 补齐后端统一统计 DTO 的分项字段(uldNumber/storedNumber/departedNumber/
  notClearNumber/clearNumber/waitCheckInNumber/checkingNumber/flightNumber)
- 内网实测:无条件 已交接=2(列表 total 同为 2),fdest=PEK 时列表与角标均为 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:14:04 -07:00
YANG JIANKUAN
f6b3842882 feat(出港移库): 对接后端新增出参 flight/businessName/opUserName(问题清单 #1~#3)
- 卡片航班信息绑 flight(航班日期/航班号),不再占位
- 详情页业务类型绑 businessName、移库人绑 opUserName(均回退编码)
- 内网实测:已移库运单 opId/opdate/opUserName 均已回填

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:14:04 -07:00
YANG JIANKUAN
18e438550c chore: 本地版本号同步线上 1.8.7(versionCode 87),避免联调时触发升级弹框
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:14:04 -07:00
0d9406af13 chore: SDK 版本管理由 vfox 迁移至 mise 原生配置
- 新增 mise.toml(java = 17,vendor 由全局 shorthand_vendor 控制)
- 删除 vfox 配置 .tool-versions / .vfox.toml
- 删除指向旧机器 vfox npx 路径的过时 MCP 定义 .claude/mcp.json
- 清理 .gitignore 与 settings.local.json 中的 vfox 残留

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-02 12:09:41 +08:00
c595a3046b chore: 引入 mobile-mcp 项目级 MCP 并沉淀 ADB×MCP 工具选型纪要
mobile-mcp 固定 1.0.7 版本;verification.md 新增 2026-10-02 工具选型纪要
(ADB/ui_probe.sh 与 Mobile MCP 的分工规则、实测踩坑、本机环境更新)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-02 12:09:11 +08:00
f90c421d44 feat: update claude md 2026-08-06 10:26:23 +08:00
49f11ef8e6 fix: 进港移库归属更正——删除误建IntImpMove,重新适配到国内进港DomImpMove
设计稿11「进港移库」经确认归属国内进港移库(DomImpMove后端8接口齐全),
此前误判为国际进港并预接了不存在的IntImpMove模块(问题清单#31作废)。

- 删除module_gjj的IntImpMove全套实现(3页面+Bean+Api+菜单+路由+权限串)
- GnjMoveStashListActivity双形态适配:待移库/已移库Tab走search/searchMoved
  双端点,Tab角标、筛选弹层4项、全选/批量移库复用平板既有链路
- 新增手机专属页:运单详情(Intent传bean零请求)+拍照上传(detail回填
  已有照片,modify最小体三字段提交,与平板移交编辑页同链路)
- 手机首页入口改挂国内Tab(GnViewModel补通用route跳转,权限串AppDomImpMove)
- 内网真实数据双端验证通过;Proxyman A/B:wbNo/awbType生效,
  fdate/fno/spCode被后端静默忽略(#32)、opId/opDate恒null(#33)
- 问题清单更正#31+新增#32~#34,CLAUDE.md/CHANGELOG/api-doc.md同步

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 12:17:10 +08:00
eb0655d3fc fix: 11模块设计稿全量走查修正——时间线状态pill/手机短标题/ULD字段顺序/计重卡片图标
设计稿逐态渲染对照走查(01~11 全模块)后的修正:
- 运单追踪竖排时间线为已发生节点补状态pill(绿底展示该环节日志content,超长省略)
- 11 页手机端标题按设计稿改短标题(DeviceUtil.isPhone 分支,平板长标题零变化):
  出库交接/出港待计重/计重记录/计重明细/开始计重/出港运抵/出港移库/出港查询/
  出港货物/进港查询/进港货物
- ULD 编辑表单「来源」移至「所在港」之后(设计稿顺序)
- 出港计重列表卡片与计重记录卡片补字段小图标(airplane/location/person/tag)

接口缺口台账复核:#31 进港移库整模块缺失成立(api-doc 全模块清单+报价单双重印证);
#19 修正定性——IntExpArrive/declare 入参已含收发货人全套字段,改登记为待确认提交口径;
#10 成立(单条 deleteUld 在、批量无)。

双端抽验:手机短标题三页生效(出港待计重/出库交接/进港货物),平板保持长标题,零崩溃。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 23:12:14 +08:00
a33776d959 feat: 完成进港查询与进港移库页面5.5寸手机端适配(设计稿11页全部完成)
进港查询(10,06出港查询的进港镜像):
- IntImpQueryActivity 手机布局:搜索行 + 三格统计(总票数/已入库/已出库客户端计数)
  + 状态标签卡片(dlvTime/inDate 前端推断)+ 筛选弹层9项(入参全部经api-doc核对存在)
- 详情页与运单追踪复用08/06已适配页零新增;平板侧滑筛选面板判空守卫,Pad端零变化
- 修复代理人空值兜底:转换器将JSON null归一为空串致DataBinding `??` 失效,
  改用Bean计算属性 agentDisplay(06出港查询同步修正)

进港移库(11,首个后端整模块缺失的设计稿页):
- 全新手机专属3页:列表(双Tab+角标/筛选4项/多选批量移库/卡片长按拍照浮层)
  + 运单详情(三卡片,Intent传bean零请求)+ 拍照上传(复用ImageSelectNewViewHolder,
  pic/originalPic/picNumber三字段规范)
- 后端无IntImpMove模块(api-doc核对),按IntExpMove同名约定预接四接口
  + 预定义权限串AppIntImpMove,后端交付后前端零改动生效(缺口#31)

其他:手机首页国际Tab新增进港查询/进港移库入口;新增资源bg_phone_btn_green/
bg_phone_card_header_orange/bg_phone_photo_add/phone_amber;问题清单登记#30~#31;
双端模拟器验证零崩溃(手机Map Local mock全交互链路,平板回归零变化)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:35:56 +08:00
b2a2cce585 feat: 完成提取出库页面5.5寸手机端适配
提取出库(09,module_gjj):
- IntImpPickUpDLVActivity 手机布局:搜索行+待出库/已出库状态Tab(outState 为
  pageQuery 文档定义入参 0/1,Tab 真实过滤+角标双 pageQueryTotal,抓包证实携带)
  +双形态卡片(isPickedUp=dlvTime非空:待出库勾选框+橙标签+库位行/已出库绿√+
  出库人·出库时间行)+卡片内 PhoneStatBox 三列+PhoneBottomBar(仅待出库Tab显示,
  复用既有 confirmOutbound)
- 手机专属新页 IntImpPickUpDetailActivity 运单详情:蓝头运单卡+绿头提货信息卡,
  Intent 传 Serializable bean 零新增请求
- 筛选弹层 4 项(航班日期为未定义入参预留传参);手机进入时清空平板默认
  「当天缴费日期」查全量,平板默认值不变
- IntImpPickUpDLVBean 增加 isPickedUp 计算属性;手机首页「国际」Tab 加入口

修复:checkAllClick 全选后底部「已选 N 项」不更新(批量勾选不走单卡 FlowBus
事件,需自行同步 selectedCountText),实测修复生效

验证:双端 Proxyman Map Local mock 口径验证通过,平板回归一致(含缴费日期默认值),
崩溃 0;后端缺口 2 项登记问题清单 #28~#29(无 fdate 入参、出参无车牌/目的港/
业务类型;outState 过滤与 dlvName=出库人语义待内网实测)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:31:58 +08:00
3b732c3aec feat: 完成出港仓库与进港仓库页面5.5寸手机端适配
出港仓库(07,module_gjc):
- IntExpStorageUseActivity 手机布局:搜索行+三格统计(总票数/未清仓/已清仓,
  clearNormal=0/1 双 pageQueryTotal 仅手机发起)+卡片级四操作按钮(清仓/修改
  库位/出库/入库,已清仓置灰),取代平板勾选+底部批量条,复用既有 performXxx 链路
- 详情/运单追踪复用出港查询已适配页(GjcQueryDetailsActivity/LogDetailActivity),零新增页面
- 清仓/入库/修改库位弹框双变体化(手机底部弹层);修改库位手机版增加「原库位」单选;
  出库确认 AlertDialog → ConfirmDialogModel(项目强制规范)

进港仓库(08,module_gjj,07 的进港镜像):
- IntImpStorageUseActivity 手机布局:搜索框「运单号/航班号」分流(含字母→fno,
  抓包证实)+始发站字段/筛选(fdep 入参)
- 进港详情 IntImpQueryDetailsActivity + 3 Fragment 首次手机适配(报文区 3 项,
  件重区直取 inPc/inWeight,库位卡片优先中文姓名)
- 修改库位手机弹层「原库位」只列未出库库位(沿用平板校验)

公共:GjcMaWb 增加 isCleared 计算属性;新增 bg_phone_btn_disabled;手机首页
「国际」Tab 增加出港仓库/进港仓库入口;debug manifest 直启项

验证:双端 Proxyman Map Local mock 口径验证通过(开发机不在内网),平板回归一致,
崩溃 0;后端缺口 5 项登记问题清单 #23~#27(筛选入参缺失/clearNormal 分项统计与
detail 推断字段待内网实测)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:49:59 +08:00
33c9f0e030 feat: 完成出港查询页面5.5寸手机端适配
- 列表/详情/运单追踪三页同名布局双变体(平板移 layout-sw600dp/,Kotlin 零设备分支)
- 列表:搜索行 + 三格统计(总票数/已入库/已离港,已加载数据客户端计数)+ 状态标签卡片
  (已离港/已入库/未入库,前端按 fclose/opDate 推断)+ 9 项底部筛选弹层(特码改下拉)
- 详情:3 Tab 复用 ViewPager2+Fragment,PhoneKvItem 网格;入库件数/重量按 warehouseList 求和
- 运单追踪:公共页 LogDetailActivity 双变体首例,手机竖排时间线动态构建,平板横向步骤条不动
- 修复 PhoneFilterPanel 筛选项超屏时底部按钮被挤出屏幕(内容插槽加 ScrollView)
- PhoneKvItem 新增 tagColor 绿色高亮(海关放行),占位符不套标签底
- 手机首页国际 Tab 加出港查询入口;后端缺口 #21/#22 登记问题清单
- 双端实机验证通过(内网真实数据 + Proxyman Map Local 补验未入库兜底态)
- skill 新增 references/proxyman.md:抓包/直调 A/B/Mock 与 api-doc 配合规范

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:13:44 +08:00
436202f15b fix: 出港运抵手机端对照设计稿review修正与重置弹框适配
- 状态重置弹框改手机底部弹层:dialog 布局同名双变体(Pad 移 layout-sw600dp),
  弹出位置按形态切 DIALOG_TYPE_BOTTOM/CENTER,为弹框适配首例
- 主列表补空态(文案按 Tab 切换)、回执页补「暂无回执内容」兜底
- 分单明细长按浮层按设计稿补「回执」按钮;底部统计加分母「已选 N / M 项」
- Tab 角标初始显示 0
- PhoneStatusTab/PhoneFilterPanel 补 dispatchSave/RestoreInstanceState 状态屏蔽(历史遗漏)
- 双端实机验证:手机端各修正点与设计稿一致且 crash=0;平板端列表与批量重置弹框回归无变化

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:21:33 +08:00
ef8e7cfacb feat: 完成出港运抵页面5.5寸手机端适配
- 主列表新增待运抵/已运抵 Tab(客户端按申报状态拆分)+ 卡片长按浮层(补充/重置/回执)
- 新增分单明细页:复用主列表已加载数据,零新增请求
- 新增回执内容只读页
- 新增补充信息页:参照 Pad 端收发货人信息批量应用模式(后端接口暂缺,已登记问题清单)
- 手机首页「国际」Tab 增加「出港运抵」入口
- 修复两个平台坑:RecyclerView 内 GONE 不塌陷高度;Serializable Intent 传递
  遇 @Transient 字段反序列化为 null 导致的崩溃
- 登记后端接口缺口 2 项(#19~#20)至问题清单

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-30 11:44:51 +08:00
1eb288a703 feat: 完成出港计重页面5.5寸手机端适配
- 待计重列表:三格统计、卡片单票提前运抵、筛选弹层(通道号/承运人/运单类型/代理人)
- 开始计重:预配统计卡 + PhoneFormRow 表单
- 计重记录:三格统计、筛选弹层(入库日期/代理人/特码/目的站/计重人)
- 计重明细:新增手机端全屏「修改记录」覆层,复用平板编辑链路
- 新增手机专属「运单详情」页
- 手机首页「国际」Tab 增加「出港计重」入口
- 登记后端接口缺口 7 项(#12~#18)至问题清单

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-29 20:09:55 +08:00
5a019a55c4 feat: 完成ULD管理页面5.5寸手机端适配
- 手机版列表(三色状态标签卡片 + 筛选弹层 + 浮动新增 + 全选/批量删除)、
  一套 activity_uld_edit 手机布局按 pageType 覆盖 ULD 新增/修改/详情三态
  (详情右上铅笔进修改、状态底部选择面板、所在港默认 HFE、手机端必填校验);
  平板布局迁至 layout-sw600dp,Kotlin 双端共用,平板端实机回归零变化
- 新增公共组件 PhoneFormRow(行式表单,INPUT/SELECT/DATE/TEXT 四形态,
  SELECT 内置底部选择面板);PhoneBottomBar 增加 actionDanger 危险按钮样式;
  新增 FAB/danger按钮/浅红标签/垃圾桶/铅笔等资源
- ULDBean 补解析接口既有出参 ifNo/efNo/checkInDate/checkOutDate 并预留 source;
  批量删除以队列串行调用单条 deleteUld 实现
- 手机首页「国际」Tab 接入 ULD管理入口(ARouter + ComprehensiveUld 权限),
  登录→菜单→页面全链路实机验证通过,方向无闪屏,crash=0
- 后端缺口 5 项(状态三态、来源字段、筛选入参、批量删除接口、字段语义)
  登记《后端接口对接问题清单.xlsx》#7~#11;skill 沉淀本次踩坑
  (FAB elevation 盖弹层、Spinner 异步回调竞态、登录哈希回填)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:30:42 +08:00
bbc29ca2e1 fix: 出库交接交接状态参数名改为handoverState并完成内网验证
- 按 api-doc 接口文档修正交接状态入参 hoState → handoverState(原参数被后端
  静默忽略,两个 Tab 返回同一批数据);内网实机验证 0/1 过滤与 App 端到端链路
  均生效,平板端回归无影响
- 新增《后端接口对接问题清单.xlsx》:按模块/页面/接口定义/接口地址索引的后端
  对接文档,含 5 项待处理缺口(移库缺航班字段/操作人姓名/数据不回填、交接缺
  交接人入参、pageQueryTotal 出参恒 0)+ 1 项已验证归档
- phone-adapt skill 新增 references/api-doc.md(api-doc MCP 核对流程 + 内网
  直调验证技巧),阶段 2 改为「先查接口文档再问用户」,修正 verification.md
  中「后端不认 hoState」的过时误判结论
- CLAUDE.md / CHANGELOG.md 同步走查与验证结论

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 15:13:43 +08:00
db062c8ee3 feat: 完成国际出港移库/出库交接页面5.5寸手机端适配
- 落地手机端双形态适配框架:同名布局+资源限定符(layout/手机、
  layout-sw600dp/平板)自动切换,DeviceUtil 设备形态判定,7个
  手机版公共组件(PhoneSearchBar/StatusTab/DataLayout/FilterPanel/
  BottomBar/StatBox/KvItem)
- 完成「国际出港移库」「国际出港出库交接」两页手机端适配,新增
  各自详情页(IntExpMoveDetailActivity/IntExpOutHandoverDetailActivity)
- 手机端首页菜单接入"出港移库""出库交接"入口
- 修复手机端进入页面先横屏后转竖屏的闪屏问题:Manifest 全项目
  192 处改为 screenOrientation="unspecified",由 BaseActivity
  按设备形态运行时锁定方向
- 修复该方案的中间版本在平板端引入的回归(先竖后横 + UI放大1.6倍)
- AutoSize 补充手机竖屏 390×844 设计基准
- 修复 CHANGELOG.md 因脚本异常导致的内容重复损坏(膨胀至12万行),
  恢复正常结构并补充本次变更记录

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 14:18:29 +08:00
532 changed files with 49620 additions and 14037 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

@@ -0,0 +1,309 @@
---
name: phone-adapt
description: 参照 documents/5.5寸手持端设计文件/ 的 HTML 设计稿,把 AirLogistics 指定页面适配成 5.5 寸手机竖屏版本——浏览器渲染设计稿逐态截图分析、复用 Phone* 公共组件写手机布局、复用现有 ViewModel 与接口、保证 Pad 端零改动,最后走完整登录流程实机联调并与设计稿逐项核对 UI。当用户说"适配手机端/做手机版/手持端适配"、点名某个设计稿入口(如"02 出库交接"、"做一下出港计重")、要求"按设计稿还原页面"、或提到 5.5 寸/手机版/双形态时,都要使用本 skill,即使没明说"适配"二字。
---
# 5.5 寸手持端页面适配
把一个已有的平板页面,按设计稿适配出手机竖屏版本。核心约束是**只重写布局,业务逻辑与接口全部复用**,
Pad 端行为必须零变化。
先读 `CLAUDE.md` 的「5.5 寸手持端(手机版)双形态适配规范」章节——那里是权威规范,本 skill 是执行流程。
## 前置确认
需要用户给出**目标页面**(设计稿目录名或业务名,如「02 出库交接」/「出港计重」)。
其余自己查:`documents/5.5寸手持端设计文件/00_全部入口总览.html` 列出全部 11 个入口,
每个子目录下可能有多个 HTML(主页 + 详情 + 子页面),都要处理。
对应的平板页面靠业务名在 `module_gjc/gjj/gnc/gnj` 里搜 Activity。**注意认准首页菜单实际跳转的那个
Activity**,同名旧版文件在这个仓库里很常见。
## 阶段 1:渲染设计稿,看图不看代码
设计稿是带 Tailwind + JS 的交互原型,一个 HTML 里常塞了列表页、Tab、详情页、筛选弹层,靠 JS 切换。
**只读 HTML 源码会出错**,必须渲染成图:
```bash
S=.claude/skills/phone-adapt/scripts
# 1) 先看有哪些状态可切
python3 $S/design_shots.py --html "documents/5.5寸手持端设计文件/02_出库交接/出库交接.html" --list
# 2) 按 --list 结果逐态渲染(照抄 onclick 里的真实参数最省事)
python3 $S/design_shots.py --html "…/出库交接.html" --out /tmp/design \
--state "01_列表:" \
--state "02_已交接Tab:switchTab('shipped');" \
--state "03_详情:showDetail({id:'PMC90901CZ',status:'已交接',rackNo:'076'});" \
--state "04_筛选弹层:toggleFilterDrawer(true);"
```
然后用 Read 工具**逐张看图**,抽取:配色、字号、圆角、间距、图标、控件顺序、空态、
各状态下哪些元素显示/隐藏(例如「已交接」Tab 下底部操作条整条消失)。
`--list` 输出里如果出现 `window.location.href='xxx.html'`,说明是多文件原型,那些文件同样要渲染。
**设计稿的 JS 可能有 bug,别照抄字段映射。** 出库交接的详情页就把「航班日期」绑成了重量、
「备注」绑成了航班号(HTML 里 id 重复)。判断依据是**静态 label 文案**:label 写「航班日期」
就绑 `fdate`,写「板型」就绑 `boardType`。label 是设计意图,JS 只是 demo 接线。
标签文案同理:卡片上写死的「IMP代码」是占位符,实际要绑 `bean.dgrCode` 的值。
## 阶段 2:对齐接口能力,不要臆想
把设计稿的每个筛选项/Tab/字段,对照平板 ViewModel 已有的字段:
- **已有字段** → 直接复用(如出港移库的 Tab 直接绑既有 `moveState`)
- **平板没有的新维度**(新 Tab 状态、新下拉、新排序)→ **先查 api-doc MCP 拿权威参数名**,
流程见 `references/api-doc.md`(连接方式 + 5 个工具 + 逐接口核对清单)
**最高决策规则(用户 2026-07-29 拍板,此后不再逐次询问):业务改动完全以设计稿为准,不做妥协。**
接口不支持设计稿的某个维度时:UI 照设计稿完整实现(绑预留字段/占位,传参后端静默忽略无副作用),
缺口直接登记 `documents/后端接口对接问题清单.xlsx` 提修改需求,后端补齐后前端自动生效。
仅当设计稿本身歧义(label 无法推断字段语义)才用 `AskUserQuestion` 问用户。
这一步不能猜,也**不要拿「实测不生效」当结论**。出库交接第一版猜了 `hoState`,实测两个 Tab
返回同一批数据,一度误判为「后端不支持」;后查 api-doc 才发现正确参数名是 `handoverState`——
后端对不认识的参数是**静默忽略**,不报错,实测只能证明「这个名字不对」,证不出「后端没这能力」。
「参数是否真的生效」的 A/B 验证优先走 Proxyman MCP(`create_compose_http_from_flow` 复制
真实请求改参重发,token 自动带上),分工与流程见 `references/proxyman.md`:
**api-doc 管契约(该有什么),Proxyman 管事实(实际发/回了什么)**。
核对完把结论落两处(模板与格式见 `references/api-doc.md`):
- 后端真实缺口(缺入参/缺出参/数据不回填)→ 记入 `documents/后端接口对接问题清单.xlsx`,
这是与后端对接的正式文档,按「模块 → 页面 → 接口定义 → 接口地址」索引,**每个页面适配完都要维护**
- 参数名/字段绑定错误这类前端问题 → 直接改代码
阶段 5 仍要用真实请求体验证新参数确实生效;没生效就在清单文档与交付说明里如实标注,
不要让它看起来是通的。
### 开工前对齐方案
分析完先把结论摊给用户,避免闷头做错方向。一次说清,别挤牙膏:
```
📱 页面适配方案:<页面名>
设计稿:documents/5.5寸手持端设计文件/<目录>/(N 个 HTML:主页 / 详情 / …)
平板页面:XxxActivity + XxxViewModel(module_xxx)
手机版结构:
搜索行:<字段>(含扫码/筛选按钮)
状态 Tab:<Tab1> / <Tab2> → 绑定 <字段名>
列表卡片:<字段…>,两种形态(<形态A> / <形态B>)
底部操作条:全选 + 已选统计 + [<按钮>]
筛选弹层:<字段1> / <字段2> / <字段3>
二级页:<详情页名>(新增 Activity / 复用已有)
复用组件:PhoneSearchBar、PhoneStatusTab、PhoneStatBox…(无需新增 / 需新增 XXX)
接口:列表 <接口> 复用;新增参数 <名>(待你确认);新增字典 <DictUtils 方法>
Pad 端影响:无(新字段默认空、额外请求由手机开关控制)
设计稿与平板的差异:<如手机版无「配运」按钮>
确认后开始编码。
```
## 阶段 3:写布局,复用公共组件
`module_base/.../ui/weight/phone/` 已有 7 个组件,**优先复用,不要重写 XML**。
组件清单、属性表与用法见 `references/components.md`(写布局前读一遍)。
改造步骤:
1. 平板布局 `git mv` 到 `res/layout-sw600dp/`(item 布局同理)
2. 在 `res/layout/` 下建**同名**文件写手机布局
3. Kotlin 侧 `layoutId()` / `itemLayoutId` **一律不动**,不做设备分支
三条硬约束(违反会编译失败或运行时崩溃,细节见 `references/pad-safety.md`):
| 约束 | 说明 |
|------|------|
| 两变体 `<variable>` 完全一致 | 手机变体新增 `activity` 变量时,平板变体也要补上(写注释说明平板不用它) |
| 共用 id 双方都要有 | ViewHolder/Activity 引用的 id(`srl`/`rv`/`iv_icon` 等)两边都保留 |
| 单侧独有 id 自动 `@Nullable` | 如手机独有的 `ll_detail`,ViewHolder 里必须判空调用 |
**一套 item 布局覆盖多种卡片形态**,不要为「待交接/已交接」建两个布局。按 bean 状态切
visibility 即可(Bean 上加个只读计算属性如 `isHandovered` 让 XML 更干净;计算属性不参与 Gson
序列化,安全)。
## 阶段 4:接业务,且不碰 Pad
手机端新增的查询维度用这个模式,能做到平板端请求次数与行为零变化:
```kotlin
// ViewModel:新字段默认空值,getData() 里仅非空才进请求
val handoverState = MutableLiveData("") // 平板布局无对应控件 → 恒为空 → 不进请求
val filterParams = mapOf(
/* 原有字段… */
"handoverState" to handoverState.value?.ifEmpty { null }, // 参数名以 api-doc 为准
)
// 手机专属的额外请求(Tab 角标计数、新下拉字典)用内部开关控制
private var phoneExtrasEnabled = false
fun initPhoneExtras() { phoneExtrasEnabled = true; handoverState.value = "0"; /* 加载字典 */ }
```
```kotlin
// Activity:只有手机形态才开启,平板不调用
if (DeviceUtil.isPhone()) viewModel.initPhoneExtras()
```
设备判断只用于**数据默认值与额外请求**,**绝不用于选布局**(选布局靠资源限定符)。
纯 UI 交互(弹层显隐、Tab 切换)写在 Activity,用 `ObservableBoolean` 暴露给 XML,不下沉到 ViewModel。
设计稿若少了平板上的某个按钮(如出库交接手机版只有「交接」没有「配运」),按设计稿做,
但要在交付说明里点出来这个功能在手机端不可达,让用户决定。
新增手机专属页面(详情页等)记得:`app/src/main/AndroidManifest.xml` 注册 +
`app/src/debug/AndroidManifest.xml` 追加 `exported="true"` 便于直启调试。
注册时 `screenOrientation` **必须写 `unspecified`**:
```xml
<activity android:name="…"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="unspecified" />
```
- 写死 `userLandscape` → 手机端「先横屏、约 1 秒后转竖屏」闪一下(方向在建窗口时就定了,代码再纠正已晚)
- 用 `@integer/xxx` 资源引用 → **更糟**:系统解析 Manifest 不套用设备限定符,平板会取到默认(竖屏)值,
先竖后横,且 AutoSize 随之选错基准导致 UI 放大约 1.6 倍
`unspecified` 让系统按设备当前物理方向建窗口,两端起始方向本就正确,
`BaseActivity` 的形态锁定不会产生二次纠正。
### ⚠️ 别忘了加手机端首页入口
手机端首页是 `module_p` 的 `PDAEnterActivity`,「国际」Tab 的菜单在
`module_p/.../ui/enter/gj/GjViewModel.kt`(国内在 `gn/GnViewModel.kt`)。
**菜单是手写清单,不是接口下发**;权限串只做过滤(`authList.contains(bean.key)`)。
2026-10-21 起清单按 `documents/5.5寸首页新样式/` 设计稿分组(`PdaMenuGroup`),新入口加进对应分组。
适配完页面不回来加一行,手机上就**根本没有入口**——出库交接第一版就漏了这步,
只能靠 adb 直启才能进去。加法:
```kotlin
// 加在 GjViewModel.groups 的「国际出港」分组里
ActionBean(
"出港移库",
R.drawable.img_pda_menu_gjc_move, // 设计稿宫格图标(icons/ 源图转 PNG 放 drawable-nodpi,放 xxhdpi 会糊);没有新图标的沿用旧图标
Constant.AuthName.GjcYiKuListActivity, // 权限串与 Pad 端菜单一致
ARouterConstants.ACTIVITY_URL_INT_EXP_MOVE // 填了 route 就走通用跳转,无需 when 分支
),
```
权限串和路由**都以 Pad 端 `app/.../HomeFragment.kt` 里该权限对应的 `ARouter.build(...)` 为准**
(同一业务常有新旧两个 Activity,Pad 菜单实际跳的才是要适配的那个)。
`module_p` 只依赖 `module_base`,看不到业务模块,所以跨模块必须走 ARouter、图标必须复制一份。
菜单没出现时先分清是「没权限」还是「没加清单」:
```bash
adb -s <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
```
## 阶段 5:端到端实机验证
先构建,再双端跑。命令与排障细节见 `references/verification.md`。
```bash
./gradlew assembleDebug 2>&1 | grep -E "^(BUILD|FAILURE)|error:"
```
构建过后核对 DataBinding 变体是否如预期(单侧 id 应为 `@Nullable`):
```bash
grep -B3 "llDetail" module_gjc/build/generated/data_binding_base_class_source_out/debug/out/\
com/lukouguoji/gjc/databinding/ItemXxxBinding.java
```
**手机端**(真实数据链路,从登录开始):
```bash
P=.claude/skills/phone-adapt/scripts/ui_probe.sh
adb -s <phone> install -r -d app/build/outputs/apk/debug/app-debug.apk
# 走登录 → 底部「国际」Tab → 点新加的菜单项进入(顺便验证入口加对了)
# 同名文字多处时 tap-text 会提示命中数,用第 4 个参数指定序号(如底部 Tab 的「国际」)
# 若账号确实没有该权限,改用冷启动直启(见 verification.md)
$P <phone> form # 应打印 PHONE(sw=411dp…)
$P <phone> wait-text "待交接" # 点击前先等页面就绪:加载弹窗会吞掉点击
$P <phone> tap-text "已交接" # 按文字点击,比手算坐标可靠
$P <phone> texts # 用文字变化断言点击确实生效(如底部条应消失)
$P <phone> shot /tmp/p1.png
$P <phone> crash # 必须为 0
```
核对请求/响应体优先用 Proxyman MCP(`filter_flows` + `get_flow_detail`,比 `req` 的
logcat 抓包完整可靠;模拟器代理一行命令接入,见 `references/proxyman.md`);
Proxyman 不可用时兜底 `$P <phone> req pageQuery`。
逐项走查:列表/卡片两种形态、详情页、筛选弹层(重置+确认)、Tab 切换、单选与全选联动、
下拉真实取数、扫码入口。每步 `crash` 都要为 0。
点完**一定要断言效果**(`texts` 或截图),不要假设点中了——加载弹窗盖屏时 `input tap` 会被静默吞掉。
手机端还要确认方向没闪屏(冷启动采样 `mRotation` 应恒为 0),命令见 `references/verification.md`。
**Pad 端回归**(必做):同样安装到平板模拟器打开同一页面截图,与改造前逐项对比——
搜索条数量、默认值、按钮、底部统计都应一致。改过 `module_base` 公共组件时这步尤其不能省。
## 阶段 6:与设计稿二次核对
把阶段 1 的设计稿截图和阶段 5 的实机截图放在一起,用 Read 工具对照看,逐项核对:
配色 / 字号 / 字重 / 圆角 / 内外间距 / 图标形状与大小 / 控件顺序 / 选中态 / 状态标签配色 /
**空值兜底**(真实数据空字段很常见——出库交接就出现过「库位」为空时橙色高亮标签只剩一个孤零零的
小色块,需要空值时不套标签底)。
服务端当天没数据时(列表空)先放宽条件重查;仍为空、或要覆盖真实数据凑不出的形态
(空字段、超长文本、罕见状态标签),**首选 Proxyman Map Local 造 mock**——零代码改动、
零重构建,见 `references/proxyman.md` 场景 3(2026-07-31 出港查询用它一次验完
已离港/已入库/未入库三种卡片形态)。验证完 `delete_rule` + 清模拟器代理。
Proxyman 不可用时才退回老办法:Activity 里临时注入样例 Bean 截图比对:
```kotlin
// import com.lukouguoji.module_base.ktx.commonAdapter
binding.rv.postDelayed({
binding.rv.commonAdapter()?.refresh(listOf(/* 覆盖每种卡片形态各一条 */))
}, 2500) // 等首次请求结束,否则会被真实结果覆盖
```
**核对完必须删掉**,删完重新构建并 grep 确认无残留。
## 收尾:文档与交付说明
代码之外还要落四处文档,否则下一个人会重复踩坑:
- 新增了公共组件/资源 → 更新 `CLAUDE.md`「手机版公共组件库」表 + `references/components.md`
- 踩到新坑(崩溃、静默失效、环境问题)→ 写进 `CLAUDE.md`「常见编译错误速查」和本 skill 的
对应 reference,把**定位手法**也写上,不只写结论
- 接口有后端缺口(阶段 2 / 阶段 5 发现的)→ 维护 `documents/后端接口对接问题清单.xlsx`;
已确认的字段语义追加到 `references/api-doc.md` 的「已核对过的语义」,别让下个页面再查一遍
- `CHANGELOG.md` 追加条目(新增 / 改进 / 修复 / 验证 四段),记忆文件
`phone-5.5inch-adaptation.md` 同步
向用户汇报时明确写出这四块:
1. 新增/修改的文件清单(布局、组件、Activity/ViewModel/Bean)
2. Pad 端零破坏的具体做法 + 回归结论
3. 复用与新增的公共组件
4. **待确认事项**:后端未生效的参数、自己推断的业务规则(如 IMP 代码红/绿的判定)、
设计稿与平板功能的差异。如实说,别含糊。
## 参考文件
- `references/api-doc.md` — api-doc MCP 连接与逐接口核对流程、后端缺口记录规范、已核对字段语义(阶段 2 前读)
- `references/proxyman.md` — Proxyman MCP 抓包/直调 A/B/Map Local mock 的接入与四个场景(阶段 2、5、6 用;与 api-doc 的分工:契约 vs 事实)
- `references/components.md` — 7 个手机版公共组件的属性表、用法与封装新组件的规矩(写布局前读)
- `references/pad-safety.md` — 布局变体约束、Pad 零破坏模式、易崩点(写代码前读)
- `references/verification.md` — 双端 adb 命令、登录与直启、已知环境坑(验证前读)
- `scripts/design_shots.py` — 设计稿逐态渲染截图
- `scripts/ui_probe.sh` — 实机取文字/按文字点击/等待/截图/抓请求体/查崩溃

View File

@@ -0,0 +1,190 @@
# api-doc MCP:接口定义核对流程(阶段 2 必读)
适配页面前,**所有用到的接口都要在 api-doc(AgentFox)里核对一遍入参/出参**,
不要靠平板代码或猜测推断参数名——出库交接就因为猜了 `hoState`(实际是 `handoverState`)
白白折腾了一轮实机排查。
## 连接方式
项目 `.mcp.json` 已配置 `api-doc` 远程 HTTP MCP。若会话内 MCP 工具可直接调用
(`mcp__api-doc__*`),直接用;**若工具没有注入会话**(ToolSearch 搜不到),用 curl 走
MCP 协议手工调用,三步:
```bash
URL=$(python3 -c "import json;print(json.load(open('.mcp.json'))['mcpServers']['api-doc']['url'])")
AUTH="Authorization: $(python3 -c "import json;print(json.load(open('.mcp.json'))['mcpServers']['api-doc']['headers']['Authorization'])")"
# 1) initialize:从响应头 mcp-session-id 取会话 id
curl -s -D /tmp/mcp_headers.txt -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-code","version":"1.0"}}}' > /dev/null
SID=$(grep -i 'mcp-session-id' /tmp/mcp_headers.txt | tr -d '\r' | awk '{print $2}')
# 2) initialized 通知(必须发,否则 tools/* 报 session 未初始化)
curl -s -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" -H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null
# 3) tools/call(响应是 SSE,`sed 's/^data: //'` 后按 JSON 解析)
curl -s -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" -H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_endpoints","arguments":{"keyword":"IntExpMove"}}}'
```
## 可用工具(5 个)
| 工具 | 用途 |
|------|------|
| `get_project_overview` | 项目概览 + 模块清单(首次调用) |
| `list_modules` | 全部模块含描述 |
| `list_endpoints` | 单模块内接口列表(分页,`moduleId` + `cursor`) |
| `search_endpoints` | 按关键字搜接口(`keyword` 用控制器前缀最准,如 `IntExpOutHandover`) |
| `get_endpoint_detail` | **核心**:单接口完整契约,schema 已拍平成字段表(`endpointId`) |
## 核对清单(每个页面做一遍)
对 ViewModel 里每个 `NetApply.api.xxx` 调用:
1. 从 `module_base/.../http/net/Api.kt` 拿到 `@POST` 路径 → `search_endpoints` 找到接口 → `get_endpoint_detail`
2. **入参**:前端传的每个 key 必须能在文档字段表里找到**同名字段**;找不到 = 传了也白传(后端静默忽略,不报错)
3. **类型**:状态类字段(`moveState`/`handoverState`/`outState`)文档均为 integer,前端传 `"0"` 字符串靠
Jackson 隐式转换(实测可用);新代码尽量传数字
4. **出参**:设计稿卡片/详情页的每个展示字段,都要能对上出参字段;对不上就是后端缺口
5. **结论落两处**:
- 后端缺口(缺入参/缺出参/数据不回填)→ 记入 `documents/后端接口对接问题清单.xlsx`
(按「模块 → 页面 → 接口定义 → 接口地址」格式,含前端现状与期望,状态标 ⏳/🔍)
- 前端可自行解决的(参数名写错、字段绑错)→ 直接改代码,别记成后端问题
## 已核对过的语义(别再猜)
- `IntExpOutHandover`:交接状态入参 = `handoverState`(不是 `hoState`),**2026-07-29 内网实测过滤生效**
(传 0/1 分别返回纯净的待/已交接集合,字符串 `"0"`/`"1"` 同样被接受);`ldId` 是**组装人**不是交接人;
交接人姓名出参 `hoUserName`,编码 `hoId`;查询入参**没有**交接人筛选(后端缺口 #4)
- `IntExpOutHandover/pageQueryTotal`:**出参恒 0**(任何条件下 totalPc/totalWeight/wbNumber 均为 0,
与同条件 pageQuery 不一致,后端缺口 #6)——依赖它的 Tab 角标/底部统计显示 0 不是前端问题
- `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)、
无批量删除(#10,`deleteUld` 是 `@Query` 单条);出参 `ifNo`/`efNo`/`checkInDate`/`checkOutDate`
文档无描述,按命名推断进/出港航班号与出入库时间(#11 待确认)
- `IntExpArrive`(出港运抵,2026-07-30 内网实测):`declareStatus`(申报状态)只在出参,
`pageQuery`/`pageQueryTotal` 入参均无此字段(缺口 #20,待运抵/已运抵 Tab 靠客户端拆分);
`declareStatus`/`GjcHaWb.arrivalStatus` 三色编码:`01`=正常(绿)、`W`=靛蓝态、其余非空=琥珀异常态;
`haWbList[].response`/`arrivalOpDate` 是回执文本与对应时间,实测确认真实存在非空数据;
分单补充收发货人信息**后端完全没有对应接口**(`search_endpoints` 对 `IntExpArrive`/`HaWb`
关键字均搜不到分单更新接口,只有国际进港的 `IntImpAirManifest/complete`),
实机调用拟定路径 `IntExpArrive/haWb/supplement` 确认 404(缺口 #19)
- `IntExpSearch`(出港查询,2026-07-31 内网实测):查询入参 `outState`/`fno`/`dest`/`spCode`/`awbType`/
`businessType`/`goodsCn`/`beginDate`/`endDate`/`agentCode`/`wbNo` 全部真实生效(平板既有口径);
文档定义的 `isFclose`(筛选航班是否关闭)**pageQuery 与 pageQueryTotal 均静默忽略**(A/B 对照
全量 72 票与单日两组,加参前后结果完全一致,缺口 #21);`pageQueryTotal` 出参无已入库/已离港
分项计数(#21);`detail` 出参 Map 无 `lockStatus`(锁定状态)与计费重量字段(#22);
详情「入库件数/入库重量」= `warehouseList` 各批次 pc/weight 求和(mock 与实测均吻合);
卡片状态推断口径:`fclose` 非空=已离港、否则 `opDate` 非空=已入库;
特码字典沿用 `DictUtils.getSpecialCodeList(flag=1, ieFlag="")`(ieFlag 必须空串)
- `IntExpStorageUse`(出港仓库,2026-07-31 文档核对,内网 A/B 未测——当日开发机不在内网):
`pageQuery`/`pageQueryTotal` 同一套入参 schema:`fdate`/`fno`/`wbNo`/`likeNo`/`spCode`/`dest`/
`agentCode`/`location`/`clearNormal`/`checkIn`/`reviewStatus`/`dep`/`hno`/`prefix`;设计稿筛选的
**运单类型/业务类型/品名(中) 均无入参**(`awbType`/`businessType`/`goodsCn`,缺口 #23);
手机三格统计按 `clearNormal=0/1` 各调一次 `pageQueryTotal` 取 `wbNumber`,该过滤**是否真实生效
待内网 A/B**(缺口 #24,参照 isFclose 有定义但被忽略的先例);出参 `clearNormal` 0=未清仓/1=已清仓
(卡片标签与「清仓」按钮置灰按 ==\"1\" 判定);详情复用 `IntExpSearch/detail`(maWbId Query 参数)
- `IntImpStorage`(进港仓库,2026-07-31 文档核对;**注意前端 Api.kt 路径前缀是 `IntImpStorage`
不是 `IntImpStorageUse`**,api-doc 按后者搜不到):`pageQuery`/`pageQueryTotal` 同一套入参:
`fdate`/`fno`/`wbNo`/`hno`/`fdep`(始发站)/`location`/`clearNormal`/`fid`;比出港仓库还少
`agentCode`/`spCode`/`dest`——设计稿筛选的代理/特码/运单类型/业务类型/品名(中) **5 项均无入参**
(缺口 #25);分项统计与 clearNormal 过滤待实测同 #24(记为 #26);详情复用
`IntImpSearch/detail`(Body 传 `prefix`+`no`,出参 `data.maWb` 无 schema,字段 key 以平板绑定为准:
`awbPc`/`awbWeight`/`cashWeight`/`inPc`/`inWeight`/`lockState`(0/1 需转中文)/`mftStatus` 原始舱单/
`tallyStatus` 理货报告/`command` 海关放行/`by1` 承运人/`dlvTime` 出库时间;`dep`/`dest1`/`dest`/
`unNumber`/`opDate` 为按出港推断,待实测 #27)
- `IntImpPickUpDlv`(提取出库,2026-07-31 文档核对):`pageQuery` 入参含 **`outState`
(0 未出库/1 已出库)**——待出库/已出库 Tab 直接真实过滤(抓包证实携带,效果待内网 A/B #29);
另有 `fno`/`dest`/`origin`/`carrier`/`awbType`/`businessType`/`goods`/`likeNo`/`pkId`/
`beginDate`/`endDate`+`dateType`(0 入库/1 离港)/`chargeFlag`(0 未提取/1 已提取);
**无 `fdate`(航班日期)入参**(缺口 #28);出参无车牌/目的港/业务类型名称(#28);
运单号筛选参数名是 **`no`**(平板既有口径);`chargeName`/`chargeTime` = 柜台办理人/缴费(提取)时间,
`dlvTime` = 出库时间,`dlvName` 按提货人语义推断作「出库人」展示(待确认 #29);
确认出库接口 `pickUpOut` 文档注明**需将全字段传至后端**(平板 `selectedItems.toRequestBody()` 已满足)
- `IntImpSearch`(进港查询,2026-07-31 文档核对):`pageQuery`/`pageQueryTotal` 分页参数名是
**`page`/`limit`**(不是 pageNum/pageSize);手机筛选 9 项入参全部真实存在:`beginDate`/`endDate`/
`fno`/`agentCode`/`spCode`/`origin`(始发站)/`awbType`/`businessType`/`goods`(品名,**不是 goodsCn**);
`outState`(0 未出库/1 已出库)文档为 integer,前端传字符串靠 Jackson 隐式转换;`pageQueryTotal`
出参无已入库/已出库分项计数(缺口 #30,手机三格统计为客户端计数口径:dlvTime 非空=已出库、
inDate 非空=已入库);出参含 `agentName`(中文名)+ `agentCode`,agentName 可为 null
- 「进港移库」(设计稿 11)**归属更正(2026-08-03 用户确认):属国内进港移库 `DomImpMove`**,
不是国际进港移库——「后端模块缺失」的旧结论(原缺口 #31)作废,原按 IntExpMove 约定预接的
IntImpMove 前端实现已整体删除。溯源:api-doc 53 模块中移库仅 `DomImpMove`(国内进港,8 接口)与
`IntExpMove`(国际出港,4 接口),移库按转关业务前缀划分(DomImpMove 处理 `CI**`、IntExpMove
处理 `IO**`),设计稿详情卡的「国际进港」红标签实为 `awbType` 值(如「国际进港(国内中转)」),
不代表页面归属
- `DomImpMove`(国内进港移库,2026-08-03 内网实测):手机 Tab 双端点——待移库 `search` /
已移库 `searchMoved`(同一套入参 schema,分页参数名 **`page`/`limit`**);入参 A/B 实测:
**`wbNo`、`awbType`(值为 CICO/CIII/CIIO 转关码)过滤生效;`fdate`/`fno`/`spCode` 文档有定义但
被静默忽略**(缺口 #32);`searchMoved`/`detail` 出参 **`opId`/`opDate` 恒 null** 且无操作人姓名
字段(缺口 #33,移库人/移库时间显示"-"),`moveId` 形如 `CICO240617155016` 疑似内嵌移库时间戳但无
文档语义;`searchMoved` 出参**无 pic 三字段**(search 有)——拍照上传页统一先调 `GET detail`
回填已有照片(#34 前端已规避);照片提交走 `modify` 最小体
`{mawbId, remark, picNumber, pic, originalPic}`(平板移交编辑页同链路,removeEmptyOrNull);
批量移库 `move` 入参 `{fid:"0", ids:[mawbId]}`;国内进港特码字典
`DictUtils.getSpecialCodeList(flag=0, ieFlag="I")` 实测取数正常
- `IntExpCheckIn`(出港计重,2026-07-29 内网实测):`likeNo` **并非任意模糊**——按 8 位 `no`
匹配(11 位 wbNo 或部分号均 0 条),11 位全号必须走 `wbNo`;`checkIn`/`checkInList`
文档有定义但 `pageQuery`/`pageQueryTotal` **均静默忽略**(缺口 #12,待计重/计重中计数依赖);
查询入参无通道号/承运人/运单类型(#13)、`checked/*` 无收运时间范围(#14);`checked/pageQuery`
出参无通道号(#15)但有 `userName`(计重人中文名);`listRecordByWh` 出参计重人仅 `opId`(#16);
`preArrive` 成功后 `pageQuery` 的 `arriveFlag` 不回填(#17);`splitCheckIn`/`completeCheckIn`
无 `palletNumber`/`carWeight` 入参(#18,手机版托盘数量/自重按设计稿传预留参数);
运单类型字典:`DictUtils.getWaybillTypeList2(ieFlag="E", type="IO")`
## 内网直调验证技巧(比驱动 UI 快得多)
登录态 token 存在 SharedPreference 里,取出后可以直接 curl 后端做行为验证
(**注意:不要把 token 打印到输出里**,落临时文件引用):
```bash
adb -s <serial> shell "run-as com.lukouguoji.aerologic cat \
/data/data/com.lukouguoji.aerologic/shared_prefs/data.xml" \
| grep -o '<string name="token">[^<]*' | sed 's/.*>//' > /tmp/air_token
curl -s -X POST "http://192.168.1.250:8093/IntExpOutHandover/pageQuery" \
-H "Authorization: $(cat /tmp/air_token)" -H "Content-Type: application/json" \
-d '{"pageNum":1,"pageSize":50,"handoverState":0}'
```
用它做「参数是否生效」的 A/B 对比(同条件只改一个参数看 total 变化),
再回 App UI 走一遍确认端到端链路。UI 里的滚轮日期选择器可用
`input swipe`(慢速 500ms、每次 3 格 ≈ 396px)驱动,滑完截图校准。
## 2026-10-09 后端反馈联调后的最新口径(与上文冲突时以本节为准)
后端在问题清单补「解决方案」后逐项内网实测的结论。**通用规则:后端对手机端新增筛选维度一律答复
「与平板端查询参数保持一致」——手机筛选只放平板已用的入参;文档里定义了但平板没用的入参,
多数实测也被忽略(agentCode/spCode/dest/opId 等),别再按文档加筛选。**
- **统一统计 DTO**:各 `*/pageQueryTotal` 出参现含 `uldNumber`/`storedNumber`/`departedNumber`/
`notClearNumber`/`clearNumber`/`waitCheckInNumber`/`checkingNumber`/`flightNumber`(`ManifestTotalDto`
已补齐)。分项统计一律取出参,**不要再按状态入参多调几次 total**
- `IntExpOutHandover/pageQueryTotal` 已修复(与列表一致);Tab 角标取 `uldNumber`(板箱数);无交接人入参
- `IntExpMove/pageQuery` 出参新增 `flight`("20260924/MU2222")/`businessName`/`opUserName`;已移库 `opId`/`opdate` 已回填
- `eqm/uld`:状态维持两态;`source`(空运/陆运) 出参与保存入参支持、**查询入参不支持**;入库时间 = `inStoreTime`
(date-time,**只传日期 400**)、所在港 = `nowPort`;批删 `deleteUldBatch`(Body 为字符串数组);
⚠️ `pageQuery` 带 `uld` 即 500(#35);`saveUld` 缺 `maxVolume`/`maxWeight` 即 500(#36)
- `IntExpCheckIn`:`listRecordByWh` 出参 `opUserName`/`trayNumber`/`trayTotalWeight`;
`checked/pageQuery` 出参 `passageWay`;`splitCheckIn`/`completeCheckIn` 托盘入参 = `trayNumber` +
`trayTotalWeight`(数量×自重);`arriveFlag` 已回填("0" 正常/"1" 提前运抵);
⚠️ `pageQuery` 的 `checkIn` 过滤仍被忽略(#40,统计用分项出参);`checked/pageQuery` 的 `dest`/`opId` 被忽略
- `IntExpArrive`:入参 `arrivalStatus`("0" 未运抵/"1" 已运抵)服务端过滤生效;`agentCode` 被忽略;补充信息页已删除
- `IntExpSearch`:**`beginDate`/`endDate` 只有携带 `dateState`(0 入库/1 离港)才生效**;`outState` 生效
- `IntExpStorageUse`:仅 `fdate`/`fno`/`wbNo`/`location`/`clearNormal` 有意义;⚠️ 列表 total 与统计 wbNumber 不一致(#38)
- `IntImpStorage`:⚠️ `pageQuery` 当前**所有**过滤条件均失效(#37),`pageQueryTotal` 正常
- `IntImpSearch/detail`:按平板展示 航程 `range`(无经停站)/`unNumber`/`lockState`/出库时间 `dlvTime`
- `IntImpPickUpDlv`:运单号入参用 `wbNo`;出参新增 `refUserName`(出库人姓名,`dlvName` 是提货人);测试环境暂无数据
- `DomImpMove`:`fdate`/`fno`/`spCode` 已生效;`searchMoved` 已回填 `opId`/`opDate`/`opUserName`;
新增 `GET movedDetail?moveId=`(含 pic 三字段);⚠️ 已移库运单无 `mawbId`,`modify` 无法补传照片(#39)

View File

@@ -0,0 +1,140 @@
# 手机版公共组件(`module_base/.../ui/weight/phone/`)
7 个组件覆盖设计稿反复出现的部件。**先复用,实在没有再新增**——每个新组件都要维护、都要踩一遍
状态恢复的坑,能少一个是一个。
所有属性都是**无命名空间写法**(与平板端 `PadSearchLayout` 一致),DataBinding 适配器集中在
`PhoneWidgetKtx.kt`。同名属性(`hint`/`value`/`title`/`list`)按控件类型解析,与 Pad 系组件不冲突。
## 组件与属性
| 组件 | 用途 | 属性 |
|------|------|------|
| `PhoneSearchBar` | 顶部搜索行:搜索框 + 扫码 + 圆形筛选按钮 | `hint`、`value`(双向)、`showScan`、`showFilter`、`upperCase`、`setOnScanClickListener`、`setOnFilterClickListener`、`setRefreshCallBack` |
| `PhoneStatusTab` | 状态 Tab 条(文字 + 角标 + 选中下划线) | `tabs`(List\<KeyValue\>)、`counts`(List\<String\>)、`value`(双向)、`setOnTabChanged` |
| `PhoneDataLayout` | 表单/筛选项,三形态 | `title`、`hint`、`type`(`PhoneDataLayoutType.INPUT`/`DATE`/`SPINNER`)、`value`(双向)、`list`、`required`、`enable`、`setRefreshCallBack` |
| `PhoneFilterPanel` | 底部筛选弹层(遮罩 + 面板 + 重置/确认) | `title`、`panelVisible`、`setOnResetClick`、`setOnConfirmClick`、`setOnDismissClick` |
| `PhoneBottomBar` | 底部操作条(全选 + 已选统计 + 主按钮) | `allChecked`、`countText`、`actionText`、`setOnAllCheckClick`、`setOnActionClick` |
| `PhoneStatBox` | 卡片内 2~4 列统计框,未传 label 的列自动隐藏 | `label1..4`、`value1..4` |
| `PhoneKvItem` | 详情页字段项(灰标签 + 数值) | `title`、`value`、`tag`(true → 高亮标签)、`tagColor`("orange" 默认 / "green" 放行正常态);占位符 "-"/"--" 不套标签底 |
| `PhoneFormRow` | 行式表单项(左标签 minWidth 110dp + 右值 + 1px 分隔线),覆盖新增/修改/详情三态表单 | `title`、`hint`、`type`(`PhoneFormRowType.INPUT`/`SELECT`/`DATE`/`TEXT`)、`value`(双向)、`list`(SELECT 选项)、`required`、`enable`、`numeric`、`setRefreshCallBack` |
`PhoneFormRow` 要点:SELECT 内置 XPopup 底部选择面板(点击整行弹出,无需页面代码);TEXT 为只读右对齐、
空值自动占位 "--"(详情态);type 用三元按 `pageType` 切换即可一套布局覆盖三态页面(参考
`app/res/layout/activity_uld_edit.xml`)。`PhoneBottomBar` 的 `actionDanger="@{true}"` 切浅红删除按钮。
`PhoneFilterPanel` 内容插槽已内置 ScrollView(2026-07-31 加):筛选项少时行为不变;
筛选项多到超屏(出港查询 9 项)时内容区收缩滚动,底部「重置/确认」按钮始终可见——
此前无滚动时按钮会被挤出屏幕外且毫无报错,只表现为"点不到确认"。
⚠️ 两个已踩坑:
- **FAB 盖住弹层**:浮动按钮带 elevation 时会浮在 `PhoneFilterPanel` 之上,弹层实例要加
`android:elevation="8dp"`(> FAB 的 6dp)
- **Spinner 选择回调是异步的**:`onItemSelected` 由系统 post,自动化测试里 tap 选项后立即 tap「确认」
可能抢在回值之前发出请求(表现为 UI 已显示选中但请求参数为 null)——脚本里选完要 `sleep 1` 以上;
真人操作间隔足够,不受影响
`KeyValue` 是 `dev.utils.app.info.KeyValue`,字段是 **`key`(显示文案)+ `value`(业务值)**,
不是 `name`。`PhoneStatusTab` 的 `tabs` 就按 `KeyValue("待交接","0")` 传。
## PhoneFilterPanel 的内容插槽
筛选项直接写在标签内,组件 `onFinishInflate` 会自动把它们移进面板中部并施加 20dp 项间距,
使用方不用关心内部层级、也不用逐个写 `marginTop`:
```xml
<com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel
android:layout_width="match_parent"
android:layout_height="match_parent"
title='@{"筛选条件"}'
panelVisible="@{activity.filterPanelVisible}"
setOnResetClick="@{(v)-> activity.resetFilter()}"
setOnConfirmClick="@{(v)-> activity.confirmFilter()}"
setOnDismissClick="@{(v)-> activity.toggleFilterPanel()}">
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
title='@{"航班日期"}' hint='@{"请选择航班日期"}'
type="@{PhoneDataLayoutType.DATE}" value="@={viewModel.flightDate}" />
</com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel>
```
记得在 `<data>` 里 `<import type="com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayoutType" />`。
## ⚠️ 新增复合组件必做:屏蔽子控件状态恢复
同一页面放多个同类组件时(比如筛选弹层里 3 个 `PhoneDataLayout`),它们的内部子控件
(`et`/`tv`/`spinner`…)**共用同一套 id**。系统按 id 保存/恢复视图状态会跨实例串档。
实际后果(出库交接踩过):DATE 项的默认日期被邻近输入框恢复的空文本覆盖,
`EditText.setText("")` 触发 `doOnTextChanged`,再经双向绑定把 ViewModel 的航班日期也写成空——
**筛选条件静默丢失,界面上只表现为"日期没了",极难联想到状态恢复**。
所以每个含内部 id 的组件都要写:
```kotlin
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) =
dispatchFreezeSelfOnly(container)
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) =
dispatchThawSelfOnly(container)
```
取值本来就由 ViewModel + DataBinding 持有,不需要视图级恢复,屏蔽掉没有副作用。
配套的第二道防线:多形态组件里,文本回写要限定形态,别让隐藏控件影响取值:
```kotlin
et.doOnTextChanged { text, _, _, _ ->
if (type == PhoneDataLayoutType.INPUT) value = text.toString()
}
```
### 遇到"值被神秘清空"怎么定位
在 setter 里打一条带堆栈的日志,一次就能看到真凶:
```kotlin
android.util.Log.d("DBG", "value '$field' -> '$v'", Throwable("trace"))
```
出库交接那次的栈是 `TextView.onRestoreInstanceState → EditText.setText → doOnTextChanged`,
指向非常明确。**排完记得删日志。**
## 新增组件的写法约定
照现有 7 个组件的模式来,保持一致:
1. 继承 `LinearLayout`/`FrameLayout`,`init` 里 `inflate(context, R.layout.layout_phone_xxx, this)`
2. 内部布局用 `<merge tools:parentTag="android.widget.LinearLayout">`,避免多一层嵌套
3. 属性做成 Kotlin `var` + setter 里立即生效;双向绑定值用
`onChangeListener: InverseBindingListener?`
4. BindingAdapter 统一加到 `PhoneWidgetKtx.kt`,函数名带组件名前缀避免顶层函数重名
5. 有互斥形态时,**`type` 要先于 `value` 生效**(把 type 放进同一个多属性 adapter 的第一个
`?.let`),否则 value 会写进错误的子控件
6. 加上上面那两个 `dispatch*SelfOnly` 覆写
7. 完成后同步更新 `CLAUDE.md` 的组件表和记忆文件 `phone-5.5inch-adaptation.md`
## 资源
配色一律用 `phone_*`,背景用 `bg_phone_*`,图标用 `ic_phone_*`,禁止写死色值。
已有的(不够再加,加完更新 CLAUDE.md):
- 色:`phone_primary` `phone_page_bg` `phone_bg` `phone_stat_bg` `phone_text_title/body/hint/label/strong/weak_btn`
`phone_divider` `phone_badge_bg` `phone_green` `phone_check_border` `phone_tag_gray_*` `phone_tag_green_*`
`phone_tag_red_*` `phone_orange_bg/text`
- 背景:`bg_phone_card` `bg_phone_search` `bg_phone_input` `bg_phone_stat_box` `bg_phone_panel_top`
`bg_phone_icon_round` `bg_phone_btn_primary` `bg_phone_btn_outline` `bg_phone_btn_weak`(浅蓝弱按钮)
`bg_phone_badge` `bg_phone_tag_green/gray/red/orange` `bg_phone_card_header_blue/green`
`bg_phone_card_top_accent`(卡片顶部主色描边条,出港计重头部卡片)
- 图标:`ic_phone_airplane` `ic_phone_location` `ic_phone_person` `ic_phone_check_circle`
`ic_phone_chevron_right/down` `ic_phone_calendar` `ic_phone_close`
`ic_phone_check_round_checked/unchecked`(圆形复选框)
`ic_phone_clipboard`(剪贴板,白色 FAB 用)`ic_phone_weight`(砝码,可 tint)
`ic_phone_package`(件数)`ic_phone_tag`(特码/标签)
- 圆点:`bg_phone_dot_green/blue/gray`(状态指示点;蓝=出港查询卡片头,灰=时间线未达节点)
- 复用 Pad 的矢量图:`img_search` `img_scan` `img_filter`(可 `app:tint` 染色)
设计稿里的 MDI 图标若缺失,直接写 24×24 的 vector(用 MDI 官方 path),比找位图靠谱且能染色。

View File

@@ -0,0 +1,267 @@
# Pad 端零破坏 & 易崩点
平板端是在跑的生产功能,适配手机不能改变它的任何行为。以下是必须遵守的约束和踩过的坑。
## 布局变体的三条硬约束
同名布局 + 资源限定符(`layout/` 手机 / `layout-sw600dp/` 平板)会让 DataBinding 生成
**一个 Binding 基类 + 两个变体实现**,因此 Kotlin 侧零改动。代价是两个变体必须兼容:
### 1. `<variable>` 必须完全一致
手机变体需要 `activity` 变量(暴露纯 UI 方法给 XML)时,**平板变体也要补上**,否则编译报
`variable not found`。平板不用它,加个注释说明即可:
```xml
<!-- 平板端不使用,仅为与手机变体保持变量一致 -->
<variable name="activity" type="com.lukouguoji.gjc.activity.XxxActivity" />
```
### 2. 共用 id 双方都要保留
Activity / ViewHolder 引用到的 id(`srl`、`rv`、`iv_icon`、`checkIcon`…)两个变体都要有。
可以换位置换样式,但 id 得在。
### 3. 单侧独有的 id 会变 `@Nullable`
只在手机变体存在的 id(如「查看详情」的 `ll_detail`),Binding 基类里是可空字段,
Kotlin 侧必须判空:
```kotlin
// 平板变体没有这个控件,为 null 时直接跳过
binding.llDetail?.setOnClickListener { XxxDetailActivity.start(itemView.context, bean) }
```
编译器会强制你处理,不用担心漏掉。改完可以核对生成代码确认:
```bash
grep -B3 "llDetail" module_gjc/build/generated/data_binding_base_class_source_out/debug/out/\
com/lukouguoji/gjc/databinding/ItemXxxBinding.java
```
### ⛔ 绝对不要用「不同文件名 + 代码判断」切布局
`activity_xxx_phone.xml` 会生成 `ActivityXxxPhoneBinding`,而字段声明的类型是
`ActivityXxxBinding`。泛型擦除让**编译期查不出来**,一进页面就 `ClassCastException`。
### 弹框(BaseDialogModel)也要双变体,最容易漏
页面布局适配完,长按/批量操作弹出的 DialogModel 仍会以 Pad 居中样式出现在手机上
(出港运抵的「状态重置」就漏过)。做法与页面完全同构:
1. dialog 布局 `git mv` 到 `layout-sw600dp/`,`layout/` 下写同名手机版(设计稿通常是底部弹层:
`bg_phone_panel_top` + 居中标题 + `ic_phone_close` + 全宽 `bg_phone_btn_primary` 确定键)
2. 弹出位置在构造参数按形态切换(XPopup 位置与布局是两回事,都要改):
```kotlin
class XxxDialogModel(...) : BaseDialogModel<DialogXxxBinding>(
if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER
)
```
3. 两变体 `<variable>` 一致(imports 不要求一致)
参考:`IntExpArriveResetDialogModel` + `dialog_int_exp_arrive_reset.xml` 双变体。
**适配收尾时逐个 grep 该页 ViewModel 里 `DialogModel(` 的调用点**,每个弹框都过一遍。
## 业务层零破坏的三个模式
### 新增查询维度:默认空值 + 仅非空进请求
平板布局没有对应控件 → 字段恒为空 → 不进请求体 → 平板查询与改造前逐字节一致。
```kotlin
val handoverState = MutableLiveData("") // 手机 Tab 绑这个,平板无控件
val hoId = MutableLiveData("")
val filterParams = mapOf(
"fdate" to flightDate.value?.ifEmpty { null },
/* 原有字段… */
"hoState" to handoverState.value?.ifEmpty { null }, // 空 → null → 不影响平板
"hoId" to hoId.value?.ifEmpty { null },
)
```
### 手机专属的额外请求:ViewModel 内部开关
Tab 角标计数、新增下拉字典这类请求,平板不需要,多发就是改变了平板行为(请求次数)。
用一个内部布尔控制,由 Activity 在手机形态下开启:
```kotlin
// ViewModel
private var phoneExtrasEnabled = false
fun initPhoneExtras() {
phoneExtrasEnabled = true
handoverState.value = "0" // 手机默认停在第一个 Tab
DictUtils.getWHSUserList(addAll = false) { hoUserList.postValue(it) }
}
override fun getData() {
/* 原有列表 + 统计请求… */
if (phoneExtrasEnabled) loadTabCounts(filterParams) // 平板不触发
}
```
```kotlin
// Activity
if (DeviceUtil.isPhone()) viewModel.initPhoneExtras()
```
`DeviceUtil.isPhone()` 只用来决定**数据默认值和额外请求**,**永远不用来选布局**。
### 纯 UI 状态放 Activity
弹层显隐、Tab 高亮这类与业务无关的状态,用 `ObservableBoolean` 放在 Activity 上暴露给 XML,
不要污染 ViewModel:
```kotlin
val filterPanelVisible = ObservableBoolean(false)
fun toggleFilterPanel() = filterPanelVisible.set(!filterPanelVisible.get())
```
### 同名 id 在两个变体里必须是同一种控件
平板变体的 `btnMove` 是 `TextView`,手机版底部换成了 `PhoneBottomBar`——**不要为了复用
Activity 里的 `binding.btnMove` 而把这个 id 安到 PhoneBottomBar 上**。同名不同类型会让
DataBinding 把字段类型退化成公共父类,编译期未必报错,运行时行为难料。
正确做法:手机端的按钮点击走组件自身的 `setOnActionClick`,`btnMove` 就只留在平板变体里,
Activity 侧判空调用:
```kotlin
binding.btnMove?.setOnClickListener { showMoveConfirmDialog() }
```
被 XML 调用的 Activity 方法记得改成 public。
### 调字典要对齐既有页面的参数口径
`DictUtils` 的方法参数常有"看起来合理但后端不认"的取值。出港移库的特码下拉一开始传
`ieFlag="E"`(出港),接口返回空数组;而同模块既有页面(`GjcWeighingStartViewModel` 等)
一律传 `ieFlag=""`,能正常返回。
所以调字典前先 grep 一下同模块其他页面怎么调的,照抄它们的参数;改完必须在实机上
**把下拉点开确认真的有数据**,空列表在 UI 上看不出报错。
### 共享方法别为手机改语义
`checkAllClick()` 这类平板也在用的方法,不要为了手机端的语义去改它。
例:手机「待交接」Tab 下理论上只有未交接记录(服务端按状态过滤),所以全选照原样全量勾选就是对的;
若为此加上「跳过已交接」的过滤,反而改变了平板端(平板列表混排两种状态)的行为。
## 屏幕方向:Manifest 一律写 `unspecified`
新增 Activity 注册时:
```xml
<activity android:name="…"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="unspecified" />
```
**两种错误方案都实测踩过,别再试:**
- 写死 `userLandscape` → 手机「先横屏、约 1 秒后转竖屏」(方向在建窗口时就定了,
`BaseActivity` 在 `super.onCreate` 之前纠正也来不及)
- 用 `@integer/screen_orientation` 资源引用(曾经以为是优雅方案)→ **更糟**:系统解析
Manifest 属性不套用设备限定符,所有 Activity 实际都拿到 `values/` 默认值(竖屏),
平板因此先竖后横,且 AutoSize 在竖屏配置下选中平板竖屏 720dp 基准
(正常应为 1152×720 横屏基准),交互几次后整个 UI 放大约 1.6 倍
`unspecified` 让系统按设备当前物理方向建窗口(平板横屏摆放→横屏起,手机→竖屏起,
两端起始方向本就正确),`BaseActivity.applyDeviceOrientation()` 再按形态锁定,
不产生二次纠正。全项目 192 处已统一为 `unspecified`。
## 改 module_base 公共代码时
`module_base` 被所有业务模块依赖,改动会影响全部页面。原则:
- **只做增量**:加新组件、新颜色、新 drawable,别改既有组件的行为
- 确实要改公共基类(`BaseActivity` / `BaseBindingActivity` / Pad 系组件)时,
**必须在平板上回归**至少 2~3 个不同类型的既有页面
- Bean 上加只读计算属性是安全的(Gson 只序列化字段,不动 getter);
加**字段**要谨慎,会进请求体
## RecyclerView 里不要用 item 级 visibility 做 Tab/分类过滤
普通 ViewGroup(LinearLayout 等)里 `View.GONE` 的子 View 会自动塌陷为 0 高度、不占布局空间——
但 **RecyclerView + LinearLayoutManager 不是这样**:它按子项的测量高度参与滚动区域计算,
不会因为子项是 GONE 就跳过。给 item 根节点加 `visibility="@{tab条件 ? VISIBLE : GONE}"`
切 Tab,会在列表里留下一段空白(隐藏项的高度仍被保留),角标数字和实际卡片数量对不上。
出港运抵的「待运抵/已运抵」Tab 就踩了这个坑(实机截图验证:切到「已运抵」后顶部有一大块
空白,隐藏的待运抵卡片高度并未塌陷)。正确做法是在数据层过滤,而不是视图层:
```kotlin
// ViewModel:自己维护完整列表,不依赖 adapter.items 的历史状态
// (见下一节:adapter.items 在 loadMore 场景可能已经是上次过滤后的子集,不能当作真源)
private var allLoadedList: List<GjcMaWb> = emptyList()
override fun getData() {
launchLoadingCollect({ ... }) {
onSuccess = { result ->
val newPageItems = result.list ?: emptyList()
allLoadedList = if (pageModel.page <= 1) newPageItems else allLoadedList + newPageItems
pageModel.handleListBean(result) {
// 无论 handleListBean 内部把 adapter.items 搞成什么样,这里用 allLoadedList
// 强制覆盖为「当前 Tab 的正确子集」,兜底纠正
val filtered = allLoadedList.filter { it.isPending == (currentTab.value == "pending") }
pageModel.rv?.commonAdapter()?.refresh(filtered)
}
}
}
}
fun onTabChangedPhone() {
val filtered = allLoadedList.filter { it.isPending == (currentTab.value == "pending") }
pageModel.rv?.commonAdapter()?.refresh(filtered) // 只是 items.clear()+addAll()+notifyDataSetChanged,无副作用
}
```
`CommonAdapter.refresh()` 只是替换内部列表并 `notifyDataSetChanged()`,不影响
SmartRefreshLayout 的分页/完成状态,可以放心在 `handleListBean` 回调里再调用一次。
## Intent 传递 Serializable Bean:`@Transient` 字段反序列化后是 null
`GjcMaWb`/`GjcHaWb` 的 `checked`/`showMore`/`actionMenuVisible` 三个 UI 状态字段都标了
`@Transient`(本意是避免 Gson 把 `ObservableBoolean` 的内部结构序列化进网络请求体)。
但 Kotlin 的 `@Transient` 注解生成的就是 JVM 的 `transient` 修饰符,这个修饰符对 **Java
对象序列化**(`Intent.putExtra(String, Serializable)` 走的正是这条路径)有实际含义:
反序列化时会跳过 `transient` 字段的恢复,对象引用类型的字段结果就是 **null**。
新增手机专属页面时,如果要把 `List<GjcHaWb>` 通过 `ArrayList<GjcHaWb>` 塞进 `Intent`
传给下一个页面,接收方拿到的对象「看起来」字段都对(wbNo、pc、weight 这些正常字段没问题,
因为它们不是 `@Transient`),但只要调用 `bean.checked.set(...)` 就会立刻崩溃:
```
NullPointerException: Attempt to invoke virtual method
'void androidx.databinding.ObservableBoolean.set(boolean)' on a null object reference
```
出港运抵的「分单明细」页第一版全选按钮就是这样崩的。**修复只需一行**,在接收方
`initOnCreated` 里对拿到的列表整体 `.copy()` 一遍——`.copy()` 会重新触发一次真正的对象构造,
类体里 `val checked = ObservableBoolean(false)` 这类初始化逻辑跟着重新跑一遍,
把 null 补成全新实例:
```kotlin
@Suppress("UNCHECKED_CAST")
fun initOnCreated(intent: Intent) {
val list = intent.getSerializableExtra(Constant.Key.DATA) as? ArrayList<GjcHaWb>
// .copy() 补上被 Java 序列化跳过的 @Transient 字段
fullList = (list ?: emptyList()).map { it.copy() }
}
```
这个坑只在「Intent 传递 Serializable Bean + 接收方要切换 checked/showMore 等状态」时才会
触发;纯只读展示(如回执页)不调用这些字段的 setter,不会崩,但补一道 `.copy()` 无害,
建议只要通过 Intent 传了 haWbList/maWbList 就统一加上,别等下次踩到才想起来。
## 已知的既有问题(不是你改坏的)
用 `am start` 直启页面时,若**先经过首页**再启目标页,会崩在
`LoadingModel.showLoading` → XPopup `popupInfo is null`。在**未改动**的页面上同样复现,
属于既有缺陷。**冷启动直启不受影响**(`force-stop` 后直接 `am start`)。
遇到崩溃先做这个对照实验再下结论:拿一个你没碰过的页面用同样方式打开,看是否同样崩。

View File

@@ -0,0 +1,80 @@
# Proxyman MCP:抓包 / 直调 / Mock(阶段 2、5、6 的调试利器)
Proxyman 桌面端 + `mcp__proxyman__*` 工具(会话内直接可用,缺 schema 时用 ToolSearch
`select:mcp__proxyman__get_flows,...` 加载)。**2026-07-31 已在出港查询页全链路实测**:
抓包核对请求体、Map Local mock 三种卡片形态(含真实数据不存在的「未入库」兜底态)一次跑通。
## 与 api-doc 的分工(先契约后事实)
| | api-doc MCP | Proxyman MCP |
|---|---|---|
| 回答什么 | **契约**:接口该有什么入参/出参、参数名叫什么 | **事实**:App 实际发了什么、后端实际回了什么 |
| 用在何时 | 阶段 2 开工前逐接口核对 | 阶段 5/6 实机验证与 corner case 补测 |
标准配合流程:
api-doc 拿权威参数名 → 前端实现 → Proxyman flow 核对请求体确实带上了 →
compose 改参 A/B 验证后端认不认 → 不认 → 记问题清单(附 `export_flow_curl` 证据)→
map local 造 mock 补 UI corner case 验证。
## 接入(一次性,实测步骤)
```bash
# 1) 确认 Proxyman 在跑(返回 Recording: Active + 端口,默认 9090)
# mcp__proxyman__get_proxy_status
# 2) 模拟器全局代理指向宿主机(模拟器视角宿主机 = 10.0.2.2)
adb -s <serial> shell settings put global http_proxy 10.0.2.2:9090
# 3) 验证完【必须】清掉,否则 Proxyman 一关模拟器就断网,下次验证莫名全挂
adb -s <serial> shell settings put global http_proxy :0
```
本项目内网服务是**纯 HTTP**(192.168.1.250:8093)→ 不需要装证书,明文直接可见。
(若未来切 HTTPS,才需要 `install_certificate` + `enable_ssl_proxying`。)
## 场景 1:核对请求/响应(替代 `ui_probe req`)
logcat 抓 OkHttp 多行 pretty JSON 脆弱且要人肉拼接;Proxyman 一步到位:
- `filter_flows`(key=url, value="IntExpSearch")→ 拿 flow ID 列表
- `get_flow_detail`(flow_id)→ 完整请求体 + 响应体 + headers(Authorization 自动脱敏)
**出参无文档定义时(如 `IntExpSearch/detail` 返回裸 Map),用真实 flow 的 responseBody
直接盘点字段清单**——判定「锁定状态/计费重量是否存在」这类问题的最快路径。
## 场景 2:直调后端 A/B 验证(替代 run-as 提 token + 手写 curl)
`create_compose_http_from_flow`(从真实 flow 复制请求,**token 自动带上,不用碰
shared_prefs**)→ `update_compose_http` 改一个参数 → `send_compose_http` 看结果对比。
isFclose 这类「参数是否生效」的 A/B 实验用这条链。给后端提缺口时用
`export_flow_curl` 导出可复现的 curl 附进问题清单。
## 场景 3:Map Local 造 mock 验证 corner case(替代代码注入样例 Bean)
老流程「Activity 临时注入 Bean → 截图 → 删码 → 重构建」彻底淘汰:**零代码改动、零重构建**。
- `create_map_local`:url 精确到接口路径,method=POST,response_body 直接写目标 JSON
(构造空字段/超长文本/罕见状态,比如出港查询的 fclose+opDate 双空「未入库」兜底态)
- 改真实响应个别字段更省事时用 `create_map_local_from_flow`
- 验证完 `delete_rule`(rule_type=maplocal)+ 清代理,**规则残留会污染后续真实数据验证**
响应结构照抄真实 flow 的顶层结构(如 pageQuery 是裸 PageInfo:`{total, list, pages, ...}`,
不要包 `{status, data}`)。
⚠️ 实测坑(2026-07-31 出港仓库):mock `BaseResultBean` 包装的接口(pageQueryTotal/detail 等)时,
**`status` 必须写 `"1"`**——`BaseResultBean.verifySuccess()` 判 `status == "1"`,写 "200" 会被当失败
静默丢弃(UI 保持默认值,无任何报错,排查靠 flow 已命中但 UI 不变这一矛盾现象)。
⚠️ 实测坑:mock 响应到达后 UI 渲染可能滞后(冷启动首帧 Davey 卡顿 2s+),
**截图断言前先 `ui_probe texts` 确认数据已上屏**——这次就因截图太早误判过一轮「mock 没生效」,
其实 `get_flow_detail` 里 `matchedTools: ["Map Local"]` 早已证明规则命中,UI 只是还没画完。
## 场景 4:环境模拟(按需)
- `create_network_condition`:弱网/高延迟,验证 loading 弹窗与超时提示
- `create_breakpoint`:单次拦截改包(比 map local 更临时的一次性实验)
- `create_map_remote`:把请求转发到另一环境(如后端修复验证用的测试实例)
## 验证收尾清单
- [ ] `list_rules` 确认无本次遗留规则(maplocal/breakpoint/network_condition)
- [ ] 模拟器代理已清(`settings get global http_proxy` 应为 `:0` 或空)
- [ ] 用真实数据再过一遍被 mock 过的页面,确认无假数据残影

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
把 5.5 寸手持端设计稿(HTML 原型)渲染成截图,供逐像素比对。
设计稿是带 Tailwind + JS 的交互原型:列表页、Tab、详情页、筛选弹层往往挤在同一个
HTML 里,靠 JS 函数切换。只读源码容易漏掉视觉细节(间距、圆角、真实配色、
以及 JS 里 id 映射写错导致的字段错位),所以务必渲染成图来看。
用法:
# 1) 先列出 HTML 里可用的状态切换函数与元素 id,据此决定要截哪些状态
python3 design_shots.py --html 出库交接.html --list
# 2) 渲染:默认状态 + 注入 JS 触发的各个状态
python3 design_shots.py --html 出库交接.html --out /tmp/shots \
--state "01_列表:" \
--state "02_已交接Tab:switchTab('shipped');" \
--state "03_详情:showDetail({id:'PMC1',status:'已交接'});" \
--state "04_筛选弹层:toggleFilterDrawer(true);"
每个 --state 形如 "名称:JS",JS 可为空表示页面初始态。
输出 <out>/<名称>.png,同时保留注入后的 <名称>.html 便于复查。
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import urllib.parse
CHROME_CANDIDATES = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
shutil.which("google-chrome") or "",
shutil.which("chromium") or "",
shutil.which("chromium-browser") or "",
]
def find_chrome() -> str:
for path in CHROME_CANDIDATES:
if path and os.path.exists(path):
return path
sys.exit(
"找不到 Chrome/Chromium。请安装,或用 --chrome 指定可执行文件路径。"
)
def list_hooks(html: str) -> None:
"""列出可用于构造状态的 JS 函数和元素 id。"""
funcs = sorted(set(re.findall(r"function\s+([A-Za-z_$][\w$]*)\s*\(", html)))
onclicks = sorted(set(re.findall(r'onclick="([^"]+)"', html)))
ids = sorted(set(re.findall(r'id="([^"]+)"', html)))
print("== JS 函数(可直接注入调用)==")
for f in funcs:
print(f" {f}()")
print("\n== onclick 表达式(含真实参数,照抄最省事)==")
for o in onclicks[:40]:
print(f" {o}")
if len(onclicks) > 40:
print(f" ... 其余 {len(onclicks) - 40} 条略")
print("\n== 元素 id(判断有哪些页面/弹层)==")
print(" " + ", ".join(ids))
def render(chrome: str, html: str, name: str, js: str, out_dir: str,
width: int, height: int, scale: float, budget: int) -> str:
# 等 load 之后再触发,确保 CDN 的 Tailwind/Iconify 已生效
inject = (
"<script>window.addEventListener('load',function(){"
f"setTimeout(function(){{{js}}},300);}});</script>"
)
page_path = os.path.join(out_dir, f"{name}.html")
with open(page_path, "w", encoding="utf-8") as fp:
fp.write(html.replace("</body>", inject + "</body>"))
png_path = os.path.join(out_dir, f"{name}.png")
subprocess.run(
[
chrome,
"--headless=new",
"--disable-gpu",
"--hide-scrollbars",
f"--force-device-scale-factor={scale}",
f"--window-size={width},{height}",
f"--virtual-time-budget={budget}",
f"--screenshot={png_path}",
"file://" + urllib.parse.quote(os.path.abspath(page_path)),
],
capture_output=True,
)
return png_path
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--html", required=True, help="设计稿 HTML 路径")
ap.add_argument("--out", default="/tmp/design_shots", help="输出目录")
ap.add_argument("--state", action="append", default=[],
help='形如 "名称:JS",可重复;JS 为空表示初始态')
ap.add_argument("--list", action="store_true",
help="只列出可用的 JS 函数 / onclick / id,不截图")
ap.add_argument("--chrome", default="", help="Chrome 可执行文件路径")
# 原型容器一般是 375×812 的手机壳,留出边距后 460×900 刚好完整收进去
ap.add_argument("--width", type=int, default=460)
ap.add_argument("--height", type=int, default=900)
ap.add_argument("--scale", type=float, default=2.0, help="缩放倍数,2 便于看清细节")
ap.add_argument("--budget", type=int, default=9000,
help="虚拟时间预算(ms),需覆盖 CDN 脚本加载")
args = ap.parse_args()
with open(args.html, encoding="utf-8") as fp:
html = fp.read()
if args.list:
list_hooks(html)
return
chrome = args.chrome or find_chrome()
os.makedirs(args.out, exist_ok=True)
states = args.state or ["01_default:"]
for spec in states:
name, _, js = spec.partition(":")
png = render(chrome, html, name.strip(), js, args.out,
args.width, args.height, args.scale, args.budget)
ok = os.path.exists(png) and os.path.getsize(png) > 0
size = os.path.getsize(png) if ok else 0
print(f"{'✓' if ok else '✗'} {name.strip():<24} {png} ({size} bytes)")
print(f"\n用 Read 工具逐张查看 {args.out}/*.png,再开始写布局。")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# 实机验证辅助:读取界面文字 / 按文字点击 / 截图 / 抓请求体。
#
# 为什么需要它:手机端验证要反复点 Tab、弹层按钮、卡片链接。靠肉眼估坐标点击
# 极易点空(本次适配就因此白跑了几轮),而 uiautomator 能拿到元素真实 bounds。
# tap-text 直接按文字定位并点中心,比手算坐标可靠得多。
#
# 用法:
# ui_probe.sh <serial> texts 列出当前界面所有可见文字
# ui_probe.sh <serial> bounds "已交接" 打印该文字元素的 bounds 与中心点
# ui_probe.sh <serial> tap-text "确认" [序号] 按文字点击(取中心点);同名多处时默认第 1 处
# ui_probe.sh <serial> wait-text "待交接" [秒] 等文字出现(默认 20 秒),点击前先等页面就绪
# ui_probe.sh <serial> wait-gone "请稍候……" [秒] 等文字消失(如等加载弹窗关闭)
# ui_probe.sh <serial> shot /tmp/a.png 截图到本地
# ui_probe.sh <serial> req <关键字> 从 logcat 抓含关键字的请求/响应体
# ui_probe.sh <serial> crash 统计并打印最近一次崩溃栈
# ui_probe.sh <serial> form 打印形态识别结果 PHONE/TABLET
set -uo pipefail
SERIAL="${1:?用法: ui_probe.sh <serial> <命令> [参数]}"
CMD="${2:?缺少命令}"
ARG="${3:-}"
PKG="com.lukouguoji.aerologic"
A="adb -s $SERIAL"
dump() {
$A shell uiautomator dump /sdcard/_probe.xml >/dev/null 2>&1
# 注意:dump 出来的 XML 是一整行,必须按 '<' 拆成一节点一行才能按节点过滤。
# (`tr '>' '>\n'` 是字符映射不是字符串替换,等于空操作,别踩这个坑。)
$A shell cat /sdcard/_probe.xml | tr '<' '\n'
}
# 打印所有 text 完全等于 $1 的节点的 bounds 与中心点
centers_of() {
dump | grep -F "text=\"$1\"" \
| grep -oE 'bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' \
| sed -E 's/bounds="\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\]"/\1 \2 \3 \4/' \
| awk '{printf "bounds=[%d,%d][%d,%d] center=%d,%d\n", $1,$2,$3,$4, ($1+$3)/2, ($2+$4)/2}'
}
case "$CMD" in
texts)
dump | tr '<' '\n' | grep -oE 'text="[^"]+"' | sed 's/text=//;s/"//g' | awk '!seen[$0]++'
;;
bounds)
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
centers_of "$ARG"
;;
tap-text)
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
idx="${4:-1}"
all=$(centers_of "$ARG")
[ -z "$all" ] && { echo "界面上找不到文字:$ARG"; exit 1; }
total=$(echo "$all" | wc -l | tr -d ' ')
# 同一文字可能在多处出现(如底部 Tab「国际」与列表里的「国际」),
# 默认点第 1 个;命中多个时提示,必要时用第 4 个参数指定序号。
if [ "$total" -gt 1 ]; then
echo "注意:${ARG} 命中 $total 处,正在点第 $idx 处(可用第 4 个参数指定序号)"
echo "$all" | nl -w2 -s' '
fi
line=$(echo "$all" | sed -n "${idx}p")
[ -z "$line" ] && { echo "序号 $idx 超出范围(共 $total 处)"; exit 1; }
cx=${line#*center=}; cx=${cx%%,*}
cy=${line##*,}
# 变量必须写成 ${ARG}:紧跟中文标点时 $ARG 会被当成变量名的一部分
echo "tap ${ARG} @ $cx,$cy"
$A shell input tap "$cx" "$cy"
;;
# 等某段文字出现 / 消失。点击前先等页面就绪很重要:
# 加载弹窗("请稍候……")会盖住整屏并吞掉 input tap,导致点击"静默失效"。
wait-text)
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
for _ in $(seq 1 "${4:-20}"); do
if dump | grep -qF "text=\"$ARG\""; then echo "出现: ${ARG}"; exit 0; fi
sleep 1
done
echo "超时未出现: ${ARG}"; exit 1
;;
wait-gone)
[ -z "$ARG" ] && { echo "需要文字参数"; exit 1; }
for _ in $(seq 1 "${4:-20}"); do
if ! dump | grep -qF "text=\"$ARG\""; then echo "已消失: ${ARG}"; exit 0; fi
sleep 1
done
echo "超时仍存在: ${ARG}"; exit 1
;;
shot)
out="${ARG:-/tmp/shot.png}"
$A exec-out screencap -p > "$out"
echo "$out ($(wc -c < "$out") bytes)"
;;
req)
# OkHttp 日志是多行 pretty JSON,用 -v raw 拿干净文本再按关键字取上下文
$A logcat -d -v raw | grep -B24 -A24 -- "${ARG:-POST}" | grep -E '^\s*"|POST|<-- [0-9]{3}' | tail -60
;;
crash)
n=$($A logcat -d | grep -c "FATAL EXCEPTION" || true)
echo "FATAL EXCEPTION 次数: $n"
if [ "$n" -gt 0 ]; then
$A logcat -d | grep -A25 "FATAL EXCEPTION" | tail -30
fi
;;
form)
$A logcat -d -s BaseActivity:D | grep -oE "(PHONE|TABLET)\(sw=[0-9]+dp[^)]*\)" | tail -3
;;
*)
echo "未知命令: $CMD"; exit 1
;;
esac

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

@@ -1,101 +1,432 @@
# 版本变更日志 (Changelog)
本文档记录 AirLogistics 项目的所有重要变更。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [1.8.4] - 2024-12-02
### 新增 (Added)
- 完善 Claude Code 配置,添加 Android 开发权限
- 创建 8 个快捷命令 (/build-debug, /install, /logs 等)
- 定制 CLAUDE.md 为 Android 项目专用开发指南
- 添加 CONTRIBUTING.md 贡献指南文档
- 添加 CHANGELOG.md 版本变更日志
### 改进 (Changed)
- 优化首页菜单交互体验
- 改进 UI 界面显示效果
### 技术栈
- Kotlin 1.6.21
- Android Gradle Plugin 7.2.0
- Gradle 7.3.3
- minSdk 24, targetSdk 30, compileSdk 31
---
## [1.8.x] - 之前版本
### 主要功能
- 国内出港业务模块 (货物收运、复磅称重、转运管理、出库装机)
- 国内进港业务模块 (舱单管理、卸机入库、出库提货、移库管理)
- 国际出港业务模块 (国际货物收运、板箱组装、ULD容器管理)
- 国际进港业务模块 (国际舱单管理、报文解析、理货管理)
- 航班管理模块 (航班查询、航班统计)
- 货物追踪模块 (货物状态追踪、运输日志)
- 监装监卸模块 (监装监卸管理)
- PDA功能模块 (PDA专用功能)
- 蓝牙打印模块 (佳博打印机集成)
### 核心架构
- MVVM 架构模式
- 组件化模块设计 (11个业务模块 + 1个基础库)
- DataBinding 双向绑定
- Kotlin Coroutines + Flow 异步处理
- ARouter 模块间通信
- Retrofit + OkHttp 网络请求
- Glide 图片加载
- MPAndroidChart 图表展示
### 开发基础设施
- BaseActivity/BaseBindingActivity 基类
- BaseViewModel/BasePageViewModel 基类
- CommonAdapter + BaseViewHolder 列表适配
- PadSearchLayout/PadDataLayout 自定义组件
- 完整的 Kotlin 扩展函数库
- DataBinding 适配器集合
---
## 版本说明
### 版本号规则
- **Major.Minor.Patch** (例: 1.8.4)
- **Major**: 重大架构变更或不兼容的 API 修改
- **Minor**: 新功能添加,向下兼容
- **Patch**: Bug 修复和小的改进
### 变更类型
- **Added**: 新增功能
- **Changed**: 功能改进或变更
- **Deprecated**: 即将废弃的功能
- **Removed**: 已移除的功能
- **Fixed**: Bug 修复
- **Security**: 安全性修复
---
## 未来计划
### 待开发功能
- [ ] 优化数据同步机制
- [ ] 增强离线模式支持
- [ ] 改进用户权限管理
- [ ] 优化蓝牙打印稳定性
- [ ] 添加数据导出功能
- [ ] 性能监控和分析
### 技术改进
- [ ] 升级到 Kotlin 2.0
- [ ] 迁移到 Jetpack Compose
- [ ] 优化构建速度
- [ ] 完善单元测试覆盖
- [ ] 添加 UI 自动化测试
---
**维护**: 本文档应在每次版本发布时更新。
**负责人**: 项目维护团队
**最后更新**: 2024-12-02
# 版本变更日志 (Changelog)
本文档记录 AirLogistics 项目的所有重要变更。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [Unreleased]
### 变更 (Changed)
- 【5.5 寸首页改版 · 2026-10-21】手持端首页 `PDAEnterActivity` 四个 Tab 按 `documents/5.5寸首页新样式/` 设计稿
1:1 还原(仅 module_p,平板不经过该页,零影响):
① 国内/国际 Tab 改为「分组标题栏 + 三列宫格」(新增 `PdaMenuGroup` / `PdaMenuRenderer`),分组与顺序按设计稿,
20 个入口换设计稿新图标(源 GIF/PNG 统一转 PNG 放 drawable-nodpi——放 xxhdpi 实测解码后再放大发糊),
设计稿未画的入口追加到组尾沿用旧图标;
权限过滤 `authList.contains(key)` 与改版前逐字一致——ADMIN 账号改版前后入口集合 diff 为空(国内 9、国际 12),
21 个入口逐个点击跳转目标与原映射一致。
② 首页航班动态改为卡片 + 国内/国际、进港/出港分段 + 日期卡片(‹ › 前后翻日)+ 浅色表格 + 右下设置悬浮钮;
默认方向按设计稿改为「进港」;日期改为补零格式(原选择器会写出 `2026-10-5`),选择器从当前选中日期打开。
③ 底部 Tab 栏、消息铃铛与角标、状态栏(#3B71E8)按设计稿重做(Tab 图标 16dp 并与文字水平居中,
不照搬设计稿偏左上的 16px 图形),「我的」顶栏同步新导航栏样式;分段控件 padding 计入 1dp 描边以上下居中;消息铃铛四个 Tab 统一按国内/国际设计稿位置(首页设计稿略有偏差),切 Tab 不再移动;
首页 AutoSize 基准由 400 改为设计稿画布 390。删除 module_p 中不再引用的旧菜单/Tab 图标副本 19 个。
- 【手机适配分支合入主干 · 2026-10-11】`feature/phone-ui-adaptation` 合并入 `main`,此后平板与 5.5 寸手机只在
`main` 上同时开发。合并以平板业务为准:
① 主干 58 个平板提交(运抵海关放行/运抵状态筛选/海关回执、出库交接装机状态/允许装机、出港移库详情与侧滑编辑、
开始计重托盘信息与运抵重量自动计算、计重明细重量/托盘自重联动、出入港仓库清仓备注与入/出库件数重量、
进港查询出库状态筛选、ULD 来源/入库时间/所在港/位置等)全部同步到手机布局;冲突的平板布局三方合并进
`layout-sw600dp/`,ViewModel/Bean 冲突取平板业务逻辑。
② 新增手机变体:出库/入库/清仓/配运/删除申报弹框、出港移库详情编辑页(`IntExpMoveEditActivity`,替代手机专属
`IntExpMoveDetailActivity`)、共用海关回执页 `CustomsResponseActivity`;`PhoneBottomBar` 增次操作按钮。
③ 手机端对齐平板:移除平板没有的查询入参(`likeNo`、进港移库 fdate/fno/spCode、出港移库 spCode)与改写的默认值
(计重/计重记录/提取出库默认日期),补齐平板表单与筛选项(运抵分单号、进港查询出库状态、计重记录航班号、
进港移库承运人、开始计重运单号/地磅重量/平板车/航班可编辑)与操作(运抵删除申报、计重记录运抵申报、计重新增);
ULD 表单移除平板没有的进港航班号与额外必填校验。
- 【后端反馈联调 · 2026-10-09】后端在《问题清单》补「解决方案/备注」后,逐项(#1~#34)api-doc 核对 +
内网直调 A/B + AeroLogic_Phone 模拟器端到端验证,并按后端意见调整前端:
① 后端已补字段/接口 → 前端对接:出港移库 flight/businessName/opUserName;出库交接 uldNumber 角标;
ULD source/inStoreTime/nowPort + `deleteUldBatch` 一次批删;出港计重 waitCheckInNumber/checkingNumber
统计、passageWay、opUserName、trayNumber/trayTotalWeight;运抵 arrivalStatus 服务端 Tab;
出港/进港查询 storedNumber/departedNumber;出港/进港仓库 notClearNumber/clearNumber(统计请求 3→1);
提取出库 refUserName;国内进港移库 opUserName + `movedDetail` 照片回填。
`ManifestTotalDto` 补齐后端统一统计 DTO 分项字段。
② 后端「按平板端调整/设计如此」→ 前端对齐:出库交接/ULD/计重/记录/出港仓库/进港仓库/提取出库/运抵
的手机筛选全部收敛为平板同款查询参数;ULD 状态回归两态;删除运抵「补充信息」页;出港详情移除
锁定状态/计费重量;进港详情按平板改 航程/出库时间(无经停站);提取出库移除无出参占位。
③ 联调新发现记入问题清单 #35~#41(ULD uld 搜索 500、saveUld 缺 maxVolume/maxWeight 500、
IntImpStorage/pageQuery 过滤全失效、出港仓库列表与统计口径不一致、已移库无 mawbId 无法补传照片等)。
- 【进港移库归属更正 · 2026-08-03】设计稿 11「进港移库」经确认归属**国内进港移库
(DomImpMove,后端 8 接口齐全,Pad 首页「国内进港→进港移库」既有页面)**,此前误判为
国际进港并按 IntExpMove 约定预接不存在的 IntImpMove 模块(原问题清单 #31 作废)。处置:
① 删除 module_gjj 的 IntImpMove 全套前端实现(3 Activity + 3 ViewModel + Holder + 4 布局 +
`IntImpMoveBean` + Api 4 方法 + 国际 Tab 菜单项 + 权限串/路由/双 Manifest 注册);
② 按 phone-adapt 流程重新适配到 `GnjMoveStashListActivity`(app 模块):主列表双形态
(Pad 布局移 layout-sw600dp 零改动;手机版 = PhoneSearchBar + 待移库/已移库 Tab
(`DomImpMove/search`/`searchMoved` 双端点,`searchMoved` 无移库标识按端点打 `isMoved`)+
角标(双端点 limit=1 取 total,仅手机发起)+ 筛选弹层 4 项 + 全选/已选计数 + 批量移库
(复用平板 `move` 链路)+ 卡片长按「拍照」浮层);新增手机专属页
`GnjMoveStashDetailActivity`(Intent 传 bean 零请求,三卡片)与
`GnjMoveStashPhotoActivity`(先调 `detail` 回填已有照片,提交走 `modify` 最小体
三字段规范,与平板移交编辑页同链路);`MoveStashBean` 增 `agentName`/`isMoved`/
`actionMenuVisible`/`agentDisplay`/`orDash`;手机首页入口改挂**国内 Tab**(`GnViewModel`
补通用 route 跳转,权限串 `GnjYiKu=AppDomImpMove` 与 Pad 一致,新路由
`/app/GnjMoveStashListActivity`)。
2026-08-03 内网真实数据双端验证通过(登录→国内 Tab→列表/Tab/详情/筛选/全选/长按拍照全链路
crash=0;平板回归原行为零变化);Proxyman A/B:`wbNo`/`awbType` 过滤生效,
`fdate`/`fno`/`spCode` 被后端静默忽略(新缺口 #32)、`searchMoved`/`detail` 出参
`opId`/`opDate` 恒 null 且无姓名字段(#33)、`searchMoved` 无 pic 三字段(#34,前端已规避)
### 修复 (Fixed)
- 出港计重「修改记录」托盘自重输入框:Double 回写导致光标跳回开头(输入 18 存成 81),改为保留输入原文
- ULD 新增:入库时间日期行仅产出 yyyy-MM-dd(后端 400)→ 保存时补 00:00:00;补充最大装载容积/最大载重
可编辑必填(缺失时 saveUld 500)
- 提取出库运单号入参 no → wbNo(与主干平板端修复一致)
### 新增 (Added)
- 「进港查询」5.5 寸手机端适配(设计稿 10_进港查询,06 出港查询的进港镜像;详情页与运单追踪
复用 08/06 已适配页零新增):列表 `IntImpQueryActivity`(module_gjj,首页菜单实际跳转页;
旧版 `GjjQueryListActivity` 路由已注释弃用)手机布局 = PhoneSearchBar(运单号+扫码+筛选)+
三格统计(总票数/已入库/已出库,已入库/已出库为已加载数据客户端计数,`pageQueryTotal` 无分项
出参,缺口 #30)+ 状态标签卡片(前端推断:`dlvTime` 非空=已出库绿 / `inDate` 非空=已入库橙 /
兜底未入库灰)+ 卡片字段件数(inPc)/重量(inWeight)/代理人/特码;筛选弹层 9 项
(航班日期起止/航班号/代理/特码/始发站/运单类型/业务类型/品名中,**全部经 api-doc 核对为
pageQuery 真实入参**,无缺口);平板侧滑筛选面板代码判空守卫,Pad 端行为零变化
- 「进港移库」5.5 寸手机端全新页面(设计稿 11_进港移库;**首个后端整模块缺失的适配页**:
api-doc 确认无 IntImpMove 模块、权限体系无对应 key,缺口 #31):
`IntImpMoveActivity`(待移库/已移库 Tab + 角标、搜索/扫码、筛选弹层 4 项(航班日期/航班号/
特码/运单类型)、多选 + 全选 + 批量移库(ConfirmDialogModel 确认)、卡片长按浮层「拍照」)+
`IntImpMoveDetailActivity`(运单详情三卡片:蓝头基本信息/绿头移库信息(仅已移库)/橙头异常照片,
Intent 传 bean 零请求)+ `IntImpMovePhotoActivity`(拍照上传:图片网格复用 module_base
`ImageSelectNewViewHolder`,提交走 pic/originalPic/picNumber 三字段规范);
接口按 IntExpMove 同名约定预接四个(pageQuery/pageQueryTotal/move/uploadPic),权限串
`AppIntImpMove` 预定义,后端交付后前端零改动生效(权限串下发前手机首页无入口,debug 直启验证)
- 新增 `IntImpMoveBean`(镜像 GjcMove + 航班/照片字段 + isMoved/flightInfo/orDash 计算属性);
`IntImpQueryBean` 新增 isQueryOutbound/isQueryStored/orDash/agentDisplay 计算属性;
手机首页「国际」Tab 增加「进港查询」(图标 gjj_query_icon)与「进港移库」(图标复用
gjc_yi_ku_icon)入口
- 新增手机版资源:`bg_phone_btn_green`(绿色主按钮)、`bg_phone_card_header_orange`(橙色详情卡
表头)、`bg_phone_photo_add`(虚线加号槽位)、色值 `phone_amber`
### 修复 (Fixed)
- 【11 模块全量设计稿走查】修正 4 类差异(2026-07-31,3 个并行审计 + 逐态渲染比对):
① 运单追踪手机竖排时间线补齐已发生节点的状态 pill(绿底标签展示该环节日志 content,
如「放行状态:01」,超长省略,设计稿 06/08 运单追踪均有此元素);
② 11 个页面手机端标题对齐设计稿短标题(出库交接/出港待计重/计重记录/计重明细/开始计重/
出港运抵/出港移库/出港查询/出港货物/进港查询/进港货物),平板保持原长标题零变化;
③ ULD 编辑表单「来源」字段移至「所在港」之后(设计稿顺序);
④ 出港计重列表卡片(日期/航程/代理/特码)与计重记录卡片(代理)补齐字段小图标
(复用 ic_phone_airplane/location/person/tag,与其余模块卡片风格统一)
- 出港查询/进港查询卡片「代理人」空值兜底失效:转换器把 JSON null 归一成空串,DataBinding
`??` 只判 null 不判空串导致 agentCode 回退永不生效;改用 Bean 计算属性 `agentDisplay`
(isNullOrEmpty 双兜底),已同步修正 06 出港查询(`item_gjc_query.xml`)
### 验证 (Verified)
- 进港查询手机端:Map Local mock 验证三格统计计数、三种状态标签、空值 "--" 兜底、筛选弹层
9 项收缩滚动;平板端回归横排搜索条/侧滑筛选面板/底部统计栏零变化;全程崩溃数 0
- 进港移库手机端:Map Local mock 验证双 Tab 角标、双形态卡片、单选/全选联动计数、空选拦截、
移库确认弹框、长按拍照浮层、运单详情、拍照上传页;全程崩溃数 0(后端模块缺失,接口联调待
后端交付,见问题清单 #31)
- 「提取出库」5.5 寸手机端适配(设计稿 09_提取出库):列表 `IntImpPickUpDLVActivity`(module_gjj)
手机布局 = PhoneSearchBar(运单号+扫码+筛选)+ **PhoneStatusTab 待出库/已出库**(`outState`
为 pageQuery 文档定义入参 0/1,Tab 真实过滤 + 角标双 pageQueryTotal,抓包证实携带)+
双形态卡片(待出库:圆形勾选框+橙标签+库位行;已出库:绿 √ 标签+出库人·出库时间行,
`isPickedUp` = dlvTime 非空)+ 卡片内 PhoneStatBox 三列(代理人/件数/重量)+「查看详情」链接 +
PhoneBottomBar(全选+已选 N 项+出库,仅待出库 Tab 显示,复用既有 confirmOutbound);
筛选弹层 4 项(航班日期为 pageQuery 未定义入参预留;航班号 fno;代理人/特码下拉);
手机端进入时清空平板默认「当天缴费日期」查全量(initPhoneExtras,平板默认值不变)
- 手机专属新页「运单详情」`IntImpPickUpDetailActivity`:蓝头运单卡(红标签国际进港+品名大标题+
两列 KV+库位橙标签)+ 绿头提货信息卡(柜台办理人/时间,已出库追加出库人/出库时间行),
数据直接 Intent 传列表 bean(Serializable),零新增请求
- `IntImpPickUpDLVBean` 新增 `isPickedUp` 计算属性;手机首页「国际」Tab 增加「提取出库」入口
(权限 AppIntImpPickUpDLV 对应权限串,图标复用 Pad 端 gjj_chu_ku_icon)
- 后端缺口 2 项登记问题清单 #28~#29(pageQuery 无 fdate 入参、出参无车牌/目的港/业务类型;
outState 过滤与 dlvName=出库人语义待内网实测)
- 「进港仓库」5.5 寸手机端适配(设计稿 08_进港仓库,07 出港仓库的进港镜像 + 进港详情页首次适配):
列表 `IntImpStorageUseActivity`(module_gjj,首页菜单实际跳转页;旧版 `GjjWareHouseActivity`
路由已注释弃用)手机布局 = PhoneSearchBar(placeholder「搜索运单号/航班号」,`phoneSearchClick`
分流:含字母→fno、其余→wbNo,实机抓包证实)+ 三格统计(总票数/未清仓/已清仓,clearNormal=0/1
双 total)+ 卡片四操作按钮(清仓/修改库位/出库/入库,已清仓置灰)+ 卡片字段件数/重量/航班号/
**始发站**(bean.dep,出港为目的站);筛选弹层 8 项(始发站走 fdep 入参;代理/特码/运单类型/
业务类型/品名中 5 项为后端未定义入参,预留传参)
- 进港详情页 `IntImpQueryDetailsActivity` + 3 Fragment(运单/仓库/库位信息)首次手机适配:
移植 06 出港详情手机布局,报文区按设计稿 3 项(原始舱单 mftStatus/理货报告 tallyStatus/
海关放行 command),件重区直取 detail 出参 awbPc/awbWeight/cashWeight/inPc/inWeight
(进港自带无需求和),库位卡片优先显示中文姓名(inOpName/outOpName);
ViewModel 增加 str/strOr/dec 取值帮助(与出港同口径)
- 进港版 3 个操作弹框双变体化(清仓/入库/修改库位,手机底部弹层);修改库位手机版增加
「原库位」单选且**只列未出库库位**(沿用平板「已出库不允许修改」校验,卡片级操作时全部已出库
toast 拦截),单库位默认选中
- 手机首页「国际」Tab 增加「进港仓库」入口(权限 AppGjjWareHouse=Constant.AuthName.GjjWareHouseActivity 对应权限串,图标复用出港仓库同款)
- 后端缺口 3 项登记问题清单 #25~#27(pageQuery 无代理/特码/运单类型/业务类型/品名中 5 项筛选
入参;clearNormal 分项统计待实测;detail 出参 dep/dest1/dest/unNumber/opDate 字段待实测确认)
- 「出港仓库」5.5 寸手机端适配(设计稿 07_出港仓库,详情/运单追踪复用已适配页零新增):
列表 `IntExpStorageUseActivity` 手机布局 = PhoneSearchBar(运单号 + 扫码 + 筛选)+ 三格统计
(总票数/未清仓/已清仓,彩色左边条卡片;分项计数按同条件 + `clearNormal=0/1` 各调一次
`pageQueryTotal`,仅手机形态发起)+ 状态标签卡片(已清仓绿/未清仓橙,`clearNormal=="1"` 判定);
**卡片级四操作按钮**(清仓/修改库位/出库/入库,已清仓时清仓置灰拦截)取代平板的勾选+底部批量条,
语义等价于「勾选该主单+全部库位」复用既有 `performClear/performModifyStorage/performOutStorage/
performInStorage` 请求链路;卡片点击进详情(复用出港查询详情 `GjcQueryDetailsActivity` 3 Tab
运单/仓库/库位信息,运单追踪同样复用 `LogDetailActivity`);筛选弹层 8 项按设计稿完整实现
(航班日期/航班号/代理/特码/目的站/运单类型/业务类型/品名中,后三项为后端未定义入参,预留传参)
- 清仓/入库/修改库位三个操作弹框(`IntExpMoveClearDialogModel`/`IntExpInStorageDialogModel`/
`IntExpModifyStorageDialogModel`)双变体化:手机底部弹层(PhoneDataLayout)+ 平板居中弹框不变,
弹出位置按 `DeviceUtil.isPhone()` 切换;修改库位弹框手机版新增「原库位」单选
(卡片级操作时运单可能有多个库位,构造参数 `sourceStorageList` 传入,单库位默认选中,平板路径不受影响)
- 出库二次确认由系统 `AlertDialog` 改为项目规范的 `ConfirmDialogModel`(平板批量出库与手机卡片出库共用)
- 手机首页「国际」Tab 增加「出港仓库」入口(权限 AppIntExpStorageUse,图标复用 Pad 端 gjc_cang_ku_icon)
- `GjcMaWb` 新增 `isCleared` 手机版计算属性;新增手机版资源 `bg_phone_btn_disabled`(灰底禁用按钮)
- 后端缺口 2 项登记问题清单 #23~#24(pageQuery/pageQueryTotal 无 awbType/businessType/goodsCn
筛选入参;pageQueryTotal 的 clearNormal 分项统计过滤待内网 A/B 实测确认)
- 「出港查询」5.5 寸手机端适配(设计稿 06_出港查询,含列表/详情/运单追踪三页):
列表 `GjcQueryActivity` 手机布局 = PhoneSearchBar(运单号 + 扫码 + 筛选)+ 三格统计
(总票数/已入库/已离港,彩色左边条卡片)+ 状态标签卡片(已离港绿/已入库橙/未入库灰,
前端推断口径:fclose 非空=已离港,否则 opDate 非空=已入库);筛选弹层 9 项(平板搜索行的
日期起止/代理并入弹层,特码按设计稿改下拉,字典 `getSpecialCodeList(flag=1, ieFlag="")`,
仅手机形态经 `initPhoneExtras()` 加载);详情 `GjcQueryDetailsActivity` 3 Tab 全部复用
ViewPager2 + Fragment,手机变体以 PhoneKvItem 网格呈现「基本信息/件重信息/回执信息」,
比平板多展示承运人/始发站/中转站/目的站/UN编号,入库件数/入库重量 = warehouseList 各批次求和;
仓库/库位 Tab 改批次/库位卡片;「运单追踪」为公共页 `LogDetailActivity` 双变体首例——
手机竖排流转时间线(Activity `buildVerticalSteps` 动态构建,节点绿=已达/灰=未达 + 环节时间),
平板横向步骤条零改动,手机标题按形态显示「运单追踪」
- 手机首页「国际」Tab 增加「出港查询」入口(权限 AppIntExpSearch,图标复用 Pad 端 gjc_query_icon)
- `PhoneKvItem` 新增 `tagColor` 属性("green" 绿色高亮,海关放行正常态;默认 "orange"),
且占位符 "-"/"--" 不再套高亮标签底
- `GjcMaWb` 新增 `isQueryDeparted`/`isQueryStored`/`orDash` 手机版计算属性
- 新增手机版资源:`ic_phone_package`(件数)、`ic_phone_tag`(特码)、`bg_phone_dot_blue/gray`
- 后端缺口 2 项登记问题清单 #21~#22(isFclose 入参实测被 pageQuery/pageQueryTotal 静默忽略且
无已入库/已离港分项统计出参;detail 出参无锁定状态/计费重量字段)
### 修复 (Fixed)
- 提取出库手机版底部条「已选 N 项」在点全选后不更新:`checkAllClick` 只改勾选状态未同步
`selectedCountText`(卡片单选走 FlowBus→updateCheckAllStatus 正常),补同步后实测「已选 3 项」正确
- **`PhoneFilterPanel` 底部按钮被挤出屏幕**:面板高度 wrap_content 且内容插槽无滚动,筛选项多
(出港查询 9 项)时「重置/确认」按钮直接超出屏幕外且无任何报错。修复:内容插槽外包
ScrollView(weight=1),筛选项少时行为不变,超屏时内容区收缩滚动、按钮始终可见
(已回归出港计重 4 项弹层,行为不变)
### 验证 (Verified)
- 提取出库双端验证通过(2026-07-31,Proxyman Map Local mock 口径):手机端菜单入口、
Tab 角标(双 total)、双形态卡片(含库位空值兜底)、单选/全选与「已选 N 项」联动、
出库 ConfirmDialogModel、待出库/已出库两种详情页形态(出库人行按需显隐)、筛选弹层 4 项、
Tab 切换请求体抓包证实 outState=1 正确携带、冷启动方向恒竖屏、崩溃 0;
平板端回归:缴费日期默认当天/5 搜索条/全选/底部统计/确认出库与改造前一致,崩溃 0。
**待内网补测**:outState 过滤 A/B、dlvName=出库人语义、出库真实提交链路
- 进港仓库双端验证通过(2026-07-31,同出港仓库为 Proxyman Map Local mock 口径):
手机端登录→国际 Tab→进港仓库菜单入口、三格统计、三种卡片形态(含空值 "--" 兜底)、
详情 3 Tab 全字段(锁定状态转中文、库位中文姓名、时间截断、空值兜底)、四操作弹框、
已出库库位过滤(双库位卡片原库位仅列未出库项并默认选中)、无库位/已清仓拦截、
筛选弹层 8 项、搜索分流实机抓包证实(输入 MU1111 → 请求体 fno="MU1111"、wbNo=null)、
冷启动方向恒竖屏、崩溃 0;平板端回归:5 搜索条/全选/底部统计/4 批量按钮与改造前一致,崩溃 0。
**待内网补测**:clearNormal 分项统计 A/B、detail 推断字段(dep/dest1/dest/unNumber/opDate)、
四操作真实提交链路
- 出港仓库双端验证通过(2026-07-31,开发机不在内网 → 全程 Proxyman Map Local mock 口径):
手机端登录→国际 Tab→出港仓库菜单入口、三格统计渲染、三种卡片形态(已清仓置灰清仓/未清仓/
全空字段 "--" 兜底)、四操作按钮(清仓/入库底部弹层、修改库位弹层含原库位默认选中、
出库 ConfirmDialogModel 文案带运单号与库位数、无库位卡片 toast 拦截、已清仓清仓点击拦截)、
筛选弹层 8 项滚动 + 底部按钮可见、卡片点击进详情(Proxyman flow 证实 maWbId 正确传参且
detail mock 命中)、详情右上运单追踪跳转、冷启动方向恒竖屏(ROTATION_0×3 采样)、崩溃 0;
平板端同页面回归:5 搜索条/全选/底部统计/4 批量按钮与改造前一致,崩溃 0。
**待内网补测**:clearNormal 分项统计 A/B、agentCode/spCode/dest 筛选实测、四操作真实提交链路
- 出港查询双端实机验证通过(2026-07-31,内网真实数据 2026-06-09 共 14 票):
手机端登录→国际 Tab→出港查询菜单入口、列表加载/状态标签/三格统计(10+4=14 与卡片一致)、
筛选弹层(日期滚轮、特码/代理下拉真实取数、重置、确认后重新查询)、运单号搜索、扫码拉起、
详情 3 Tab(含仓库批次卡与库位空态)、运单追踪时间线、冷启动方向恒竖屏(mRotation=0×8 采样)、
全程 FATAL 0;平板端回归:列表 5 搜索条 + 底部统计 + 侧滑筛选面板 + 详情表格样式均与改造前一致
- 「出港运抵」5.5 寸手机端适配(主列表 + 3 个手机专属新页,设计稿 04_出港运抵):
主列表 `IntExpArriveActivity` 新增「待运抵/已运抵」Tab(客户端按 declareStatus 是否为空拆分数据集,
接口无此查询参数)+ 卡片长按浮层(待运抵→补充、已运抵→重置+回执,纯 UI 长按)+ 底部全选/已选/申报
(仅待运抵 Tab 显示,选中范围独立于 Pad 全量选择逻辑);新增「分单明细」`IntExpArriveSubOrderActivity`
(数据直接复用主列表已加载的 haWbList,零新增请求,长按单条重置 + 底部批量申报);新增「回执内容」
`IntExpArriveReceiptActivity`(只读页,复用 haWbList[].response + arrivalOpDate);新增「补充信息」
`IntExpArriveSupplementActivity`(参照 Pad 端国际进港「收发货人信息」批量应用模式,对整个运单的
haWbList 批量应用同一份表单,后端接口不存在,已实机确认 404,见问题清单 #19)
- 手机首页「国际」Tab 增加「出港运抵」入口
- `GjcMaWb`/`GjcHaWb` 新增 `isPending`/`declareTagColor`/`haWbTagColor`/`arrivalOpDateText` 等
手机版专属计算属性;`GjcHaWb` 补充 `Serializable` 实现(供 Intent 跨页传递分单列表)
- 新增手机版资源:`bg_phone_tag_indigo`/`bg_phone_tag_amber`(申报状态三色标签)、
`bg_phone_fab_purple`/`bg_phone_fab_amber`/`bg_phone_fab_green`(长按浮层圆形操作按钮)
- 后端缺口 2 项登记问题清单 #19~#20(分单补充信息保存接口不存在、Tab 无申报状态查询参数)
- 「出港运抵」对照设计稿 review 修正:状态重置弹框手机端改为设计稿底部弹层
(**弹框适配首个先例**:`dialog_int_exp_arrive_reset.xml` 同名布局双变体,Pad 版移至
`layout-sw600dp/`,`IntExpArriveResetDialogModel` 构造参数按 `DeviceUtil.isPhone()` 切
`DIALOG_TYPE_BOTTOM`/`CENTER`);主列表补空态(图标 + 文案按 Tab 切换「暂无待/已运抵的运单」);
回执页补空态兜底「暂无回执内容」(单分单入口常无回执,避免纯白屏);分单明细长按浮层按设计稿
补「回执」按钮(单分单跳回执页);分单页底部统计加分母「已选 N / M 项」;Tab 角标初始显示 0
### 修复 (Fixed)
- **RecyclerView + Tab 过滤新坑**:给 item 根节点加 `visibility="@{...}"` 切 Tab 会在列表里
留下空白——RecyclerView/LinearLayoutManager 不像普通 ViewGroup 那样让 GONE 子项自动塌陷为
0 高度。改为在 ViewModel 数据层维护完整列表,Tab 切换时把过滤后的子集整体 `refresh()` 给
adapter(已实机截图验证空白消失、卡片数与角标一致)
- **Serializable Intent + @Transient 崩溃新坑**:`GjcMaWb`/`GjcHaWb` 的 `checked` 等 UI 状态
字段标了 `@Transient`(避免 Gson 序列化 ObservableBoolean),但这同时是 JVM `transient`
修饰符,Java 序列化(Intent 传递机制)会跳过其恢复,反序列化后为 null,切换选中状态时
NullPointerException 崩溃。修复:接收方 `initOnCreated` 对列表 `.map { it.copy() }` 重新
触发类体初始化补上(已实机崩溃复现并验证修复)
- `PhoneStatusTab`/`PhoneFilterPanel` 补上遗漏的 `dispatchSaveInstanceState`/
`dispatchRestoreInstanceState` 屏蔽覆写(其余 5 个手机版复合组件均已重写,这两个是历史遗漏;
同页多实例时子控件状态可能按 id 跨实例串档)
### 验证 (Verified)
- 出港运抵双端实机验证通过:手机端 Tab 切换(角标/卡片双向正确)/全选/单选/长按菜单/查看分单
/分单明细全选与长按重置/补充信息(真实数据回填+下拉字典+提交请求体完整+后端404如实呈现)/
回执页 全链路 crash=0、方向无闪屏;平板端布局与交互(4 搜索条+分单内嵌展开+底部 5 元素操作栏)
与改造前逐项一致
- 出港运抵 review 修正双端实机验证通过:手机端重置底部弹层(长按→重置→下拉展开选项)/主列表空态
/回执页空态/分单浮层重置+回执双按钮/「已选 0 / 2 项」分母 全部与设计稿一致且 crash=0;
平板端回归:列表、批量「状态重置」弹框仍为居中 Pad 样式,行为与改造前一致
- 「出港计重」5.5 寸手机端适配(首个全链路 4+1 页模块,设计稿 03_出港计重):
待计重列表(三格统计 总票数/待计重/计重中 + 卡片状态标签 未计重/计重中 + 卡片内单票「提前运抵」+
FAB 进计重记录 + 筛选弹层 通道号/承运人/运单类型/代理人)、开始计重(头部预配统计卡 + PhoneFormRow 表单,
托盘数量/托盘自重按设计稿手输)、计重记录(三格统计 + 卡片地磅图标进明细/「查看详情」进运单详情 +
筛选弹层 入库日期起止/代理人/特码/目的站/计重人)、计重明细(汇总卡含托盘总重 + 记录卡「修改」→
全屏「修改记录」覆层,复用平板 isEditMode 编辑链路,保存后留在本页)、手机专属新页「运单详情」
`GjcWeighingWbDetailActivity`(12 字段只读网格);手机首页「国际」Tab 增「出港计重」入口
- 新增手机版公共资源:`bg_phone_btn_weak`(浅蓝弱按钮)、`bg_phone_card_top_accent`(卡片顶部主色描边条)、
`ic_phone_clipboard`、`ic_phone_weight`
- `GjcMaWb` 预留 `palletNumber`/`carWeight` 字段、`GjcCheckInRecord` 增 `carWeightStr` 双向绑定属性
(均为手机版专属,平板请求体零变化);后端缺口 7 项登记问题清单 #12~#18
(checkIn 过滤不生效、主列表筛选三入参、入库日期范围、通道号出参、计重人姓名、arriveFlag 不回填、
托盘数量/自重入参)
### 修复 (Fixed)
- 出港计重手机版搜索分流按实测修正:`likeNo` 并非任意模糊(按 8 位 no 匹配),
11 位全号改走 `wbNo` 精确查询,含字母走 `fno`
### 验证 (Verified)
- 出港计重双端实机验证通过:手机端 待计重列表/筛选/开始计重(必填校验)/计重记录/记录筛选/运单详情/
计重明细/修改覆层保存/单票提前运抵 全链路 crash=0、方向无闪屏、请求体新参数全部正确携带;
平板端 待计重/计重记录/开始计重 三页回归无变化(横屏、5 搜索条、日期默认今天、地磅区/托盘车号/浮动按钮组均在)
- 「ULD管理」5.5 寸手机端适配(app 模块首个适配页):列表卡片(三色状态标签)+ 筛选弹层(入库时间/进港航班号/所属航司/来源)+
右下角浮动新增 + 底部全选/批量删除(循环调用单条删除接口)+ 一套 `activity_uld_edit` 手机布局覆盖 新增/修改/详情 三态
(详情态右上铅笔进入修改、状态底部选择面板、所在港默认 HFE、必填校验仅手机生效)
- 新增手机版公共组件 `PhoneFormRow`(行式表单项,INPUT/SELECT/DATE/TEXT 四形态,SELECT 内置底部选择面板);
`PhoneBottomBar` 增加 `actionDanger` 危险按钮样式;新增 `bg_phone_fab`、`bg_phone_btn_danger`、`bg_phone_tag_red_light`、
`ic_phone_trash`、`ic_phone_edit` 等资源
- `ULDBean` 补解析接口既有出参 `ifNo`/`efNo`/`checkInDate`/`checkOutDate` 并预留 `source`;后端缺口 5 项
(状态三态、来源字段、筛选入参、批量删除接口、字段语义)登记问题清单 #7~#11
### 验证 (Verified)
- ULD管理双端实机验证通过:手机端列表/筛选/详情/修改/新增/单选全选/批删确认全链路 crash=0、方向无闪屏;
平板端列表与详情回归无变化
### 修复 (Fixed)
- 出库交接手机版「待/已交接」Tab 与角标计数的过滤参数名修正:`hoState` → `handoverState`
(以 api-doc 接口文档为准;此前误传 `hoState` 被后端静默忽略,两个 Tab 返回同一批数据)。
2026-07-29 内网实机验证通过:fdate=2026-07-16 下待交接/已交接 Tab 分别精确返回对应板箱,与后端直调结果一致。
验证中发现 `IntExpOutHandover/pageQueryTotal` 出参恒 0(后端缺陷,致 Tab 角标与平板底部统计为 0,已记录问题清单 #6)
### 新增 (Added)
- 新增《后端接口对接问题清单》(`documents/后端接口对接问题清单.xlsx`):手机端适配走查发现的接口缺口
(移库列表缺航班字段、缺操作人姓名、`opId/opdate/businessType` 不回填、出库交接查询缺「交接人」入参),
按模块/页面/接口定义/接口地址索引,作为与后端对接的持续维护文档
- phone-adapt skill 新增 `references/api-doc.md`:api-doc MCP 连接与逐接口核对流程,接口对齐从「问用户」升级为「先查接口文档」
---
## [1.9.0] - 2026-07-29
### 新增 (Added)
- 5.5 寸手持端双形态适配框架:同名布局 + 资源限定符(`layout/` 手机、`layout-sw600dp/` 平板)自动切换,业务逻辑(ViewModel/ViewHolder)零改动
- 新增 `DeviceUtil` 设备形态判定工具,及手机版公共组件库(`PhoneSearchBar`、`PhoneStatusTab`、`PhoneDataLayout`、`PhoneFilterPanel`、`PhoneBottomBar`、`PhoneStatBox`、`PhoneKvItem`)
- 完成「国际出港移库」「国际出港出库交接」两个页面的手机端适配,含各自的详情页(`IntExpMoveDetailActivity`、`IntExpOutHandoverDetailActivity`)
- 手机端首页菜单接入「出港移库」「出库交接」入口
### 修复 (Fixed)
- 修复手机端进入页面时「先横屏、约 1 秒后转竖屏」的闪屏问题:Manifest 统一改为 `android:screenOrientation="unspecified"`(全项目 192 处),由 `BaseActivity.applyDeviceOrientation()` 按设备形态在运行时锁定方向,避免系统建窗口时用错误方向
- 修复平板端因中间方案(`@integer/screen_orientation` 资源限定符)引入的回归:先竖屏再转横屏,且交互后 UI 整体放大约 1.6 倍(AutoSize 误选平板竖屏基准所致);验证系统解析 Manifest 属性不套用设备限定符,恢复为 `unspecified` 方案后两端方向与缩放均恢复正常
- AutoSize 增加手机竖屏专用设计基准 390×844dp,避免手机端沿用平板竖屏 720dp 基准导致 UI 压缩
### 技术栈
- Kotlin 1.6.21
- Android Gradle Plugin 7.2.0
- Gradle 7.3.3
- minSdk 24, targetSdk 30, compileSdk 31
---
## [1.8.4] - 2024-12-02
### 新增 (Added)
- 完善 Claude Code 配置,添加 Android 开发权限
- 创建 8 个快捷命令 (/build-debug, /install, /logs 等)
- 定制 CLAUDE.md 为 Android 项目专用开发指南
- 添加 CONTRIBUTING.md 贡献指南文档
- 添加 CHANGELOG.md 版本变更日志
### 改进 (Changed)
- 优化首页菜单交互体验
- 改进 UI 界面显示效果
### 技术栈
- Kotlin 1.6.21
- Android Gradle Plugin 7.2.0
- Gradle 7.3.3
- minSdk 24, targetSdk 30, compileSdk 31
---
## [1.8.x] - 之前版本
### 主要功能
- 国内出港业务模块 (货物收运、复磅称重、转运管理、出库装机)
- 国内进港业务模块 (舱单管理、卸机入库、出库提货、移库管理)
- 国际出港业务模块 (国际货物收运、板箱组装、ULD容器管理)
- 国际进港业务模块 (国际舱单管理、报文解析、理货管理)
- 航班管理模块 (航班查询、航班统计)
- 货物追踪模块 (货物状态追踪、运输日志)
- 监装监卸模块 (监装监卸管理)
- PDA功能模块 (PDA专用功能)
- 蓝牙打印模块 (佳博打印机集成)
### 核心架构
- MVVM 架构模式
- 组件化模块设计 (11个业务模块 + 1个基础库)
- DataBinding 双向绑定
- Kotlin Coroutines + Flow 异步处理
- ARouter 模块间通信
- Retrofit + OkHttp 网络请求
- Glide 图片加载
- MPAndroidChart 图表展示
### 开发基础设施
- BaseActivity/BaseBindingActivity 基类
- BaseViewModel/BasePageViewModel 基类
- CommonAdapter + BaseViewHolder 列表适配
- PadSearchLayout/PadDataLayout 自定义组件
- 完整的 Kotlin 扩展函数库
- DataBinding 适配器集合
---
## 版本说明
### 版本号规则
- **Major.Minor.Patch** (例: 1.8.4)
- **Major**: 重大架构变更或不兼容的 API 修改
- **Minor**: 新功能添加,向下兼容
- **Patch**: Bug 修复和小的改进
### 变更类型
- **Added**: 新增功能
- **Changed**: 功能改进或变更
- **Deprecated**: 即将废弃的功能
- **Removed**: 已移除的功能
- **Fixed**: Bug 修复
- **Security**: 安全性修复
---
## 未来计划
### 待开发功能
- [ ] 优化数据同步机制
- [ ] 增强离线模式支持
- [ ] 改进用户权限管理
- [ ] 优化蓝牙打印稳定性
- [ ] 添加数据导出功能
- [ ] 性能监控和分析
### 技术改进
- [ ] 升级到 Kotlin 2.0
- [ ] 迁移到 Jetpack Compose
- [ ] 优化构建速度
- [ ] 完善单元测试覆盖
- [ ] 添加 UI 自动化测试
---
**维护**: 本文档应在每次版本发布时更新。
**负责人**: 项目维护团队
**最后更新**: 2026-07-29

468
CLAUDE.md
View File

@@ -5,8 +5,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 项目概况
**项目名称**: AirLogistics - 航空物流信息管理系统
**架构模式**: MVVM + 组件化 | **语言**: Kotlin 1.6.21 + Java | **版本**: 1.8.4 (versionCode 84)
**SDK**: minSdk 24 / targetSdk 30 / compileSdk 31 | **Gradle**: 7.3.3 | **JDK**: 1.8
**架构模式**: MVVM + 组件化 | **语言**: Kotlin 1.6.21 + Java | **版本**: 1.8.7 (versionCode 87)
**SDK**: minSdk 24 / targetSdk 30 / compileSdk 31 | **Gradle**: 7.3.3 | **JDK**: 17(`mise.toml`,运行 Gradle 用);编译目标 Java 1.8(`sourceCompatibility` / `jvmTarget`)
### 核心架构
@@ -51,6 +51,15 @@ aerologic-app/
└── 其他业务模块...
```
### ⛔ `air-cargo/` 目录:一律忽略
根目录下的 `air-cargo/` 是同一份源码的另一个独立 git 仓库(远程地址不同,工作分支 `feature/hefei`),**只用于把本项目代码提交到另一个仓库**,不参与本项目任何开发;它已在 `.gitignore` 中忽略。
- 代码扫描、搜索、实现功能、排查问题、查 git 历史时**一律跳过该目录**,不读取、不修改、不当作参考实现(其中文件是同步过去的副本,可能落后于主干)
- `find` / `grep -r` 这类不认 `.gitignore` 的命令要显式排除(如 `find . -path ./air-cargo -prune -o …`、`grep -r --exclude-dir=air-cargo`);`git grep`、`rg` 默认已跳过
- 唯一允许操作它的场景是 `sync-air-cargo` skill(同步并在其本地提交,不 push)
- 其中不应存在任何 Claude / Agent 相关文件(`CLAUDE.md`、`.claude/`、`.mcp.json` 等),sync 脚本和 air-cargo 本地的 `.git/info/exclude` 都已屏蔽
---
## 6 种典型页面类型
@@ -846,6 +855,415 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
---
## 5.5 寸手持端(手机版)双形态适配规范
项目需同时支持 **10 寸平板横屏** 与 **5.5 寸手机竖屏**。核心原则:**业务逻辑层完全复用,只重写布局**。
> **分支约定(2026-10-11 起)**:手机适配分支 `feature/phone-ui-adaptation` 已合并入 `main`,此后平板与手机**只在 `main` 上同时开发**,不再区分分支。合并时的状态、验证覆盖与待决事项见 `documents/双端合入主干交接文档.md`。
> **业务以平板为准**:主流程交互、表单/筛选项、接口入参一律与平板一致,手机端只做 UI 适配——平板底部批量按钮在手机上可改为卡片级操作或 `PhoneBottomBar` 次按钮(`subAction1Text/subAction2Text`),
> 但不得新增平板没有的查询入参(如 `likeNo`)、不得改写平板的默认查询值(如默认当天日期)、不得少平板的表单项。
> 平板新增/修改功能时,必须同步检查对应手机布局与该页全部弹框的手机变体。
### 核心机制:布局按资源限定符自动切换
```
res/layout-sw600dp/activity_xxx.xml ← 平板(最小宽度 >= 600dp)
res/layout/activity_xxx.xml ← 手机(兜底,5.5 寸约 360~411dp)
```
两个变体**同名**,DataBinding 会生成同一个 Binding 基类 + 两个变体实现:
```
ActivityXxxBinding.java 公共基类(Activity 中引用的就是它)
ActivityXxxBindingImpl.java 手机变体
ActivityXxxBindingSw600dpImpl.java 平板变体
```
因此 **Activity / ViewModel / ViewHolder 的 `layoutId()`、`itemLayoutId` 全部保持原样,无需任何设备判断**。
### ⛔ 严禁:用「不同文件名 + 代码判断」切换布局
```kotlin
// ❌ 错误:会生成两个不同的 Binding 类
override fun layoutId() =
if (DeviceUtil.isPhone()) R.layout.activity_xxx_phone else R.layout.activity_xxx
```
`activity_xxx_phone.xml` 生成的是 `ActivityXxxPhoneBinding`,而 `binding` 字段声明的类型是 `ActivityXxxBinding`。
泛型擦除导致**编译期不报错**,但一进入该页面就 `ClassCastException` 崩溃。**必须用同名文件 + 资源限定符。**
### 布局变体的强制约束
| 约束 | 说明 |
|------|------|
| **DataBinding 变量必须完全一致** | 两个变体 `<data>` 中的 `<variable>` 名称与类型必须相同,否则编译失败 |
| **共用控件 id 必须双方都有** | Activity / ViewHolder 中引用的 id(如 `srl`/`rv`/`checkIcon`/`iv_icon`)两个变体都要保留 |
| **单侧独有的 id 自动变 `@Nullable`** | 只在一个变体存在的 id,Binding 基类中会是可空字段,Kotlin 侧强制 null 检查(编译器自动保障,不用担心漏改) |
### 已改造的基础设施
| 文件 | 作用 |
|------|------|
| `module_base/.../util/DeviceUtil.kt` | 设备形态判定。按 `smallestScreenWidthDp >= 600` 区分平板/手机;读 `Resources.getSystem()` 以绕开 AutoSize 对 density 的改写,保证判定稳定 |
| `module_base/BaseActivity.kt` | `applyDeviceOrientation()` 按形态锁方向(手机竖屏 / 平板横屏)。Manifest 统一写 `unspecified` 配合它使用。子类覆写 `lockOrientationByDevice()` 返回 false 可跳过 |
| `module_base/base/BaseBindingActivity.kt` | 注释固化变体约定(`layoutId()` 不做设备分支) |
| `module_base/MyApplication.kt` | **AutoSize 增加手机竖屏基准 390×844**(见下节) |
| `module_base/res/values/colors.xml` | 手机端配色 `phone_*` 系列 |
| `module_base/res/drawable/bg_phone_*.xml` | 手机端卡片、搜索框、状态标签、主按钮背景 |
| `module_base/res/layout/title_tool_bar.xml` | 手机版公共标题栏(平板版已移至 `layout-sw600dp/`) |
`DeviceUtil` 用法:
```kotlin
DeviceUtil.isPhone() // 是否手机
DeviceUtil.isTablet() // 是否平板
DeviceUtil.describe() // "PHONE(sw=411dp, force=AUTO)",日志排查用
DeviceUtil.forceMode = DeviceUtil.ForceMode.PHONE // 调试:在平板上强制走手机布局
```
> `DeviceUtil.layout(padId, phoneId)` 仅用于**极少数**确实需要代码级判断的场景(如非 DataBinding 的布局)。
> **常规页面一律用资源限定符,不要调用它。**
### ⚠️ 屏幕方向:Manifest 一律写 `unspecified`,由 `BaseActivity` 按形态锁定
**症状(改造前)**:手机上打开页面(开屏页最明显)先显示横屏,约 1 秒后才转竖屏,伴随 Activity 重建。
**原因**:`android:screenOrientation` 由系统在**创建 Activity 窗口时**生效,此时业务代码还没执行。
Manifest 写死 `userLandscape` 时,即使 `BaseActivity` 在 `super.onCreate` 之前
`setRequestedOrientation(PORTRAIT)`,也只能在横屏窗口已建好之后再纠正 → 必然闪一下。
**做法**:Manifest 统一写 `unspecified`,让系统按设备**当前物理方向**建窗口
(平板横屏摆放 → 横屏起,手机 → 竖屏起,两端起始方向本就正确),
`BaseActivity.applyDeviceOrientation()` 再按形态锁定,不产生二次纠正:
```xml
<activity android:name="…"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="unspecified" />
```
全项目 192 处已统一(`module_p` 等 24 处本就是 `portrait` 的 PDA 页面保持不变)。
#### ⛔ 别用 `@integer/screen_orientation` 这类资源引用(已验证行不通)
一度改成 `android:screenOrientation="@integer/screen_orientation"` + `values/`=portrait、
`values-sw600dp/`=userLandscape,**看似优雅,实测是错的**:
系统解析 Manifest 属性时**用的是默认配置,不套用设备限定符**,所以 `values-sw600dp/` 永远不会被选中,
所有 Activity 实际都拿到了 `values/` 的竖屏值。后果是平板端:
1. 先竖屏、随后被 `BaseActivity` 纠正成横屏 —— 反而把闪屏问题搬到了平板上
2. **AutoSize 在竖屏配置下选了平板竖屏基准 720dp**(density=1280/720≈1.78,正常应为 1280/1152≈1.11)
→ 交互几次后整个 UI 放大约 1.6 倍
> 验证方法(可复现):把 `values/` 与 `values-sw600dp/` 的值**互换**,并临时让
> `lockOrientationByDevice()` 返回 false(排除运行时纠正),装到平板上看首屏方向。
> 结果是平板取了 `values/` 的值 → 证明限定符被忽略。
### ⚠️ AutoSize 设计基准必须区分手机(易漏且后果严重)
项目使用 AndroidAutoSize,`MyApplication.initAutoSizeConfig()` 按方向+设备切换设计尺寸:
| 场景 | 设计基准 |
|------|----------|
| 横屏(平板) | 1152 × 720 dp |
| 竖屏 + 手机 | **390 × 844 dp**(对应 5.5 寸设计稿画布) |
| 竖屏 + 平板 | 720 × 1280 dp |
| PictureSelector | 360 × 480 dp |
**若手机沿用平板竖屏的 720dp 基准,手机上所有 dp 会被压缩约一半,布局全部错位。**
### 手机版 UI 规范(依据 `documents/5.5寸手持端设计文件/` 设计稿)
| 部位 | 规范 |
|------|------|
| 画布基准 | 390 × 844 dp |
| 顶栏 | 高 48dp,`@color/phone_primary`(#2563EB),左侧「‹ 返回」,标题居中 18sp 加粗 |
| 页面背景 | `@color/phone_bg` (#F3F4F6) |
| 列表卡片 | `@drawable/bg_phone_card`,圆角 12dp,`marginHorizontal=16dp`,`marginTop=12dp`,`padding=14dp` |
| 卡片头行 | 圆形勾选框 20dp(`radiobtn_checked_style`/`radiobtn_unchecked_style`)+ 运单号 15sp 加粗 + 右侧状态标签 |
| 卡片字段 | 两列网格,13sp,`@color/phone_text_body`,行距 8dp |
| 状态标签 | `bg_phone_tag_green`(已完成)/ `bg_phone_tag_gray`(未完成),11sp |
| 搜索行 | 搜索框 40dp 高 + `bg_phone_search`,内含放大镜 + 输入 + 扫码;右侧筛选按钮 40dp |
| 状态 Tab | 高 42dp,选中文字 `phone_primary` + 底部 28×3dp 指示条 |
| 底部操作条 | 白底 + `elevation=8dp`,全选 + 统计(13sp/11sp 两行)+ 主按钮 `bg_phone_btn_primary` |
| 筛选弹层 | `FrameLayout` 遮罩 `#80000000` + 底部白色面板,重置 / 确定各占一半 |
### 手机版页面改造步骤
1. **移动平板布局**:`git mv res/layout/activity_xxx.xml res/layout-sw600dp/activity_xxx.xml`(item 布局同理)
2. **新建手机布局**:在 `res/layout/` 下创建**同名**文件,保持相同 `<variable>` 与共用控件 id
3. **Kotlin 侧不动**:`layoutId()`、`itemLayoutId`、ViewHolder 全部保持原样
4. **纯 UI 交互**(Tab 切换、弹层显隐)写在 Activity,不污染 ViewModel;状态过滤优先复用 ViewModel 既有字段
5. 手机布局顶部 `<include layout="@layout/title_tool_bar" />` 不变(变体自动切换)
6. **弹框(BaseDialogModel)同样要适配**:dialog 布局照样走同名双变体(Pad 版移 `layout-sw600dp/`、
手机版在 `layout/` 写设计稿的底部弹层样式),弹出位置在构造参数按形态切换:
`BaseDialogModel<XxxBinding>(if (DeviceUtil.isPhone()) DIALOG_TYPE_BOTTOM else DIALOG_TYPE_CENTER)`
(参考 `IntExpArriveResetDialogModel` + `dialog_int_exp_arrive_reset.xml` 双变体)。
页面适配完要**逐个排查该页所有 DialogModel**,漏掉的会在手机上弹出 Pad 居中样式
7. **回到手机端首页加菜单入口**(最易遗忘,见下节)——不加的话页面在手机上根本进不去
### 手机端首页菜单入口(适配完必须加,否则没入口)
手机端首页是 `module_p` 的 `PDAEnterActivity`(手持机首页),底部四个 Tab 对应:
| Tab | ViewModel | 位置 |
|-----|-----------|------|
| 国内 | `GnViewModel` | `module_p/.../ui/enter/gn/` |
| 国际 | `GjViewModel` | `module_p/.../ui/enter/gj/` |
**菜单项是手写清单,不是接口下发**。登录接口只下发「分组 → 权限码」(`auths`,形如
`{"IntExp":["AppIntExpCheckWeighing",…],"Supervision":[…]}`,存在
`SharedPreferenceUtil.getString(Constant.Share.authList)`),菜单名称/图标/跳转在平板和手机上都是前端写死的映射。
手机端菜单项按 `authList.contains(bean.key)` 决定显不显示(`PdaMenuGroup.kt` 的 `filterByAuth()`,
**2026-10-21 首页改版时刻意保持与改版前逐字一致**,保证同一账号的入口集合不变)。
所以「适配完页面但手机首页看不到入口」的原因通常**不是没权限,而是清单里没加**。
排查顺序:先确认权限串里有对应 key,再去看清单。
```bash
# 看当前登录账号实际拥有的权限串
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
```
**2026-10-21 起首页按 `documents/5.5寸首页新样式/` 设计稿改版**:清单按设计稿分组
(`PdaMenuGroup(标题, 入口列表)`,如国际 Tab =「国际出港 / 国际进港 / 综合管理」),
由 `PdaMenuRenderer` 渲染成「分组标题栏 + 三列宫格」,过滤后为空的分组整组隐藏。
**分组口径以设计稿为准,不是接口的 auths 分组**(例如单证交接/转运记录在接口里属 Supervision,
设计稿放在「国内出港」)。
加一项就是在对应分组里加一行 —— `ActionBean` 的第 4 个参数填 ARouter 路由即可,无需写 when 分支
(`module_p` 只依赖 `module_base`,看不到 `module_gjc` 等业务模块,跨模块只能走路由):
```kotlin
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 菜单实际跳的才是要适配的那个
- **图标**:设计稿图标源文件在 `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` 已实现)
```kotlin
// Activity 中新增纯 UI 方法,写入 ViewModel 既有的 moveState 字段("" 全部 / "0" 未移库 / "1" 已移库)
fun switchTab(state: String) {
if (viewModel.moveState.value == state) return
viewModel.moveState.value = state
viewModel.searchClick() // 复用原有查询链路
}
```
```xml
<!-- XML 中判等要把字面量放前面,避免 LiveData 为 null 时崩溃 -->
android:textColor="@{`0`.equals(viewModel.moveState) ? @color/phone_primary : @color/phone_text_body}"
```
### 手机版公共组件库(`module_base/.../ui/weight/phone/`)
7 个组件覆盖设计稿中反复出现的部件,**新页面优先复用,不要重写 XML**。属性均为无命名空间写法,
DataBinding 适配器统一放在 `PhoneWidgetKtx.kt`(同名属性按控件类型解析,与平板端 Pad* 组件互不冲突)。
| 组件 | 用途 | 关键属性 |
|------|------|----------|
| `PhoneSearchBar` | 顶部搜索行:搜索框 + 扫码 + 圆形筛选按钮 | `hint`、`value`(双向)、`showScan`、`showFilter`、`upperCase`、`setOnScanClickListener`、`setOnFilterClickListener`、`setRefreshCallBack` |
| `PhoneStatusTab` | 状态 Tab 条(文字 + 角标,选中态下划线) | `tabs`(List\<KeyValue\>)、`counts`(List\<String\>)、`value`(双向)、`setOnTabChanged` |
| `PhoneDataLayout` | 表单/筛选项,`PhoneDataLayoutType` = `INPUT`/`DATE`/`SPINNER` | `title`、`hint`、`type`、`value`(双向)、`list`、`required`、`enable`、`setRefreshCallBack` |
| `PhoneFilterPanel` | 底部筛选弹层(遮罩 + 面板 + 重置/确认) | `title`、`panelVisible`、`setOnResetClick`、`setOnConfirmClick`、`setOnDismissClick` |
| `PhoneBottomBar` | 底部操作条:全选 + 已选统计 + 主按钮 | `allChecked`、`countText`、`actionText`、`setOnAllCheckClick`、`setOnActionClick` |
| `PhoneStatBox` | 卡片内 2~4 列统计框(未传 label 的列自动隐藏) | `label1..4`、`value1..4` |
| `PhoneKvItem` | 详情页字段项(灰标签 + 数值),`tag=true` 转高亮标签(占位符 "-"/"--" 不套底) | `title`、`value`、`tag`、`tagColor`("orange" 默认/"green") |
| `PhoneFormRow` | 行式表单项(左标签+右值+分隔线),`PhoneFormRowType` = `INPUT`/`SELECT`(内置底部选择面板)/`DATE`/`TEXT`(只读右对齐,空值 "--") | `title`、`hint`、`type`、`value`(双向)、`list`、`required`、`enable`、`numeric`、`setRefreshCallBack` |
`PhoneBottomBar` 追加 `actionDanger` 属性:true 时主按钮转浅红底红字 + 垃圾桶图标(ULD 管理批量删除)。
新增资源:`bg_phone_btn_danger`、`bg_phone_fab`(圆形浮动 + 按钮)、`bg_phone_tag_red_light`(浅红标签)、
`ic_phone_trash`、`ic_phone_edit`、色值 `phone_danger_bg/text`。
出港计重追加:`bg_phone_btn_weak`(浅蓝弱按钮,卡片内提前运抵)、`bg_phone_card_top_accent`
(卡片顶部主色描边条)、`ic_phone_clipboard`(FAB 剪贴板)、`ic_phone_weight`(砝码图标,可 tint)。
出港查询追加:`ic_phone_package`(件数)、`ic_phone_tag`(特码)、`bg_phone_dot_blue/gray`(状态圆点);
`PhoneFilterPanel` 内容插槽已内置 ScrollView(筛选项超屏时收缩滚动,底部按钮始终可见——
9 个筛选项曾把「重置/确认」挤出屏幕)。
出港仓库追加:`bg_phone_btn_disabled`(灰底禁用按钮,与 `bg_phone_btn_weak` 同 10dp 圆角,
卡片操作按钮禁用态配 `phone_tag_gray_text` 文字)。
进港移库追加:`bg_phone_btn_green`(绿色主按钮,拍照上传「确认提交」)、`bg_phone_card_header_orange`
(橙色详情卡表头,异常照片)、`bg_phone_photo_add`(虚线加号槽位)、色值 `phone_amber`。
⚠️ **弹层与浮动按钮的层级**:`PhoneFilterPanel` 默认无 elevation,页面里有带 elevation 的浮动按钮(FAB)时
会穿透到弹层之上——给弹层实例加 `android:elevation="8dp"`(大于 FAB 的 6dp)。
**`PhoneFilterPanel` 自带内容插槽**:把 `PhoneDataLayout` 直接写在其标签内即可,组件会自动移入面板中部
并施加 20dp 项间距,无需关心内部层级:
```xml
<com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel
android:layout_width="match_parent"
android:layout_height="match_parent"
panelVisible="@{activity.filterPanelVisible}"
setOnResetClick="@{(v)-> activity.resetFilter()}"
setOnConfirmClick="@{(v)-> activity.confirmFilter()}"
setOnDismissClick="@{(v)-> activity.toggleFilterPanel()}">
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
android:layout_width="match_parent" android:layout_height="wrap_content"
title='@{"航班日期"}' hint='@{"请选择航班日期"}'
type="@{PhoneDataLayoutType.DATE}" value="@={viewModel.flightDate}" />
</com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel>
```
#### ⚠️ 复合自定义 View 必须屏蔽子控件状态恢复(已踩坑)
同一页面放多个 `PhoneDataLayout`/`PhoneStatBox` 时,它们的内部子控件(`et`/`tv`/`spinner`…)**共用同一套 id**。
系统按 id 保存/恢复视图状态会跨实例串档 —— 实测表现为:DATE 项的默认日期被邻近输入框恢复的空文本清空,
并通过双向绑定把 ViewModel 字段也写成了空值(筛选条件静默丢失)。
因此每个含内部 id 的手机版组件都重写了:
```kotlin
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) = dispatchFreezeSelfOnly(container)
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) = dispatchThawSelfOnly(container)
```
**新增同类组件时必须照做**(取值由 ViewModel + DataBinding 持有,本就不需要视图级恢复)。
### 已完成 / 待办
- ✅ **已完成**:适配框架 + 手机端公共资源 + 7 个手机版公共组件
- ✅ **已完成页面**:
- 「国际出港移库」`IntExpMoveActivity`(框架参考页);详情/编辑与平板共用 `IntExpMoveEditActivity`
(手机布局为设计稿「运单详情」样式,卡片「查看详情」/「编辑」分别对应平板点击卡片/侧滑编辑)
- 「国际出港出库交接」`IntExpOutHandoverActivity` + 「板箱详情」`IntExpOutHandoverDetailActivity`
(公共组件首个完整落地页,双端实机验证通过)
- 「ULD管理」`UldListActivity` + `UldEditActivity`(app 模块首个适配页;一套编辑布局覆盖
新增/修改/详情三态;批量删除为前端循环调用单条接口;状态三态/来源等 5 项后端缺口见问题清单 #7~#11)
- 「出港计重」全链路 4+1 页:`GjcWeighingListActivity`(待计重列表,三格统计 + 卡片单票提前运抵 +
FAB 进记录)、`GjcWeighingStartActivity`(开始计重,托盘数量/自重按设计稿手输)、
`GjcWeighingRecordListActivity`(计重记录)、`GjcWeighingRecordDetailsActivity`(计重明细,
isEditMode 复用为手机全屏「修改记录」覆层)+ 手机专属新页 `GjcWeighingWbDetailActivity`(运单详情)。
合并搜索框只分流到平板同名入参:含字母→fno、其余→wbNo;
checkIn 过滤等 7 项后端缺口见问题清单 #12~#18
- 「出港运抵」主列表 + 3 个手机专属新页:`IntExpArriveActivity`(待运抵/已运抵 Tab,客户端按
`declareStatus` 是否为空拆分数据集,卡片长按浮层仅已运抵:重置 / 删除申报 / 回执)、
`IntExpArriveSubOrderActivity`(分单明细,数据直接复用主列表已加载的 `haWbList`,零新增请求)、
`IntExpArriveReceiptActivity`(分单回执只读页,复用 `haWbList[].response`;主单「回执」与平板侧滑同走
`getResponse` → 共用 `CustomsResponseActivity`)。
(原手机专属「补充信息」页 `IntExpArriveSupplementActivity` 因后端无接口(#19)已于 2026-10-09 删除。)
Tab 过滤两个坑:① **RecyclerView 里 `View.GONE` 不会让 item 塌陷为 0 高度**(不同于普通
ViewGroup),必须在数据层整体 `refresh()` 过滤后的子集给 adapter,不能给 item 根节点加
visibility 绑定(实机截图验证过,见「常见编译错误速查」);② 通过 `Intent.putExtra(Serializable)`
传递的 `GjcHaWb`/`GjcMaWb`,其 `checked`/`showMore`/`actionMenuVisible` 字段标了
`@Transient`(= JVM `transient`,Java 序列化会跳过),反序列化后为 null,直接
`.checked.set(...)` 会崩溃——接收方 `initOnCreated` 必须对列表 `.map { it.copy() }` 补上。
2026-07-30 review 修正:状态重置弹框改手机底部弹层(弹框双变体首例,见改造步骤第 6 条)、
主列表/回执页补空态、分单浮层补「回执」、分单统计加分母、Tab 角标初始 0
- 「提取出库」`IntImpPickUpDLVActivity`(module_gjj,类型2 多选批量页 + 手机专属详情新页
`IntImpPickUpDetailActivity`(Intent 传 Serializable bean 零请求)。待出库/已出库 Tab 绑
**文档定义入参 `outState`**(0/1,抓包证实携带,效果待内网 #29)+ 角标双 pageQueryTotal;
双形态卡片按 `isPickedUp`(dlvTime 非空)切换;手机进入时清空平板默认「当天缴费日期」查全量;
缺口 #28:无 fdate 入参、出参无车牌(提货车辆)/目的港/业务类型。
⚠️ 踩坑:`checkAllClick` 全选后底部「已选 N 项」不更新——批量勾选不走 FlowBus 单卡事件,
必须在 checkAllClick 里同步 selectedCountText)
- 「进港查询」`IntImpQueryActivity`(module_gjj;06 出港查询的进港镜像:三格统计
总票数/已入库/已出库为已加载数据客户端计数(`pageQueryTotal` 无分项出参,缺口 #30),
卡片状态前端推断:`dlvTime` 非空=已出库(绿)/`inDate` 非空=已入库(橙)/兜底未入库(灰);
筛选弹层 9 项**入参全部经 api-doc 核对真实存在**(分页参数名 `page`/`limit`,品名参数名
`goods`);详情页与运单追踪复用 08/06 已适配页零新增;平板侧滑筛选面板代码已判空守卫。
⚠️ 踩坑:代理人绑定不能写 `bean.agentName ?? bean.agentCode`——转换器把 JSON null 归一成
空串,`??` 只判 null 不判空串,须用 Bean 计算属性 `agentDisplay`(06 出港查询同步修正))
- 「进港移库」`GnjMoveStashListActivity`(app 模块,**2026-08-03 归属更正后重做**:设计稿 11
实际归属**国内进港移库 DomImpMove**(后端 8 接口齐全、Pad 首页「国内进港→进港移库」既有页面),
最初误判为国际进港并按 IntExpMove 约定预接了不存在的 IntImpMove 模块(原缺口 #31),该套
module_gjj 前端实现已整体删除。现行实现:主列表双形态(Pad 布局移 layout-sw600dp 零改动),
待移库/已移库 Tab 走 **`search`/`searchMoved` 双端点**(`searchMoved` 出参无移库标识,前端按
端点给 bean 打 `isMoved`),Tab 角标 = 两端点各调一次 limit=1 取分页 total(`initPhoneExtras`
开关,仅手机发起);手机专属新页 `GnjMoveStashDetailActivity`(Intent 传 bean 零请求,三卡片:
基本信息/移库信息(仅已移库)/异常照片)+ `GnjMoveStashPhotoActivity`(进入先调 `detail` 回填
已有照片——`searchMoved` 出参无 pic 三字段 #34,提交走 **`modify` 最小体**
`{mawbId,remark,picNumber,pic,originalPic}`,与平板移交编辑页同链路);批量移库复用平板
`moveStashClick`(`move`:fid+ids)。手机首页入口在**国内 Tab**(`GnViewModel`,权限串
`GnjYiKu=AppDomImpMove`,需补通用 route 跳转逻辑,图标从 module_gnj 复制);
GnjMoveStashListActivity 补 @Route `/app/GnjMoveStashListActivity`。
2026-08-03 内网真实数据双端验证通过;筛选 A/B:`wbNo`/`awbType` 生效,
`fdate`/`fno`/`spCode` 被后端静默忽略(#32)、`opId`/`opDate` 恒 null(#33))
- 「进港仓库」`IntImpStorageUseActivity`(module_gjj,07 出港仓库的进港镜像;**首页菜单实际跳转页,
旧版 `GjjWareHouseActivity` 路由已注释弃用**。差异:卡片/筛选用始发站(fdep 入参)替代目的站;
搜索框「运单号/航班号」分流(含字母→fno、其余→wbNo,抓包证实);筛选 5 项无入参(缺口 #25,
比出港多 2 项);连带首次适配进港详情 `IntImpQueryDetailsActivity` + 3 Fragment(报文区 3 项:
原始舱单/理货报告/海关放行,件重区直取 inPc/inWeight 无需求和,detail keys 以平板绑定为准 +
dep/dest1/dest/unNumber/opDate 按出港推断待实测 #27);修改库位手机弹层「原库位」只列未出库
库位(沿用平板校验);**前端接口前缀是 `IntImpStorage` 不是 `IntImpStorageUse`**,api-doc 按
后者搜不到)
- 「出港仓库」`IntExpStorageUseActivity`(嵌套多选列表页型的首个适配:平板「勾选主/子列表 +
底部批量操作」在手机上改为**卡片级四按钮**(清仓/修改库位/出库/入库,已清仓置灰),语义等价
「勾选该主单+全部库位」直接复用既有 performXxx 链路,ViewHolder 对双侧独有 id 全判空;
三格统计 总票数/未清仓/已清仓(后两格 = 同条件 + `clearNormal=0/1` 各调一次 pageQueryTotal,
过滤是否生效待内网 A/B,缺口 #24);筛选弹层 8 项中运单类型/业务类型/品名(中) 为后端未定义
入参(缺口 #23,预留传参);三个操作弹框双变体化 + 手机版修改库位弹层加「原库位」单选;
出库确认 AlertDialog → ConfirmDialogModel;详情与运单追踪完全复用出港查询已适配页,零新增页面)
- 「出港查询」`GjcQueryActivity`(列表:三格统计 总票数/已入库/已离港 + 卡片状态标签,
状态为前端推断:`fclose` 非空=已离港/`opDate` 非空=已入库;筛选弹层 9 项,日期起止与代理
从平板搜索行并入,特码改下拉 `getSpecialCodeList(flag=1, ieFlag="")`;分项统计为已加载
数据客户端计数,`isFclose` 实测被后端忽略,缺口 #21)+ `GjcQueryDetailsActivity`(3 Tab
复用 ViewPager2+Fragment,手机变体 PhoneKvItem 网格;入库件数/重量= warehouseList 求和;
锁定状态/计费重量后端无字段,缺口 #22)+ 「运单追踪」`LogDetailActivity`(app 模块公共页
双变体首例:手机竖排时间线由 Activity `buildVerticalSteps` 动态构建,平板横向步骤条不动;
手机标题按形态切「运单追踪」)。平板侧滑筛选面板代码已判空守卫(手机变体无 filter_panel
include,Binding 字段为 @Nullable)
- ✅ **设计稿 11 个入口(01~11)已于 2026-07-31 全部适配完成**
- ⏳ **待办**:
- `PhoneSearchBar` 接入 AutoQuery 联想(`AutoQueryManager` 目前只对接了 Pad 系组件)
- 联调新发现的后端缺口 #35~#41(ULD uld 搜索 500、IntImpStorage 列表过滤全失效、已移库无 mawbId
无法补传照片等)待后端处理,详见问题清单
- ✅ **2026-10-09 后端反馈联调完成**:问题清单 #1~#34 逐项内网直调 + 模拟器端到端验证,结论写入清单 P 列。
**后端口径:手机筛选一律与平板端查询参数保持一致**(不为手机新增入参);分项统计统一取
`pageQueryTotal` 出参(`ManifestTotalDto` 已补齐分项字段),不要再按状态多调 total。
最新字段语义见 `.claude/skills/phone-adapt/references/api-doc.md` 末节
- 📋 **接口对接**:适配中发现的后端接口缺口统一记录在 `documents/后端接口对接问题清单.xlsx`
(按模块/页面/接口定义/接口地址索引,与后端对接的正式文档,每页适配完必须维护);
接口入参/出参一律以 api-doc MCP 为准核对,流程见
`.claude/skills/phone-adapt/references/api-doc.md`——**后端对不认识的参数静默忽略**,
猜参数名会得出「后端不支持」的误判(出库交接 `hoState`→`handoverState` 已踩过)
### 调试便利:直接拉起页面
`app/src/debug/AndroidManifest.xml`(**仅 Debug 生效,不进 Release 包**)通过 `tools:replace` 将适配调试页临时置为 `exported="true"`,免去每次登录再逐级点入:
```bash
adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.page.move.IntExpMoveActivity
adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.activity.IntExpOutHandoverActivity
```
新增调试页时在该文件追加对应 `<activity>` 即可。
> 模拟器通常连不上内网服务(提示「接口访问失败」),列表为空。若需验证卡片/详情的还原度,
> 可在 Activity 里临时注入几条样例 Bean(`binding.rv.commonAdapter()?.refresh(list)`)截图比对,**验证后务必删除**。
**参考文件**:`module_base/.../util/DeviceUtil.kt`、`module_gjc/res/layout/activity_int_exp_move.xml`(手机)、`module_gjc/res/layout-sw600dp/activity_int_exp_move.xml`(平板)
---
## Import 路径速查
### 基类与常用类
@@ -877,6 +1295,7 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
| 类 | 正确路径 |
|----|----------|
| `DictUtils` | `com.lukouguoji.module_base.util.DictUtils` |
| `DeviceUtil` | `com.lukouguoji.module_base.util.DeviceUtil` |
| `MediaUtil` | `com.lukouguoji.module_base.util.MediaUtil` |
| `UploadUtil` | `com.lukouguoji.module_base.util.UploadUtil` |
| `KeyValue` | `dev.utils.app.info.KeyValue` |
@@ -903,6 +1322,20 @@ adb logcat | grep "com.lukouguoji.aerologic" # 日志
| 资源引用不存在 | drawable/color/string 缺失 | 先检查资源是否存在,不存在则创建或用已有资源 |
| `View.VISIBLE` 报错 | 未导入 | XML `<data>` 中加 `<import type="android.view.View" />` |
| `textStyle` DataBinding 报错 | 不支持 | 用固定值 `android:textStyle="bold"` |
| 手机版页面一进入就 `ClassCastException` | 用了「不同文件名 + 代码判断」切换布局,生成了两个 Binding 类 | 改用同名布局 + 资源限定符(`layout/` 与 `layout-sw600dp/`),见「5.5 寸手持端双形态适配规范」 |
| 布局变体编译报 `variable not found` | 两个变体的 `<data>` 中 `<variable>` 不一致 | 手机与平板变体的变量名/类型必须完全相同 |
| 手机上元素小一半 / 布局错位 | AutoSize 仍按平板 720dp 基准换算 | 确认 `MyApplication.initAutoSizeConfig()` 中手机竖屏走 390×844 分支 |
| 手机版筛选项默认值(如日期)自动被清空 | 多个复合自定义 View 内部子控件 id 相同,系统按 id 恢复状态时跨实例串档 | 组件重写 `dispatchSaveInstanceState`/`dispatchRestoreInstanceState` 为 `dispatchFreezeSelfOnly`/`dispatchThawSelfOnly`(见「手机版公共组件库」) |
| 手机上页面先横屏再转竖屏(闪一下) | Manifest 写死 `screenOrientation="userLandscape"`,系统建窗口时已按横屏,代码纠正为时已晚 | 改成 `android:screenOrientation="unspecified"`,由 `BaseActivity` 按形态锁定(见「屏幕方向」小节) |
| 平板先竖屏再转横屏 + 交互后 UI 放大约 1.6 倍 | Manifest 用了 `@integer/screen_orientation` 资源引用;系统解析 Manifest 不套用限定符,全部取到竖屏值,AutoSize 随之选了平板竖屏 720dp 基准 | Manifest 改回 `unspecified`(见「屏幕方向」小节的 ⛔ 说明) |
| Tab/分类切换后列表顶部留一大块空白,卡片数量却不对 | 给 item 根节点加了 `visibility="@{tab条件 ? VISIBLE : GONE}"`——RecyclerView/LinearLayoutManager 不会像普通 ViewGroup 那样让 GONE 的子项自动塌陷为 0 高度,仍按测量高度参与滚动区域计算 | 不要用 item 级 visibility 做过滤;在 ViewModel 里自行维护完整列表,Tab 切换时把过滤后的子集整体 `commonAdapter()?.refresh(filteredList)` 给 adapter |
| 通过 `Intent.putExtra(Serializable)` 传递的 Bean,切换其 `checked`/`showMore` 等状态时崩溃 `NullPointerException: ... on a null object reference` | 这些字段标了 `@Transient`(= JVM `transient`,本意是避免 Gson 网络请求序列化 `ObservableBoolean`),但 Java 对象序列化(Intent 传递走的机制)会跳过 `transient` 字段的恢复,反序列化后是 null | 接收方 `initOnCreated` 里对列表做 `.map { it.copy() }`——`.copy()` 会重新走一遍类体初始化逻辑,用全新 `ObservableBoolean(false)` 补上 |
| 布局构建报 `Error: The string "--" is not permitted within comments` | XML 注释中出现连续双横线(如写"兜底 \"--\""),XML 规范禁止 | 注释里改用文字描述(如「兜底双横线」),代码绑定表达式中的 `"--"` 不受影响 |
| 多选页手机版点「全选」后底部「已选 N 项」不更新(单卡勾选正常) | 批量勾选在 `checkAllClick` 里直接 set,不走 FlowBus 的单卡 EVENT_CHECK_CHANGED 事件,`selectedCountText` 只在事件回调里更新 | `checkAllClick` 末尾自行同步 `selectedCountText`(平板布局不绑定该字段,无影响) |
| 手机版数字输入框(`PhoneFormRow` 双向绑 Double 的 `xxxStr`)输入 "18" 变成 "81" | getter 由 Double 反推字符串("1" → "1.0"),与输入框文本不等触发 `setText`,光标回到开头 | Bean 里保留输入原文(`@Transient` 私有 String),getter 优先返回原文(参考 `GjcCheckInRecord.trayTotalWeightStr`) |
| 固定 dp 尺寸显示的位图图标在手机上发糊(设计稿里很清晰) | 位图放进了比「源像素 ÷ 显示 dp」更高的密度桶(如 167px 的宫格图放 xxhdpi,在 ~295dpi 设备上解码先缩到 103px,再拉伸到 110px 显示) | 只按固定 dp 尺寸显示的设计稿位图放 `drawable-nodpi`(原尺寸解码,View 一次缩小);排查:截图放大对比设计稿,算 `源px × 设备dpi / 桶dpi` 是否小于显示 px |
| 分段/胶囊控件里选中块上下不居中(上窄下宽) | 设计稿 CSS 的 `border` 计入盒子尺寸,而 Android shape 的 `stroke` 画在 background 上、不占 padding——照抄 CSS 的 `padding: 2px` 少了 1dp 描边 | 容器 padding = CSS padding + border 宽度(如 2 + 1 = 3dp),且容器高度用 `wrap_content`——写死 28dp 时 28/3/22dp 各自取整成 px,内框比选中块少 1px,选中块底边被截;用像素列取色(`magick … txt:-`)量上下间隙验证 |
| XML 里 `bean.a ?? bean.b` 兜底不生效,字段明明有值却显示占位符 | 网络层转换器把 JSON null 归一成空串 `""`,DataBinding 的 `??` 只判 null 不判空串,永远取到左侧的空串 | 在 Bean 上加计算属性做 `isNullOrEmpty` 双兜底(如 `agentDisplay`),XML 绑计算属性(进港查询/出港查询代理人已踩过) |
---
@@ -968,10 +1401,16 @@ private fun loadDictLists() {
```xml
<activity android:name="com.lukouguoji.gjc.activity.XxxActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false" android:screenOrientation="userLandscape" />
android:exported="false"
android:screenOrientation="unspecified" />
```
> `screenOrientation` **必须写 `unspecified`**,不要写死 `userLandscape`、也不要用资源引用。
> 写死横屏会让手机端「先横屏、约 1 秒后转竖屏」闪一下;资源引用则会让平板端出问题
> (详见下方「屏幕方向」小节)。
3. 在 `ARouterConstants` 注册路由(如需)
4. 标题栏统一用 `<include layout="@layout/title_tool_bar" />`,Activity 中 `setBackArrow("标题")`
5. **双形态布局**:需同时支持手机的页面,平板布局放 `res/layout-sw600dp/`、手机布局放 `res/layout/`,两者**同名**且变量与共用 id 一致。详见「5.5 寸手持端双形态适配规范」
### 常见业务操作
@@ -1119,6 +1558,9 @@ Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail)
- `ic_new_expand`(全部展开/收起)36dp + padding 4dp
- **图标之间间距统一 `marginStart=16dp`**(禁止使用 8dp,会导致视觉过近)
- 常用资源: `bg_white_radius_8`、`colorPrimary`、`text_normal`、`text_gray`、`color_bottom_layout`
- **双形态适配:逻辑与 UI 严格分离** — 手机版只重写布局,业务逻辑一律复用现有 ViewModel / ViewHolder;纯 UI 交互(Tab 切换、弹层显隐)写在 Activity,不下沉到 ViewModel
- **禁止用不同文件名切换布局** — 手机/平板布局必须同名 + 资源限定符,否则 `ClassCastException`(详见「5.5 寸手持端双形态适配规范」)
- 手机版资源统一使用 `phone_*` 颜色与 `bg_phone_*` drawable,禁止直接写死色值
### 环境配置
@@ -1132,4 +1574,22 @@ Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail)
2. 资源引用错误 → 检查 drawable/color/string 是否存在
3. DataBinding 错误 → 检查 import、枚举值、View 类导入
4. suspend function 错误 → 在 `viewModelScope.launch` 中调用
5. 仍有问题 → `./gradlew clean` 后重新构建
5. 双形态布局问题 → 查「常见编译错误速查」中 3 条布局变体相关条目;可用 `DeviceUtil.describe()` 打日志确认当前形态
6. 仍有问题 → `./gradlew clean` 后重新构建
### 双端验证命令
```bash
/opt/homebrew/share/android-commandlinetools/emulator/emulator -list-avds # 可用:AeroLogic_Phone(手机)、AeroLogic_Tablet(平板)
adb -s <设备> shell wm size && adb -s <设备> shell wm density # 确认屏幕参数
adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果 PHONE(sw=394dp) / TABLET(sw=800dp)
adb -s <设备> exec-out screencap -p > /tmp/shot.png # 截图比对
```
> 手机端用 **`AeroLogic_Phone`**:按真实交付手持机 1:1 定制——720×1440、5.51 寸(292dpi,sw=394dp)、
> Android 12 / API 31、arm64-v8a,与实机的差异最小。
> 平板端用 **`AeroLogic_Tablet`**:1280×800、160dpi(sw=800dp)、横屏、API 31。平板模拟器没有物理按键,
> `adb shell monkey` 拉起会失败,改用 `am start -n com.lukouguoji.aerologic/com.lukouguoji.module_base.LoginActivity`。
> 模拟器可执行文件在 Homebrew 的 android-commandlinetools 下(见上方命令),不在 `~/Library/Android/sdk`。
> 改动 `BaseActivity` / `BaseBindingActivity` / 公共组件后,**必须在平板上回归**,确认现有平板版行为不变。

627
README.md
View File

@@ -1,301 +1,326 @@
# AirLogistics - 航空物流信息管理系统
一款专业的航空物流管理Android应用,为机场货运站提供全流程数字化解决方案,涵盖国内外货物进出港、仓储管理、车辆调度等核心业务。
## 📋 项目信息
- **版本**: 1.7.9 (versionCode 79)
- **包名**: com.lukouguoji.aerologic
- **最低支持**: Android 7.0 (API 24)
- **目标版本**: Android 10 (API 30)
- **开发语言**: Kotlin + Java
- **架构模式**: MVVM + 组件化
## 🏗️ 架构设计
### 核心架构
```
┌─────────────────────────────────────────────────────┐
│ 应用层 (App) │
├─────────────────────────────────────────────────────┤
│ 国内出港 │ 国内进港 │ 国际出港 │ 国际进港 │
│ (GNC) │ (GNJ) │ (GJC) │ (GJJ) │
├─────────────────────────────────────────────────────┤
│ 基础服务层 (module_base) │
│ • MVVM基类 • 网络框架 • UI组件 • 工具类 │
├─────────────────────────────────────────────────────┤
│ 蓝牙打印 │ 图表库 │ 航班管理 │ 货物追踪 │
│ (Printer) │ (Chart) │ (Hangban) │ (Cargo) │
└─────────────────────────────────────────────────────┘
```
### MVVM模式
- **View层**: Activity/Fragment + DataBinding
- **ViewModel层**: BaseViewModel / BasePageViewModel
- **Model层**: Repository + Retrofit API
- **数据流**: 协程(Coroutines) + Flow
### 组件化架构
- **模块独立**: 通过`isBuildModule`参数控制模块独立运行或集成
- **路由通信**: ARouter实现模块间页面跳转和通信
- **事件总线**: FlowBus(基于Flow) + EventBus
- **依赖注入**: 基于ServiceLoader的服务发现机制
### 基础设施
#### 核心基类
- `BaseActivity`: 协程支持、Loading管理、扫码功能、键盘控制
- `BaseBindingActivity`: DataBinding自动绑定、ViewModel生命周期管理
- `BaseViewModel`: Loading管理、Activity结果处理
- `BasePageViewModel`: 分页列表专用,集成PageModel自动处理分页
- `CommonAdapter + BaseViewHolder`: 统一列表适配器封装
#### 网络请求层
- **ServiceCreator**: Retrofit实例创建,动态IP配置
- **拦截器**: 自动添加Token、时间戳,统一错误处理
- **扩展函数**:
- `launchCollect`: 无Loading的后台请求
- `launchLoadingCollect`: 带Loading的关键操作
- `toRequestBody`: Map/Bean自动转JSON
#### 统一UI组件
- **PadSearchLayout**: 搜索输入框组合控件(支持文本/数字/下拉/日期)
- **PadDataLayout**: 数据展示/编辑组合控件(标题对齐)
- **StatusView**: 状态栏占位View
- **统一样式**: 颜色、字体、圆角、间距的全局规范
## 🚀 技术栈
### 核心框架
- **Kotlin**: 1.6.21 + Coroutines 1.6.0
- **Jetpack**: ViewModel、LiveData、DataBinding
- **网络**: Retrofit 2.6.1 + OkHttp 3.12.12
- **JSON**: FastJSON 1.2.73 + Gson 2.10.1
### UI相关
- **Material Design**: Material Components
- **屏幕适配**: AutoSize 1.2.+ (横屏1152dp × 720dp)
- **下拉刷新**: SmartRefreshLayout 2.0.3
- **图表**: MPAndroidChart (定制版)
- **弹窗**: XPopup 2.9.19
- **图片**: Glide 4.15.1 + PictureSelector v3.11.2
### 功能组件
- **路由**: ARouter 1.5.2
- **扫码**: ZXing 2.2.9
- **权限**: AndPermission 2.0.2
- **打印**: 佳博SDK 2.0.4
- **日志**: Timber 5.0.1
## 💼 核心业务模块
### 国内出港 (module_gnc)
货物收运、复磅称重、转运管理、出库装机、仓库管理、数据统计
### 国内进港 (module_gnj)
舱单管理、卸机入库、出库提货、移库管理、货物交接、实时查询
### 国际出港 (module_gjc)
国际货物收运、板箱组装、ULD容器管理、复磅称重、仓储移库
### 国际进港 (module_gjj)
国际舱单管理、报文解析、理货管理、出库操作、货物交接
### 支持模块
- **航班管理** (module_hangban): 航班查询、航班统计
- **货物追踪** (module_cargo): 货物状态追踪、运输日志
- **监装监卸** (module_mit): 监装监卸管理
- **PDA功能** (module_p): PDA专用功能
- **蓝牙打印** (Printer): 佳博打印机SDK集成
## 📦 项目结构
```
aerologic-app/
├── app/ # 应用壳层,整合所有业务模块
├── module_base/ # 核心基础库,提供所有通用能力
│ ├── base/ # MVVM基类
│ ├── http/ # 网络请求框架
│ ├── ui/weight/ # 统一UI组件
│ ├── ktx/ # Kotlin扩展函数
│ ├── bean/ # 数据模型
│ └── router/ # ARouter路由常量
├── module_gnc/ # 国内出港业务
├── module_gnj/ # 国内进港业务
├── module_gjc/ # 国际出港业务
├── module_gjj/ # 国际进港业务
├── module_hangban/ # 航班查询管理
├── module_cargo/ # 货物追踪查询
├── module_mit/ # 监装监卸管理
├── module_p/ # PDA专用功能
├── Printer/ # 蓝牙打印模块
├── MPChartLib/ # 定制图表库
├── CLAUDE.md # 详细开发指南
└── README.md # 项目说明文档
```
## 🚀 快速开始
### 环境要求
- **IDE**: Android Studio Arctic Fox (2020.3.1) 或更高版本
- **JDK**: 1.8
- **Gradle**: 7.3.3
- **Kotlin**: 1.6.21
### 依赖配置
如遇依赖下载问题:
1. 下载 gradle-7.3.3-bin.zip
- 百度网盘: https://pan.baidu.com/s/18wsuGRlNxjMYbxLhBH9yeg
- 提取码: 1029
2. 配置Gradle:
- Settings → Build, Execution, Deployment → Build Tools → Gradle
- 解压并替换到 "Gradle user home" 目录
### 构建项目
```bash
# 克隆项目
git clone [repository-url]
cd aerologic-app
# 清理构建
./gradlew clean
# 构建Debug版本
./gradlew assembleDebug
# 构建Release版本(已签名)
./gradlew assembleRelease
# 安装到设备
./gradlew installDebug
```
### 组件化开发模式
支持模块独立运行调试:
1. 编辑 `gradle.properties`
2. 设置 `isBuildModule=true` (独立模块) 或 `false` (集成模式)
3. Sync项目并运行对应模块
## ⚙️ 配置说明
### 网络配置
服务器地址配置位置:
- **文件**: `module_base/src/main/res/values/strings.xml`
- **主服务器**: `system_url_inner`
- **地磅服务器**: `weight_url`
- **运行时**: 可通过SharedPreferences动态修改IP地址
### 签名配置
Release版本签名信息:
- **KeyStore**: `key.jks` (项目根目录)
- **密码**: storePassword/keyPassword均为 `123321`
- **别名**: `key`
### 网络安全
- **超时时间**: 30秒(连接/读取/写入)
- **认证方式**: Bearer Token(自动添加)
- **HTTP支持**: 配置在 `res/xml/network_security_config.xml`
## 📚 开发文档
详细的开发指南请参考 **[CLAUDE.md](./CLAUDE.md)**,包含:
- ✅ 完整的架构详解(基类、网络、UI组件)
- ✅ 标准开发模板(Activity、ViewModel、ViewHolder)
- ✅ 实际业务示例(列表页、详情页、编辑页)
- ✅ 开发检查清单(7步列表页、4步详情页、5步编辑页)
- ✅ DataBinding适配器(10+种)
- ✅ Kotlin扩展函数(20+种)
- ✅ 常见业务场景实现
## 🔄 Git工作流
### 分支策略
- **develop**: 主开发分支
- **feature/xxx**: 功能开发分支
- **release/x.x.x**: 发布分支
- **hotfix/xxx**: 紧急修复分支
### 当前分支
- **开发分支**: feature/hefei
- **主分支**: develop(用于PR)
### 提交规范
提交前确保:
- ✅ 代码编译通过
- ✅ 无明显错误和警告
- ✅ 遵循项目代码规范
## 📋 开发原则
1. ✅ **优先使用现有基类和封装**,避免重复造轮子
2. ✅ **充分利用PadDataLayout和PadSearchLayout组件**,保证UI一致性
3. ✅ **遵循统一的命名和目录组织规范**
4. ✅ **使用DataBinding简化代码**,减少findViewById
5. ✅ **利用扩展函数处理通用逻辑**(Toast、Dialog、权限等)
6. ✅ **保持架构一致性**,新增业务参考现有模块
## 🔐 特性亮点
### 1. 高度组件化
- 业务模块完全解耦,支持独立开发、测试、发布
- 通过ARouter实现模块间通信,无需直接依赖
### 2. 统一UI规范
- 自定义PadSearchLayout、PadDataLayout组合控件
- 全局统一的颜色、字体、圆角、间距规范
- 丰富的DataBinding适配器,简化UI开发
### 3. 高效网络框架
- 协程+Flow的现代化异步方案
- 统一的请求封装(launchCollect/launchLoadingCollect)
- 自动的Loading管理和错误处理
### 4. 完善的基础设施
- BaseActivity提供扫码、Loading、键盘控制等通用能力
- BasePageViewModel自动处理分页逻辑
- CommonAdapter统一列表适配器管理
### 5. 丰富的工具扩展
- 20+ Kotlin扩展函数(Toast、Dialog、JSON、日期等)
- 10+ DataBinding适配器(图片加载、可见性、Shape等)
- 完整的业务开发模板和检查清单
## 🛠️ 技术特点
- **响应式编程**: Kotlin Coroutines + Flow
- **声明式UI**: DataBinding双向绑定
- **函数式编程**: Kotlin扩展函数和高阶函数
- **依赖注入**: 基于ServiceLoader的轻量级方案
- **模块化路由**: ARouter组件化通信
- **横屏适配**: AutoSize自动适配1152dp × 720dp
- **蓝牙打印**: 佳博打印机SDK深度集成
- **二维码扫描**: ZXing高性能扫码
## 📄 许可证
本项目为企业内部系统,版权归所属公司所有。
## ⚠️ 注意事项
**重要**: 本项目包含敏感业务信息和配置,仅供授权人员使用,严禁外传。
---
**文档维护**: 本README提供项目概览和架构信息,详细开发指南请参考 [CLAUDE.md](./CLAUDE.md)
# AirLogistics - 航空物流信息管理系统
一款专业的航空物流管理Android应用,为机场货运站提供全流程数字化解决方案,涵盖国内外货物进出港、仓储管理、车辆调度等核心业务。
## 📋 项目信息
- **版本**: 1.7.9 (versionCode 79)
- **包名**: com.lukouguoji.aerologic
- **最低支持**: Android 7.0 (API 24)
- **目标版本**: Android 10 (API 30)
- **开发语言**: Kotlin + Java
- **架构模式**: MVVM + 组件化
## 🏗️ 架构设计
### 核心架构
```
┌─────────────────────────────────────────────────────┐
│ 应用层 (App) │
├─────────────────────────────────────────────────────┤
│ 国内出港 │ 国内进港 │ 国际出港 │ 国际进港 │
│ (GNC) │ (GNJ) │ (GJC) │ (GJJ) │
├─────────────────────────────────────────────────────┤
│ 基础服务层 (module_base) │
│ • MVVM基类 • 网络框架 • UI组件 • 工具类 │
├─────────────────────────────────────────────────────┤
│ 蓝牙打印 │ 图表库 │ 航班管理 │ 货物追踪 │
│ (Printer) │ (Chart) │ (Hangban) │ (Cargo) │
└─────────────────────────────────────────────────────┘
```
### MVVM模式
- **View层**: Activity/Fragment + DataBinding
- **ViewModel层**: BaseViewModel / BasePageViewModel
- **Model层**: Repository + Retrofit API
- **数据流**: 协程(Coroutines) + Flow
### 组件化架构
- **模块独立**: 通过`isBuildModule`参数控制模块独立运行或集成
- **路由通信**: ARouter实现模块间页面跳转和通信
- **事件总线**: FlowBus(基于Flow) + EventBus
- **依赖注入**: 基于ServiceLoader的服务发现机制
### 基础设施
#### 核心基类
- `BaseActivity`: 协程支持、Loading管理、扫码功能、键盘控制
- `BaseBindingActivity`: DataBinding自动绑定、ViewModel生命周期管理
- `BaseViewModel`: Loading管理、Activity结果处理
- `BasePageViewModel`: 分页列表专用,集成PageModel自动处理分页
- `CommonAdapter + BaseViewHolder`: 统一列表适配器封装
#### 网络请求层
- **ServiceCreator**: Retrofit实例创建,动态IP配置
- **拦截器**: 自动添加Token、时间戳,统一错误处理
- **扩展函数**:
- `launchCollect`: 无Loading的后台请求
- `launchLoadingCollect`: 带Loading的关键操作
- `toRequestBody`: Map/Bean自动转JSON
#### 统一UI组件
- **PadSearchLayout**: 搜索输入框组合控件(支持文本/数字/下拉/日期)
- **PadDataLayout**: 数据展示/编辑组合控件(标题对齐)
- **StatusView**: 状态栏占位View
- **统一样式**: 颜色、字体、圆角、间距的全局规范
#### 双形态适配(平板 / 手机)
同时支持 **10 寸平板横屏** 与 **5.5 寸手机竖屏**,业务逻辑层完全复用,只重写布局。
- **DeviceUtil**: 设备形态判定(按 `smallestScreenWidthDp >= 600` 区分),提供调试用强制形态开关
- **布局自动切换**: 通过 Android 资源限定符实现,`res/layout-sw600dp/`(平板)与 `res/layout/`(手机)下放**同名**布局,DataBinding 生成同一个 Binding 类,Activity / ViewModel / ViewHolder 无需任何设备判断
- **屏幕方向**: Manifest 统一写 `android:screenOrientation="unspecified"`,系统按设备当前物理方向建窗口,`BaseActivity.applyDeviceOrientation()` 再按形态锁定(手机竖屏 / 平板横屏),两端均无「先横屏再转竖屏」闪屏。**不要改用资源引用**——系统解析 Manifest 不套用限定符,会导致平板取到竖屏值并触发 AutoSize 放大
- **AutoSize 基准**: 横屏 1152×720 / 手机竖屏 390×844 / 平板竖屏 720×1280
- **手机端资源**: `phone_*` 配色 + `bg_phone_*` drawable + 手机版公共标题栏
> ⚠️ 禁止用「不同文件名 + 代码判断」切换布局,会生成两个 Binding 类导致运行时 `ClassCastException`。
> 详见 [`documents/5.5寸手持端适配技术方案.md`](documents/5.5寸手持端适配技术方案.md) 与 `CLAUDE.md` 对应章节。
## 🚀 技术栈
### 核心框架
- **Kotlin**: 1.6.21 + Coroutines 1.6.0
- **Jetpack**: ViewModel、LiveData、DataBinding
- **网络**: Retrofit 2.6.1 + OkHttp 3.12.12
- **JSON**: FastJSON 1.2.73 + Gson 2.10.1
### UI相关
- **Material Design**: Material Components
- **屏幕适配**: AutoSize 1.2.+ (横屏1152dp × 720dp)
- **下拉刷新**: SmartRefreshLayout 2.0.3
- **图表**: MPAndroidChart (定制版)
- **弹窗**: XPopup 2.9.19
- **图片**: Glide 4.15.1 + PictureSelector v3.11.2
### 功能组件
- **路由**: ARouter 1.5.2
- **扫码**: ZXing 2.2.9
- **权限**: AndPermission 2.0.2
- **打印**: 佳博SDK 2.0.4
- **日志**: Timber 5.0.1
## 💼 核心业务模块
### 国内出港 (module_gnc)
货物收运、复磅称重、转运管理、出库装机、仓库管理、数据统计
### 国内进港 (module_gnj)
舱单管理、卸机入库、出库提货、移库管理、货物交接、实时查询
### 国际出港 (module_gjc)
国际货物收运、板箱组装、ULD容器管理、复磅称重、仓储移库
### 国际进港 (module_gjj)
国际舱单管理、报文解析、理货管理、出库操作、货物交接
### 支持模块
- **航班管理** (module_hangban): 航班查询、航班统计
- **货物追踪** (module_cargo): 货物状态追踪、运输日志
- **监装监卸** (module_mit): 监装监卸管理
- **PDA功能** (module_p): PDA专用功能
- **蓝牙打印** (Printer): 佳博打印机SDK集成
## 📦 项目结构
```
aerologic-app/
├── app/ # 应用壳层,整合所有业务模块
├── module_base/ # 核心基础库,提供所有通用能力
│ ├── base/ # MVVM基类
│ ├── http/ # 网络请求框架
│ ├── ui/weight/ # 统一UI组件
│ ├── ktx/ # Kotlin扩展函数
│ ├── bean/ # 数据模型
│ ├── util/ # 工具类(含 DeviceUtil 设备形态判定)
│ └── router/ # ARouter路由常量
├── module_gnc/ # 国内出港业务
├── module_gnj/ # 国内进港业务
├── module_gjc/ # 国际出港业务
├── module_gjj/ # 国际进港业务
├── module_hangban/ # 航班查询管理
├── module_cargo/ # 货物追踪查询
├── module_mit/ # 监装监卸管理
├── module_p/ # PDA专用功能
├── Printer/ # 蓝牙打印模块
├── MPChartLib/ # 定制图表库
├── documents/ # 设计稿、报价单、技术方案文档
├── CLAUDE.md # 详细开发指南
└── README.md # 项目说明文档
```
各业务模块内的布局按设备形态分目录存放:
```
module_gjc/src/main/res/
├── layout/ # 手机布局(5.5 寸竖屏,兜底)
└── layout-sw600dp/ # 平板布局(10 寸横屏)
```
> 两目录下**同名**的布局是同一布局的资源变体,系统按屏幕最小宽度自动选择。
> 尚未适配手机的页面,布局保留在 `layout/` 即可(两端共用),支持渐进式适配。
## 🚀 快速开始
### 环境要求
- **IDE**: Android Studio Arctic Fox (2020.3.1) 或更高版本
- **JDK**: 1.8
- **Gradle**: 7.3.3
- **Kotlin**: 1.6.21
### 依赖配置
如遇依赖下载问题:
1. 下载 gradle-7.3.3-bin.zip
- 百度网盘: https://pan.baidu.com/s/18wsuGRlNxjMYbxLhBH9yeg
- 提取码: 1029
2. 配置Gradle:
- Settings → Build, Execution, Deployment → Build Tools → Gradle
- 解压并替换到 "Gradle user home" 目录
### 构建项目
```bash
# 克隆项目
git clone [repository-url]
cd aerologic-app
# 清理构建
./gradlew clean
# 构建Debug版本
./gradlew assembleDebug
# 构建Release版本(已签名)
./gradlew assembleRelease
# 安装到设备
./gradlew installDebug
```
### 组件化开发模式
支持模块独立运行调试:
1. 编辑 `gradle.properties`
2. 设置 `isBuildModule=true` (独立模块) 或 `false` (集成模式)
3. Sync项目并运行对应模块
## ⚙️ 配置说明
### 网络配置
服务器地址配置位置:
- **文件**: `module_base/src/main/res/values/strings.xml`
- **主服务器**: `system_url_inner`
- **地磅服务器**: `weight_url`
- **运行时**: 可通过SharedPreferences动态修改IP地址
### 签名配置
Release版本签名信息:
- **KeyStore**: `key.jks` (项目根目录)
- **密码**: storePassword/keyPassword均为 `123321`
- **别名**: `key`
### 网络安全
- **超时时间**: 30秒(连接/读取/写入)
- **认证方式**: Bearer Token(自动添加)
- **HTTP支持**: 配置在 `res/xml/network_security_config.xml`
## 📚 开发文档
详细的开发指南请参考 **[CLAUDE.md](./CLAUDE.md)**,包含:
- ✅ 完整的架构详解(基类、网络、UI组件)
- ✅ 标准开发模板(Activity、ViewModel、ViewHolder)
- ✅ 实际业务示例(列表页、详情页、编辑页)
- ✅ 开发检查清单(7步列表页、4步详情页、5步编辑页)
- ✅ DataBinding适配器(10+种)
- ✅ Kotlin扩展函数(20+种)
- ✅ 常见业务场景实现
## 🔄 Git工作流
### 分支策略
- **develop**: 主开发分支
- **feature/xxx**: 功能开发分支
- **release/x.x.x**: 发布分支
- **hotfix/xxx**: 紧急修复分支
### 当前分支
- **开发分支**: feature/hefei
- **主分支**: develop(用于PR)
### 提交规范
提交前确保:
- ✅ 代码编译通过
- ✅ 无明显错误和警告
- ✅ 遵循项目代码规范
## 📋 开发原则
1. ✅ **优先使用现有基类和封装**,避免重复造轮子
2. ✅ **充分利用PadDataLayout和PadSearchLayout组件**,保证UI一致性
3. ✅ **遵循统一的命名和目录组织规范**
4. ✅ **使用DataBinding简化代码**,减少findViewById
5. ✅ **利用扩展函数处理通用逻辑**(Toast、Dialog、权限等)
6. ✅ **保持架构一致性**,新增业务参考现有模块
## 🔐 特性亮点
### 1. 高度组件化
- 业务模块完全解耦,支持独立开发、测试、发布
- 通过ARouter实现模块间通信,无需直接依赖
### 2. 统一UI规范
- 自定义PadSearchLayout、PadDataLayout组合控件
- 全局统一的颜色、字体、圆角、间距规范
- 丰富的DataBinding适配器,简化UI开发
### 3. 高效网络框架
- 协程+Flow的现代化异步方案
- 统一的请求封装(launchCollect/launchLoadingCollect)
- 自动的Loading管理和错误处理
### 4. 完善的基础设施
- BaseActivity提供扫码、Loading、键盘控制等通用能力
- BasePageViewModel自动处理分页逻辑
- CommonAdapter统一列表适配器管理
### 5. 丰富的工具扩展
- 20+ Kotlin扩展函数(Toast、Dialog、JSON、日期等)
- 10+ DataBinding适配器(图片加载、可见性、Shape等)
- 完整的业务开发模板和检查清单
## 🛠️ 技术特点
- **响应式编程**: Kotlin Coroutines + Flow
- **声明式UI**: DataBinding双向绑定
- **函数式编程**: Kotlin扩展函数和高阶函数
- **依赖注入**: 基于ServiceLoader的轻量级方案
- **模块化路由**: ARouter组件化通信
- **横屏适配**: AutoSize自动适配1152dp × 720dp
- **蓝牙打印**: 佳博打印机SDK深度集成
- **二维码扫描**: ZXing高性能扫码
## 📄 许可证
本项目为企业内部系统,版权归所属公司所有。
## ⚠️ 注意事项
**重要**: 本项目包含敏感业务信息和配置,仅供授权人员使用,严禁外传。
---
**文档维护**: 本README提供项目概览和架构信息,详细开发指南请参考 [CLAUDE.md](./CLAUDE.md)

View File

@@ -26,8 +26,8 @@ android {
applicationId "com.lukouguoji.aerologic"
minSdkVersion 24
targetSdkVersion 30
versionCode 88
versionName "1.8.8"
versionCode 87
versionName "1.8.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
仅 Debug 构建生效的 Manifest 覆盖(不会合并进 Release 包)
用途:手机版布局适配调试期间,允许通过 adb 直接拉起指定页面,
免去每次都要登录并从首页菜单逐级点入。
adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.page.move.IntExpMoveActivity
注意:这里只放调试便利配置,正式包的 exported 仍为 app/src/main/AndroidManifest.xml 中的 false。
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<!-- 出港移库(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.page.move.IntExpMoveActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出库交接(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.IntExpOutHandoverActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出库交接-板箱详情(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.IntExpOutHandoverDetailActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- ULD管理(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.aerologic.page.uld.list.UldListActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- ULD新增/修改/详情(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.aerologic.page.uld.edit.UldEditActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港待计重(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.GjcWeighingListActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港计重-开始计重(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.GjcWeighingStartActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港计重-计重记录(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.GjcWeighingRecordListActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港计重-计重明细(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.GjcWeighingRecordDetailsActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港计重-运单详情(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.GjcWeighingWbDetailActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港运抵(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.IntExpArriveActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港运抵-分单明细(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.IntExpArriveSubOrderActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港运抵-回执内容(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.IntExpArriveReceiptActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港查询(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.GjcQueryActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港查询-详情(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.GjcQueryDetailsActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 运单追踪/操作日志详情(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.aerologic.page.log.detail.LogDetailActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 出港仓库(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjc.activity.IntExpStorageUseActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 进港仓库(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjj.activity.IntImpStorageUseActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 进港查询-详情(手机版适配验证页,进港仓库卡片点击进入) -->
<activity
android:name="com.lukouguoji.gjj.activity.IntImpQueryDetailsActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 提取出库(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjj.activity.IntImpPickUpDLVActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 提取出库-运单详情(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjj.activity.IntImpPickUpDetailActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 进港查询(手机版适配验证页) -->
<activity
android:name="com.lukouguoji.gjj.activity.IntImpQueryActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 进港移库(国内进港,手机版适配验证页) -->
<activity
android:name="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 进港移库-运单详情(手机版专属页) -->
<activity
android:name="com.lukouguoji.aerologic.page.gnj.move.stash.detail.GnjMoveStashDetailActivity"
android:exported="true"
tools:replace="android:exported" />
<!-- 进港移库-拍照上传(手机版专属页) -->
<activity
android:name="com.lukouguoji.aerologic.page.gnj.move.stash.photo.GnjMoveStashPhotoActivity"
android:exported="true"
tools:replace="android:exported" />
</application>
</manifest>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
package com.lukouguoji.aerologic.page.gnj.move.stash.detail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.google.gson.Gson
import com.lukouguoji.aerologic.R
import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashDetailBinding
import com.lukouguoji.aerologic.page.gnj.move.stash.photo.GnjMoveStashPhotoActivity
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.bean.MoveStashBean
import com.lukouguoji.module_base.common.Constant
/**
* 国内进港移库「运单详情」(5.5 寸手持端专属页面,无平板变体)
*
* 由列表卡片的「查看详情」进入;「异常照片」卡片的加号槽位跳拍照上传页。
* 展示字段全部由列表项带入(Gson JSON),零新增请求。
*/
class GnjMoveStashDetailActivity :
BaseBindingActivity<ActivityGnjMoveStashDetailBinding, GnjMoveStashDetailViewModel>() {
override fun layoutId() = R.layout.activity_gnj_move_stash_detail
override fun viewModelClass() = GnjMoveStashDetailViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("运单详情")
binding.viewModel = viewModel
binding.activity = this
viewModel.initOnCreated(intent)
}
/** 「异常照片」加号槽位 → 拍照上传页 */
fun openPhoto() {
viewModel.dataBean.value?.let {
GnjMoveStashPhotoActivity.start(this, it.mawbId, it.wbNo, it.moveId)
}
}
companion object {
@JvmStatic
fun start(context: Context, bean: MoveStashBean) {
context.startActivity(
Intent(context, GnjMoveStashDetailActivity::class.java)
.putExtra(Constant.Key.DATA, Gson().toJson(bean))
)
}
}
}

View File

@@ -0,0 +1,25 @@
package com.lukouguoji.aerologic.page.gnj.move.stash.detail
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
import com.lukouguoji.module_base.base.BaseViewModel
import com.lukouguoji.module_base.bean.MoveStashBean
import com.lukouguoji.module_base.common.Constant
/**
* 国内进港移库「运单详情」ViewModel(5.5 寸手持端专属页面)
*
* 列表项数据已包含详情页所需全部字段(含 isMoved 端点打标),直接由列表带入,不额外请求。
*/
class GnjMoveStashDetailViewModel : BaseViewModel() {
val dataBean = MutableLiveData(MoveStashBean())
fun initOnCreated(intent: Intent) {
val json = intent.getStringExtra(Constant.Key.DATA).orEmpty()
if (json.isNotEmpty()) {
dataBean.value = Gson().fromJson(json, MoveStashBean::class.java)
}
}
}

View File

@@ -1,36 +1,102 @@
package com.lukouguoji.aerologic.page.gnj.move.stash.list
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.lukouguoji.aerologic.R
import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashListBinding
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.ktx.getLifecycleOwner
class GnjMoveStashListActivity :
BaseBindingActivity<ActivityGnjMoveStashListBinding, GnjMoveStashListViewModel>() {
override fun layoutId() = R.layout.activity_gnj_move_stash_list
override fun viewModelClass() = GnjMoveStashListViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("国内进港移库")
binding.viewModel = viewModel
viewModel.pageModel
.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner())
viewModel.refresh()
}
companion object {
@JvmStatic
fun start(context: Context) {
val starter = Intent(context, GnjMoveStashListActivity::class.java)
context.startActivity(starter)
}
}
}
package com.lukouguoji.aerologic.page.gnj.move.stash.list
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.databinding.ObservableBoolean
import com.alibaba.android.arouter.facade.annotation.Route
import com.lukouguoji.aerologic.R
import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashListBinding
import com.lukouguoji.aerologic.page.gnj.move.stash.photo.GnjMoveStashPhotoActivity
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.bean.MoveStashBean
import com.lukouguoji.module_base.common.ConstantEvent
import com.lukouguoji.module_base.impl.FlowBus
import com.lukouguoji.module_base.impl.observe
import com.lukouguoji.module_base.ktx.getLifecycleOwner
import com.lukouguoji.module_base.router.ARouterConstants
import com.lukouguoji.module_base.util.DeviceUtil
@Route(path = ARouterConstants.ACTIVITY_URL_GNJ_MOVE_STASH_LIST)
class GnjMoveStashListActivity :
BaseBindingActivity<ActivityGnjMoveStashListBinding, GnjMoveStashListViewModel>() {
override fun layoutId() = R.layout.activity_gnj_move_stash_list
override fun viewModelClass() = GnjMoveStashListViewModel::class.java
/** 筛选弹层显隐(仅手机 UI 状态,平板布局不绑定) */
val filterPanelVisible = ObservableBoolean(false)
override fun initOnCreate(savedInstanceState: Bundle?) {
// 设计稿手机端标题为「进港移库」,平板保持原标题
setBackArrow(if (DeviceUtil.isPhone()) "进港移库" else "国内进港移库")
binding.viewModel = viewModel
binding.activity = this
viewModel.pageModel
.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, getLifecycleOwner())
if (DeviceUtil.isPhone()) {
// 手机专属:Tab 角标 + 特码字典等额外请求
viewModel.initPhoneExtras()
// 卡片勾选变化 → 更新全选状态与已选计数
FlowBus.with<String>(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) {
viewModel.updateCheckAllStatus()
}
}
// 拍照上传等子页面完成后刷新列表(平板无发射方,观察无副作用)
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) {
viewModel.refresh()
}
viewModel.refresh()
}
// ==================== 筛选弹层(仅手机) ====================
fun toggleFilterPanel() {
filterPanelVisible.set(!filterPanelVisible.get())
}
/** 筛选:重置(只清空弹层内条件,不影响 Tab 与顶部搜索框) */
fun resetFilter() {
viewModel.carrier.value = ""
viewModel.businessType.value = ""
}
fun confirmFilter() {
filterPanelVisible.set(false)
viewModel.refresh()
}
// ==================== 卡片长按「拍照」浮层(仅手机) ====================
/** 卡片长按:展开操作浮层(View.OnLongClickListener 要求返回 Boolean) */
fun showCardActionPhone(bean: MoveStashBean): Boolean {
bean.actionMenuVisible.set(true)
return true
}
/** 点浮层空白处关闭 */
fun hideCardActionPhone(bean: MoveStashBean) {
bean.actionMenuVisible.set(false)
}
/** 浮层「拍照」→ 拍照上传页 */
fun openPhoto(bean: MoveStashBean) {
bean.actionMenuVisible.set(false)
GnjMoveStashPhotoActivity.start(this, bean.mawbId, bean.wbNo, bean.moveId)
}
companion object {
@JvmStatic
fun start(context: Context) {
val starter = Intent(context, GnjMoveStashListActivity::class.java)
context.startActivity(starter)
}
}
}

View File

@@ -3,34 +3,64 @@ package com.lukouguoji.aerologic.page.gnj.move.stash.list
import android.view.View
import android.widget.Toast
import com.lukouguoji.aerologic.databinding.ItemGnjMoveStashListBinding
import com.lukouguoji.aerologic.page.gnj.move.stash.detail.GnjMoveStashDetailActivity
import com.lukouguoji.gnj.page.yiku.handover.GnjYiKuHandoverActivity
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.MoveStashBean
import com.lukouguoji.module_base.common.ConstantEvent
import com.lukouguoji.module_base.impl.FlowBus
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
/**
* 国内进港移库列表项 ViewHolder(双形态共用)
*
* 平板/手机布局差异较大,单侧独有的控件 id 在 Binding 基类中为 @Nullable,
* 全部走判空调用(平板:swipe_menu/btn_edit/ll;手机:ll_detail)。
*/
class GnjMoveStashListViewHolder(view: View) :
BaseViewHolder<MoveStashBean, ItemGnjMoveStashListBinding>(view) {
override fun onBind(item: Any?, position: Int) {
val bean = getItemBean(item)!!
binding.bean = bean
binding.position = position
binding.activity = itemView.context as? GnjMoveStashListActivity
// 勾选框(双形态共有):切换选中;手机端另行广播刷新全选/已选计数
binding.ivIcon.setOnClickListener {
bean.checked.set(!bean.checked.get())
binding.executePendingBindings()
GlobalScope.launch {
FlowBus.with<String>(ConstantEvent.EVENT_CHECK_CHANGED).emit("check_changed")
}
}
// ===== 平板独有交互 =====
// 点击列表项进入移交详情
binding.ll.setOnClickListener {
binding.ll?.setOnClickListener {
GnjYiKuHandoverActivity.startForDetails(itemView.context, bean.mawbId)
}
// 侧滑菜单 - 编辑按钮
binding.btnEdit.setOnClickListener {
binding.swipeMenu.quickClose()
binding.btnEdit?.setOnClickListener {
binding.swipeMenu?.quickClose()
if (bean.pickFlag == "1") {
Toast.makeText(itemView.context, "已出库,不可编辑", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
GnjYiKuHandoverActivity.startForEdit(itemView.context, bean.mawbId)
}
// ===== 手机独有交互 =====
// 「查看详情」→ 手机版运单详情页
binding.llDetail?.setOnClickListener {
GnjMoveStashDetailActivity.start(itemView.context, bean)
}
binding.executePendingBindings()
}
}

View File

@@ -49,6 +49,31 @@ class GnjMoveStashListViewModel : BasePageViewModel() {
val number = MutableLiveData("0")
val weight = MutableLiveData("0")
// ========== 5.5 寸手持端专属字段(平板布局不绑定,默认值保证平板行为零变化) ==========
/** 移库状态 Tab:"0" 待移库(DomImpMove/search)/ "1" 已移库(DomImpMove/searchMoved) */
val moveState = MutableLiveData("0")
val moveStateTabs = MutableLiveData(
listOf(KeyValue("待移库", "0"), KeyValue("已移库", "1"))
)
/** 状态 Tab 角标(与 [moveStateTabs] 等长,取两端点分页 total) */
val moveStateCounts = MutableLiveData(listOf("", ""))
/** 底部操作条「已选 N 项」 */
val selectedCount = MutableLiveData("0")
/** 全选状态(手机 PhoneBottomBar 勾选图标) */
val isAllChecked = MutableLiveData(false)
/** 手机专属的额外请求(Tab 角标)开关,仅手机形态在 Activity 中开启 */
private var phoneExtrasEnabled = false
fun initPhoneExtras() {
phoneExtrasEnabled = true
}
///////////////////////////////////////////////////////////////////////////
// 方法区
///////////////////////////////////////////////////////////////////////////
@@ -95,25 +120,69 @@ class GnjMoveStashListViewModel : BasePageViewModel() {
}
/** 查询条件(分页参数除外),search / searchMoved / searchTotal 三处共用同一套入参 */
private fun buildFilterParams(): Map<String, Any?> = mapOf(
"awbType" to businessType.value!!.ifEmpty { null },
"wbNo" to waybill.value!!.ifEmpty { null },
"carrier" to carrier.value!!.ifEmpty { null },
)
override fun getData() {
val body = mapOf(
val body = (buildFilterParams() + mapOf(
"page" to pageModel.page,
"limit" to pageModel.limit,
"awbType" to businessType.value!!.ifEmpty { null },
"wbNo" to waybill.value!!.ifEmpty { null },
"carrier" to carrier.value!!.ifEmpty { null },
).toRequestBody()
)).toRequestBody()
val moved = moveState.value == "1"
launchLoadingCollect({
NetApply.api.getGnjMoveStashList(
body
)
// 平板 moveState 恒为 "0" → 恒走 search(原行为);已移库 Tab 走 searchMoved
if (moved) NetApply.api.getGnjMoveStashMovedList(body)
else NetApply.api.getGnjMoveStashList(body)
}) {
onSuccess = {
pageModel.handleListBean(it)
// searchMoved 出参无移库标识,按端点打标(卡片形态/详情移库信息卡依赖)
it.list?.forEach { bean -> bean.isMoved = moved }
pageModel.handleListBean(it) { updateCheckAllStatus() }
count.value = it.total
}
}
getCountData(body)
if (phoneExtrasEnabled) loadTabCounts()
}
/** Tab 角标:search / searchMoved 各以 limit=1 查一次取分页 total(仅手机形态发起) */
private fun loadTabCounts() {
val body = (buildFilterParams() + mapOf("page" to 1, "limit" to 1)).toRequestBody()
launchCollect({ NetApply.api.getGnjMoveStashList(body) }) {
onSuccess = { updateTabCount(0, it.total) }
}
launchCollect({ NetApply.api.getGnjMoveStashMovedList(body) }) {
onSuccess = { updateTabCount(1, it.total) }
}
}
private fun updateTabCount(index: Int, total: Int?) {
val current = moveStateCounts.value?.toMutableList() ?: mutableListOf("", "")
if (index < current.size) {
current[index] = (total ?: 0).toString()
moveStateCounts.value = current
}
}
// ========== 手机端全选 / 已选计数 ==========
fun toggleSelectAll() {
val list = pageModel.rv?.commonAdapter()?.items
?.filterIsInstance<MoveStashBean>() ?: return
CheckUtil.handleAllCheck(list)
updateCheckAllStatus()
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
fun updateCheckAllStatus() {
val list = pageModel.rv?.commonAdapter()?.items
?.filterIsInstance<MoveStashBean>() ?: return
isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() }
selectedCount.value = list.count { it.checked.get() }.toString()
}
private fun getCountData(body: RequestBody) {

View File

@@ -0,0 +1,49 @@
package com.lukouguoji.aerologic.page.gnj.move.stash.photo
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.lukouguoji.aerologic.R
import com.lukouguoji.aerologic.databinding.ActivityGnjMoveStashPhotoBinding
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.ktx.addOnItemClickListener
/**
* 国内进港移库「拍照上传」(5.5 寸手持端专属页面,无平板变体)
*
* 由列表卡片长按浮层「拍照」或运单详情「异常照片」加号槽位进入。
* 已有照片经 DomImpMove/detail 回填,提交走 DomImpMove/modify
* (mawbId + pic/originalPic/picNumber 三字段规范,与平板移交编辑页同一链路)。
*/
class GnjMoveStashPhotoActivity :
BaseBindingActivity<ActivityGnjMoveStashPhotoBinding, GnjMoveStashPhotoViewModel>() {
override fun layoutId() = R.layout.activity_gnj_move_stash_photo
override fun viewModelClass() = GnjMoveStashPhotoViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("拍照上传")
binding.viewModel = viewModel
viewModel.rv = binding.rv
binding.rv.addOnItemClickListener(viewModel)
viewModel.initOnCreated(intent)
}
companion object {
/** 已移库运单的 moveId(movedDetail 回填照片用) */
const val EXTRA_MOVE_ID = "moveId"
@JvmStatic
fun start(context: Context, mawbId: String, wbNo: String, moveId: String = "") {
context.startActivity(
Intent(context, GnjMoveStashPhotoActivity::class.java)
.putExtra(Constant.Key.ID, mawbId)
.putExtra(Constant.Key.CODE, wbNo)
.putExtra(EXTRA_MOVE_ID, moveId)
)
}
}
}

View File

@@ -0,0 +1,188 @@
package com.lukouguoji.aerologic.page.gnj.move.stash.photo
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.RecyclerView
import com.lukouguoji.module_base.base.BaseViewModel
import com.lukouguoji.module_base.bean.FileBean
import com.lukouguoji.module_base.bean.GnjYiKuBean
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.common.ConstantEvent
import com.lukouguoji.module_base.http.net.NetApply
import com.lukouguoji.module_base.impl.FlowBus
import com.lukouguoji.module_base.impl.ImageSelectNewViewHolder
import com.lukouguoji.module_base.interfaces.IOnItemClickListener
import com.lukouguoji.module_base.ktx.commonAdapter
import com.lukouguoji.module_base.ktx.launchCollect
import com.lukouguoji.module_base.ktx.launchLoadingCollect
import com.lukouguoji.module_base.ktx.noNull
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.ktx.toRequestBody
import com.lukouguoji.module_base.util.MediaUtil
import com.lukouguoji.module_base.util.UploadUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
/**
* 国内进港移库「拍照上传」ViewModel(5.5 寸手持端专属页面)
*
* 图片选择/预览/删除复用 module_base 的 ImageSelectNewViewHolder(末位加号槽位)。
* 已有照片回填:待移库经 DomImpMove/detail(mawbId),已移库经 DomImpMove/movedDetail(moveId,
* searchMoved 出参不含 mawbId)。提交走 DomImpMove/modify 最小体
* (mawbId/remark/picNumber/pic/originalPic),与平板移交编辑页同一链路。
*/
class GnjMoveStashPhotoViewModel : BaseViewModel(), IOnItemClickListener {
var mawbId = ""
/** 已移库运单的移库记录 ID(searchMoved 出参),用于 movedDetail 回填 */
var moveId = ""
/** 运单号(副标题展示,由列表带入) */
val wbNo = MutableLiveData("")
/** detail 回填的完整 bean(提交时保留 remark 等原值) */
private var detailBean = GnjYiKuBean()
// 图片网格适配器配置(复用公共图片选择组件)
val itemViewHolder = ImageSelectNewViewHolder::class.java
val itemLayoutId = com.lukouguoji.module_base.R.layout.item_image_select_new
var rv: RecyclerView? = null
fun initOnCreated(intent: Intent) {
mawbId = intent.getStringExtra(Constant.Key.ID).orEmpty()
wbNo.value = intent.getStringExtra(Constant.Key.CODE).orEmpty()
moveId = intent.getStringExtra(GnjMoveStashPhotoActivity.EXTRA_MOVE_ID).orEmpty()
// 已有照片回填(pic 缩略图 + originalPic 原图),末位补加号槽位;
// 已移库运单无 mawbId,改按 moveId 走 movedDetail
launchCollect({
if (mawbId.isEmpty() && moveId.isNotEmpty()) NetApply.api.getGnjMoveStashMovedDetail(moveId)
else NetApply.api.getGnjMoveStashDetail(mawbId)
}) {
onSuccess = {
detailBean = it.data ?: GnjYiKuBean()
val picList = detailBean.pic.split(",").filter { p -> p.isNotEmpty() }
val originalList = detailBean.originalPic.split(",").filter { p -> p.isNotEmpty() }
val images = picList.mapIndexed { index, picUrl ->
FileBean(
path = MediaUtil.fillUrl(picUrl),
url = picUrl,
originalPic = MediaUtil.fillUrl(originalList.getOrElse(index) { picUrl })
)
}
rv?.post {
rv?.commonAdapter()?.refresh(images + FileBean())
}
}
onFailed = { _, _ ->
// 详情取不到也允许直接新增照片
rv?.post { rv?.commonAdapter()?.refresh(listOf(FileBean())) }
}
}
}
/**
* 确认提交:先补传未上传的本地图片,再按三字段规范提交 modify
*/
fun submitClick() {
// modify 以 mawbId 定位运单;已移库运单 searchMoved/movedDetail 均不返回 mawbId(后端缺口),无法提交
if (mawbId.isEmpty()) {
showToast("已移库运单缺少运单ID,暂不支持提交照片")
return
}
(rv?.commonAdapter()?.items ?: emptyList())
.asFlow()
.map { it as FileBean }
.filter { it.path.isNotEmpty() && it.url.isEmpty() }
.onEach {
val data = UploadUtil.upload(it.path).data
// UploadUtil 返回:newName=原图(较大),zipFileName=缩略图(较小)
it.url = data?.zipFileName ?: ""
it.originalPic = data?.newName ?: ""
}
.flowOn(Dispatchers.IO)
.onStart { showLoading() }
.catch {
showToast(it.message.noNull("上传图片失败"))
dismissLoading()
}
.onCompletion {
launchLoadingCollect({
val list = (rv?.commonAdapter()?.items ?: emptyList())
.filterIsInstance<FileBean>()
.filter { it.path.isNotEmpty() }
// 最小体提交:只更新图片三字段(remark 保留 detail 原值),与平板移交编辑页一致
val params = mapOf(
"mawbId" to mawbId,
"remark" to detailBean.remark,
"picNumber" to list.size.toString(),
"pic" to list.joinToString(",") { MediaUtil.removeUrl(it.url) },
"originalPic" to list.joinToString(",") { MediaUtil.removeUrl(it.originalPic) },
).toRequestBody(removeEmptyOrNull = true)
NetApply.api.modifyGnjMoveStash(params)
}) {
onSuccess = {
showToast("提交成功")
viewModelScope.launch {
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
}
getTopActivity().finish()
}
onFailed = { _, msg ->
showToast(msg.noNull("提交失败"))
}
}
}
.launchIn(viewModelScope)
}
/**
* 「拍照/上传」按钮:直接唤起图片选择(含拍照入口)
*/
fun pickClick() {
MediaUtil.pickImage(getTopActivity(), maxNum = 9) { results ->
val adapter = rv?.commonAdapter() ?: return@pickImage
results.forEach { result ->
// 在加号槽位(最后一项)之前插入
val insertPos = (adapter.items.size - 1).coerceAtLeast(0)
adapter.items.add(insertPos, FileBean(path = result.realPath))
adapter.notifyItemInserted(insertPos)
}
}
}
/**
* 图片网格点击回调(长按出删除角标 / 点删除角标移除)
*/
override fun onItemClick(position: Int, type: Int) {
val adapter = rv?.commonAdapter() ?: return
val bean = adapter.getItem(position) as? FileBean ?: return
when (type) {
com.lukouguoji.module_base.R.id.rl -> {
if (bean.path.isNotEmpty()) {
bean.canDelete.set(!bean.canDelete.get())
}
}
com.lukouguoji.module_base.R.id.iv_delete -> {
adapter.removeItem(position)
}
else -> {}
}
}
}

View File

@@ -19,6 +19,7 @@ import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.bean.LogBean
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.router.ARouterConstants
import com.lukouguoji.module_base.util.DeviceUtil
@Route(path = ARouterConstants.ACTIVITY_URL_LOG_DETAIL)
class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetailViewModel>() {
@@ -34,7 +35,8 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
}
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("操作日志详情")
// 手机版设计稿标题为「运单追踪」,平板保持原标题
setBackArrow(if (DeviceUtil.isPhone()) "运单追踪" else "操作日志详情")
binding.viewModel = viewModel
// 配置操作详情 RecyclerView(垂直时间线)
@@ -58,7 +60,139 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
val activeCodes = viewModel.activeStepCodes.value ?: emptySet()
val latestCode = viewModel.latestStepCode.value ?: ""
if (steps.isEmpty()) return
buildStepProgressBar(steps, activeCodes, latestCode)
// 手机布局是竖排时间线(layout/activity_log_detail.xml),平板保持横向步骤条
if (DeviceUtil.isPhone()) {
buildVerticalSteps(steps, activeCodes, latestCode)
} else {
buildStepProgressBar(steps, activeCodes, latestCode)
}
}
/**
* 手机版竖排流转时间线(对应设计稿「运单追踪」):
* 每个节点 = 左侧圆点 + 连接竖线,右侧步骤名 + 该环节最新操作时间。
* 已发生节点绿色,未发生节点灰色。
*/
private fun buildVerticalSteps(
steps: List<StepInfo>,
activeCodes: Set<String>,
latestCode: String
) {
val container = binding.llSteps
container.removeAllViews()
container.orientation = LinearLayout.VERTICAL
val logs = viewModel.statusLogList.value ?: emptyList()
val colorGreen = 0xFF16A34A.toInt()
val colorGray = 0xFF9CA3AF.toInt()
val colorLine = 0xFFE5E7EB.toInt()
for (i in steps.indices) {
val step = steps[i]
val isActive = step.code in activeCodes
val isLast = i == steps.size - 1
val stepLog = logs.lastOrNull { it.status == step.code }
val time = stepLog?.opDate ?: ""
val statusContent = stepLog?.content ?: ""
val row = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
// 左列:圆点 + 连接竖线
val dotColumn = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER_HORIZONTAL
layoutParams = LinearLayout.LayoutParams(dp(20), LinearLayout.LayoutParams.MATCH_PARENT)
}
val dot = View(this).apply {
setBackgroundResource(
if (isActive) com.lukouguoji.module_base.R.drawable.bg_phone_dot_green
else com.lukouguoji.module_base.R.drawable.bg_phone_dot_gray
)
layoutParams = LinearLayout.LayoutParams(
dp(if (isActive) 12 else 8), dp(if (isActive) 12 else 8)
).apply { topMargin = dp(if (isActive) 2 else 4) }
}
dotColumn.addView(dot)
if (!isLast) {
val line = View(this).apply {
setBackgroundColor(colorLine)
layoutParams = LinearLayout.LayoutParams(dp(2), 0).apply {
weight = 1f
topMargin = dp(2)
bottomMargin = dp(2)
}
}
dotColumn.addView(line)
}
row.addView(dotColumn)
// 右列:步骤名 + 时间
val content = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
weight = 1f
leftMargin = dp(10)
}
setPadding(0, 0, 0, if (isLast) 0 else dp(24))
}
// 名称行:步骤名(左)+ 已发生节点的状态 pill(右,设计稿如「放行状态:01」)
val nameRow = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
nameRow.addView(TextView(this).apply {
text = step.name
setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
setTextColor(if (isActive) colorGreen else colorGray)
if (isActive) setTypeface(typeface, android.graphics.Typeface.BOLD)
layoutParams = LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT
).apply { weight = 1f }
})
if (isActive && statusContent.isNotEmpty()) {
nameRow.addView(TextView(this).apply {
text = statusContent
setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
setTextColor(colorGreen)
setBackgroundResource(com.lukouguoji.module_base.R.drawable.bg_phone_tag_green)
setPadding(dp(8), dp(3), dp(8), dp(3))
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
maxWidth = dp(180)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { leftMargin = dp(8) }
})
}
content.addView(nameRow)
if (time.isNotEmpty()) {
content.addView(TextView(this).apply {
text = time
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
setTextColor(colorGray)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = dp(4) }
})
}
row.addView(content)
container.addView(row)
}
}
private fun buildStepProgressBar(

View File

@@ -7,7 +7,15 @@ import com.lukouguoji.aerologic.R
import com.lukouguoji.aerologic.databinding.ActivityUldEditBinding
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.common.DetailsPageType
import com.lxj.xpopup.XPopup
/**
* ULD 新增 / 修改 / 详情(一页三态,标题按 pageType 切换)。
*
* 布局 id 不区分设备,平板/手机由资源限定符自动选择:
* layout-sw600dp/activity_uld_edit.xml(平板) / layout/activity_uld_edit.xml(手机)
*/
class UldEditActivity : BaseBindingActivity<ActivityUldEditBinding, UldEditViewModel>() {
override fun layoutId() = R.layout.activity_uld_edit
@@ -18,6 +26,26 @@ class UldEditActivity : BaseBindingActivity<ActivityUldEditBinding, UldEditViewM
viewModel.initOnCreate(intent)
setBackArrow("ULD${viewModel.pageType.value!!.title}")
binding.viewModel = viewModel
binding.activity = this
}
// ==================== 手机版专用交互(纯 UI) ====================
/** 详情态右上铅笔 → 关闭详情并进入修改页 */
fun onEditClick() {
start(this, viewModel.id, modify = true)
finish()
}
/** 状态行点击 → 底部选择面板(详情态只读不弹) */
fun onStatusRowClick() {
if (viewModel.pageType.value == DetailsPageType.Details) return
val options = viewModel.statusPhoneList
XPopup.Builder(this)
.asBottomList("请选择状态", options.map { it.key }.toTypedArray()) { position, _ ->
options.getOrNull(position)?.let { viewModel.onStatusSelected(it.value) }
}
.show()
}
companion object {

View File

@@ -11,6 +11,7 @@ import com.lukouguoji.module_base.ktx.launchLoadingCollect
import com.lukouguoji.module_base.ktx.noNull
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.ktx.toRequestBody
import com.lukouguoji.module_base.util.DeviceUtil
import com.lukouguoji.module_base.util.DictUtils
import dev.DevUtils
import dev.utils.app.info.KeyValue
@@ -41,6 +42,17 @@ class UldEditViewModel : BaseViewModel() {
// 所属航司下拉列表(数据源:POST /typeCode/intCarrier)
val carrierList = MutableLiveData<List<KeyValue>>(emptyList())
// ========== 手机版(5.5 寸手持端)专用 ==========
/**
* 手机版状态选项。后端枚举维持两态(0 正常 / 1 故障,不新增「占用」),
* 按设计稿文案映射 可用=0 / 维修=1。
*/
val statusPhoneList = listOf(
KeyValue("可用", "0"),
KeyValue("维修", "1"),
)
fun initOnCreate(intent: Intent) {
id = intent.getStringExtra(Constant.Key.ID) ?: ""
val modify = intent.getBooleanExtra(Constant.Key.DATA, false)
@@ -56,6 +68,11 @@ class UldEditViewModel : BaseViewModel() {
}
}
/** 手机版状态行选中回调(copy 重新赋值以触发标签样式刷新) */
fun onStatusSelected(statusValue: String) {
uldBean.value = uldBean.value?.copy(status = statusValue)
}
private fun getData() {
if (id.isEmpty()) return
launchLoadingCollect({
@@ -81,6 +98,10 @@ class UldEditViewModel : BaseViewModel() {
showToast("ULD编号不能为空")
return
}
// 手机版日期行只产出 yyyy-MM-dd,而 inStoreTime 为 date-time(仅日期后端返回 400),补齐时分秒
if (DeviceUtil.isPhone() && bean.inStoreTime.length == 10) {
bean.inStoreTime = "${bean.inStoreTime} 00:00:00"
}
val isAdd = pageType.value == DetailsPageType.Add
launchLoadingCollect({
if (isAdd) NetApply.api.saveUld(bean.toRequestBody())

View File

@@ -3,12 +3,27 @@ package com.lukouguoji.aerologic.page.uld.list
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.databinding.ObservableBoolean
import com.alibaba.android.arouter.facade.annotation.Route
import com.lukouguoji.aerologic.R
import com.lukouguoji.aerologic.databinding.ActivityUldListBinding
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.common.ConstantEvent
import com.lukouguoji.module_base.impl.FlowBus
import com.lukouguoji.module_base.impl.observe
import com.lukouguoji.module_base.ktx.addOnItemClickListener
import com.lukouguoji.module_base.ktx.getLifecycleOwner
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.model.ConfirmDialogModel
import com.lukouguoji.module_base.router.ARouterConstants
/**
* ULD管理列表页。
*
* 布局 id 不区分设备,平板/手机由资源限定符自动选择:
* layout-sw600dp/activity_uld_list.xml(平板) / layout/activity_uld_list.xml(手机)
*/
@Route(path = ARouterConstants.ACTIVITY_URL_ULD_LIST)
class UldListActivity : BaseBindingActivity<ActivityUldListBinding, UldListViewModel>() {
override fun layoutId(): Int {
@@ -19,9 +34,13 @@ class UldListActivity : BaseBindingActivity<ActivityUldListBinding, UldListViewM
return UldListViewModel::class.java
}
/** 手机版筛选弹层显隐(仅 UI 状态,不涉及业务逻辑) */
val filterPanelVisible = ObservableBoolean(false)
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("ULD管理")
binding.viewModel = viewModel
binding.activity = this
viewModel.pageModel.bindSmartRefreshLayout(
binding.srl,
binding.rv,
@@ -29,6 +48,11 @@ class UldListActivity : BaseBindingActivity<ActivityUldListBinding, UldListViewM
getLifecycleOwner()
)
binding.rv.addOnItemClickListener(viewModel)
// 手机版卡片勾选变化 → 刷新全选与已选统计(平板 item 无勾选框,不会触发)
FlowBus.with<String>(ConstantEvent.EVENT_CHECK_CHANGED).observe(this) {
viewModel.onItemCheckChanged()
}
}
override fun onResume() {
@@ -36,6 +60,44 @@ class UldListActivity : BaseBindingActivity<ActivityUldListBinding, UldListViewM
viewModel.refresh()
}
// ==================== 手机版专用交互(纯 UI,复用既有 ViewModel 字段) ====================
/** 手机版筛选弹层显隐切换 */
fun toggleFilterPanel() {
filterPanelVisible.set(!filterPanelVisible.get())
}
/**
* 手机版筛选:重置
*
* 只清空弹层内的两个条件(状态 / 所属航司),不影响顶部搜索框。
*/
fun resetFilter() {
viewModel.status.value = ""
viewModel.uldSuffix.value = ""
}
/** 手机版筛选:确认 */
fun confirmFilter() {
filterPanelVisible.set(false)
viewModel.refresh()
}
/** 手机版批量删除(二次确认后调用批量删除接口) */
fun showDeleteConfirmDialog() {
val selected = viewModel.getSelectedItems()
if (selected.isEmpty()) {
showToast("请选择要删除的ULD")
return
}
ConfirmDialogModel(
message = "确定要删除选中的 ${selected.size} 条ULD记录吗?",
title = "删除确认"
) {
viewModel.deleteSelected()
}.show()
}
companion object {
@JvmStatic
fun start(context: Context) {

View File

@@ -4,13 +4,35 @@ import android.view.View
import com.lukouguoji.aerologic.databinding.ItemUldListBinding
import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.ULDBean
import com.lukouguoji.module_base.common.ConstantEvent
import com.lukouguoji.module_base.impl.FlowBus
/**
* ULD管理列表项 ViewHolder。
*
* 平板 / 手机共用:布局按资源限定符切换,本类不做设备判断。
* 平板独有 id(tv_modify / tv_delete 侧滑按钮)与手机独有 id(iv_check 勾选框)
* 在 Binding 基类中为 @Nullable,各自判空使用。
*/
class UldListViewHolder(view: View) : BaseViewHolder<ULDBean, ItemUldListBinding>(view) {
override fun onBind(item: Any?, position: Int) {
val itemBean = getItemBean(item)!!
binding.bean = itemBean
// 点卡片进详情(两端共用,走 ViewModel.onItemClick 的 R.id.ll_content 分支)
notifyItemClick(position, binding.llContent)
notifyItemClick(position, binding.tvModify)
notifyItemClick(position, binding.tvDelete)
// 平板侧滑按钮(手机变体无此控件)
binding.tvModify?.let { notifyItemClick(position, it) }
binding.tvDelete?.let { notifyItemClick(position, it) }
// 手机版勾选框(平板变体无此控件):切换选中态并通知页面刷新全选/已选统计
binding.ivCheck?.setOnClickListener {
itemBean.checked.set(!itemBean.checked.get())
FlowBus.with<String>(ConstantEvent.EVENT_CHECK_CHANGED).tryEmit("check_changed")
binding.executePendingBindings()
}
binding.executePendingBindings()
}
}

View File

@@ -27,6 +27,22 @@ class UldListViewModel : BasePageViewModel() {
val uldSuffix = MutableLiveData("")
val uldNo = MutableLiveData("")
// ========== 手机版(5.5 寸手持端)筛选 ==========
// 后端确认查询入参与平板端保持一致(uld / status / uldSuffix),手机筛选弹层只放 状态 + 所属航司。
/** 手机版状态下拉:文案与卡片标签一致(可用=0 / 维修=1) */
val statusPhoneFilterList = MutableLiveData<List<KeyValue>>(
listOf(
KeyValue("全部", ""),
KeyValue("可用", "0"),
KeyValue("维修", "1"),
)
)
// ========== 手机版批量选择状态 ==========
val isAllChecked = MutableLiveData(false)
val selectedCount = MutableLiveData("0")
// 状态下拉列表
val statusList = MutableLiveData<List<KeyValue>>(
listOf(
@@ -65,12 +81,54 @@ class UldListViewModel : BasePageViewModel() {
)
}) {
onSuccess = {
pageModel.handleListBean(it.toBaseListBean())
pageModel.handleListBean(it.toBaseListBean()) { updateCheckAllStatus() }
count.value = it.total
}
}
}
// ========== 手机版:多选与批量删除 ==========
fun onItemCheckChanged() {
updateCheckAllStatus()
}
fun updateCheckAllStatus() {
val list = pageModel.rv?.commonAdapter()?.items?.filterIsInstance<ULDBean>() ?: return
isAllChecked.value = list.isNotEmpty() && list.all { it.checked.get() }
selectedCount.value = list.count { it.checked.get() }.toString()
}
fun toggleSelectAll() {
val list = pageModel.rv?.commonAdapter()?.items?.filterIsInstance<ULDBean>() ?: return
val shouldCheckAll = !(isAllChecked.value ?: false)
list.forEach { it.checked.set(shouldCheckAll) }
isAllChecked.value = shouldCheckAll
selectedCount.value = (if (shouldCheckAll) list.size else 0).toString()
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
fun getSelectedItems(): List<ULDBean> =
pageModel.rv?.commonAdapter()?.items?.filterIsInstance<ULDBean>()
?.filter { it.checked.get() } ?: emptyList()
/**
* 批量删除选中的 ULD(/eqm/uld/deleteUldBatch,请求体为 ULD 编号数组,一次调用)。
*/
fun deleteSelected() {
val ulds = getSelectedItems().map { it.uld }
if (ulds.isEmpty()) {
showToast("请选择要删除的ULD")
return
}
launchLoadingCollect({ NetApply.api.deleteUldBatch(ulds.toRequestBody()) }) {
onSuccess = {
showToast("已删除 ${ulds.size} 条")
refresh()
}
}
}
fun onUldScanClick() {
ScanModel.startScan(DevUtils.getTopActivity(), Constant.RequestCode.ULD)
}

View File

@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.lukouguoji.module_base.ui.weight.search.layout.SearchLayoutType" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListViewModel" />
<!-- 平板不使用,仅为与手机变体保持 <variable> 一致(DataBinding 双变体强制约束) -->
<variable
name="activity"
type="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListActivity" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical">
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请选择业务类型"}'
list="@{viewModel.businessTypeList}"
type="@{SearchLayoutType.SPINNER}"
value="@={viewModel.businessType}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请输入运单号"}'
icon="@{@drawable/img_scan}"
setInputWaybill="@{true}"
setOnIconClickListener="@{()-> viewModel.waybillScanClick()}"
setRefreshCallBack="@{viewModel::refresh}"
setSearchListRefresh="@{viewModel::getWayBillList}"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.waybill}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请输入承运人"}'
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.carrier}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
style="@style/iv_search_action"
android:onClick="@{()-> viewModel.refresh()}"
android:src="@drawable/img_search" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:onClick="@{()->viewModel.checkAllClick()}"
android:text="全选" />
<ImageView
android:id="@+id/checkIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="5dp"
android:onClick="@{()->viewModel.checkAllClick()}"
android:src="@drawable/img_check_all" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_gnj_move_stash_list" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/color_bottom_layout"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text='@{"合计:"+viewModel.count+"条,总件数:"+viewModel.number+",总重量:"+viewModel.weight+"KG"}'
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="合计:3条,总件数:212,总重量:342KG" />
<TextView
style="@style/tv_bottom_btn"
android:onClick="@{()-> viewModel.moveStashClick()}"
android:text="移库" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.log.detail.LogDetailViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp">
<!-- 运单信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 标题区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_white_radius_top_8"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单信息"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 分割线(贯穿卡片宽度) -->
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E7E7E7" />
<!-- 内容区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_bottom_8"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginEnd="15dp"
android:layout_marginVertical="12dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单号:"
android:textColor="#666666"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.waybillNo}"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单类型:"
android:textColor="#666666"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.waybillType}"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- 流转状态 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:orientation="vertical">
<!-- 标题区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_white_radius_top_8"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="流转状态"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 分割线 -->
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E7E7E7" />
<!-- 内容区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_bottom_8"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_steps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="23dp"
android:layout_marginEnd="15dp"
android:layout_marginVertical="12dp"
android:orientation="horizontal" />
</LinearLayout>
</LinearLayout>
<!-- 操作详情 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:orientation="vertical">
<!-- 标题区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_white_radius_top_8"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="操作详情"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 分割线 -->
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E7E7E7" />
<!-- 内容区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_bottom_8"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_timeline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:paddingStart="8dp"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.lukouguoji.module_base.ui.weight.data.layout.DataLayoutType" />
<import type="com.lukouguoji.module_base.common.DetailsPageType" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.uld.edit.UldEditViewModel" />
<!-- 平板端不使用,但两个布局变体的 <variable> 必须一致(手机变体用它做详情态编辑入口与状态选择) -->
<variable
name="activity"
type="com.lukouguoji.aerologic.page.uld.edit.UldEditActivity" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_8"
android:orientation="vertical"
android:padding="15dp">
<!-- Row 1: ULD编号 / 状态 / 自重 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType == DetailsPageType.Add}"
hint='@{"请输入ULD编号"}'
title='@{"ULD编号"}'
titleLength="@{7}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.uld}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择状态"}'
list="@{viewModel.statusList}"
title='@{"状态"}'
titleLength="@{5}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.uldBean.status}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入自重"}'
title='@{"自重"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.uldWeight}' />
</LinearLayout>
<!-- Row 2: 最大装载容积 / 最大载重 / 所属航司 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入最大装载容积"}'
title='@{"最大装载容积"}'
titleLength="@{7}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.maxVolume}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入最大载重"}'
title='@{"最大载重"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.maxWeight}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择所属航司"}'
list="@{viewModel.carrierList}"
title='@{"所属航司"}'
titleLength="@{5}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.uldBean.carrier}' />
</LinearLayout>
<!-- Row 3: 来源 / 入库时间 / 所在港 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择来源"}'
list="@{viewModel.sourceList}"
title='@{"来源"}'
titleLength="@{7}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.uldBean.source}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择入库时间"}'
title='@{"入库时间"}'
titleLength="@{5}"
type="@{DataLayoutType.DATETIME}"
value='@={viewModel.uldBean.inStoreTime}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入所在港"}'
title='@{"所在港"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.nowPort}' />
</LinearLayout>
<!-- 备注(全行,多行输入) -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入备注"}'
inputHeight="@{80}"
title='@{"备注"}'
titleLength="@{7}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.remark}' />
</LinearLayout>
<!-- 底部操作按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="24dp"
android:gravity="center"
visible="@{viewModel.pageType != DetailsPageType.Details}">
<TextView
style="@style/tv_bottom_btn"
android:layout_width="120dp"
android:layout_marginEnd="20dp"
android:onClick="@{()-> viewModel.onCancelClick()}"
android:text="取消" />
<TextView
style="@style/tv_bottom_btn"
android:layout_width="120dp"
android:onClick="@{()-> viewModel.onSaveClick()}"
android:text="保存" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.lukouguoji.module_base.ui.weight.search.layout.SearchLayoutType" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.uld.list.UldListViewModel" />
<!-- 平板端不使用,但两个布局变体的 <variable> 必须一致(手机变体用它驱动筛选弹层) -->
<variable
name="activity"
type="com.lukouguoji.aerologic.page.uld.list.UldListActivity" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical">
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"选择状态"}'
list="@{viewModel.statusList}"
type="@{SearchLayoutType.SPINNER}"
value="@={viewModel.status}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.33" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"选择所属航司"}'
list="@{viewModel.carrierList}"
type="@{SearchLayoutType.SPINNER}"
value="@={viewModel.uldSuffix}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.33" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请输入ULD编号"}'
icon="@{@drawable/img_scan}"
setOnIconClickListener="@{()-> viewModel.onUldScanClick()}"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.uldNo}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.33" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:orientation="horizontal"
android:paddingHorizontal="24dp">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:onClick="@{()-> viewModel.refresh()}"
android:padding="2dp"
android:src="@drawable/img_search" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="16dp"
android:onClick="@{()-> viewModel.onAddClick()}"
android:src="@drawable/img_add" />
</LinearLayout>
</LinearLayout>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_uld_list" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text='@{"合计:" + viewModel.count + "条"}'
android:textColor="@color/bottom_tool_tips_text_color"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,296 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="bean"
type="com.lukouguoji.module_base.bean.MoveStashBean" />
<!-- 以下两个变量平板不使用,仅为与手机变体保持 <variable> 一致(DataBinding 双变体强制约束) -->
<variable
name="position"
type="Integer" />
<variable
name="activity"
type="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListActivity" />
</data>
<!-- 外层容器承载间距 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginVertical="5dp"
android:orientation="vertical">
<!-- 侧滑布局 -->
<com.mcxtzhang.swipemenulib.SwipeMenuLayout
android:id="@+id/swipe_menu"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 主列表项容器 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_item"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:id="@+id/iv_icon"
loadImage="@{bean.checked ? @drawable/img_plane_s : @drawable/img_plane}"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/img_plane" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单号:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.wbNo}"
android:textColor="@color/colorPrimary"
tools:text="789165431" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="件数:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{bean.pc}' />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重量:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.weight}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="代理人:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{bean.agentCode}' />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<TextView
completeSpace="@{5}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单类型:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{bean.awbType}' />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="航班:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.flight}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="始发港:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.origin}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="起始站:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.dep}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="目的港:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.dest}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<TextView
completeSpace="@{5}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="品名:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.goods}" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
<!-- 右侧箭头 -->
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:src="@drawable/img_pda_right" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 侧滑菜单区域 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- 编辑按钮 -->
<TextView
android:id="@+id/btn_edit"
style="@style/tv_item_action"
android:background="@color/colorPrimary"
android:text="编辑" />
</androidx.appcompat.widget.LinearLayoutCompat>
</com.mcxtzhang.swipemenulib.SwipeMenuLayout>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,320 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="bean"
type="com.lukouguoji.module_base.bean.ULDBean" />
</data>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.mcxtzhang.swipemenulib.SwipeMenuLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginVertical="5dp"
android:background="@drawable/bg_item"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/img_plane"
tools:src="@drawable/img_plane" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:orientation="vertical">
<!-- 第一行:ULD编号 | 状态 | 自重 | 所属航司 | 最大装载容积 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- ULD编号 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ULD编号:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.uld}"
android:textColor="@color/colorPrimary"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 状态 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="状态:"
android:textSize="15sp"
completeSpace="@{3}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.statusName()}"
android:textColor='@{bean.status.equals("0") ? @color/text_green : @color/red}'
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 自重 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自重:"
android:textSize="15sp"
completeSpace="@{4}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.uldWeight}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 所属航司 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="所属航司:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.carrier}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 最大装载容积 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.2"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最大装载容积:"
android:textSize="15sp"
completeSpace="@{7}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.maxVolume}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 第二行:最大载重 | 来源 | 所在港 | 位置 | 入库时间 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<!-- 最大载重 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最大载重:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.maxWeight}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 来源 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="来源:"
android:textSize="15sp"
completeSpace="@{3}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.source}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 所在港 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="所在港:"
android:textSize="15sp"
completeSpace="@{4}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.nowPort}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 位置 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="位置:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.nowPosition}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 入库时间 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.2"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="入库时间:"
android:textSize="15sp"
completeSpace="@{7}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.inStoreTime}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:src="@drawable/img_pda_right" />
</androidx.appcompat.widget.LinearLayoutCompat>
</FrameLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_modify"
style="@style/tv_item_action"
android:background="#8313ef"
android:text="修改" />
<TextView
android:id="@+id/tv_delete"
style="@style/tv_item_action"
android:background="@color/red"
android:text="删除" />
</androidx.appcompat.widget.LinearLayoutCompat>
</com.mcxtzhang.swipemenulib.SwipeMenuLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>

View File

@@ -0,0 +1,332 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
国内进港移库「运单详情」—— 5.5 寸手持端专属页面(无平板变体)
对应设计稿 11_进港移库 的「运单详情」页:
· 卡片一:蓝色表头(运单号 + 绿点 + 操作时间)+ 类型标签行(红「运单类型」+ 货物类型)
+ 品名大字 + 两列字段网格(件数/重量/特码/代理/出运路径/承运人)
· 卡片二:绿色表头「移库信息」+ 移库人 / 移库时间(仅已移库显示)
· 卡片三:橙色表头「异常照片」+ 三个虚线加号槽位(点击跳拍照上传页)
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.gnj.move.stash.detail.GnjMoveStashDetailViewModel" />
<variable
name="activity"
type="com.lukouguoji.aerologic.page.gnj.move.stash.detail.GnjMoveStashDetailActivity" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/phone_page_bg"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- ==================== 卡片一:运单基本信息 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card"
android:orientation="vertical">
<!-- 蓝色表头:运单号 + 绿点 + 操作时间 -->
<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">
<TextView
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" />
<View
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginEnd="6dp"
android:background="@drawable/bg_phone_dot_green" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.dataBean.orDash(viewModel.dataBean.opDate)}"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 类型标签行:运单类型(红标签) + 货物类型 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
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.orDash(viewModel.dataBean.awbType)}"
android:textColor="@color/phone_tag_red_text"
android:textSize="12sp"
android:textStyle="bold" />
<View
android:layout_width="1dp"
android:layout_height="14dp"
android:layout_marginHorizontal="10dp"
android:background="@color/phone_divider" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{viewModel.dataBean.orDash(viewModel.dataBean.cargoType)}"
android:textColor="@color/phone_text_body"
android:textSize="14sp" />
</LinearLayout>
<!-- 品名(主字段大字) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:text="@{viewModel.dataBean.orDash(viewModel.dataBean.goods)}"
android:textColor="@color/phone_text_strong"
android:textSize="22sp"
android:textStyle="bold" />
<!-- 第 1 行:件数 / 重量 -->
<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"
title='@{"件数"}'
value="@{viewModel.dataBean.orDash(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.orDash(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.agentDisplay}" />
</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.by1}" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- ==================== 卡片二:移库信息(仅已移库显示) ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:visibility="@{viewModel.dataBean.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:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<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.opUserDisplay}" />
<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.opDate}" />
</LinearLayout>
</LinearLayout>
<!-- ==================== 卡片三:异常照片 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/bg_phone_card"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card_header_orange"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp"
android:text="异常照片"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<!-- 三个虚线加号槽位(等宽正方形由 weight + 固定高实现),点击进拍照上传页 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="96dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_photo_add"
android:gravity="center"
android:onClick="@{()-> activity.openPhoto()}"
android:text="+"
android:textColor="@color/phone_text_hint"
android:textSize="24sp" />
<TextView
android:layout_width="0dp"
android:layout_height="96dp"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_photo_add"
android:gravity="center"
android:onClick="@{()-> activity.openPhoto()}"
android:text="+"
android:textColor="@color/phone_text_hint"
android:textSize="24sp" />
<TextView
android:layout_width="0dp"
android:layout_height="96dp"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_photo_add"
android:gravity="center"
android:onClick="@{()-> activity.openPhoto()}"
android:text="+"
android:textColor="@color/phone_text_hint"
android:textSize="24sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>

View File

@@ -1,148 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.lukouguoji.module_base.ui.weight.search.layout.SearchLayoutType" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical">
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请选择业务类型"}'
list="@{viewModel.businessTypeList}"
type="@{SearchLayoutType.SPINNER}"
value="@={viewModel.businessType}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请输入运单号"}'
icon="@{@drawable/img_scan}"
setInputWaybill="@{true}"
setOnIconClickListener="@{()-> viewModel.waybillScanClick()}"
setRefreshCallBack="@{viewModel::refresh}"
setSearchListRefresh="@{viewModel::getWayBillList}"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.waybill}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请输入承运人"}'
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.carrier}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
style="@style/iv_search_action"
android:onClick="@{()-> viewModel.refresh()}"
android:src="@drawable/img_search" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:onClick="@{()->viewModel.checkAllClick()}"
android:text="全选" />
<ImageView
android:id="@+id/checkIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="5dp"
android:onClick="@{()->viewModel.checkAllClick()}"
android:src="@drawable/img_check_all" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_gnj_move_stash_list" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/color_bottom_layout"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text='@{"合计:"+viewModel.count+"条,总件数:"+viewModel.number+",总重量:"+viewModel.weight+"KG"}'
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="合计:3条,总件数:212,总重量:342KG" />
<TextView
style="@style/tv_bottom_btn"
android:onClick="@{()-> viewModel.moveStashClick()}"
android:text="移库" />
</LinearLayout>
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<!--
国内进港移库 —— 5.5 寸手持端布局(平板变体见 layout-sw600dp/ 同名文件)
对应设计稿 documents/5.5寸手持端设计文件/11_进港移库/进港移库.html:
· 顶部搜索框(运单号,含扫码)+ 筛选弹层(航班日期 / 航班号 / 特码 / 运单类型)
· 「待移库 / 已移库」Tab 绑定 viewModel.moveState("0"/"1"),
分别走 DomImpMove/search 与 DomImpMove/searchMoved 两个端点
· 已移库 Tab 下隐藏底部操作条(与设计稿一致)
· 卡片长按出「拍照」浮层(见 item_gnj_move_stash_list.xml)
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayoutType" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListViewModel" />
<variable
name="activity"
type="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListActivity" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/phone_page_bg">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<!-- ==================== 搜索行 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="12dp">
<com.lukouguoji.module_base.ui.weight.phone.PhoneSearchBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
hint='@{"搜索运单号"}'
setOnFilterClickListener="@{(v)-> activity.toggleFilterPanel()}"
setOnScanClickListener="@{(v)-> viewModel.waybillScanClick()}"
setRefreshCallBack="@{viewModel::refresh}"
upperCase="@{true}"
value="@={viewModel.waybill}" />
</LinearLayout>
<!-- ==================== 待移库 / 已移库 Tab ==================== -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneStatusTab
android:layout_width="match_parent"
android:layout_height="wrap_content"
counts="@{viewModel.moveStateCounts}"
setOnTabChanged="@{viewModel::refresh}"
tabs="@{viewModel.moveStateTabs}"
value="@={viewModel.moveState}" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/phone_divider" />
<!-- ==================== 列表 ==================== -->
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="0dp"
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:paddingBottom="12dp"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_gnj_move_stash_list" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<!-- ==================== 底部操作条(已移库 Tab 下隐藏) ==================== -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneBottomBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
actionText='@{"移库"}'
allChecked="@{viewModel.isAllChecked}"
countText='@{"已选 " + viewModel.selectedCount + " 项"}'
setOnActionClick="@{(v)-> viewModel.moveStashClick()}"
setOnAllCheckClick="@{(v)-> viewModel.toggleSelectAll()}"
android:visibility="@{`1`.equals(viewModel.moveState) ? View.GONE : View.VISIBLE}" />
</LinearLayout>
<!-- ==================== 筛选弹层 ==================== -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel
android:layout_width="match_parent"
android:layout_height="match_parent"
panelVisible="@{activity.filterPanelVisible}"
setOnConfirmClick="@{(v)-> activity.confirmFilter()}"
setOnDismissClick="@{(v)-> activity.toggleFilterPanel()}"
setOnResetClick="@{(v)-> activity.resetFilter()}"
title='@{"筛选条件"}'>
<!-- 承运人(入参 carrier,与平板搜索栏一致) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
hint='@{"请输入承运人"}'
title='@{"承运人"}'
type="@{PhoneDataLayoutType.INPUT}"
value="@={viewModel.carrier}" />
<!-- 运单类型(入参 awbType,复用平板既有 businessType 字段与字典) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
hint='@{"请选择运单类型"}'
list="@{viewModel.businessTypeList}"
title='@{"运单类型"}'
type="@{PhoneDataLayoutType.SPINNER}"
value="@={viewModel.businessType}" />
</com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel>
</FrameLayout>
</layout>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
国内进港移库「拍照上传」—— 5.5 寸手持端专属页面(无平板变体)
对应设计稿 11_进港移库/photo.html:
· 白卡「运单照片上传」+ 图片预览网格(复用公共 ImageSelectNewViewHolder,末位加号槽位)
· 底部两个按钮:拍照/上传(蓝) + 确认提交(绿)
提交走 DomImpMove/modify(pic/originalPic/picNumber 三字段规范)。
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.gnj.move.stash.photo.GnjMoveStashPhotoViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/phone_page_bg"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题:运单照片上传(副行展示运单号) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单照片上传"
android:textColor="@color/phone_text_strong"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@{viewModel.wbNo}"
android:textColor="@color/phone_text_body"
android:textSize="13sp"
tools:text="78133363573" />
<!-- 图片预览网格(末位加号槽位,长按出删除角标) -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:overScrollMode="never"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
tools:itemCount="3" />
<!-- 底部按钮:拍照/上传(蓝) + 确认提交(绿) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_btn_primary"
android:gravity="center"
android:onClick="@{()-> viewModel.pickClick()}"
android:text="拍照/上传"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_btn_green"
android:gravity="center"
android:onClick="@{()-> viewModel.submitClick()}"
android:text="确认提交"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>

View File

@@ -1,6 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
操作日志详情(运单追踪)—— 手机版布局(5.5 寸手持端,最小宽度 < 600dp 时生效)
平板版为同名文件 layout-sw600dp/activity_log_detail.xml(横向步骤条 + 操作详情时间线),
两者是同一布局的资源变体,DataBinding 因此生成同一个 ActivityLogDetailBinding 类。
变体之间必须满足:
· DataBinding 变量完全一致:viewModel
· 保留 Activity 引用的控件 id:ll_steps / rv_timeline
手机版按设计稿「运单追踪」呈现:
· 顶卡:运单号 + 运单类型标签
· 流转状态:竖排时间线(LogDetailActivity 手机分支 buildVerticalSteps 动态填充 ll_steps)
· 平板的「操作详情」时间线列表在手机设计稿中不存在,rv_timeline 保留 id 但隐藏
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
@@ -12,7 +28,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:background="@color/phone_page_bg"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
@@ -20,208 +36,99 @@
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp">
android:padding="12dp">
<!-- 运单信息 -->
<!-- ==================== 运单卡片 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_white_radius_top_8"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单信息"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 分割线(贯穿卡片宽度) -->
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E7E7E7" />
<!-- 内容区 -->
<!-- 运单号 + 运单类型标签 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_bottom_8"
android:orientation="vertical">
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginEnd="15dp"
android:layout_marginVertical="12dp"
android:orientation="horizontal">
android:layout_weight="1"
android:text="@{viewModel.waybillNo}"
android:textColor="@color/phone_text_title"
android:textSize="20sp"
android:textStyle="bold"
tools:text="78133358743" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单号:"
android:textColor="#666666"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.waybillNo}"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单类型:"
android:textColor="#666666"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.waybillType}"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="22dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_phone_tag_indigo"
android:gravity="center"
android:paddingHorizontal="10dp"
android:text="@{viewModel.waybillType}"
android:textColor="@color/phone_tag_indigo_text"
android:textSize="12sp"
tools:text="国际出港" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="14dp"
android:layout_marginBottom="14dp"
android:background="@color/phone_divider" />
<!-- 流转状态标题 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="流转状态"
android:textColor="@color/phone_text_title"
android:textSize="15sp"
android:textStyle="bold" />
<!-- 竖排时间线(Activity 手机分支动态填充) -->
<LinearLayout
android:id="@+id/ll_steps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:orientation="vertical" />
<!-- 平板的操作详情时间线,手机设计稿无此区块,保留 id 但隐藏 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_timeline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<!-- 流转状态 -->
<LinearLayout
android:layout_width="match_parent"
<!-- 触底提示 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:orientation="vertical">
<!-- 标题区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_white_radius_top_8"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="流转状态"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 分割线 -->
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E7E7E7" />
<!-- 内容区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_bottom_8"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_steps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="23dp"
android:layout_marginEnd="15dp"
android:layout_marginVertical="12dp"
android:orientation="horizontal" />
</LinearLayout>
</LinearLayout>
<!-- 操作详情 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:orientation="vertical">
<!-- 标题区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_white_radius_top_8"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="操作详情"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 分割线 -->
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E7E7E7" />
<!-- 内容区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_bottom_8"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_timeline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:paddingStart="8dp"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</LinearLayout>
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:letterSpacing="0.3"
android:text="查询信息已触底"
android:textColor="@color/phone_text_hint"
android:textSize="12sp" />
</LinearLayout>

View File

@@ -1,210 +1,260 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!--
ULD新增 / 修改 / 详情 —— 手机版布局(5.5 寸手持端,最小宽度 < 600dp 时生效)
平板版为同名文件 layout-sw600dp/activity_uld_edit.xml(PadDataLayoutNew 三列表单),
两者是同一布局的资源变体,DataBinding 生成同一个 ActivityUldEditBinding 类。
变体之间必须满足:
· DataBinding 变量完全一致:viewModel / activity
→ UldEditViewModel 的加载/保存逻辑全部复用,平板端行为不受影响
一套布局覆盖设计稿三个页面(标题由 Activity 按 pageType 设置):
· ULD新增(Add) :可编辑 + 底部保存;所在港默认 HFE、必填星(编号/自重/入库时间)
· ULD修改(Modify):编号只读,其余可编辑 + 底部保存
· ULD详情(Details):全只读右对齐 + 顶栏右侧铅笔进入修改 + 额外展示最大装载容积/最大载重
字段与平板端一致:来源 source(空运/陆运)、入库时间 inStoreTime、所在港 nowPort;
状态维持两态(0 可用 / 1 维修,后端不新增「占用」)。
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.lukouguoji.module_base.ui.weight.data.layout.DataLayoutType" />
<import type="android.view.View" />
<import type="com.lukouguoji.module_base.common.DetailsPageType" />
<import type="com.lukouguoji.module_base.ui.weight.phone.PhoneFormRowType" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.uld.edit.UldEditViewModel" />
<variable
name="activity"
type="com.lukouguoji.aerologic.page.uld.edit.UldEditActivity" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:background="@color/phone_page_bg"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<!-- 标题栏 + 详情态右侧铅笔(编辑入口) -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/title_tool_bar" />
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:onClick="@{()-> activity.onEditClick()}"
android:padding="14dp"
android:src="@drawable/ic_phone_edit"
android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.VISIBLE : View.GONE}" />
</FrameLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp">
android:padding="16dp">
<!-- ==================== 表单卡片 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_8"
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:padding="15dp">
android:paddingHorizontal="16dp">
<!-- Row 1: ULD编号 / 状态 / 自重 -->
<!-- ULD编号:新增可输,修改/详情只读 -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{viewModel.pageType == DetailsPageType.Add}"
hint='@{"请输入ULD编号"}'
required="@{viewModel.pageType == DetailsPageType.Add}"
title='@{"ULD编号"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.uldBean.uld}" />
<!-- 状态:彩色标签 + 底部选择面板(详情态右对齐、无箭头) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="54dp"
android:onClick="@{()-> activity.onStatusRowClick()}"
android:orientation="horizontal">
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
<TextView
android:layout_width="110dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType == DetailsPageType.Add}"
hint='@{"请输入ULD编号"}'
title='@{"ULD编号"}'
titleLength="@{7}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.uld}' />
android:text="状态"
android:textColor="@color/phone_text_label"
android:textSize="15sp" />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
<!-- 详情态用这个占位把标签推到右侧 -->
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="1dp"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择状态"}'
list="@{viewModel.statusList}"
title='@{"状态"}'
titleLength="@{5}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.uldBean.status}' />
android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.VISIBLE : View.GONE}" />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
<TextView
android:layout_width="wrap_content"
android:layout_height="22dp"
android:background="@{viewModel.uldBean.status.empty ? null : (viewModel.uldBean.isStatusAvailable ? @drawable/bg_phone_tag_green : @drawable/bg_phone_tag_red_light)}"
android:gravity="center"
android:paddingHorizontal="8dp"
android:text='@{viewModel.uldBean.status.empty ? "请选择状态" : viewModel.uldBean.statusPhoneName}'
android:textColor="@{viewModel.uldBean.status.empty ? @color/phone_text_hint : (viewModel.uldBean.isStatusAvailable ? @color/phone_tag_green_text : @color/phone_danger_text)}"
android:textSize="12sp" />
<!-- 编辑态右箭头 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入自重"}'
title='@{"自重"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.uldWeight}' />
android:gravity="end|center_vertical"
android:orientation="horizontal"
android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.GONE : View.VISIBLE}">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_phone_chevron_right" />
</LinearLayout>
</LinearLayout>
<!-- Row 2: 最大装载容积 / 最大载重 / 所属航司 -->
<LinearLayout
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/phone_divider" />
<!-- 所属航司 -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择所属航司"}'
list="@{viewModel.carrierList}"
title='@{"所属航司"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.SELECT}"
value="@={viewModel.uldBean.carrier}" />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入最大装载容积"}'
title='@{"最大装载容积"}'
titleLength="@{7}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.maxVolume}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入最大载重"}'
title='@{"最大载重"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.maxWeight}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择所属航司"}'
list="@{viewModel.carrierList}"
title='@{"所属航司"}'
titleLength="@{5}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.uldBean.carrier}' />
</LinearLayout>
<!-- Row 3: 来源 / 入库时间 / 所在港 -->
<LinearLayout
<!-- 自重(kg) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入自重"}'
numeric="@{true}"
title='@{"自重(kg)"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.uldBean.uldWeight}" />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择来源"}'
list="@{viewModel.sourceList}"
title='@{"来源"}'
titleLength="@{7}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.uldBean.source}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择入库时间"}'
title='@{"入库时间"}'
titleLength="@{5}"
type="@{DataLayoutType.DATETIME}"
value='@={viewModel.uldBean.inStoreTime}' />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入所在港"}'
title='@{"所在港"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.nowPort}' />
</LinearLayout>
<!-- 备注(全行,多行输入) -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
<!-- 最大装载容积 -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入最大装载容积"}'
numeric="@{true}"
title='@{"最大装载容积(m³)"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.uldBean.maxVolume}" />
<!-- 最大载重 -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入最大载重"}'
numeric="@{true}"
title='@{"最大载重(kg)"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.uldBean.maxWeight}" />
<!-- 所在港(nowPort,新增默认 HFE) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入所在港"}'
title='@{"所在港"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.uldBean.nowPort}" />
<!-- 来源(source:空运/陆运,设计稿顺序位于所在港之后) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请选择来源"}'
list="@{viewModel.sourceList}"
title='@{"来源"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.SELECT}"
value="@={viewModel.uldBean.source}" />
<!-- 入库时间(inStoreTime,与平板端一致) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入入库时间"}'
title='@{"入库时间"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.DATE}"
value="@={viewModel.uldBean.inStoreTime}" />
<!-- 备注 -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入备注"}'
inputHeight="@{80}"
title='@{"备注"}'
titleLength="@{7}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.uldBean.remark}' />
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.uldBean.remark}" />
</LinearLayout>
<!-- 底部操作按钮 -->
<LinearLayout
<!-- ==================== 保存按钮(详情态隐藏) ==================== -->
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:background="@drawable/bg_phone_btn_primary"
android:gravity="center"
visible="@{viewModel.pageType != DetailsPageType.Details}">
<TextView
style="@style/tv_bottom_btn"
android:layout_width="120dp"
android:layout_marginEnd="20dp"
android:onClick="@{()-> viewModel.onCancelClick()}"
android:text="取消" />
<TextView
style="@style/tv_bottom_btn"
android:layout_width="120dp"
android:onClick="@{()-> viewModel.onSaveClick()}"
android:text="保存" />
</LinearLayout>
android:onClick="@{()-> viewModel.onSaveClick()}"
android:text="保存"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="@{viewModel.pageType == DetailsPageType.Details ? View.GONE : View.VISIBLE}" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>

View File

@@ -1,118 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
ULD管理 —— 手机版布局(5.5 寸手持端,最小宽度 < 600dp 时生效)
平板版为同名文件 layout-sw600dp/activity_uld_list.xml,两者是同一布局的资源变体,
DataBinding 因此生成同一个 ActivityUldListBinding 类。
变体之间必须满足:
· DataBinding 变量完全一致:viewModel / activity
· 保留 Activity 中引用的控件 id:srl / rv
→ UldListViewModel 的业务逻辑与接口调用全部复用,平板端行为不受影响
交互差异(均复用既有 ViewModel 字段与接口):
· 平板 3 个横排搜索条 → 手机顶部搜索框(ULD 编号)+ 底部筛选弹层
(入库时间 / 进港航班号 / 所属航司 / 来源;前两项与来源的筛选入参后端暂不支持,
已登记 documents/后端接口对接问题清单.xlsx,后端支持后自动生效)
· 平板右上「新增」按钮 → 手机右下角浮动 + 按钮
· 平板侧滑单条删除 → 手机底部操作条(全选 + 已选统计 + 批量删除)
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.lukouguoji.module_base.ui.weight.search.layout.SearchLayoutType" />
<import type="com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayoutType" />
<variable
name="viewModel"
type="com.lukouguoji.aerologic.page.uld.list.UldListViewModel" />
<variable
name="activity"
type="com.lukouguoji.aerologic.page.uld.list.UldListActivity" />
</data>
<LinearLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_f2"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
android:background="@color/phone_page_bg">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical">
android:layout_height="match_parent"
android:orientation="vertical">
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"选择状态"}'
list="@{viewModel.statusList}"
type="@{SearchLayoutType.SPINNER}"
value="@={viewModel.status}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.33" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"选择所属航司"}'
list="@{viewModel.carrierList}"
type="@{SearchLayoutType.SPINNER}"
value="@={viewModel.uldSuffix}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.33" />
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
hint='@{"请输入ULD编号"}'
icon="@{@drawable/img_scan}"
setOnIconClickListener="@{()-> viewModel.onUldScanClick()}"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.uldNo}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.33" />
<include layout="@layout/title_tool_bar" />
<!-- ==================== 搜索行 ==================== -->
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:orientation="horizontal"
android:paddingHorizontal="24dp">
android:background="@color/white"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="12dp">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:onClick="@{()-> viewModel.refresh()}"
android:padding="2dp"
android:src="@drawable/img_search" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="16dp"
android:onClick="@{()-> viewModel.onAddClick()}"
android:src="@drawable/img_add" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneSearchBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
hint='@{"搜索ULD编号"}'
setOnFilterClickListener="@{(v)-> activity.toggleFilterPanel()}"
setOnScanClickListener="@{(v)-> viewModel.onUldScanClick()}"
setRefreshCallBack="@{viewModel::refresh}"
upperCase="@{true}"
value="@={viewModel.uldNo}" />
</LinearLayout>
</LinearLayout>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
<!-- ==================== 列表 ==================== -->
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_uld_list" />
android:layout_height="0dp"
android:layout_weight="1">
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<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:paddingBottom="80dp"
itemLayoutId="@{viewModel.itemLayoutId}"
viewHolder="@{viewModel.itemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/item_uld_list" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/white"
android:gravity="center_vertical"
android:paddingHorizontal="15dp">
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text='@{"合计:" + viewModel.count + "条"}'
android:textColor="@color/bottom_tool_tips_text_color"
android:textSize="18sp"
android:textStyle="bold" />
<!-- ==================== 底部操作条:全选 + 已选统计 + 批量删除 ==================== -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneBottomBar
actionDanger="@{true}"
actionText='@{"删除"}'
allChecked="@{viewModel.isAllChecked}"
countText='@{"已选 " + viewModel.selectedCount + " / " + viewModel.count + " 项"}'
setOnActionClick="@{(v)-> activity.showDeleteConfirmDialog()}"
setOnAllCheckClick="@{(v)-> viewModel.toggleSelectAll()}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
<!-- ==================== 右下角浮动新增按钮 ==================== -->
<TextView
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="20dp"
android:layout_marginBottom="92dp"
android:background="@drawable/bg_phone_fab"
android:elevation="6dp"
android:gravity="center"
android:includeFontPadding="false"
android:onClick="@{()-> viewModel.onAddClick()}"
android:text="+"
android:textColor="@color/white"
android:textSize="32sp" />
<!-- ==================== 筛选弹层 ==================== -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="8dp"
panelVisible="@{activity.filterPanelVisible}"
setOnConfirmClick="@{(v)-> activity.confirmFilter()}"
setOnDismissClick="@{(v)-> activity.toggleFilterPanel()}"
setOnResetClick="@{(v)-> activity.resetFilter()}"
title='@{"筛选条件"}'>
<!-- 状态(后端入参 status,与平板端查询参数一致) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
hint='@{"请选择状态"}'
list="@{viewModel.statusPhoneFilterList}"
title='@{"状态"}'
type="@{PhoneDataLayoutType.SPINNER}"
value="@={viewModel.status}" />
<!-- 所属航司(后端入参 uldSuffix,真实生效) -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
hint='@{"请选择所属航司"}'
list="@{viewModel.carrierList}"
title='@{"所属航司"}'
type="@{PhoneDataLayoutType.SPINNER}"
value="@={viewModel.uldSuffix}" />
</com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel>
</FrameLayout>
</layout>

View File

@@ -1,287 +1,227 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
国内进港移库 —— 列表项 5.5 寸手持端布局(平板变体见 layout-sw600dp/ 同名文件)
一套布局覆盖设计稿两种卡片形态,按 bean.isMoved 切换(载入时按端点打标):
· 待移库:圆形勾选框 + 蓝色运单号 + 橙色「待移库」标签
· 已移库:无勾选框 + 深色运单号 + 绿色对勾「已移库」
长按卡片出「拍照」浮层(跳拍照上传页),点浮层空白处收起。
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="bean"
type="com.lukouguoji.module_base.bean.MoveStashBean" />
<variable
name="position"
type="Integer" />
<variable
name="activity"
type="com.lukouguoji.aerologic.page.gnj.move.stash.list.GnjMoveStashListActivity" />
</data>
<!-- 外层容器承载间距 -->
<LinearLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginVertical="5dp"
android:orientation="vertical">
android:layout_marginHorizontal="12dp"
android:layout_marginTop="12dp">
<!-- 侧滑布局 -->
<com.mcxtzhang.swipemenulib.SwipeMenuLayout
android:id="@+id/swipe_menu"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card"
android:onLongClick="@{()-> activity.showCardActionPhone(bean)}"
android:orientation="vertical"
android:padding="16dp">
<!-- 主列表项容器 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll"
<!-- ==================== 卡片头部:勾选框 + 运单号 + 状态 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_item"
android:orientation="horizontal"
android:padding="10dp">
android:gravity="center_vertical"
android:orientation="horizontal">
<!-- 圆形勾选框(已移库的记录不可勾选,隐藏) -->
<ImageView
android:id="@+id/iv_icon"
loadImage="@{bean.checked ? @drawable/img_plane_s : @drawable/img_plane}"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/img_plane" />
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_phone_check_round_unchecked"
android:visibility="@{bean.isMoved ? View.GONE : View.VISIBLE}"
loadImage="@{bean.checked.get() ? @drawable/ic_phone_check_round_checked : @drawable/ic_phone_check_round_unchecked}" />
<LinearLayout
<!-- 运单号 -->
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:orientation="vertical">
android:text="@{bean.wbNo}"
android:textColor="@{bean.isMoved ? @color/phone_text_title : @color/phone_primary}"
android:textSize="15sp"
android:textStyle="bold" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 待移库:橙色状态标签 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_phone_tag_orange"
android:gravity="center"
android:paddingHorizontal="8dp"
android:text="待移库"
android:textColor="@color/phone_tag_orange_text"
android:textSize="11sp"
android:visibility="@{bean.isMoved ? View.GONE : View.VISIBLE}" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<!-- 已移库:对勾 + 状态文字 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="@{bean.isMoved ? View.VISIBLE : View.GONE}">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单号:" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_phone_check_circle" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.wbNo}"
android:textColor="@color/colorPrimary"
tools:text="789165431" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="件数:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{bean.pc}' />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重量:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.weight}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="代理人:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{bean.agentCode}' />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<TextView
completeSpace="@{5}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运单类型:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{bean.awbType}' />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="航班:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.flight}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="始发港:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.origin}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="起始站:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.dep}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
completeSpace="@{4}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="目的港:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.dest}" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.1"
android:gravity="center_vertical">
<TextView
completeSpace="@{5}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="品名:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.goods}" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
android:layout_marginStart="4dp"
android:text="已移库"
android:textColor="@color/phone_text_title"
android:textSize="13sp" />
</LinearLayout>
<!-- 右侧箭头 -->
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:src="@drawable/img_pda_right" />
</LinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 侧滑菜单区域 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="match_parent"
<!-- ==================== 航班信息行 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<!-- 编辑按钮 -->
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_phone_airplane" />
<TextView
android:id="@+id/btn_edit"
style="@style/tv_item_action"
android:background="@color/colorPrimary"
android:text="编辑" />
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:text='@{"航班信息:" + bean.orDash(bean.flight)}'
android:textColor="@color/phone_text_body"
android:textSize="13sp"
tools:text="航班信息:20260709/MU2311" />
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
</com.mcxtzhang.swipemenulib.SwipeMenuLayout>
<!-- ==================== 统计框:特码 / 件数 / 重量 ==================== -->
<com.lukouguoji.module_base.ui.weight.phone.PhoneStatBox
android:layout_width="match_parent"
android:layout_height="wrap_content"
label1='@{"特码"}'
label2='@{"件数"}'
label3='@{"重量"}'
value1="@{bean.orDash(bean.spCode)}"
value2='@{bean.orDash(bean.pc) + "件"}'
value3='@{bean.orDash(bean.weight) + "kg"}' />
</LinearLayout>
<!-- ==================== 出运路径 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_phone_location" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text='@{"出运路径:" + bean.orDash(bean.range)}'
android:textColor="@color/phone_text_body"
android:textSize="13sp" />
</LinearLayout>
<!-- ==================== 查看详情 ==================== -->
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="16dp"
android:background="@color/phone_stat_bg" />
<LinearLayout
android:id="@+id/ll_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|end"
android:orientation="horizontal"
android:paddingTop="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查看详情"
android:textColor="@color/phone_primary"
android:textSize="14sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_phone_chevron_right" />
</LinearLayout>
</LinearLayout>
<!-- ==================== 长按操作浮层:拍照 ==================== -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#66000000"
android:clickable="true"
android:onClick="@{()-> activity.hideCardActionPhone(bean)}"
android:visibility="@{bean.actionMenuVisible ? View.VISIBLE : View.GONE}">
<TextView
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="center"
android:background="@drawable/bg_phone_fab_purple"
android:gravity="center"
android:onClick="@{()-> activity.openPhoto(bean)}"
android:text="拍照"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold" />
</FrameLayout>
</FrameLayout>
</layout>

View File

@@ -1,320 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
ULD管理 —— 列表项手机版布局(5.5 寸手持端,最小宽度 < 600dp 时生效)
平板版为同名文件 layout-sw600dp/item_uld_list.xml(SwipeMenuLayout 侧滑修改/删除),
两者是同一布局的资源变体,DataBinding 生成同一个 ItemUldListBinding 类。
变体之间必须满足:
· DataBinding 变量完全一致:bean
· 保留 ViewHolder / ViewModel 引用的 id:ll_content(点卡片进详情,复用平板 onItemClick 链路)
· 平板独有 id(tv_modify / tv_delete / iv_icon)与手机独有 id(iv_check)
在 Binding 基类中自动为 @Nullable,ViewHolder 判空使用
状态标签:可用=浅绿 / 维修=浅红(后端枚举维持两态 0/1,不含「占用」),见 ULDBean.statusPhoneName。
「来源」字段后端暂无(恒空显示 "-"),已登记 documents/后端接口对接问题清单.xlsx。
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="bean"
type="com.lukouguoji.module_base.bean.ULDBean" />
</data>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_marginHorizontal="12dp"
android:layout_marginTop="12dp"
android:background="@drawable/bg_phone_card"
android:orientation="vertical"
android:padding="16dp">
<com.mcxtzhang.swipemenulib.SwipeMenuLayout
<!-- ==================== 头行:勾选框 + ULD编号 + 状态标签 + 箭头 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_check"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_phone_check_round_unchecked"
loadImage="@{bean.checked.get() ? @drawable/ic_phone_check_round_checked : @drawable/ic_phone_check_round_unchecked}" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginVertical="5dp"
android:background="@drawable/bg_item"
android:orientation="horizontal"
android:padding="10dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{bean.uld}"
android:textColor="@color/phone_text_title"
android:textSize="15sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/iv_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/img_plane"
tools:src="@drawable/img_plane" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:orientation="vertical">
<!-- 第一行:ULD编号 | 状态 | 自重 | 所属航司 | 最大装载容积 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- ULD编号 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ULD编号:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.uld}"
android:textColor="@color/colorPrimary"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 状态 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="状态:"
android:textSize="15sp"
completeSpace="@{3}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.statusName()}"
android:textColor='@{bean.status.equals("0") ? @color/text_green : @color/red}'
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 自重 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自重:"
android:textSize="15sp"
completeSpace="@{4}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.uldWeight}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 所属航司 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="所属航司:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.carrier}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 最大装载容积 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.2"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最大装载容积:"
android:textSize="15sp"
completeSpace="@{7}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.maxVolume}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 第二行:最大载重 | 来源 | 所在港 | 位置 | 入库时间 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<!-- 最大载重 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最大载重:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.maxWeight}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 来源 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="来源:"
android:textSize="15sp"
completeSpace="@{3}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.source}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 所在港 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="所在港:"
android:textSize="15sp"
completeSpace="@{4}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.nowPort}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 位置 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="位置:"
android:textSize="15sp"
completeSpace="@{5}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.nowPosition}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 入库时间 -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.2"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="入库时间:"
android:textSize="15sp"
completeSpace="@{7}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.inStoreTime}"
android:textSize="15sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:src="@drawable/img_pda_right" />
</androidx.appcompat.widget.LinearLayoutCompat>
</FrameLayout>
<androidx.appcompat.widget.LinearLayoutCompat
<!-- 状态标签:可用(绿) / 维修(红,浅红底红字) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent">
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:background="@{bean.isStatusAvailable ? @drawable/bg_phone_tag_green : @drawable/bg_phone_tag_red_light}"
android:gravity="center"
android:paddingHorizontal="8dp"
android:text="@{bean.statusPhoneName}"
android:textColor="@{bean.isStatusAvailable ? @color/phone_tag_green_text : @color/phone_danger_text}"
android:textSize="11sp" />
<TextView
android:id="@+id/tv_modify"
style="@style/tv_item_action"
android:background="#8313ef"
android:text="修改" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="6dp"
android:src="@drawable/ic_phone_chevron_right"
android:tint="@color/phone_text_body" />
<TextView
android:id="@+id/tv_delete"
style="@style/tv_item_action"
android:background="@color/red"
android:text="删除" />
</LinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- ==================== 第二行:所属航司 / 来源 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
</com.mcxtzhang.swipemenulib.SwipeMenuLayout>
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_phone_airplane" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text='@{"所属航司:" + (bean.carrier.empty ? "-" : bean.carrier)}'
android:textColor="@color/phone_text_body"
android:textSize="13sp" />
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_phone_location" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text='@{"来源:" + (bean.source.empty ? "-" : bean.source)}'
android:textColor="@color/phone_text_body"
android:textSize="13sp" />
</LinearLayout>
<!-- ==================== 第三行:入库时间 ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_phone_calendar" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text='@{"入库时间:" + (bean.inStoreTime.empty ? "-" : bean.inStoreTime)}'
android:textColor="@color/phone_text_body"
android:textSize="13sp" />
</LinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>

View File

@@ -0,0 +1,55 @@
# 5.5 寸手持端适配 —— 后端接口补充清单
适配手机端时,设计稿要求展示/筛选的部分内容,现有接口没有提供或未生效。
以下为**实机联调(内网真实数据)验证过**的结论,客户端侧已按设计稿把 UI 与参数都做好,
后端补齐后即可直接生效,客户端基本无需再改。
整理时间:2026-07-29  验证环境:内网 `192.168.1.250:8093`,账号 ADMIN
---
## 一、参数已发送但后端未生效(客户端已就绪,等后端支持)
| 接口 | 参数 | 用途 | 实测现象 |
|------|------|------|----------|
| `IntExpOutHandover/pageQuery`<br>`IntExpOutHandover/pageQueryTotal` | `hoState`<br>("0" 待交接 / "1" 已交接) | 出库交接页「待交接 / 已交接」Tab 过滤与角标计数 | 请求体确认携带 `"hoState":"0"` / `"1"`,但两个 Tab 返回**完全相同**的数据;`pageQueryTotal` 的 `wbNumber` 也不随之变化 |
| `IntExpMove/pageQuery`<br>`IntExpMove/pageQueryTotal` | `spCode`<br>(特码,如 "PER") | 出港移库页筛选弹层「特码」 | 请求体确认携带 `"spCode":"PER"`,但返回结果中仍含特码为 `PEF` 的记录,角标计数也无变化 |
> 对照参考:同一接口的 `moveState`、`by1`(承运人)、`awbType`(运单类型)**均正常生效**
> (实测承运人填 MU 后待移库数量 67 → 60),因此可以排除客户端传参方式的问题。
>
> 若贵方使用的参数名与上表不同,请告知正确名称,客户端改一处常量即可。
---
## 二、接口未返回、设计稿需要展示的字段
以下字段目前接口完全不返回,或恒为 `null`。客户端已按设计稿保留完整 UI 结构,
数据为空时统一占位 `-`,后端补齐后自动显示。
### 2.1 `IntExpMove/pageQuery`(出港移库)
| 设计稿位置 | 需要的字段 | 当前状况 |
|------------|-----------|----------|
| 列表卡片第二行「航班信息:20260709/MU2311」 | 航班日期 + 航班号(如 `fdate` / `fno`) | **接口无任何航班相关字段** |
| 详情页「移库信息」卡片 —— 移库人 | 移库人**姓名**(现只有 `opId`) | `opId` 恒为 `null`,且即便有值也是 ID 而非姓名 |
| 详情页「移库信息」卡片 —— 移库时间 | `opdate` | 恒为 `null`,**已移库记录也是 null** |
| 详情页蓝色表头右侧时间戳 | 同上 `opdate` | 同上 |
| 详情页类型行右侧「普通货物运输」 | `businessType` 或 `cargoType` | 两者均恒为 `null` |
### 2.2 说明
- 出库交接页的详情所需字段(板型、库位、复磅人/时间等)接口均已返回,无需补充。
- 移库详情页红色标签绑的是 `awbTypeName`(真实值形如「国际出港(国际直航)」),已正常。
设计稿上写的「国际进港」经判断为设计稿笔误(该页属国际出港),未按字面实现。
---
## 三、附:字典接口调用口径(客户端已按此对齐,仅作备忘)
`typeCode/getSpecialCodeByParam`(特码字典):
- `flag=1`(国际)+ `ieFlag=""`(**留空**)→ 正常返回 RXE/PER/PEM/PEF/LDP/GJJ/DGR 等
- `flag=1` + `ieFlag="E"` → 返回 `"data": []` 空数组
国际出港各页面统一使用 `ieFlag=""`。如该行为非预期,请后端确认 `ieFlag` 的取值约定。

View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<title>5.5 寸手持端设计稿 · 全部入口总览</title>
<style>
:root {
--cols: 4;
--scale: 1;
--pw: 430px;
--ph: 890px;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0 32px 48px;
background: #EEF1F6;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", Roboto, Arial, sans-serif;
color: #1F2937;
}
header {
padding: 32px 0 20px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
}
h1 {
margin: 0;
font-size: 26px;
font-weight: 700;
letter-spacing: .5px;
}
h1 small {
display: block;
margin-top: 6px;
font-size: 13px;
font-weight: 400;
color: #6B7280;
letter-spacing: 0;
}
.toolbar {
display: flex;
align-items: center;
gap: 18px;
background: #fff;
border: 1px solid #E3E8F0;
border-radius: 10px;
padding: 10px 16px;
font-size: 13px;
box-shadow: 0 1px 3px rgba(0,0,0,.05);
}
.toolbar label { color: #6B7280; margin-right: 6px; }
.toolbar select {
font-size: 13px;
padding: 4px 8px;
border: 1px solid #D8DEE9;
border-radius: 6px;
background: #fff;
color: #1F2937;
}
.toolbar button {
font-size: 13px;
padding: 5px 12px;
border: 1px solid #2563EB;
background: #2563EB;
color: #fff;
border-radius: 6px;
cursor: pointer;
}
.toolbar button:active { opacity: .8; }
.grid {
display: grid;
grid-template-columns: repeat(var(--cols), max-content);
gap: 28px 24px;
justify-content: start;
}
.card {
background: #fff;
border: 1px solid #E3E8F0;
border-radius: 14px;
padding: 12px 12px 14px;
box-shadow: 0 2px 10px rgba(17,24,39,.06);
width: calc(var(--pw) * var(--scale) + 24px);
}
.cap {
display: flex;
align-items: center;
gap: 8px;
margin: 0 2px 10px;
}
.no {
flex: none;
width: 24px;
height: 24px;
border-radius: 6px;
background: #2563EB;
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.name { font-size: 15px; font-weight: 600; }
.tag {
margin-left: auto;
font-size: 11px;
color: #2563EB;
background: #EAF1FE;
border-radius: 5px;
padding: 2px 7px;
}
.frame {
width: calc(var(--pw) * var(--scale));
height: calc(var(--ph) * var(--scale));
overflow: hidden;
border-radius: 10px;
background: #E5E7EB;
}
.frame iframe {
width: var(--pw);
height: var(--ph);
border: 0;
display: block;
transform: scale(var(--scale));
transform-origin: top left;
}
body.clean .toolbar { display: none; }
body.clean header { padding-bottom: 14px; }
</style>
</head>
<body>
<header>
<h1>5.5 寸手持端设计稿 · 全部入口总览
<small>共 11 个业务入口 · 单机画布 390 × 844</small>
</h1>
<div class="toolbar">
<span><label>列数</label>
<select id="cols" onchange="setVar('--cols', this.value)">
<option>2</option><option>3</option><option selected>4</option><option>5</option><option>6</option>
</select>
</span>
<span><label>缩放</label>
<select id="scale" onchange="setVar('--scale', this.value)">
<option value="1">100%</option>
<option value="0.85">85%</option>
<option value="0.75">75%</option>
<option value="0.6">60%</option>
<option value="0.5">50%</option>
</select>
</span>
<button onclick="document.body.classList.toggle('clean')">隐藏工具栏</button>
</div>
</header>
<div class="grid" id="grid"></div>
<script>
const PAGES = [
['01', 'ULD 管理', '综合管理', '01_ULD管理/ULD管理.html'],
['02', '出库交接', '国际出港', '02_出库交接/出库交接.html'],
['03', '出港计重', '国际出港', '03_出港计重/main.html'],
['04', '出港运抵', '国际出港', '04_出港运抵/main.html'],
['05', '出港移库', '国际出港', '05_出港移库/出港移库.html'],
['06', '出港查询', '国际出港', '06_出港查询/出港查询.html'],
['07', '出港仓库', '国际出港', '07_出港仓库/出港仓库.html'],
['08', '进港仓库', '国际进港', '08_进港仓库/进港仓库.html'],
['09', '提取出库', '国际进港', '09_提取出库/提取出库.html'],
['10', '进港查询', '国际进港', '10_进港查询/进港查询.html'],
['11', '进港移库', '国际进港', '11_进港移库/进港移库.html'],
];
document.getElementById('grid').innerHTML = PAGES.map(([no, name, menu, src]) => `
<div class="card">
<div class="cap">
<span class="no">${no}</span>
<span class="name">${name}</span>
<span class="tag">${menu}</span>
</div>
<div class="frame">
<iframe src="${src}" loading="eager" scrolling="no"></iframe>
</div>
</div>
`).join('');
function setVar(k, v) {
document.documentElement.style.setProperty(k, v);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,650 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>ULD管理</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>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #F3F4F6;
font-size: 13px;
}
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
.custom-checkbox-round {
appearance: none;
-webkit-appearance: none;
border-radius: 9999px;
width: 18px;
height: 18px;
border: 2px solid #D1D5DB;
background-color: #fff;
position: relative;
cursor: pointer;
outline: none;
flex-shrink: 0;
}
.custom-checkbox-round:checked {
background-color: #0D6EFD;
border-color: #0D6EFD;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
background-size: 14px 14px;
background-position: center;
background-repeat: no-repeat;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.btn-active:active {
opacity: 0.7;
}
</style>
</head>
<body class="bg-gray-200 min-h-screen flex items-center justify-center p-4">
<div class="relative w-[390px] h-[844px] bg-[#1a1a1a] rounded-[55px] shadow-2xl border-[6px] border-[#333] flex flex-col overflow-hidden">
<div class="flex-1 bg-white mt-0 mb-1 mx-1 rounded-[46px] overflow-hidden flex flex-col relative">
<div class="bg-[#2563EB] text-white h-11 px-7 flex items-end pb-2 justify-between text-[14px] font-medium z-50">
<span>09:41</span>
<div class="flex items-center gap-1.5 pb-0.5">
<iconify-icon icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon icon="mdi:wifi"></iconify-icon>
<iconify-icon icon="mdi:battery"></iconify-icon>
</div>
</div>
<div class="flex-1 flex flex-col overflow-hidden relative bg-gray-100">
<header class="relative bg-[#2563EB] text-white flex items-center h-12 px-3 sticky top-0 z-50">
<div class="flex items-center btn-active" onclick="alert('返回首页')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-medium text-white -ml-1">返回</span>
</div>
<h1 class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-white">ULD管理</h1>
</header>
<section class="px-4 pt-4 pb-3 flex-none">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 rounded-[12px] h-10 px-3 gap-2 flex-1 shadow-sm">
<iconify-icon class="text-gray-500 text-[14px] text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-700 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索ULD编号" type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="toggleFilterDrawer(true)">
<iconify-icon class="text-lg" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<main class="flex-1 overflow-y-auto px-4 pt-3 pb-[80px] no-scrollbar">
<!-- 卡片1 -->
<div class="bg-white rounded-xl shadow-sm mb-3 p-4 relative cursor-pointer" onclick="showUldDetail('PMC78133363396', '占用', 'CA', '150', '12.5', '6800', 'CA1111', 'PEK', 'B区-05货位', '--')">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center gap-3">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="event.stopPropagation(); handleCheckboxClick(this, event)" type="checkbox"/>
<span class="text-[15px] font-bold text-[#1F2937]">PMC78133363396</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-[#FFF3E0] text-[#E67722] text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">占用</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right" width="14"></iconify-icon>
</div>
</div>
<div class="space-y-2 text-gray-500 text-[13px]">
<div class="grid grid-cols-2 gap-x-3">
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:office-building-outline"></iconify-icon>
<span>所属航司:CA</span>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>来源:空运</span>
</div>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:calendar-clock"></iconify-icon>
<span>入库时间:2026-07-20 15:59:20</span>
</div>
</div>
</div>
<!-- 卡片2 -->
<div class="bg-white rounded-xl shadow-sm mb-3 p-4 relative cursor-pointer" onclick="showUldDetail('PMC78133363397', '可用', 'CA', '120', '10.0', '5500', 'CA2222', 'CAN', 'C区-12货位', '--')">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center gap-3">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="event.stopPropagation(); handleCheckboxClick(this, event)" type="checkbox"/>
<span class="text-[15px] font-bold text-[#1F2937]">PMC78133363397</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">可用</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right" width="14"></iconify-icon>
</div>
</div>
<div class="space-y-2 text-gray-500 text-[13px]">
<div class="grid grid-cols-2 gap-x-3">
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:office-building-outline"></iconify-icon>
<span>所属航司:CA</span>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>来源:空运</span>
</div>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:calendar-clock"></iconify-icon>
<span>入库时间:2026-07-20 15:59:20</span>
</div>
</div>
</div>
<!-- 卡片3 -->
<div class="bg-white rounded-xl shadow-sm mb-3 p-4 relative cursor-pointer" onclick="showUldDetail('PMC78133363398', '维修', 'CA', '0', '0.0', '0', 'CA3333', 'PVG', '维修区', '维修中')">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center gap-3">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="event.stopPropagation(); handleCheckboxClick(this, event)" type="checkbox"/>
<span class="text-[15px] font-bold text-[#1F2937]">PMC78133363398</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-red-50 text-red-600 text-[11px] px-2 py-0.5 rounded font-medium">维修</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right" width="14"></iconify-icon>
</div>
</div>
<div class="space-y-2 text-gray-500 text-[13px]">
<div class="grid grid-cols-2 gap-x-3">
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:office-building-outline"></iconify-icon>
<span>所属航司:CA</span>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>来源:陆运</span>
</div>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:calendar-clock"></iconify-icon>
<span>入库时间:2026-07-20 15:59:20</span>
</div>
</div>
</div>
<!-- 卡片4 -->
<div class="bg-white rounded-xl shadow-sm mb-3 p-4 relative cursor-pointer" onclick="showUldDetail('PMC78133363399', '占用', 'CA', '280', '20.0', '8500', 'CA4444', 'CTU', 'A区-08货位', '--')">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center gap-3">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="event.stopPropagation(); handleCheckboxClick(this, event)" type="checkbox"/>
<span class="text-[15px] font-bold text-[#1F2937]">PMC78133363399</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-[#FFF3E0] text-[#E67722] text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">占用</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right" width="14"></iconify-icon>
</div>
</div>
<div class="space-y-2 text-gray-500 text-[13px]">
<div class="grid grid-cols-2 gap-x-3">
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:office-building-outline"></iconify-icon>
<span>所属航司:CA</span>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>来源:空运</span>
</div>
</div>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:calendar-clock"></iconify-icon>
<span>入库时间:2026-07-20 15:59:20</span>
</div>
</div>
</div>
</main>
<div class="absolute bottom-0 w-full bg-white border-t border-gray-200 z-40 flex flex-col pt-3 pb-3" id="bottom-toolbar" style="display: flex;">
<div class="absolute right-4 -top-[73px] z-50">
<button class="bg-[#2563EB] text-white w-11 h-11 rounded-full flex items-center justify-center shadow-lg shadow-blue-500/40" onclick="showAddDialog()">
<iconify-icon icon="mdi:plus" width="24"></iconify-icon>
</button>
</div>
<div class="px-4 flex items-center w-full">
<div class="flex items-center gap-2">
<input class="custom-checkbox-round flex-none" id="select-all-cb" onclick="toggleSelectAll(this.checked)" type="checkbox"/>
<label class="text-[13px] text-[#1F2937] font-medium cursor-pointer" for="select-all-cb">全选</label>
</div>
<div class="flex-1"></div>
<div class="flex items-center gap-3">
<span class="text-[12px] text-[#9CA3AF]">已选 <span id="selected-count">0 / 4</span> 项</span>
<button class="bg-[#FDEDED] text-[#DC2626] text-[15px] font-bold px-5 py-2 rounded-[10px] flex items-center gap-1.5 btn-active" onclick="handleBatchDelete()">
<iconify-icon class="text-base" icon="mdi:delete-outline"></iconify-icon>
<span>删除</span>
</button>
</div>
</div>
</div>
</div>
<!-- 新增弹窗 -->
<div class="absolute inset-0 bg-gray-100 z-[60] flex flex-col hidden" id="addUldOverlay">
<div class="bg-[#2563EB] text-white h-11 px-7 flex items-end pb-2 justify-between text-[14px] font-medium z-50 flex-none">
<span>09:41</span>
<div class="flex items-center gap-1.5 pb-0.5">
<iconify-icon icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon icon="mdi:wifi"></iconify-icon>
<iconify-icon icon="mdi:battery"></iconify-icon>
</div>
</div>
<header class="relative bg-[#2563EB] text-white flex items-center h-12 px-3 flex-none">
<div class="flex items-center btn-active" onclick="hideAddDialog()">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-medium text-white -ml-1">返回</span>
</div>
<h1 class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-white">ULD新增</h1>
</header>
<div class="flex-1 overflow-y-auto p-4 no-scrollbar">
<div class="bg-white rounded-xl shadow-[0_2px_12px_rgba(0,0,0,0.05)] overflow-hidden px-4">
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">ULD编号<span class="text-red-500 ml-0.5">*</span></span>
<input id="add-uldNo" class="flex-1 bg-transparent outline-none text-gray-800 placeholder-gray-400 text-[14px]" placeholder="请输入ULD编号" type="text"/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">状态</span>
<div class="flex-1 flex items-center justify-between cursor-pointer status-display" onclick="showSelectPanel('status', '请选择状态', ['可用', '占用', '维修'])">
<span id="add-status-text" class="text-[14px] text-gray-500">请选择状态</span>
<iconify-icon class="text-xl text-gray-500" icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">所属航司</span>
<div class="flex-1 flex items-center justify-between cursor-pointer airline-display" onclick="showSelectPanel('airline', '请选择所属航司', ['CA', 'MU', 'CZ', '3U'])">
<span id="add-airline-text" class="text-[14px] text-gray-500">请选择所属航司</span>
<iconify-icon class="text-xl text-gray-500" icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">来源</span>
<div class="flex-1 flex items-center justify-between cursor-pointer source-display" onclick="showSelectPanel('source', '请选择来源', ['空运', '陆运'])">
<span id="add-source-text" class="text-[14px] text-gray-500">请选择来源</span>
<iconify-icon class="text-xl text-gray-500" icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">自重(kg)<span class="text-red-500 ml-0.5">*</span></span>
<input id="add-weight" class="flex-1 bg-transparent outline-none text-gray-800 placeholder-gray-400 text-[14px]" placeholder="请输入自重" type="number"/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">进港航班号</span>
<input id="add-flightNo" class="flex-1 bg-transparent outline-none text-gray-800 placeholder-gray-400 text-[14px]" placeholder="请输入进港航班号" type="text"/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">所在港</span>
<input id="add-port" class="flex-1 bg-transparent outline-none text-gray-800 placeholder-gray-400 text-[14px]" placeholder="HFE" type="text"/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">入库时间<span class="text-red-500 ml-0.5">*</span></span>
<input id="add-inTime" class="flex-1 bg-transparent outline-none text-gray-800 placeholder-gray-400 text-[14px]" placeholder="请输入入库时间" type="text"/>
</div>
<div class="flex items-center py-[14px]">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">备注</span>
<input id="add-remark" class="flex-1 bg-transparent outline-none text-gray-800 placeholder-gray-400 text-[14px]" placeholder="请输入备注" type="text"/>
</div>
</div>
<button class="w-full mt-8 bg-[#2563EB] text-white py-3.5 rounded-[10px] font-bold text-[16px] shadow-lg shadow-blue-500/30 btn-active" onclick="saveUld()">保存</button>
</div>
</div>
<!-- 详情弹窗 -->
<div class="absolute inset-0 bg-gray-100 z-[70] flex flex-col hidden" id="detailOverlay">
<div class="bg-[#2563EB] text-white h-11 px-7 flex items-end pb-2 justify-between text-[14px] font-medium z-50 flex-none">
<span>09:41</span>
<div class="flex items-center gap-1.5 pb-0.5">
<iconify-icon icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon icon="mdi:wifi"></iconify-icon>
<iconify-icon icon="mdi:battery"></iconify-icon>
</div>
</div>
<header class="relative bg-[#2563EB] text-white flex items-center h-12 px-3 flex-none">
<div class="flex items-center btn-active" onclick="hideUldDetail()">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-medium text-white -ml-1">返回</span>
</div>
<h1 class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-white">ULD详情</h1>
<button class="absolute right-4 top-1/2 -translate-y-1/2 text-white text-lg" onclick="openEdit()">
<iconify-icon icon="mdi:pencil" width="22"></iconify-icon>
</button>
</header>
<div class="flex-1 overflow-y-auto p-4 no-scrollbar">
<div class="bg-white rounded-xl shadow-sm overflow-hidden px-4 mb-6">
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">ULD编号</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-uldNo">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">状态</span>
<span class="text-[13px] font-medium px-2 py-0.5 rounded-[6px]" id="detail-status">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">所属航司</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-airline">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">自重 (kg)</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-weight">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">最大装载容积 (m³)</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-volume">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">最大载重 (kg)</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-maxWeight">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">进港航班号</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-flightNo">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">所在港</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-port">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">来源</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-source">--</span>
</div>
<div class="flex items-center justify-between py-3.5 border-b border-gray-100">
<span class="text-[13px] font-medium text-[#6B7280]">入库时间</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-inTime">--</span>
</div>
<div class="flex items-center justify-between py-3.5">
<span class="text-[13px] font-medium text-[#6B7280]">备注</span>
<span class="text-[14px] font-normal text-[#111827]" id="detail-remark">--</span>
</div>
</div>
</div>
</div>
<!-- 修改页面 -->
<div class="absolute inset-0 bg-gray-100 z-[80] flex flex-col hidden" id="editOverlay">
<div class="bg-[#2563EB] text-white h-11 px-7 flex items-end pb-2 justify-between text-[14px] font-medium z-50 flex-none">
<span>09:41</span>
<div class="flex items-center gap-1.5 pb-0.5">
<iconify-icon icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon icon="mdi:wifi"></iconify-icon>
<iconify-icon icon="mdi:battery"></iconify-icon>
</div>
</div>
<header class="relative bg-[#2563EB] text-white flex items-center h-12 px-3 flex-none">
<div class="flex items-center btn-active" onclick="closeEdit()">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-medium text-white -ml-1">返回</span>
</div>
<h1 class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-white">ULD修改</h1>
</header>
<div class="flex-1 overflow-y-auto p-4 no-scrollbar">
<div class="bg-white rounded-xl shadow-[0_2px_12px_rgba(0,0,0,0.05)] overflow-hidden px-4">
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">ULD编号</span>
<input id="edit-uldNo" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]" type="text" readonly/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">状态</span>
<div class="flex-1 flex items-center justify-between cursor-pointer status-display" onclick="showSelectPanel('editStatus', '请选择状态', ['可用', '占用', '维修'])">
<span id="edit-status-text" class="text-[13px] font-medium px-2 py-0.5 rounded-[6px] text-gray-800">--</span>
<iconify-icon class="text-xl text-gray-500" icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">所属航司</span>
<div class="flex-1 flex items-center justify-between cursor-pointer airline-display" onclick="showSelectPanel('editAirline', '请选择航司', ['CA', 'MU', 'CZ', '3U'])">
<span id="edit-airline-text" class="text-[14px] text-gray-800">--</span>
<iconify-icon class="text-xl text-gray-500" icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">自重(kg)</span>
<input id="edit-weight" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]" type="number"/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">进港航班号</span>
<input id="edit-flightNo" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]" type="text"/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">所在港</span>
<input id="edit-port" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]" type="text"/>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">来源</span>
<div class="flex-1 flex items-center justify-between cursor-pointer source-display" onclick="showSelectPanel('editSource', '请选择来源', ['空运', '陆运'])">
<span id="edit-source-text" class="text-[14px] text-gray-800">--</span>
<iconify-icon class="text-xl text-gray-500" icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">入库时间</span>
<input id="edit-inTime" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]" type="text"/>
</div>
<div class="flex items-center py-[14px]">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">备注</span>
<input id="edit-remark" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]" type="text"/>
</div>
</div>
<button class="w-full mt-8 bg-[#2563EB] text-white py-3.5 rounded-[10px] font-bold text-[16px] shadow-lg shadow-blue-500/30 btn-active" onclick="saveEdit()">保存</button>
</div>
</div>
<!-- 筛选弹窗 -->
<div class="absolute inset-0 z-[100] bg-black/50 flex items-end justify-center" id="filterOverlay" onclick="toggleFilterDrawer(false)" style="display: none;">
<div class="bg-white rounded-t-[20px] w-full max-w-[390px] px-5 pt-6 pb-8" onclick="event.stopPropagation()">
<div class="relative flex items-center justify-center mb-6">
<button class="absolute right-0 top-0 text-gray-400" onclick="toggleFilterDrawer(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
</div>
<div class="space-y-5">
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">入库时间</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 bg-transparent outline-none text-gray-800 placeholder-gray-400 text-[14px]" placeholder="请输入入库时间" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">进港航班号</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入进港航班号" type="text"/>
</div>
</div>
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">所属航司</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择所属航司" readonly type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">来源</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择来源" readonly type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
</div>
<div class="flex gap-3 mt-8">
<button class="flex-1 h-11 text-[15px] font-medium text-gray-600 bg-white border border-gray-200 rounded-[10px]" onclick="resetFilter()">重置</button>
<button class="flex-1 h-11 text-[15px] font-medium text-white bg-[#2563EB] rounded-[10px] shadow-sm" onclick="confirmFilter()">确认</button>
</div>
</div>
</div>
<!-- 选择面板 -->
<div class="fixed inset-0 z-[100] bg-black/40 flex items-end justify-center" id="selectPanel" onclick="hideSelectPanel(event)" style="display: none;">
<div class="bg-white rounded-t-[20px] w-full max-w-[390px] px-4 pt-4 pb-8" onclick="event.stopPropagation()">
<div class="flex items-center justify-between mb-4">
<div class="w-6"></div>
<div class="text-center text-[16px] font-bold text-gray-900" id="selectPanelTitle">请选择</div>
<button class="w-6 h-6 flex items-center justify-center text-gray-500" onclick="hideSelectPanel()">
<iconify-icon icon="mdi:close" width="20"></iconify-icon>
</button>
</div>
<div class="space-y-1" id="selectPanelOptions"></div>
</div>
</div>
</div>
</div>
<script>
let currentDetail = {};
function showUldDetail(uldNo, status, airline, weight, volume, maxWeight, flightNo, port, location, remark) {
currentDetail = { uldNo, status, airline, weight, volume, maxWeight, flightNo, port, location, remark };
document.getElementById('detail-uldNo').innerText = uldNo;
document.getElementById('detail-airline').innerText = airline;
document.getElementById('detail-weight').innerText = weight;
document.getElementById('detail-volume').innerText = volume;
document.getElementById('detail-maxWeight').innerText = maxWeight;
document.getElementById('detail-flightNo').innerText = flightNo;
document.getElementById('detail-port').innerText = port;
document.getElementById('detail-remark').innerText = remark;
document.getElementById('detail-inTime').innerText = "2026-07-20 15:59:20";
document.getElementById('detail-source').innerText = "空运";
const st = document.getElementById('detail-status');
st.innerText = status;
st.className = 'text-[11px] px-2 rounded-[6px] font-medium leading-none h-5 flex items-center';
if (status === '占用') {
st.classList.add('bg-orange-100', 'text-orange-600');
} else if (status === '可用') {
st.classList.add('bg-green-100','text-green-600');
} else if (status === '维修') {
st.classList.add('bg-red-100','text-red-600');
} else {
st.className = 'text-[14px] font-normal text-[#111827]';
}
document.getElementById('detailOverlay').classList.remove('hidden');
}
function hideUldDetail() {
document.getElementById('detailOverlay').classList.add('hidden');
}
function openEdit() {
document.getElementById('editOverlay').classList.remove('hidden');
document.getElementById('edit-uldNo').value = currentDetail.uldNo;
document.getElementById('edit-weight').value = currentDetail.weight;
document.getElementById('edit-flightNo').value = currentDetail.flightNo;
document.getElementById('edit-port').value = currentDetail.port;
document.getElementById('edit-inTime').value = "2026-07-20 15:59:20";
document.getElementById('edit-remark').value = currentDetail.remark;
const st = document.getElementById('edit-status-text');
st.textContent = currentDetail.status;
st.className = 'text-[13px] font-medium px-2 py-0.5 rounded-[6px]';
if (currentDetail.status === '占用') {
st.classList.add('bg-[#FFF3E0]','text-[#E67722]');
} else if (currentDetail.status === '可用') {
st.classList.add('bg-green-100','text-green-600');
} else if (currentDetail.status === '维修') {
st.classList.add('bg-red-100','text-red-600');
} else {
st.classList.add('text-gray-800');
}
document.getElementById('edit-airline-text').textContent = currentDetail.airline;
document.getElementById('edit-source-text').textContent = '空运';
}
function closeEdit() {
document.getElementById('editOverlay').classList.add('hidden');
}
function saveEdit() {
alert('修改成功');
closeEdit();
hideUldDetail();
}
function toggleFilterDrawer(show) {
document.getElementById('filterOverlay').style.display = show ? 'flex' : 'none';
}
function resetFilter() {
document.querySelectorAll('#filterOverlay input').forEach(i=>i.value='');
}
function confirmFilter() {
toggleFilterDrawer(false);
}
function toggleSelectAll(checked) {
document.querySelectorAll('main .custom-checkbox-round').forEach(cb=>cb.checked=checked);
updateSelectedCount();
}
function handleCheckboxClick() {
updateSelectedCount();
}
function updateSelectedCount() {
const total = document.querySelectorAll('main .custom-checkbox-round').length;
const ckd = document.querySelectorAll('main .custom-checkbox-round:checked').length;
document.getElementById('selected-count').innerText = ckd+' / '+total;
document.getElementById('select-all-cb').checked = total > 0 && total === ckd;
}
function showAddDialog() {
document.getElementById('addUldOverlay').classList.remove('hidden');
}
function hideAddDialog() {
document.getElementById('addUldOverlay').classList.add('hidden');
}
function saveUld() {
alert('保存成功');
hideAddDialog();
}
let currentSelectField = '';
function showSelectPanel(field, title, options) {
currentSelectField = field;
document.getElementById('selectPanelTitle').textContent = title;
const box = document.getElementById('selectPanelOptions');
box.innerHTML = '';
options.forEach(opt=>{
const d = document.createElement('div');
d.className = 'py-3 px-4 text-center border-b cursor-pointer hover:bg-gray-50';
if(field.includes('status')){
let cls = opt=='占用'?'bg-[#FFF3E0] text-[#E67722]':
opt=='可用'?'bg-green-100 text-green-600':'bg-red-100 text-red-600';
d.innerHTML = `<span class="${cls} px-2 py-0.5 rounded-[6px] text-sm font-medium">${opt}</span>`;
} else {
d.textContent = opt;
}
d.onclick = ()=>{
if(field === 'editStatus'){
const t = document.getElementById('edit-status-text');
t.textContent = opt;
t.className = 'text-[13px] font-medium px-2 py-0.5 rounded-[6px]';
if(opt=='占用'){
t.classList.add('bg-[#FFF3E0]','text-[#E67722]');
}
if(opt=='可用')t.classList.add('bg-green-100','text-green-600');
if(opt=='维修')t.classList.add('bg-red-100','text-red-600');
}
if(field === 'editAirline') document.getElementById('edit-airline-text').textContent = opt;
if(field === 'editSource') document.getElementById('edit-source-text').textContent = opt;
if(field === 'status') document.getElementById('add-status-text').textContent = opt;
if(field === 'airline') document.getElementById('add-airline-text').textContent = opt;
if(field === 'source') document.getElementById('add-source-text').textContent = opt;
hideSelectPanel();
};
box.appendChild(d);
});
document.getElementById('selectPanel').style.display = 'flex';
}
function hideSelectPanel() {
document.getElementById('selectPanel').style.display = 'none';
}
function handleBatchDelete() {
const ckd = document.querySelectorAll('main .custom-checkbox-round:checked').length;
if(ckd===0)return alert('请选择');
if(confirm('确定删除?')) alert('删除成功');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,715 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>出库交接 - 移动端原型</title>
<!-- Tailwind CSS -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
/* 模拟手机容器 */
.phone-container {
width: 375px;
height: 812px;
background-color: #f5f6fa;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 8px solid #1a1a1a;
border-radius: 40px;
}
/* 隐藏滚动条但保留功能 */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* 页面切换动画 */
.page {
display: none;
flex-direction: column;
height: 100%;
}
.page.active {
display: flex;
}
/* 按钮点击态 */
.btn-active:active {
opacity: 0.7;
transform: scale(0.98);
}
/* 复选框样式 */
.custom-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.custom-checkbox-round {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 50%;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox-round:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox-round:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* 卡片选中样式已移除 */
</style>
</head>
<body>
<div class="phone-container">
<!-- 状态栏模拟 -->
<div class="h-10 w-full flex justify-between items-center px-8 flex-none bg-[#2563EB] text-white">
<span class="text-xs font-semibold">09:41</span>
<div class="flex space-x-1 items-center">
<iconify-icon icon="mdi:signal" width="14"></iconify-icon>
<iconify-icon icon="mdi:wifi" width="14"></iconify-icon>
<iconify-icon icon="mdi:battery" width="18"></iconify-icon>
</div>
</div>
<!-- 页面1: 货运列表首页 -->
<div class="page active" id="page-home">
<!-- 顶部导航 -->
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="alert('返回首页')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">出库交接</h1>
<div class="w-16"></div>
</div>
<!-- 内容滚动区 -->
<div class="flex-1 overflow-y-auto no-scrollbar pb-24">
<!-- 搜索栏 -->
<section class="px-4 pt-4 pb-3 bg-white">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-500 text-[14px] text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索ULD编号" type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="toggleFilterDrawer(true)">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- 子Tab切换栏 -->
<div class="flex bg-white border-b sticky top-0 z-20">
<button class="flex-1 py-3 text-sm font-bold text-[#1F2937] border-b-2 border-[#1F2937] transition-all" id="tab-pending" onclick="switchTab('pending')">
待交接 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
<button class="flex-1 py-3 text-sm font-bold text-gray-500 text-[14px] border-b-2 border-transparent transition-all" id="tab-shipped" onclick="switchTab('shipped')">
已交接 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
</div>
<!-- 列表内容 -->
<div class="p-3 pt-4 space-y-3" id="list-pending">
<!-- 卡片 1 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">PMC90901CZ</div>
</div>
<div class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">IMP代码</div>
</div>
<!-- 航班信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260706/MU1111</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">架子车号</span>
<span class="text-[#1F2937] text-[14px] font-semibold">076</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">目的港</span>
<span class="text-[#1F2937] text-[14px] font-semibold">LAX</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">装机重</span>
<span class="text-[#1F2937] text-[14px] font-semibold">200kg</span>
</div>
</div>
<!-- 库位信息行 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">库位:大棚A3</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: 'PMC87678MU', status: '待交接', rackNo: '076', location: 'A区-03货位', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 2 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">AKE87865MU</div>
</div>
<div class="bg-red-600 text-white text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">IMP代码</div>
</div>
<!-- 航班信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260706/CA2222</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">架子车号</span>
<span class="text-[#1F2937] text-[14px] font-semibold">066</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">目的港</span>
<span class="text-[#1F2937] text-[14px] font-semibold">LAX</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">装机重</span>
<span class="text-[#1F2937] text-[14px] font-semibold">150kg</span>
</div>
</div>
<!-- 库位信息行 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">库位:空侧-05</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: 'AKE87865MU', status: '待交接', agent: '043', flightDate: '20260706', flightNo: 'CA2222', pieces: '85', weight: '150', rackNo: '055', carrier: 'CA', origin: 'PEK', location: 'B区-05货位', code: '076', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 3 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">PMC87678MU</div>
</div>
<div class="bg-red-600 text-white text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">IMP代码</div>
</div>
<!-- 航班信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260707/3U3333</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">架子车号</span>
<span class="text-[#1F2937] text-[14px] font-semibold">055</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">目的港</span>
<span class="text-[#1F2937] text-[14px] font-semibold">LAX</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">装机重</span>
<span class="text-[#1F2937] text-[14px] font-semibold">450kg</span>
</div>
</div>
<!-- 库位信息行 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">库位:C区-01货位</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: 'PMC87678MU', status: '待交接', agent: 'DHL', flightDate: '20260707', flightNo: '3U3333', pieces: '200', weight: '450', rackNo: '055', carrier: '3U', origin: 'CTU', location: 'C区-01货位', code: '076', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
</div>
<!-- 已交接列表内容 -->
<div class="p-3 pt-4 space-y-3 hidden" id="list-shipped">
<!-- 卡片 1 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">PMC90901CZ</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已交接</span>
</div>
</div>
<!-- 操作信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260704/MU1111</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">架子车号</span>
<span class="text-[#1F2937] text-[14px] font-semibold">076</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">目的港</span>
<span class="text-[#1F2937] text-[14px] font-semibold">LAX</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">装机重</span>
<span class="text-[#1F2937] text-[14px] font-semibold">80kg</span>
</div>
</div>
<!-- 操作人信息 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">交接人:张三 · 交接时间:2026-07-05 11:45</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer" onclick="showDetail({id: 'PMC90901CZ', status: '已交接', agent: '076', rackNo: '076', location: 'B区-05货位', code: '076', counterTime: '2026-07-05 10:30', counterPerson: '张三', outboundTime: '2026-07-05 11:45', outboundPerson: '张三'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 2 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">AKE87865MU</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已交接</span>
</div>
</div>
<!-- 操作信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260705/CA2222</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">架子车号</span>
<span class="text-[#1F2937] text-[14px] font-semibold">043</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">目的港</span>
<span class="text-[#1F2937] text-[14px] font-semibold">LAX</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">装机重</span>
<span class="text-[#1F2937] text-[14px] font-semibold">300kg</span>
</div>
</div>
<!-- 操作人信息 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">交接人:赵六 · 交接时间:2026-07-06 10:30</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer" onclick="showDetail({id: 'AKE87865MU', status: '已交接', agent: '043', flightDate: '20260705', flightNo: 'CA2222', pieces: '120', weight: '300', pickupNo: 'TH20260702002', rackNo: '鲜花绿植', carrier: 'CA', origin: 'SFO', location: 'A区-08货位', code: '076', counterTime: '2026-07-06 09:15', counterPerson: '王五', outboundTime: '2026-07-06 10:30', outboundPerson: '赵六'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 3 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">PMC87678MU</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已交接</span>
</div>
</div>
<!-- 操作信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260705/3U3333</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">架子车号</span>
<span class="text-[#1F2937] text-[14px] font-semibold">077</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">目的港</span>
<span class="text-[#1F2937] text-[14px] font-semibold">LAX</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">装机重</span>
<span class="text-[#1F2937] text-[14px] font-semibold">160kg</span>
</div>
</div>
<!-- 操作人信息 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">交接人:孙八 · 交接时间:2026-07-06 15:20</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer" onclick="showDetail({id: 'PMC87678MU', status: '已交接', agent: 'DHL', flightDate: '20260705', flightNo: '3U3333', pieces: '75', weight: '160', pickupNo: 'TH20260703003', rackNo: '生鲜食品', carrier: '3U', origin: 'CDG', location: 'D区-02货位', code: 'NOR', counterTime: '2026-07-06 14:00', counterPerson: '钱七', outboundTime: '2026-07-06 15:20', outboundPerson: '孙八'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
</div>
</div>
<!-- 底部工具栏 -->
<div class="absolute bottom-0 w-full bg-white border-t border-gray-200 z-40 flex flex-col pt-4 pb-4" id="bottom-toolbar">
<!-- 操作区域 -->
<div class="px-4 flex items-center w-full">
<div class="flex items-center gap-2">
<input class="custom-checkbox-round" id="select-all-cb" onclick="toggleSelectAll(this.checked)" type="checkbox"/>
<label class="text-[13px] text-[#1F2937] font-medium cursor-pointer" for="select-all-cb">全选</label>
</div>
<div class="flex-1"></div>
<div class="flex items-center gap-3">
<span class="text-[12px] text-[#9CA3AF]">已选 <span id="selected-count">0</span> 项</span>
<button class="bg-[#2563EB] text-white text-[15px] font-bold px-5 py-2 rounded-[10px] shadow-lg shadow-blue-500/25 btn-active" onclick="alert('交接操作')">
交接
</button>
</div>
</div>
</div>
</div>
<!-- 页面2: 详情页 -->
<div class="page" id="page-detail">
<!-- 顶部导航 -->
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="switchPage('home')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">板箱详情</h1>
<div class="w-16"></div>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar pb-8 p-4">
<!-- 货运单据样式卡片 -->
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100">
<!-- 1. 顶部蓝色条带 -->
<div class="bg-[#2563EB] px-4 py-3 flex justify-between items-center text-white">
<span class="font-bold text-[17px] leading-tight" id="detail-awb">781-22222620</span>
</div>
<div class="p-4">
<!-- 3. 货物名称 -->
<div class="text-[22px] font-bold text-[#111827] mb-3" id="detail-rack-no">076</div>
<!-- 4. 两列信息表格 -->
<div class="grid grid-cols-2 gap-x-8 gap-y-3">
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">装机重</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-count">55</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">航班日期</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-weight">20260706</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">航班号</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-carrier">MU2231</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">目的港</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-flight-date">LAX</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">IMP代码</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-carrier">X</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">板型</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-flight-date">QP</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">库位</div>
<div class="flex">
<span class="bg-orange-50 text-orange-600 px-1 rounded text-[15px] font-bold" id="detail-location">1111111</span>
</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">备注</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-flight-no">危险品</div>
</div>
</div>
</div>
</div>
<!-- 复磅信息卡片 -->
<div class="mt-4 bg-white rounded-xl shadow-sm overflow-hidden border border-gray-200 hidden" id="detail-outbound-card">
<div class="bg-[#10B981] px-4 py-2.5">
<span class="text-white font-semibold text-[15px]">复磅信息</span>
</div>
<div class="bg-white px-4 py-3 grid grid-cols-2 gap-y-3">
<div class="flex flex-col">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">复磅人</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-counter-person">-</span>
</div>
<div class="flex flex-col">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">复磅时间</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-counter-time">-</span>
</div>
<div class="flex flex-col" id="detail-outbound-person-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">交接人</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-person">-</span>
</div>
<div class="flex flex-col" id="detail-outbound-time-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">交接时间</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-time">-</span>
</div>
</div>
</div>
</div>
</div>
<!-- 筛选条件弹窗(从ULD管理移植) -->
<div class="absolute inset-0 z-[100] bg-black/50 flex items-end justify-center" id="filterOverlay" onclick="toggleFilterDrawer(false)" style="display: none;">
<div class="bg-white rounded-t-[20px] w-full max-w-[375px] px-5 pt-6 pb-8" onclick="event.stopPropagation()">
<!-- 标题 -->
<div class="relative flex items-center justify-center mb-6">
<button class="absolute right-0 top-0 text-gray-400" onclick="toggleFilterDrawer(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
</div>
<div class="space-y-5">
<!-- 航班日期 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<!-- 进港航班号 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班号</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入航班号" type="text"/>
</div>
</div>
<!-- 交接人 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">交接人</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择交接人" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex gap-3 mt-8">
<button class="flex-1 h-11 text-[15px] font-medium text-gray-600 bg-white border border-gray-200 rounded-[10px]" onclick="resetFilter()">重置</button>
<button class="flex-1 h-11 text-[15px] font-medium text-white bg-[#2563EB] rounded-[10px] shadow-sm" onclick="confirmFilter()">确认</button>
</div>
</div>
</div>
<script>
// 页面切换逻辑
function switchPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById('page-' + pageId).classList.add('active');
if(pageId === 'detail') {
document.querySelector('#page-detail .overflow-y-auto').scrollTop = 0;
}
}
// 子Tab切换逻辑
function switchTab(type) {
const tabPending = document.getElementById('tab-pending');
const tabShipped = document.getElementById('tab-shipped');
const listPending = document.getElementById('list-pending');
const listShipped = document.getElementById('list-shipped');
const bottomToolbar = document.getElementById('bottom-toolbar');
if (type === 'pending') {
// 待交接
tabPending.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabPending.classList.remove('text-gray-500', 'border-transparent');
tabShipped.classList.add('text-gray-500', 'border-transparent');
tabShipped.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listPending.classList.remove('hidden');
listShipped.classList.add('hidden');
bottomToolbar.style.display = 'flex';
} else {
// 已交接
tabShipped.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabShipped.classList.remove('text-gray-500', 'border-transparent');
tabPending.classList.add('text-gray-500', 'border-transparent');
tabPending.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listShipped.classList.remove('hidden');
listPending.classList.add('hidden');
bottomToolbar.style.display = 'none';
}
}
// 全选/取消全选
function toggleSelectAll(checked) {
const checkboxes = document.querySelectorAll('.item-checkbox');
checkboxes.forEach(cb => {
cb.checked = checked;
});
updateSelectionCount();
}
// 处理卡片复选框点击
function handleCheckboxClick(checkbox, event) {
event.stopPropagation();
updateSelectionCount();
const checkboxes = document.querySelectorAll('.item-checkbox');
const selectAllCb = document.getElementById('select-all-cb');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
selectAllCb.checked = allChecked;
}
// 更新选中计数
function updateSelectionCount() {
const checkedCount = document.querySelectorAll('.item-checkbox:checked').length;
document.getElementById('selected-count').textContent = checkedCount;
}
// 显示详情页并填充数据
function showDetail(data) {
if (!data) return;
const setText = (id, text) => {
const el = document.getElementById(id);
if (el) el.textContent = text || '-';
};
setText('detail-awb', data.id);
setText('detail-rack-no', data.rackNo);
setText('detail-weight', (data.weight || '') + (data.weight && !data.weight.toString().includes('kg') ? ' kg' : ''));
setText('detail-flight-date', data.flightDate);
setText('detail-flight-no', data.flightNo);
setText('detail-location', data.location);
const outboundCard = document.getElementById('detail-outbound-card');
if (outboundCard) {
outboundCard.classList.remove('hidden');
setText('detail-counter-person', data.counterPerson);
setText('detail-counter-time', data.counterTime);
setText('detail-outbound-person', data.outboundPerson);
setText('detail-outbound-time', data.outboundTime);
const outboundPersonRow = document.getElementById('detail-outbound-person-row');
const outboundTimeRow = document.getElementById('detail-outbound-time-row');
if (data.status === '已交接') {
outboundPersonRow.classList.remove('hidden');
outboundTimeRow.classList.remove('hidden');
} else {
outboundPersonRow.classList.add('hidden');
outboundTimeRow.classList.add('hidden');
}
}
switchPage('detail');
}
// 筛选面板(移植自ULD管理)
function toggleFilterDrawer(show) {
const overlay = document.getElementById('filterOverlay');
overlay.style.display = show ? 'flex' : 'none';
}
function resetFilter() {
const inputs = document.querySelectorAll('#filterOverlay input');
inputs.forEach(input => input.value = '');
}
function confirmFilter() {
toggleFilterDrawer(false);
}
// 初始化日期
const today = new Date().toISOString().split('T')[0];
console.log("System date initialized:", today);
</script>
</div>
</body>
</html>

View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #F3F4F6;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Helvetica Neue", sans-serif;
}
/* 统一手机外壳容器 */
.phone-container {
width: 390px;
height: 844px;
background: #1a1a1a;
border-radius: 55px;
border: 8px solid #1F2937;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
position: relative;
}
/* 手机内部屏幕区域 */
.phone-screen {
flex: 1;
background: #F2F5F9;
margin: 0 4px 4px 4px;
border-radius: 46px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.content-scroll {
flex: 1;
overflow-y: auto;
}
.content-scroll::-webkit-scrollbar {
display: none;
}
</style>
</head>
<body>
<div class="phone-container">
<div class="phone-screen">
<!-- 状态栏 + 导航栏 蓝色连体区域 -->
<div class="bg-[#2563EB] flex flex-col flex-none">
<!-- 手机状态栏 -->
<div class="flex items-center justify-between h-[28px] text-white text-[12px] px-6">
<span class="text-white font-medium tracking-wide">09:10</span>
<div class="flex items-center gap-[5px]">
<iconify-icon class="text-white text-[13px]" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-white text-[13px]" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-white text-[15px]" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="text-white h-12 flex items-center px-4">
<a class="flex items-center gap-1" href="records.html">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span class="text-[16px]">返回</span>
</a>
<h1 class="flex-1 text-center text-[17px] font-semibold mr-10">运单详情</h1>
</div>
</div>
<div class="content-scroll">
<!-- 卡片1 — 运单概览 -->
<div class="bg-white rounded-xl shadow-md mb-4 overflow-hidden border border-gray-100 mx-4 mt-4">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800" id="det-uldTitle">基本信息</span>
</div>
<div class="p-3 grid grid-cols-2 gap-y-3 gap-x-4 border-t border-gray-50 pt-3">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">运单号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billNo">78111112222</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">件数</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-piece">85</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">重量</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-weight">156 kg</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">特码</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-code">DGR</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">代理</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-agent">ATR</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">航班日期</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-flightDate">2026-07-03</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">航班号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-flightNo">MU2222</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">航程</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-route">PVG - CAN</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">业务类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-type">普通货物运输</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">通道号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-channel">出港收运1</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 mb-0.5">品名</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemName">电子产品</span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 mb-0.5">备注</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-remark">测试</span>
</div>
</div>
</div>
<div class="pb-6"></div>
</div>
<!-- iOS 底部指示条 -->
<div class="h-5 bg-[#F2F5F9] flex justify-center items-center flex-none">
<div class="w-[134px] h-[5px] bg-gray-300 rounded-full"></div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,419 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #F3F4F6;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Helvetica Neue", sans-serif;
}
/* 统一手机外壳容器 */
.phone-container {
width: 390px;
height: 844px;
background: #1a1a1a;
border-radius: 55px;
border: 8px solid #1F2937;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
position: relative;
}
/* 手机内部屏幕区域 */
.phone-screen {
flex: 1;
background: #F2F5F9;
margin: 0 4px 4px 4px;
border-radius: 46px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 隐藏滚动条 */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* 卡片淡入动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(18px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-animate {
animation: fadeInUp 0.5s ease-out both;
}
.card-animate:nth-child(1) { animation-delay: 0.05s; }
.card-animate:nth-child(2) { animation-delay: 0.12s; }
.card-animate:nth-child(3) { animation-delay: 0.19s; }
.card-animate:nth-child(4) { animation-delay: 0.26s; }
/* 卡片点击涟漪效果 */
.card-item {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.card-item:active {
transform: scale(0.985);
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
/* 筛选弹窗动画 */
.filter-modal {
transition: transform 0.3s ease-out;
transform: translateY(100%);
}
.filter-modal.show {
transform: translateY(0);
}
.filter-overlay {
transition: opacity 0.3s ease;
opacity: 0;
pointer-events: none;
}
.filter-overlay.show {
opacity: 1;
pointer-events: auto;
}
</style>
</head>
<body>
<div class="phone-container">
<div class="phone-screen">
<!-- ===== 顶部连体蓝色区域【状态栏+导航栏】 ===== -->
<div class="bg-[#2563EB] flex flex-col flex-none">
<!-- 手机状态栏 -->
<div class="flex items-center justify-between h-[28px] text-white text-[12px] px-6">
<span class="text-white font-medium tracking-wide">09:10</span>
<div class="flex items-center gap-[5px]">
<iconify-icon class="text-white text-[13px]" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-white text-[13px]" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-white text-[15px]" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<header class="text-white h-12 flex items-center px-4">
<button class="flex items-center text-[15px] gap-0.5" onclick="window.history.back()">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="flex-1 text-center text-[17px] font-semibold">出港待计重</h1>
<div class="w-12"></div>
</header>
</div>
<!-- ===== 主内容区 ===== -->
<main class="flex-1 overflow-y-auto no-scrollbar bg-[#F2F5F9]">
<!-- 搜索栏 -->
<div class="h-[3px] bg-blue-500 mx-0"></div>
<section class="px-4 pt-4 pb-3">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索运单号/航班号..." type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="toggleFilterDrawer(true)">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- 统计标签 -->
<div class="flex items-center gap-2 px-4 pb-3">
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#2563EB]">
<div class="text-[11px] text-gray-500">总票数</div>
<div class="text-[22px] font-bold text-[#2563EB] leading-tight mt-0.5">4</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-orange-500">
<div class="text-[11px] text-gray-500">待计重</div>
<div class="text-[22px] font-bold text-orange-500 leading-tight mt-0.5">2</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#10B981]">
<div class="text-[11px] text-gray-500">计重中</div>
<div class="text-[22px] font-bold text-[#10B981] leading-tight mt-0.5">2</div>
</div>
</div>
<!-- ===== 运单卡片列表 ===== -->
<div class="px-0 pt-3 pb-8">
<!-- 卡片 1 -->
<div class="card-item card-animate bg-white rounded-2xl shadow-sm shadow-black/5 px-4 py-3.5 mx-4 mb-2.5 cursor-pointer" onclick="window.location.href='weighing.html'">
<div class="flex items-center">
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center">
<span class="text-[15px] font-bold text-[#1F2937]">78133363398</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">未计重</span>
<iconify-icon class="text-gray-400" icon="mdi:chevron-right" width="14"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span>日期:20260721</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>航程:HFE-LAX</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:truck-outline"></iconify-icon>
<span>代理:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center" onclick="event.stopPropagation(); this.disabled=true; this.classList.remove('bg-[#EFF6FF]','text-[#2563EB]'); this.classList.add('bg-gray-100','text-gray-400','cursor-not-allowed'); showToast('执行提前运抵:运单号 78133363396')">
提前运抵
</button>
</div>
</div>
</div>
</div>
<!-- 卡片 2 -->
<div class="card-item card-animate bg-white rounded-2xl shadow-sm shadow-black/5 px-4 py-3.5 mx-4 mb-2.5 cursor-pointer" onclick="window.location.href='weighing.html'">
<div class="flex items-center">
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center">
<span class="text-[15px] font-bold text-[#1F2937]">78133363399</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">计重中</span>
<iconify-icon class="text-gray-400" icon="mdi:chevron-right" width="14"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span>日期:20260721</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>航程:HFE-LAX</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:truck-outline"></iconify-icon>
<span>代理:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center" onclick="event.stopPropagation(); this.disabled=true; this.classList.remove('bg-[#EFF6FF]','text-[#2563EB]'); this.classList.add('bg-gray-100','text-gray-400','cursor-not-allowed'); showToast('执行提前运抵:运单号 78133363397')">
提前运抵
</button>
</div>
</div>
</div>
</div>
<!-- 卡片 3 -->
<div class="card-item card-animate bg-white rounded-2xl shadow-sm shadow-black/5 px-4 py-3.5 mx-4 mb-2.5 cursor-pointer" onclick="window.location.href='weighing.html'">
<div class="flex items-center">
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center">
<span class="text-[15px] font-bold text-[#1F2937]">78133363398</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">未计重</span>
<iconify-icon class="text-gray-400" icon="mdi:chevron-right" width="14"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span>日期:20260721</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>航程:HFE-LAX</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:truck-outline"></iconify-icon>
<span>代理:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center" onclick="event.stopPropagation(); this.disabled=true; this.classList.remove('bg-[#EFF6FF]','text-[#2563EB]'); this.classList.add('bg-gray-100','text-gray-400','cursor-not-allowed'); showToast('执行提前运抵:运单号 78133363398')">
提前运抵
</button>
</div>
</div>
</div>
</div>
<!-- 卡片 4 -->
<div class="card-item card-animate bg-white rounded-2xl shadow-sm shadow-black/5 px-4 py-3.5 mx-4 mb-2.5 cursor-pointer" onclick="window.location.href='weighing.html'">
<div class="flex items-center">
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center">
<span class="text-[15px] font-bold text-[#1F2937]">78133363399</span>
</div>
<div class="flex items-center gap-1">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">计重中</span>
<iconify-icon class="text-gray-400 text-lg shrink-0" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span>日期:20260721</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>航程:HFE-LAX</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:truck-outline"></iconify-icon>
<span>代理:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center" onclick="event.stopPropagation(); this.disabled=true; this.classList.remove('bg-[#EFF6FF]','text-[#2563EB]'); this.classList.add('bg-gray-100','text-gray-400','cursor-not-allowed'); showToast('执行提前运抵:运单号 78133363399')">
提前运抵
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- ===== iOS 底部指示条 ===== -->
<div class="h-5 bg-[#F2F5F9] flex justify-center items-center flex-none">
<div class="w-[134px] h-[5px] bg-gray-300 rounded-full"></div>
</div>
<!-- 计重记录悬浮按钮 -->
<a class="absolute bottom-[20px] right-[20px] w-[52px] h-[52px] bg-[#2563EB] rounded-full shadow-lg shadow-blue-500/30 flex items-center justify-center z-50 hover:bg-[#1d4ed8] active:scale-95 transition-transform" href="records.html">
<iconify-icon class="text-white text-[24px]" icon="mdi:clipboard-text-clock"></iconify-icon>
</a>
<!-- ================== 筛选弹窗(底部弹出) ================== -->
<div class="filter-overlay absolute inset-0 bg-black/40 z-40" id="filterOverlay" onclick="toggleFilterDrawer(false)"></div>
<div class="filter-modal absolute bottom-0 left-0 right-0 bg-white rounded-t-3xl z-50 p-6 shadow-2xl" id="filter-panel">
<div class="relative flex items-center justify-center mb-6">
<h2 class="text-center text-[18px] font-bold text-gray-900">筛选条件</h2>
<button class="absolute right-0 top-0 text-gray-400" onclick="toggleFilterDrawer(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">通道号</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择通道号</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="space-y-4 mt-4">
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">承运人</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-800">
<span class="text-sm font-medium">MU</span>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">运单类型</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择运单类型</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">代理人</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择代理人</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="flex gap-4 pt-2">
<button class="flex-1 bg-white border border-gray-300 text-gray-600 py-3.5 rounded-xl font-bold" onclick="resetFilter()">
重置
</button>
<button class="flex-1 bg-[#2563EB] text-white py-3.5 rounded-xl font-bold shadow-lg shadow-blue-500/20" onclick="confirmFilter()">
确认
</button>
</div>
</div>
<div class="h-8"></div>
</div>
</div>
</div>
<script>
// Toast 提示函数
function showToast(msg) {
var existing = document.querySelector('.custom-toast');
if (existing) existing.remove();
var toast = document.createElement('div');
toast.className = 'custom-toast fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-900 text-white text-sm px-5 py-3 rounded-xl shadow-lg z-50 transition-opacity duration-200';
toast.style.maxWidth = '280px';
toast.style.textAlign = 'center';
toast.style.pointerEvents = 'none';
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(function() { toast.style.opacity = '0'; setTimeout(function() { toast.remove(); }, 200); }, 1500);
}
// 筛选弹窗显示/隐藏
function toggleFilterDrawer(show) {
const overlay = document.getElementById('filterOverlay');
const modal = document.getElementById('filter-panel');
if (show) {
overlay.classList.add('show');
modal.classList.add('show');
} else {
overlay.classList.remove('show');
modal.classList.remove('show');
}
}
// 重置筛选
function resetFilter() {
showToast('已重置筛选条件');
}
// 确认筛选
function confirmFilter() {
toggleFilterDrawer(false);
showToast('已应用筛选条件');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" 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>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-tap-highlight-color: transparent;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.filter-modal {
transition: transform 0.3s ease-out;
transform: translateY(100%);
}
.filter-modal.show {
transform: translateY(0);
}
.filter-overlay {
transition: opacity 0.3s ease;
opacity: 0;
pointer-events: none;
}
.filter-overlay.show {
opacity: 1;
pointer-events: auto;
}
.btn-active:active {
opacity: 0.7;
transform: scale(0.98);
}
</style>
</head>
<body class="bg-gray-200 min-h-screen flex justify-center items-center p-4">
<!-- 手机容器 -->
<div class="w-[375px] h-[812px] bg-[#F3F4F6] overflow-hidden rounded-[40px] border-[8px] border-gray-800 shadow-2xl relative flex flex-col">
<!-- iOS 状态栏 -->
<div class="flex-none bg-[#2563EB] text-white h-11 px-6 flex justify-between items-center z-50">
<span class="text-sm font-semibold">09:10</span>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-lg" icon="mdi:signal"></iconify-icon>
<iconify-icon class="text-lg" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-lg rotate-90" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="flex-none bg-[#2563EB] h-12 flex items-center px-4 relative">
<button class="flex items-center text-white text-base btn-active" onclick="window.location.href='main.html'">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="absolute left-1/2 -translate-x-1/2 text-white font-medium text-lg">计重记录</h1>
<!-- 导航栏地磅按钮已移除 -->
</div>
<!-- 内容滚动区域 -->
<div class="flex-1 overflow-y-auto hide-scrollbar pb-10">
<!-- 搜索栏 -->
<div class="h-[3px] bg-blue-500 mx-0"></div>
<section class="px-4 pt-4 pb-3">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索运单号" type="text"/>
<button class="flex-none text-gray-500 btn-active">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none btn-active" onclick="toggleFilterDrawer(true)">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- 统计标签 -->
<div class="px-4 mb-4">
<div class="flex gap-2">
<!-- 总票数 -->
<div class="flex-1 bg-white p-3 rounded-lg border-l-4 border-blue-500 shadow-sm flex flex-col items-center">
<span class="text-[11px] text-gray-500 mb-1">总票数</span>
<span class="text-xl font-bold text-blue-600">3</span>
</div>
<!-- 总件数 -->
<div class="flex-1 bg-white p-3 rounded-lg border-l-4 border-orange-500 shadow-sm flex flex-col items-center">
<span class="text-[11px] text-gray-500 mb-1">总件数</span>
<span class="text-xl font-bold text-orange-600">1</span>
</div>
<!-- 总重量 -->
<div class="flex-1 bg-white p-3 rounded-lg border-l-4 border-green-500 shadow-sm flex flex-col items-center">
<span class="text-[11px] text-gray-500 mb-1">总重量</span>
<span class="text-xl font-bold text-green-600">2</span>
</div>
</div>
</div>
<!-- 记录列表 -->
<div class="px-4 flex flex-col gap-4">
<!-- 卡片1 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<div class="flex justify-between items-start mb-3">
<span class="text-[15px] font-bold text-gray-800">78133363396</span>
<!-- 绿色地磅图标,跳转称重明细 -->
<button class="btn-active" onclick="window.location.href='weight-detail.html'">
<iconify-icon class="text-2xl text-gray-500" icon="mdi:weight"></iconify-icon>
</button>
</div>
<div class="flex items-center gap-2 text-gray-500 text-[13px] mb-3">
<iconify-icon inline icon="mdi:truck-outline" width="14" height="14"></iconify-icon>
<span>代理 : 马道</span>
</div>
<div class="grid grid-cols-3 gap-2 bg-gray-50 p-3 rounded-md mb-3">
<div class="flex flex-col items-center">
<span class="text-[11px] text-gray-400">运抵件数</span>
<div class="flex items-center gap-1">
<span class="text-[14px] font-semibold text-gray-700">85件</span>
</div>
</div>
<div class="flex flex-col items-center border-x border-gray-200 px-2">
<span class="text-[11px] text-gray-400">运抵重量</span>
<div class="flex items-center gap-1">
<span class="text-[14px] font-semibold text-gray-700">1560kg</span>
</div>
</div>
<div class="flex flex-col items-center">
<span class="text-[11px] text-gray-400">运抵体积</span>
<div class="flex items-center gap-1">
<span class="text-[14px] font-semibold text-gray-700">12.5m³</span>
</div>
</div>
</div>
<div class="flex items-center gap-2 text-gray-500 text-[13px] mb-3">
<iconify-icon inline icon="mdi:account-outline" width="16" height="16"></iconify-icon>
<span>计重人:张三 · 计重时间:2026-07-05 11:45</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="window.location.href='detail.html'">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片2 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<div class="flex justify-between items-start mb-3">
<span class="text-[15px] font-bold text-gray-800">78133363398</span>
<!-- 绿色地磅图标 -->
<button class="btn-active" onclick="window.location.href='weight-detail.html'">
<iconify-icon class="text-2xl text-gray-500" icon="mdi:weight"></iconify-icon>
</button>
</div>
<div class="flex items-center gap-2 text-gray-500 text-[13px] mb-3">
<iconify-icon inline icon="mdi:truck-outline" width="14" height="14"></iconify-icon>
<span>代理 : 徽远</span>
</div>
<div class="grid grid-cols-3 gap-2 bg-gray-50 p-3 rounded-md mb-3">
<div class="flex flex-col items-center">
<span class="text-[11px] text-gray-400">运抵件数</span>
<div class="flex items-center gap-1">
<span class="text-[14px] font-semibold text-gray-700">60件</span>
</div>
</div>
<div class="flex flex-col items-center border-x border-gray-200 px-2">
<span class="text-[11px] text-gray-400">运抵重量</span>
<div class="flex items-center gap-1">
<span class="text-[14px] font-semibold text-gray-700">920kg</span>
</div>
</div>
<div class="flex flex-col items-center">
<span class="text-[11px] text-gray-400">运抵体积</span>
<div class="flex items-center gap-1">
<span class="text-[14px] font-semibold text-gray-700">8.1m³</span>
</div>
</div>
</div>
<div class="flex items-center gap-2 text-gray-500 text-[13px] mb-3">
<iconify-icon inline icon="mdi:account-outline" width="16" height="16"></iconify-icon>
<span>计重人:张三 · 计重时间:2026-07-05 11:45</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="window.location.href='detail.html'">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
</div>
</div>
<!-- Toast 提示容器 -->
<div class="absolute left-1/2 -translate-x-1/2 bottom-20 bg-black/80 text-white text-[13px] px-4 py-2 rounded-full opacity-0 transition-opacity pointer-events-none z-[100]" id="toast"></div>
<!-- 筛选遮罩层 -->
<div class="filter-overlay absolute inset-0 bg-black/40 z-40" id="filterOverlay" onclick="toggleFilterDrawer(false)"></div>
<!-- 筛选弹窗 -->
<div class="filter-modal absolute bottom-0 left-0 right-0 bg-white rounded-t-3xl z-50 p-6 shadow-2xl" id="filter-panel">
<div class="relative flex items-center justify-center mb-6">
<h2 class="text-center text-[18px] font-bold text-gray-900">筛选条件</h2>
<button class="absolute right-0 top-0 text-gray-400 btn-active" onclick="toggleFilterDrawer(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">入库日期起</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择入库日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">入库日期止</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择入库日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">代理人</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-[14px]">全部</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-[14px]">全部</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">目的站</label>
<input type="text" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-3 py-3 text-[14px] outline-none" placeholder="请输入目的站">
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">计重人</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-[14px]">全部</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="flex gap-4 pt-2">
<button class="flex-1 bg-white border border-gray-300 text-gray-600 py-3.5 rounded-xl font-bold btn-active" onclick="showToast('已重置筛选');toggleFilterDrawer(false)">
重置
</button>
<button class="flex-1 bg-[#2563EB] text-white py-3.5 rounded-xl font-bold shadow-lg btn-active" onclick="toggleFilterDrawer(false);showToast('已应用筛选')">
确认
</button>
</div>
</div>
<div class="h-8"></div>
</div>
</div>
<script>
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('opacity-100');
setTimeout(() => {
toast.classList.remove('opacity-100');
}, 1500);
}
function toggleFilterDrawer(show) {
const overlay = document.getElementById('filterOverlay');
const modal = document.getElementById('filter-panel');
if (show) {
overlay.classList.add('show');
modal.classList.add('show');
} else {
overlay.classList.remove('show');
modal.classList.remove('show');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 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>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #F3F4F6;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Helvetica Neue", sans-serif;
}
/* 统一手机外壳容器 */
.phone-container {
width: 390px;
height: 844px;
background: #1a1a1a;
border-radius: 55px;
border: 8px solid #1F2937;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
position: relative;
}
/* 手机内部屏幕区域 */
.phone-screen {
flex: 1;
background: #F2F5F9;
margin: 0 4px 4px 4px;
border-radius: 46px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card-animate {
animation: fadeInUp 0.35s ease forwards;
opacity: 0;
}
.card-animate:nth-child(1) { animation-delay: 0s; }
.card-animate:nth-child(2) { animation-delay: 0.05s; }
.card-animate:nth-child(3) { animation-delay: 0.1s; }
.card-animate:nth-child(4) { animation-delay: 0.15s; }
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.animate-slideUp { animation: slideUp 0.3s ease-out; }
input { transition: all 0.2s ease; }
input:focus { transition: all 0.2s ease; }
::-webkit-scrollbar { display: none; }
* { -ms-overflow-style: none; scrollbar-width: none; }
</style>
</head>
<body>
<div class="phone-container">
<div class="phone-screen">
<!-- ===== ① 顶部连体蓝色区域(状态栏+导航栏) ===== -->
<div class="bg-[#2563EB] flex flex-col flex-none">
<!-- 手机状态栏 -->
<div class="flex items-center justify-between h-[28px] text-white text-[12px] px-6">
<span class="text-white font-medium tracking-wide">09:10</span>
<div class="flex items-center gap-[5px]">
<iconify-icon class="text-white text-[13px]" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-white text-[13px]" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-white text-[15px]" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<header class="text-white h-12 flex items-center px-4">
<button class="flex items-center gap-0.5 text-white font-medium text-[15px] active:opacity-70 transition-opacity" onclick="window.location.href='main.html'">
<iconify-icon class="text-white" height="24" icon="mdi:chevron-left" width="24"></iconify-icon>
<span>返回</span>
</button>
<h1 class="flex-1 text-center text-[17px] font-semibold">开始计重</h1>
<div class="w-[60px]"></div>
</header>
</div>
<!-- ===== ③ 主内容区(滚动) ===== -->
<main class="flex-1 overflow-y-auto no-scrollbar bg-[#F2F5F9] pb-4">
<!-- 顶部运单卡片(原样不动) -->
<div class="mx-4 mt-4 card-animate">
<div class="bg-white rounded-2xl shadow-md shadow-black/5 overflow-hidden relative">
<!-- 顶部渐变装饰条 -->
<div class="h-1" style="background: linear-gradient(90deg, #2563EB, #60A5FA, #2563EB);"></div>
<!-- 内容区 -->
<div class="px-5 py-4">
<!-- 第一行:运单号 + 状态徽标 -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></div>
<span class="text-[20px] font-bold text-gray-900 tracking-wider">781333617</span>
</div>
<span class="text-[11px] font-medium text-blue-600 bg-blue-50 px-2.5 py-1 rounded-full border border-blue-100">国际出港</span>
</div>
<!-- 分割线 -->
<div class="border-t border-gray-50 mb-3"></div>
<!-- 统一信息区域:浅灰底圆角容器,包含预配 -->
<div class="bg-gray-50/70 rounded-xl px-4 py-3">
<!-- 预配信息三列 -->
<div class="grid grid-cols-3 gap-2">
<div class="text-center">
<p class="text-[10px] text-gray-400 mb-0.5">预配件数</p>
<p class="text-[15px] font-bold text-gray-800">85</p>
</div>
<div class="text-center">
<p class="text-[10px] text-gray-400 mb-0.5">预配重量</p>
<p class="text-[15px] font-bold text-gray-800">156 kg</p>
</div>
<div class="text-center">
<p class="text-[10px] text-gray-400 mb-0.5">预配体积</p>
<p class="text-[15px] font-bold text-gray-800">4.52 m³</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== 统一表单大卡片(字体样式统一为APP风格) ===== -->
<div class="mx-4 mt-3 bg-white rounded-2xl shadow-sm shadow-black/5 overflow-hidden card-animate">
<!-- 1. 通道号(必填)-->
<div class="flex items-center h-12 px-4 border-b border-gray-100 cursor-pointer active:bg-gray-50" onclick="openChannelPicker()">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">通道号<span class="text-red-500 ml-0.5">*</span></span>
<span class="flex-1 text-[14px] text-gray-500 text-left" id="channelDisplay">请选择通道号</span>
<iconify-icon class="text-gray-400 shrink-0" icon="mdi:chevron-right"></iconify-icon>
</div>
<!-- 2. 代理人 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100 cursor-pointer active:bg-gray-50" onclick="openAgentPicker()">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">代理人</span>
<span class="flex-1 text-[14px] text-gray-500 text-left" id="agentDisplay">请选择代理人</span>
<iconify-icon class="text-gray-400 shrink-0" icon="mdi:chevron-right"></iconify-icon>
</div>
<!-- 3. 特码 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100 cursor-pointer active:bg-gray-50" onclick="openCodePicker()">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">特码</span>
<span class="flex-1 text-[14px] text-gray-500 text-left" id="codeDisplay">请选择特码</span>
<iconify-icon class="text-gray-400 shrink-0" icon="mdi:chevron-right"></iconify-icon>
</div>
<!-- 4. 运抵件数 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">运抵件数</span>
<input class="flex-1 text-[14px] text-gray-800 bg-transparent border-none outline-none text-left" placeholder="请输入运抵件数" type="text"/>
</div>
<!-- 5. 运抵重量 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">运抵重量</span>
<input class="flex-1 text-[14px] text-gray-800 bg-transparent border-none outline-none text-left" type="text" value="0"/>
</div>
<!-- 6. 运抵体积 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">运抵体积</span>
<input class="flex-1 text-[14px] text-gray-800 bg-transparent border-none outline-none text-left" type="text" value="0.00"/>
</div>
<!-- 7. 托盘数量 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">托盘数量</span>
<input class="flex-1 text-[14px] text-gray-800 bg-transparent border-none outline-none text-left" placeholder="请输入托盘数量" type="text"/>
</div>
<!-- 8. 托盘自重 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">托盘自重</span>
<input class="flex-1 text-[14px] text-gray-800 bg-transparent border-none outline-none text-left" value="18" placeholder="请输入托盘自重" type="text"/>
</div>
<!-- 航班信息 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">航班日期</span>
<span class="flex-1 text-[14px] text-gray-800 text-left">20260703</span>
</div>
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">航班号</span>
<span class="flex-1 text-[14px] text-gray-800 text-left">MU1111</span>
</div>
<div class="flex items-center h-12 px-4 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">航程</span>
<span class="flex-1 text-[14px] text-gray-800 text-left">HFE-LAX</span>
</div>
<!-- 业务类型 -->
<div class="flex items-center h-12 px-4 border-b border-gray-100 cursor-pointer active:bg-gray-50" onclick="openBizPicker()">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">业务类型</span>
<span class="flex-1 text-[14px] text-gray-800 text-left" id="bizDisplay">普通货物运输</span>
<iconify-icon class="text-gray-400 shrink-0" icon="mdi:chevron-right"></iconify-icon>
</div>
<!-- 备注 -->
<div class="flex items-start h-auto min-h-[80px] px-4 py-3 border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0 pt-1">备注</span>
<textarea class="flex-1 text-[14px] text-gray-800 bg-transparent rounded-lg py-2 h-[60px] resize-none outline-none text-left" placeholder="请输入备注信息"></textarea>
</div>
</div>
<!-- 底部按钮 -->
<div class="px-4 mt-4 mb-2">
<div class="flex gap-3">
<button class="flex-1 h-[52px] rounded-2xl text-[17px] font-semibold active:opacity-90 transition-all text-blue-600" style="background: #fff; border: 2px solid #2563EB;">
分托计重
</button>
<button class="flex-1 h-[52px] rounded-2xl bg-[#2563EB] text-white text-[17px] font-semibold active:opacity-90 shadow-lg shadow-blue-200 transition-all" onclick="submitWeight()">
完成计重
</button>
</div>
</div>
</main>
<!-- iOS 底部指示条 -->
<div class="h-5 bg-[#F2F5F9] flex justify-center items-center flex-none">
<div class="w-[134px] h-[5px] bg-gray-300 rounded-full"></div>
</div>
</div>
</div>
<script>
function openChannelPicker() {
const options = ['A-01', 'A-02', 'B-01', 'B-02', 'C-01'];
document.getElementById('channelDisplay').innerText = "A-01";
document.getElementById('channelDisplay').classList.remove('text-gray-500');
document.getElementById('channelDisplay').classList.add('text-gray-800');
}
function openAgentPicker() {
document.getElementById('agentDisplay').innerText = "上海中远海运物流";
document.getElementById('agentDisplay').classList.remove('text-gray-500');
document.getElementById('agentDisplay').classList.add('text-gray-800');
}
function openCodePicker() {
document.getElementById('codeDisplay').innerText = "GEN (普货)";
document.getElementById('codeDisplay').classList.remove('text-gray-500');
document.getElementById('codeDisplay').classList.add('text-gray-800');
}
function openBizPicker() {
}
function submitWeight() {
alert("提交成功");
}
</script>
</body>
</html>

View File

@@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" 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>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #F3F4F6;
font-size: 13px;
}
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.btn-active:active {
opacity: 0.7;
}
</style>
</head>
<body class="bg-gray-200 min-h-screen flex items-center justify-center p-4">
<div class="relative w-[390px] h-[844px] bg-[#1a1a1a] rounded-[55px] shadow-2xl border-[6px] border-[#333] flex flex-col overflow-hidden">
<div class="flex-1 bg-white mt-0 mb-1 mx-1 rounded-[46px] overflow-hidden flex flex-col relative">
<!-- 状态栏 -->
<div class="bg-[#2563EB] text-white h-11 px-7 flex items-end pb-2 justify-between text-[14px] font-medium z-50">
<span>09:41</span>
<div class="flex items-center gap-1.5 pb-0.5">
<iconify-icon icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon icon="mdi:wifi"></iconify-icon>
<iconify-icon icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 列表页 -->
<div id="listPage" class="flex-1 flex flex-col overflow-hidden relative bg-gray-100">
<header class="relative bg-[#2563EB] text-white flex items-center h-12 px-3 sticky top-0 z-50">
<div class="flex items-center btn-active" onclick="window.location.href='records.html'">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-medium text-white -ml-1">返回</span>
</div>
<h1 class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-white">计重明细</h1>
</header>
<div class="flex-1 overflow-y-auto p-4 no-scrollbar">
<!-- 运单卡片(与 781333617 样式统一) -->
<div class="bg-white rounded-2xl shadow-md shadow-black/5 overflow-hidden relative mb-3">
<!-- 顶部渐变装饰条 -->
<div class="h-1" style="background: linear-gradient(90deg, #2563EB, #60A5FA, #2563EB);"></div>
<!-- 内容区 -->
<div class="px-5 py-4">
<!-- 第一行:运单号 + 状态徽标 -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></div>
<span class="text-[20px] font-bold text-gray-900 tracking-wider">78133363396</span>
</div>
<span class="text-[11px] font-medium text-blue-600 bg-blue-50 px-2.5 py-1 rounded-full border border-blue-100">国际出港</span>
</div>
<!-- 分割线 -->
<div class="border-t border-gray-50 mb-3"></div>
<!-- 统一信息区域:浅灰底圆角容器 -->
<div class="bg-gray-50/70 rounded-xl px-4 py-3">
<div class="grid grid-cols-3 gap-2 text-center">
<div>
<p class="text-[10px] text-gray-400 mb-0.5">总件数</p>
<p class="text-[15px] font-bold text-gray-800">85</p>
</div>
<div>
<p class="text-[10px] text-gray-400 mb-0.5">总重量(kg)</p>
<p class="text-[15px] font-bold text-gray-800">1560</p>
</div>
<div>
<p class="text-[10px] text-gray-400 mb-0.5">托盘总重(kg)</p>
<p class="text-[15px] font-bold text-gray-800">54</p>
</div>
</div>
</div>
</div>
</div>
<!-- 左滑卡片1 -->
<div class="swipe-item bg-white rounded-xl shadow-sm mb-3 overflow-hidden relative" data-id="1">
<div class="swipe-content p-4 relative z-10 bg-white">
<div class="flex justify-between py-2">
<span class="text-gray-600">运抵件数</span>
<span class="font-medium piece">20</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-600">运抵重量(kg)</span>
<span class="font-medium weight">320</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">托盘自重(kg)</span>
<span class="text-gray-600">18</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">计重人</span>
<span class="text-gray-600 user">张三</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">计重时间</span>
<span class="text-gray-600 time">2026-07-05 10:02:15</span>
</div>
</div>
<div class="swipe-action absolute top-0 right-0 bottom-0 w-[60px] bg-[#2563EB] flex items-center justify-center z-0">
<button class="text-white text-sm" onclick="goEdit(1)">修改</button>
</div>
</div>
<!-- 卡片2 -->
<div class="swipe-item bg-white rounded-xl shadow-sm mb-3 overflow-hidden relative" data-id="2">
<div class="swipe-content p-4 relative z-10 bg-white">
<div class="flex justify-between py-2">
<span class="text-gray-600">运抵件数</span>
<span class="font-medium piece">35</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-600">运抵重量(kg)</span>
<span class="font-medium weight">590</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">托盘自重(kg)</span>
<span class="text-gray-600">18</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">计重人</span>
<span class="text-gray-600 user">张三</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">计重时间</span>
<span class="text-gray-600 time">2026-07-05 10:25:30</span>
</div>
</div>
<div class="swipe-action absolute top-0 right-0 bottom-0 w-[60px] bg-[#2563EB] flex items-center justify-center z-0">
<button class="text-white text-sm" onclick="goEdit(2)">修改</button>
</div>
</div>
<!-- 卡片3 -->
<div class="swipe-item bg-white rounded-xl shadow-sm mb-3 overflow-hidden relative" data-id="3">
<div class="swipe-content p-4 relative z-10 bg-white">
<div class="flex justify-between py-2">
<span class="text-gray-600">运抵件数</span>
<span class="font-medium piece">30</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-600">运抵重量(kg)</span>
<span class="font-medium weight">650</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">托盘自重(kg)</span>
<span class="text-gray-600">18</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">计重人</span>
<span class="text-gray-600 user">张三</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">计重时间</span>
<span class="text-gray-600 time">2026-07-05 11:45:08</span>
</div>
</div>
<div class="swipe-action absolute top-0 right-0 bottom-0 w-[60px] bg-[#2563EB] flex items-center justify-center z-0">
<button class="text-white text-sm" onclick="goEdit(3)">修改</button>
</div>
</div>
</div>
</div>
<!-- 修改记录页面(完全在手机容器内) -->
<div id="editPage" class="absolute inset-0 bg-gray-100 z-[60] flex flex-col hidden">
<div class="bg-[#2563EB] text-white h-11 px-7 flex items-end pb-2 justify-between text-[14px] font-medium z-50 flex-none">
<span>09:41</span>
<div class="flex items-center gap-1.5 pb-0.5">
<iconify-icon icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon icon="mdi:wifi"></iconify-icon>
<iconify-icon icon="mdi:battery"></iconify-icon>
</div>
</div>
<header class="relative bg-[#2563EB] text-white flex items-center h-12 px-3 flex-none">
<div class="flex items-center btn-active" onclick="closeEditPage()">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-medium text-white -ml-1">返回</span>
</div>
<h1 class="absolute left-1/2 -translate-x-1/2 text-lg font-bold text-white">修改记录</h1>
</header>
<div class="flex-1 overflow-y-auto p-4 no-scrollbar">
<div class="bg-white rounded-xl shadow-sm overflow-hidden px-4">
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">运抵件数<span class="text-red-500 ml-0.5">*</span></span>
<input id="edit_piece" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]">
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">运抵重量(kg)<span class="text-red-500 ml-0.5">*</span></span>
<input id="edit_weight" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]">
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">托盘自重(kg)<span class="text-red-500 ml-0.5">*</span></span>
<input value="18" class="flex-1 bg-transparent outline-none text-gray-800 text-[14px]">
</div>
<div class="flex items-center py-[14px] border-b border-gray-100">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">计重人</span>
<input id="edit_user" disabled class="flex-1 bg-transparent outline-none text-gray-500 text-[14px]">
</div>
<div class="flex items-center py-[14px]">
<span class="w-[90px] text-[13px] font-medium text-[#6B7280] shrink-0">计重时间</span>
<input id="edit_time" disabled class="flex-1 bg-transparent outline-none text-gray-500 text-[14px]">
</div>
</div>
<button class="w-full mt-8 bg-[#2563EB] text-white py-3.5 rounded-[10px] font-bold text-[16px] shadow-lg shadow-blue-500/30 btn-active" onclick="saveEdit()">保存</button>
</div>
</div>
</div>
</div>
<script>
let currentEditId = null
// 左滑效果(鼠标+触摸)
document.querySelectorAll('.swipe-item').forEach(item => {
const content = item.querySelector('.swipe-content')
let startX = 0
let isDragging = false
item.addEventListener('mousedown', e => {
isDragging = true
startX = e.clientX
e.preventDefault()
})
window.addEventListener('mousemove', e => {
if (!isDragging) return
const dx = startX - e.clientX
if (dx > 0) {
const off = Math.min(dx, 60)
content.style.transform = `translateX(-${off}px)`
}
})
window.addEventListener('mouseup', () => {
if (!isDragging) return
isDragging = false
const dx = startX - event.clientX
if (dx > 30) {
content.style.transform = 'translateX(-60px)'
} else {
content.style.transform = ''
}
})
item.addEventListener('touchstart', e => {
startX = e.touches[0].clientX
})
item.addEventListener('touchmove', e => {
const dx = startX - e.touches[0].clientX
if (dx > 0) {
const off = Math.min(dx, 60)
content.style.transform = `translateX(-${off}px)`
}
})
item.addEventListener('touchend', () => {
const dx = startX - event.changedTouches[0].clientX
content.style.transform = dx > 30 ? 'translateX(-60px)' : ''
})
})
function goEdit(id) {
currentEditId = id
const item = document.querySelector(`.swipe-item[data-id="${id}"]`)
document.getElementById('edit_piece').value = item.querySelector('.piece').innerText
document.getElementById('edit_weight').value = item.querySelector('.weight').innerText
document.getElementById('edit_user').value = item.querySelector('.user').innerText
document.getElementById('edit_time').value = item.querySelector('.time').innerText
document.getElementById('listPage').classList.add('hidden')
document.getElementById('editPage').classList.remove('hidden')
}
function closeEditPage() {
document.getElementById('editPage').classList.add('hidden')
document.getElementById('listPage').classList.remove('hidden')
}
function saveEdit() {
const p = document.getElementById('edit_piece').value
const w = document.getElementById('edit_weight').value
const item = document.querySelector(`.swipe-item[data-id="${currentEditId}"]`)
item.querySelector('.piece').innerText = p
item.querySelector('.weight').innerText = w
closeEditPage()
}
</script>
</body>
</html>

View File

@@ -0,0 +1,535 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>出港运抵</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Iconify -->
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"></script>
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
body {
background-color: #f3f6f8;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, 10px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
</style>
</head>
<body class="bg-gray-100">
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] overflow-hidden flex flex-col relative rounded-[48px] border-[10px] border-gray-900">
<div class="bg-[#2563EB] flex-none">
<div class="h-[44px] flex justify-between items-center px-8 text-white">
<span class="text-[15px] font-semibold">09:41</span>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-lg" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-lg" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-xl" icon="mdi:battery"></iconify-icon>
</div>
</div>
<div class="h-[44px] flex items-center px-4 relative">
<div class="flex items-center text-white cursor-pointer" onclick="window.history.back()">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span class="text-sm">返回</span>
</div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<h1 class="text-white text-lg font-bold">出港运抵</h1>
</div>
<div class="ml-auto w-10"></div>
</div>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar">
<section class="px-4 pt-4 pb-3 bg-white">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索运单号" type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="showFilterSheet2()">
<iconify-icon class="text-lg" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<div class="flex bg-white border-b sticky top-0 z-20">
<button class="tab-btn flex-1 py-3 text-sm font-bold border-b-2 transition-all text-[#1F2937] border-[#1F2937]" id="tab-pending" onclick="switchTab('pending')">
待运抵 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5" id="count-pending">1</span>
</button>
<button class="tab-btn flex-1 py-3 text-sm font-bold border-b-2 transition-all text-gray-400 border-transparent" id="tab-shipped" onclick="switchTab('shipped')">
已运抵 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5" id="count-shipped">4</span>
</button>
</div>
<div class="px-5 pb-5 pt-3">
<div class="flex flex-col items-center justify-center py-16 text-gray-400 hidden" id="empty-pending">
<svg class="w-16 h-16 text-gray-300 mb-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-sm">暂无待运抵的运单</p>
</div>
<!-- 待运抵卡片(长按显示补充按钮) -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-4 uld-card relative" data-status="pending">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="radio-btn border-2 border-gray-300 rounded-full w-5 h-5 inline-flex items-center justify-center flex-none cursor-pointer"></span>
<div class="flex items-baseline">
<span class="text-sm font-bold text-gray-900">78133363396</span>
</div>
</div>
<div class="flex items-center gap-2">
<span class="bg-red-500 text-white text-[11px] font-medium px-2.5 py-0.5 rounded-full">提前运抵</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-gray-500 text-[13px] mt-3">
<div><span>代理:</span><span>马道</span></div>
<div><span>件数:</span><span>444</span></div>
<div><span>重量:</span><span>150kg</span></div>
<div><span>航班日期:</span><span>20260713</span></div>
<div><span>航班号:</span><span>MU2033</span></div>
<div><span>航程:</span><span>HFE-LAX</span></div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center gap-2" onclick="event.stopPropagation(); window.location.href='suborder.html'">
查看分单
<span class="text-[#2563EB] text-[10px] font-bold">3</span>
</button>
</div>
<!-- 长按弹出操作层 -->
<div class="card-action-overlay absolute inset-0 bg-black/40 rounded-2xl z-10 hidden flex items-center justify-center pointer-events-none">
<div class="flex items-center gap-4 pointer-events-auto shipped-actions hidden">
<button class="bg-amber-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-amber-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); setTimeout(() => showResetStatusSheet(), 200)">重置</button>
<button class="bg-emerald-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-emerald-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); window.location.href='receipt.html'">回执</button>
</div>
</div>
</div>
<!-- 已运抵卡片1 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-4 uld-card relative" data-status="shipped" style="display: none;">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-gray-900">78133363397</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-indigo-100 text-indigo-500 text-[11px] font-medium px-2.5 py-0.5 rounded-full">W</span>
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2 py-0.5 rounded-md">11</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-gray-500 text-[13px] mt-3">
<div><span>代理:</span><span>徽远</span></div>
<div><span>件数:</span><span>300</span></div>
<div><span>重量:</span><span>120kg</span></div>
<div><span>航班日期:</span><span>20260713</span></div>
<div><span>航班号:</span><span>MU2033</span></div>
<div><span>航程:</span><span>HFE-LAX</span></div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center gap-2" onclick="event.stopPropagation(); window.location.href='suborder.html'">
查看分单
<span class="text-[#2563EB] text-[10px] font-bold">2</span>
</button>
</div>
<div class="card-action-overlay absolute inset-0 bg-black/40 rounded-2xl z-10 hidden flex items-center justify-center pointer-events-none">
<div class="flex items-center gap-4 pointer-events-auto pending-actions hidden">
<button class="bg-purple-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); window.location.href='supplement.html'">补充</button>
</div>
<div class="flex items-center gap-4 pointer-events-auto shipped-actions hidden">
<button class="bg-amber-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-amber-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); setTimeout(() => showResetStatusSheet(), 200)">重置</button>
<button class="bg-emerald-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-emerald-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); window.location.href='receipt.html'">回执</button>
</div>
</div>
</div>
<!-- 已运抵卡片2 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-4 uld-card relative" data-status="shipped" style="display: none;">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-gray-900">78133363398</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2.5 py-0.5 rounded-full">01</span>
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2 py-0.5 rounded-md">11</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-gray-500 text-[13px] mt-3">
<div><span>代理:</span><span>中外运</span></div>
<div><span>件数:</span><span>130</span></div>
<div><span>重量:</span><span>130kg</span></div>
<div><span>航班日期:</span><span>20260713</span></div>
<div><span>航班号:</span><span>MU2033</span></div>
<div><span>航程:</span><span>HFE-LAX</span></div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center gap-2" onclick="event.stopPropagation(); window.location.href='suborder.html'">
查看分单
<span class="text-[#2563EB] text-[10px] font-bold">2</span>
</button>
</div>
<div class="card-action-overlay absolute inset-0 bg-black/40 rounded-2xl z-10 hidden flex items-center justify-center pointer-events-none">
<div class="flex items-center gap-4 pointer-events-auto pending-actions hidden">
<button class="bg-purple-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); window.location.href='supplement.html'">补充</button>
</div>
<div class="flex items-center gap-4 pointer-events-auto shipped-actions hidden">
<button class="bg-amber-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-amber-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); setTimeout(() => showResetStatusSheet(), 200)">重置</button>
<button class="bg-emerald-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-emerald-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); window.location.href='receipt.html'">回执</button>
</div>
</div>
</div>
<!-- 已运抵卡片3:78133363399 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-4 uld-card relative" data-status="shipped" style="display: none;">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-gray-900">78133363399</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-amber-100 text-amber-600 text-[11px] font-medium px-2.5 py-0.5 rounded-full">E</span>
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2 py-0.5 rounded-md">11</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-gray-500 text-[13px] mt-3">
<div><span>代理:</span><span>MU</span></div>
<div><span>件数:</span><span>300</span></div>
<div><span>重量:</span><span>110kg</span></div>
<div><span>航班日期:</span><span>20260713</span></div>
<div><span>航班号:</span><span>MU2034</span></div>
<div><span>航程:</span><span>HFE-LAX</span></div>
</div>
<div class="mt-2">
<button class="w-full bg-[#EFF6FF] text-[#2563EB] text-xs py-2 rounded-[6px] font-medium h-8 flex items-center justify-center gap-2" onclick="event.stopPropagation(); window.location.href='https://modao.cc/agent-py/workspace/6a50a1993333bc5bbd8ee054/waybill-arrival.html/suborder.html?render_html=true'">
查看分单
<span class="text-[#2563EB] text-[10px] font-bold">1</span>
</button>
</div>
<!-- 卡片操作菜单(默认隐藏) -->
<div class="card-action-overlay absolute inset-0 bg-black/40 rounded-2xl z-10 hidden flex items-center justify-center pointer-events-none">
<!-- 已运抵按钮容器(显示重置+回执) -->
<div class="flex items-center gap-4 pointer-events-auto shipped-actions hidden">
<button class="bg-amber-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-amber-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); setTimeout(() => showResetStatusSheet(), 200)">
重置
</button>
<button class="bg-emerald-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-emerald-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); window.location.href='https://modao.cc/agent-py/workspace/6a50a1993333bc5bbd8ee054/waybill-arrival.html/receipt.html?render_html=true'">
回执
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 底部操作栏 -->
<div class="flex-none bg-white border-t border-gray-100" id="bottom-action-bar">
<div class="flex items-center justify-between px-5 py-3">
<div class="flex items-center gap-2 select-all cursor-pointer">
<span class="radio-select-all w-5 h-5 rounded-full border-2 border-gray-300 bg-white flex items-center justify-center"></span>
<span class="text-sm text-gray-700 font-medium">全选</span>
</div>
<div class="flex items-center gap-3 bottom-action-container">
<span class="text-[12px] text-[#9CA3AF]">已选 <span id="selected-count">0</span> 项</span>
<button class="bg-[#2563EB] text-white text-[15px] font-bold px-5 py-2 rounded-[10px] shadow-lg shadow-blue-500/25" onclick="alert('运抵申报')">申报</button>
</div>
</div>
<div class="pb-2 flex justify-center">
<div class="w-32 h-1.5 bg-gray-200 rounded-full"></div>
</div>
</div>
<!-- 筛选弹窗 -->
<div class="absolute inset-0 bg-black/40 z-50 hidden" id="filter-sheet2-overlay" onclick="hideFilterSheet2()"></div>
<div class="absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl shadow-2xl z-50 transform translate-y-full transition-transform duration-300 ease-in-out" id="filter-sheet2">
<div class="px-5 pt-5 pb-8">
<div class="flex items-center justify-center mb-6 relative">
<h3 class="text-base font-bold text-gray-900">筛选条件</h3>
<button class="absolute right-0 w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center" onclick="hideFilterSheet2()">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
<line x1="18" x2="6" y1="6" y2="18"></line>
<line x1="6" x2="18" y1="6" y2="18"></line>
</svg>
</button>
</div>
<div class="space-y-4">
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">航班日期</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:calendar"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none px-2" placeholder="选择日期" type="text" value="2026-07-10"/>
</div>
</div>
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">航班号</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入航班号" type="text"/>
</div>
</div>
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">代理</label>
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%239CA3AF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')] bg-[length:14px] bg-[right_12px_center] bg-no-repeat">
<option disabled selected value="">请选择代理</option>
<option value="马道">马道</option>
<option value="徽远">徽远</option>
<option value="中外运">中外运</option>
<option value="MU">MU</option>
</select>
</div>
</div>
<div class="flex gap-3 mt-6">
<button class="flex-1 py-3 text-sm font-medium text-gray-500 bg-gray-100 rounded-xl" onclick="hideFilterSheet2()">重置</button>
<button class="flex-1 py-3 text-sm font-medium text-white bg-[#2563EB] rounded-xl" onclick="hideFilterSheet2()">确定</button>
</div>
</div>
</div>
<!-- 重置状态弹窗 -->
<div class="absolute inset-0 bg-black/40 z-50 hidden" id="reset-sheet-overlay" onclick="hideResetStatusSheet()"></div>
<div class="absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl shadow-2xl z-50 transform translate-y-full transition-transform duration-300 ease-in-out" id="reset-sheet">
<div class="px-5 pt-5 pb-8">
<div class="flex items-center justify-center mb-6 relative">
<h3 class="text-base font-bold text-gray-900">重置状态</h3>
<button class="absolute right-0 w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center" onclick="hideResetStatusSheet()">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
<line x1="18" x2="6" y1="6" y2="18"></line>
<line x1="6" x2="18" y1="6" y2="18"></line>
</svg>
</button>
</div>
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">状态</label>
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%239CA3AF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')] bg-[length:14px] bg-[right_12px_center] bg-no-repeat" id="reset-status-select">
<option disabled selected value="">请选择状态</option>
<option value="">空</option>
<option value="01">01</option>
</select>
</div>
<button class="w-full mt-6 py-3 text-sm font-medium text-white bg-[#2563EB] rounded-xl" onclick="confirmResetStatus()">确定</button>
</div>
</div>
</div>
<script>
let currentTab = 'pending';
let longPressTimer = null;
let isLongPress = false;
// Toast提示
function showToast(message) {
const existingToast = document.querySelector('.custom-toast');
if (existingToast) existingToast.remove();
const toast = document.createElement('div');
toast.className = 'custom-toast fixed bottom-32 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-sm px-5 py-3 rounded-xl shadow-lg z-50 animate-fadeIn';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
// 清空所有选中
function resetAllSelections() {
document.querySelectorAll('.radio-btn').forEach(radio => toggleRadio(radio, false));
const selectAllRadio = document.querySelector('.radio-select-all');
toggleRadio(selectAllRadio, false);
}
// 切换标签页
function switchTab(tab) {
currentTab = tab;
const tabPending = document.getElementById('tab-pending');
const tabShipped = document.getElementById('tab-shipped');
const bottomBar = document.getElementById('bottom-action-bar');
const emptyTip = document.getElementById('empty-pending');
// 切换tab样式
if (tab === 'pending') {
tabPending.classList.remove('text-gray-400', 'border-transparent');
tabPending.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabShipped.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
tabShipped.classList.add('text-gray-400', 'border-transparent');
bottomBar.classList.remove('hidden');
} else {
tabPending.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
tabPending.classList.add('text-gray-400', 'border-transparent');
tabShipped.classList.remove('text-gray-400', 'border-transparent');
tabShipped.classList.add('text-[#1F2937]', 'border-[#1F2937]');
bottomBar.classList.add('hidden');
}
// 切换卡片显示
const allCards = document.querySelectorAll('.uld-card');
let showCount = 0;
allCards.forEach(card => {
if (card.dataset.status === tab) {
card.style.display = 'block';
showCount++;
} else {
card.style.display = 'none';
}
});
emptyTip.classList.toggle('hidden', showCount > 0);
resetAllSelections();
updateSelectCount();
}
// 更新选中数量
function updateSelectCount() {
const list = document.querySelectorAll(`.uld-card[data-status="${currentTab}"] .radio-btn.bg-blue-500`);
document.getElementById('selected-count').textContent = list.length;
syncSelectAllStatus();
}
// 同步全选按钮样式
function syncSelectAllStatus() {
const allCards = document.querySelectorAll(`.uld-card[data-status="${currentTab}"]`);
const selectedCards = document.querySelectorAll(`.uld-card[data-status="${currentTab}"] .radio-btn.bg-blue-500`);
const selectAllBtn = document.querySelector('.radio-select-all');
if (allCards.length > 0 && allCards.length === selectedCards.length) {
selectAllBtn.classList.add('bg-blue-500', 'border-blue-500');
selectAllBtn.classList.remove('border-gray-300', 'bg-white');
selectAllBtn.innerHTML = `<svg class="w-3 h-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
} else {
selectAllBtn.classList.remove('bg-blue-500', 'border-blue-500');
selectAllBtn.classList.add('border-gray-300', 'bg-white');
selectAllBtn.innerHTML = '';
}
}
// 单选按钮切换
function toggleRadio(el, force) {
const isChecked = force != null ? force : !el.classList.contains('bg-blue-500');
if (isChecked) {
el.classList.add('bg-blue-500', 'border-blue-500');
el.classList.remove('border-gray-300', 'bg-white');
el.innerHTML = `<svg class="w-3 h-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
} else {
el.classList.remove('bg-blue-500', 'border-blue-500');
el.classList.add('border-gray-300', 'bg-white');
el.innerHTML = '';
}
updateSelectCount();
}
// 单个单选点击
document.querySelectorAll('.radio-btn').forEach(radio => {
radio.onclick = (e) => {
e.stopPropagation();
toggleRadio(radio);
};
});
// 全选按钮
document.querySelector('.select-all').onclick = function() {
const currentCards = document.querySelectorAll(`.uld-card[data-status="${currentTab}"] .radio-btn`);
const allSelected = Array.from(currentCards).every(item => item.classList.contains('bg-blue-500'));
currentCards.forEach(radio => toggleRadio(radio, !allSelected));
};
// ========== 完整长按逻辑(待运抵弹出补充,已运抵弹出重置回执) ==========
document.querySelectorAll('.uld-card').forEach(card => {
// 触摸长按
card.addEventListener('touchstart', (e) => {
// 点击单选框、按钮不触发长按
if (e.target.closest('.radio-btn') || e.target.closest('button')) return;
isLongPress = false;
longPressTimer = setTimeout(() => {
isLongPress = true;
showCardAction(card);
}, 600);
});
card.addEventListener('touchend', () => clearTimeout(longPressTimer));
card.addEventListener('touchmove', () => clearTimeout(longPressTimer));
// 鼠标长按兼容
card.addEventListener('mousedown', (e) => {
if (e.target.closest('.radio-btn') || e.target.closest('button')) return;
isLongPress = false;
longPressTimer = setTimeout(() => {
isLongPress = true;
showCardAction(card);
}, 600);
});
card.addEventListener('mouseup', () => clearTimeout(longPressTimer));
card.addEventListener('mouseleave', () => clearTimeout(longPressTimer));
});
// 展示卡片操作面板
function showCardAction(card) {
// 关闭所有卡片操作层
document.querySelectorAll('.card-action-overlay').forEach(overlay => overlay.classList.add('hidden'));
const overlay = card.querySelector('.card-action-overlay');
const pendingAction = overlay.querySelector('.pending-actions');
const shippedAction = overlay.querySelector('.shipped-actions');
pendingAction.classList.add('hidden');
shippedAction.classList.add('hidden');
// 根据状态展示对应按钮
if (card.dataset.status === 'pending') {
pendingAction.classList.remove('hidden');
} else {
shippedAction.classList.remove('hidden');
}
overlay.classList.remove('hidden');
}
// 点击空白区域关闭操作弹窗
document.addEventListener('click', (e) => {
if (!e.target.closest('.uld-card') && !e.target.closest('.card-action-overlay')) {
document.querySelectorAll('.card-action-overlay').forEach(overlay => overlay.classList.add('hidden'));
}
});
// 筛选弹窗
function showFilterSheet2() {
const overlay = document.getElementById('filter-sheet2-overlay');
const sheet = document.getElementById('filter-sheet2');
overlay.classList.remove('hidden');
setTimeout(() => sheet.classList.remove('translate-y-full'), 10);
}
function hideFilterSheet2() {
const overlay = document.getElementById('filter-sheet2-overlay');
const sheet = document.getElementById('filter-sheet2');
sheet.classList.add('translate-y-full');
setTimeout(() => overlay.classList.add('hidden'), 300);
}
// 重置状态弹窗
function showResetStatusSheet() {
document.getElementById('reset-sheet-overlay').classList.remove('hidden');
document.getElementById('reset-sheet').classList.remove('translate-y-full');
}
function hideResetStatusSheet() {
document.getElementById('reset-sheet-overlay').classList.add('hidden');
document.getElementById('reset-sheet').classList.add('translate-y-full');
}
function confirmResetStatus() {
const selectVal = document.getElementById('reset-status-select').value;
if (!selectVal) {
alert('请选择状态');
return;
}
alert('已选择状态:' + (selectVal === '' ? '置空' : selectVal));
hideResetStatusSheet();
}
// 页面初始化
switchTab('pending');
</script>
</body>
</html>

View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" 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>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center font-sans">
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] rounded-[48px] border-[10px] border-gray-900 overflow-hidden relative flex flex-col shadow-2xl">
<!-- 状态栏 -->
<div class="bg-blue-600 text-white text-xs px-6 py-1 flex justify-between items-center shrink-0 h-[44px]">
<span class="text-[15px] font-semibold">09:41</span>
<div class="flex gap-1.5 items-center">
<iconify-icon class="text-lg" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-lg" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-xl" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="bg-blue-600 px-4 py-3 flex items-center gap-2 text-white shrink-0 h-[44px]">
<a class="text-white text-sm flex items-center gap-1" href="main.html">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
返回
</a>
<span class="flex-1 text-center text-white font-bold text-base">回执内容</span>
<div class="w-12"></div>
</div>
<!-- 内容列表 -->
<div class="p-4 space-y-3 flex-1 overflow-y-auto no-scrollbar">
<!-- 回执 1 -->
<div class="bg-white rounded-lg p-4 shadow-sm">
<p class="text-gray-700 text-sm leading-relaxed">
<span class="inline-block w-2 h-2 bg-blue-500 rounded-full mr-2.5 align-middle"></span>
单一窗口回执内容:45201 空运出口运抵报告传输成功。比对结果为:运抵正常。
</p>
<p class="text-gray-400 text-xs mt-2 ml-[18px]">2026-07-10 11:03:38</p>
</div>
<!-- 回执 2 -->
<div class="bg-white rounded-lg p-4 shadow-sm">
<p class="text-gray-700 text-sm leading-relaxed">
<span class="inline-block w-2 h-2 bg-blue-500 rounded-full mr-2.5 align-middle"></span>
单一窗口回执内容:45202 空运出口运抵报告传输成功。比对结果为:运抵正常。
</p>
<p class="text-gray-400 text-xs mt-2 ml-[18px]">2026-07-10 11:02:15</p>
</div>
<!-- 回执 3 -->
<div class="bg-white rounded-lg p-4 shadow-sm">
<p class="text-gray-700 text-sm leading-relaxed">
<span class="inline-block w-2 h-2 bg-blue-500 rounded-full mr-2.5 align-middle"></span>
单一窗口回执内容:45203 空运出口运抵报告传输成功。比对结果为:运抵正常。
</p>
<p class="text-gray-400 text-xs mt-2 ml-[18px]">2026-07-10 10:58:42</p>
</div>
</div>
<!-- Home Indicator -->
<div class="h-8 w-full flex items-center justify-center shrink-0">
<div class="w-32 h-1.5 bg-gray-300 rounded-full"></div>
</div>
</div>
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</body>
</html>

View File

@@ -0,0 +1,413 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>分单明细 - 出港运抵</title>
<!-- Tailwind CSS -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.phone-shell {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
</style>
</head>
<body class="bg-gray-200 h-screen flex items-center justify-center font-sans">
<!-- 手机外壳容器 -->
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] rounded-[48px] border-[10px] border-gray-900 overflow-hidden flex flex-col relative">
<!-- 顶部蓝色背景区域 -->
<div class="bg-[#1A73E8] flex-none">
<!-- 状态栏 -->
<div class="h-11 w-full flex justify-between items-center px-8 text-white">
<span class="text-sm font-medium">09:41</span>
<div class="flex items-center gap-1.5">
<iconify-icon icon="mingcute:signal-fill" width="16"></iconify-icon>
<iconify-icon icon="mingcute:wifi-line" width="16"></iconify-icon>
<iconify-icon class="rotate-90" icon="mingcute:battery-fill" width="18"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="h-[44px] px-4 flex items-center justify-between text-white">
<button class="flex items-center gap-1 text-sm" onclick="window.location.href='main.html'">
<iconify-icon icon="lucide:chevron-left" width="20"></iconify-icon>
<span>返回</span>
</button>
<h1 class="text-lg font-bold">分单明细</h1>
<div class="w-10"></div> <!-- 占位 -->
</div>
</div>
<!-- 搜索栏 -->
<section class="px-5 pt-4 pb-3">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索运单号" type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="showFilterSheet()">
<iconify-icon class="text-lg" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- 可滚动内容区 -->
<div class="flex-1 overflow-y-auto no-scrollbar px-5 pb-24 pt-2">
<!-- 分单列表内容 -->
<div class="space-y-4" id="suborder-list">
<!-- 分单1 -->
<div class="suborder-card bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-4 cursor-pointer transition-all relative active:scale-[0.98]" data-id="10001" onclick="toggleSuborderSelect(this)">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="select-indicator border-2 border-gray-300 bg-white rounded-full w-5 h-5 inline-flex items-center justify-center flex-none cursor-pointer transition-colors"></span>
<span class="text-sm font-bold text-gray-900">10001</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-indigo-100 text-indigo-500 text-[11px] font-medium px-2.5 py-0.5 rounded-full">W</span>
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2 py-0.5 rounded-md">11</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-gray-500 text-[13px] mt-3">
<div>
<span>件数:</span><span>150</span>
</div>
<div>
<span>重量:</span><span>50kg</span>
</div>
</div>
<!-- 卡片操作菜单(默认隐藏) -->
<div class="card-action-overlay absolute inset-0 bg-black/40 rounded-2xl z-10 hidden flex items-center justify-center pointer-events-none">
<div class="flex items-center gap-4 pointer-events-auto">
<!-- 重置按钮 -->
<button class="bg-amber-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-amber-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); setTimeout(() => showResetStatusSheet(), 200)">
重置
</button>
<!-- 回执按钮 -->
<button class="bg-emerald-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-emerald-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); window.location.href='receipt.html'">
回执
</button>
</div>
</div>
</div>
<!-- 分单2 -->
<div class="suborder-card bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-4 cursor-pointer transition-all relative active:scale-[0.98]" data-id="10002" onclick="toggleSuborderSelect(this)">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="select-indicator border-2 border-gray-300 bg-white rounded-full w-5 h-5 inline-flex items-center justify-center flex-none cursor-pointer transition-colors"></span>
<span class="text-sm font-bold text-gray-900">10002</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-amber-100 text-amber-600 text-[11px] font-medium px-2.5 py-0.5 rounded-full">E</span>
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2 py-0.5 rounded-md">11</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-gray-500 text-[13px] mt-3">
<div>
<span>件数:</span><span>120</span>
</div>
<div>
<span>重量:</span><span>40kg</span>
</div>
</div>
<!-- 卡片操作菜单(默认隐藏) -->
<div class="card-action-overlay absolute inset-0 bg-black/40 rounded-2xl z-10 hidden flex items-center justify-center pointer-events-none">
<div class="flex items-center gap-4 pointer-events-auto">
<!-- 重置按钮 -->
<button class="bg-amber-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-amber-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); setTimeout(() => showResetStatusSheet(), 200)">
重置
</button>
<!-- 回执按钮 -->
<button class="bg-emerald-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-emerald-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); window.location.href='receipt.html'">
回执
</button>
</div>
</div>
</div>
<!-- 分单3 -->
<div class="suborder-card bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-4 cursor-pointer transition-all relative active:scale-[0.98]" data-id="10003" onclick="toggleSuborderSelect(this)">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="select-indicator border-2 border-gray-300 bg-white rounded-full w-5 h-5 inline-flex items-center justify-center flex-none cursor-pointer transition-colors"></span>
<span class="text-sm font-bold text-gray-900">10003</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2.5 py-0.5 rounded-full">01</span>
<span class="bg-green-100 text-green-600 text-[11px] font-medium px-2 py-0.5 rounded-md">11</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-gray-500 text-[13px] mt-3">
<div>
<span>件数:</span><span>174</span>
</div>
<div>
<span>重量:</span><span>60kg</span>
</div>
</div>
<!-- 卡片操作菜单(默认隐藏) -->
<div class="card-action-overlay absolute inset-0 bg-black/40 rounded-2xl z-10 hidden flex items-center justify-center pointer-events-none">
<div class="flex items-center gap-4 pointer-events-auto">
<!-- 重置按钮 -->
<button class="bg-amber-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-amber-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); this.closest('.card-action-overlay').classList.add('hidden'); setTimeout(() => showResetStatusSheet(), 200)">
重置
</button>
<!-- 回执按钮 -->
<button class="bg-emerald-500 text-white text-[10px] font-bold w-14 h-14 rounded-full shadow-lg shadow-emerald-500/30 flex items-center justify-center leading-tight" onclick="event.stopPropagation(); window.location.href='receipt.html'">
回执
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 底部操作栏 -->
<div class="flex-none bg-white border-t border-gray-100">
<div class="flex items-center justify-between px-5 py-3">
<div class="flex items-center gap-2 select-all-container cursor-pointer" onclick="toggleSuborderSelectAll(this)">
<span class="radio-select-all w-5 h-5 rounded-full border-2 border-gray-300 bg-white flex items-center justify-center transition-colors"></span>
<span class="text-sm text-gray-700 font-medium">全选</span>
</div>
<div class="flex items-center gap-3">
<span class="text-[12px] text-[#9CA3AF]" id="suborder-count">已选 0 / 3 项</span>
<button class="bg-[#2563EB] text-white text-[15px] font-bold px-5 py-2 rounded-[10px] shadow-lg shadow-blue-500/25 active:scale-95 transition-transform" onclick="alert('申报成功')">申报</button>
</div>
</div>
<!-- 筛选弹框遮罩 -->
<div class="absolute inset-0 bg-black/40 z-30 hidden" id="filter-sheet-overlay" onclick="hideFilterSheet()"></div>
<!-- 筛选弹框 -->
<div class="absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl shadow-2xl z-40 transform translate-y-full transition-transform duration-300 ease-in-out" id="filter-sheet">
<div class="px-5 pt-5 pb-8">
<!-- 标题 -->
<div class="flex items-center justify-center mb-6 relative">
<h3 class="text-base font-bold text-gray-900">筛选条件</h3>
<button class="absolute right-0 w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center" onclick="hideFilterSheet()">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
<line x1="18" x2="6" y1="6" y2="18"></line>
<line x1="6" x2="18" y1="6" y2="18"></line>
</svg>
</button>
</div>
<!-- 筛选条件区域 -->
<div class="space-y-4">
<!-- 航班日期 -->
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">航班日期</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:calendar"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none px-2" placeholder="选择日期" type="text" value="2026-07-10"/>
</div>
</div>
<!-- 航班号 -->
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">航班号</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入航班号" type="text"/>
</div>
</div>
<!-- 代理(已改为下拉框) -->
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">代理</label>
<div class="relative">
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none focus:border-blue-500 focus:bg-white transition-all">
<option disabled selected value="">请选择代理</option>
<option value="马道">马道</option>
<option value="徽远">徽远</option>
<option value="中外运">中外运</option>
</select>
<iconify-icon class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none text-xl" icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<!-- 分单号 -->
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">分单号</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入分单号" type="text"/>
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="flex gap-3 mt-6">
<button class="flex-1 py-3 text-sm font-medium text-gray-500 bg-gray-100 rounded-xl" onclick="hideFilterSheet()">重置</button>
<button class="flex-1 py-3 text-sm font-medium text-white bg-[#2563EB] rounded-xl" onclick="hideFilterSheet()">确定</button>
</div>
</div>
</div>
<!-- Home Indicator 底部横条 -->
<div class="h-8 w-full flex justify-center items-center">
<div class="w-32 h-1.5 bg-black rounded-full opacity-20"></div>
</div>
</div>
<!-- 重置状态弹框 -->
<div class="absolute inset-0 bg-black/40 z-30 hidden" id="reset-sheet-overlay" onclick="hideResetStatusSheet()"></div>
<div class="absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl shadow-2xl z-40 transform translate-y-full transition-transform duration-300 ease-in-out" id="reset-sheet">
<div class="px-5 pt-5 pb-8">
<!-- 标题 -->
<div class="flex items-center justify-center mb-6 relative">
<h3 class="text-base font-bold text-gray-900">重置状态</h3>
<button class="absolute right-0 w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center" onclick="hideResetStatusSheet()">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
<line x1="18" x2="6" y1="6" y2="18"></line>
<line x1="6" x2="18" y1="6" y2="18"></line>
</svg>
</button>
</div>
<!-- 状态下拉选择 -->
<div>
<label class="text-sm text-gray-500 font-medium mb-1.5 block">状态</label>
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%202000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%239CA3AF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')] bg-[length:14px] bg-[right_12px_center] bg-no-repeat" id="reset-status-select">
<option disabled="" selected="" value="">请选择状态</option>
<option value="">空</option>
<option value="01">01</option>
</select>
</div>
<!-- 确认按钮 -->
<button class="w-full mt-6 py-3 text-sm font-medium text-white bg-[#2563EB] rounded-xl" onclick="confirmResetStatus()">
确定
</button>
</div>
</div>
</div>
<script>
// 单选分单卡片
function toggleSuborderSelect(card) {
if (window.isLongPressing) return;
card.classList.toggle('selected');
const indicator = card.querySelector('.select-indicator');
if (card.classList.contains('selected')) {
indicator.classList.remove('border-gray-300', 'bg-white');
indicator.classList.add('bg-blue-500', 'border-blue-500');
indicator.innerHTML = `<svg class="w-3 h-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
} else {
indicator.classList.remove('bg-blue-500', 'border-blue-500');
indicator.classList.add('border-gray-300', 'bg-white');
indicator.innerHTML = '';
}
updateSuborderCount();
}
// 全选/取消全选
function toggleSuborderSelectAll(btn) {
const allCards = document.querySelectorAll('.suborder-card');
const isAllSelected = Array.from(allCards).every(card => card.classList.contains('selected'));
if (isAllSelected) {
allCards.forEach(card => {
card.classList.remove('selected');
const ci = card.querySelector('.select-indicator');
ci.classList.remove('bg-blue-500', 'border-blue-500');
ci.classList.add('border-gray-300', 'bg-white');
ci.innerHTML = '';
});
} else {
allCards.forEach(card => {
card.classList.add('selected');
const ci = card.querySelector('.select-indicator');
ci.classList.remove('border-gray-300', 'bg-white');
ci.classList.add('bg-blue-500', 'border-blue-500');
ci.innerHTML = `<svg class="w-3 h-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
});
}
updateSuborderCount();
}
// 更新底部选中数量
function updateSuborderCount() {
const selected = document.querySelectorAll('.suborder-card.selected').length;
const total = document.querySelectorAll('.suborder-card').length;
document.getElementById('suborder-count').innerText = `已选 ${selected} / ${total} 项`;
updateSelectAllState(selected, total);
}
// 同步全选按钮状态
function updateSelectAllState(selectedCount, totalCount) {
const allBtn = document.querySelector('.radio-select-all');
if (selectedCount === totalCount && totalCount > 0) {
allBtn.classList.remove('border-gray-300', 'bg-white');
allBtn.classList.add('bg-blue-500', 'border-blue-500');
allBtn.innerHTML = `<svg class="w-3 h-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
} else {
allBtn.classList.remove('bg-blue-500', 'border-blue-500');
allBtn.classList.add('border-gray-300', 'bg-white');
allBtn.innerHTML = '';
}
}
// 筛选弹窗打开
function showFilterSheet() {
const overlay = document.getElementById('filter-sheet-overlay');
const sheet = document.getElementById('filter-sheet');
overlay.classList.remove('hidden');
setTimeout(() => {
sheet.classList.remove('translate-y-full');
sheet.classList.add('translate-y-0');
}, 10);
}
// 筛选弹窗关闭
function hideFilterSheet() {
const overlay = document.getElementById('filter-sheet-overlay');
const sheet = document.getElementById('filter-sheet');
sheet.classList.add('translate-y-full');
sheet.classList.remove('translate-y-0');
setTimeout(() => {
overlay.classList.add('hidden');
}, 300);
}
// 卡片长按弹出操作菜单
window.isLongPressing = false;
document.querySelectorAll('.suborder-card').forEach(card => {
let timer;
// 鼠标按下
card.addEventListener('mousedown', e => {
if (e.target.closest('.select-indicator') || e.target.closest('button')) return;
window.isLongPressing = false;
timer = setTimeout(() => {
window.isLongPressing = true;
showSuborderActionMenu(card);
}, 600);
});
card.addEventListener('mouseup', () => { clearTimeout(timer); setTimeout(()=>{window.isLongPressing=false},50) });
card.addEventListener('mouseleave', () => { clearTimeout(timer); window.isLongPressing=false });
// 触屏兼容
card.addEventListener('touchstart', e => {
if (e.target.closest('.select-indicator') || e.target.closest('button')) return;
window.isLongPressing = false;
timer = setTimeout(() => {
window.isLongPressing = true;
showSuborderActionMenu(card);
}, 600);
});
card.addEventListener('touchend', () => { clearTimeout(timer); setTimeout(()=>{window.isLongPressing=false},50) });
card.addEventListener('touchmove', () => { clearTimeout(timer); window.isLongPressing=false });
});
function showSuborderActionMenu(card) {
document.querySelectorAll('.card-action-overlay').forEach(o => o.classList.add('hidden'));
const overlay = card.querySelector('.card-action-overlay');
if (overlay) overlay.classList.remove('hidden');
}
// 重置状态弹窗
function showResetStatusSheet() {
document.getElementById('reset-sheet-overlay').classList.remove('hidden');
document.getElementById('reset-sheet').classList.remove('translate-y-full');
}
function hideResetStatusSheet() {
document.getElementById('reset-sheet-overlay').classList.add('hidden');
document.getElementById('reset-sheet').classList.add('translate-y-full');
}
function confirmResetStatus() {
const s = document.getElementById('reset-status-select');
if (!s.value && s.selectedIndex===0) { alert('请选择状态'); return; }
alert(s.value==='' ? '已选择状态: 置空' : '已选择状态: '+s.value);
hideResetStatusSheet();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>补充信息</title>
<!-- Tailwind CSS -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
body {
background-color: #f3f4f6;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
</style>
</head>
<body class="bg-gray-200">
<!-- Phone Shell -->
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] overflow-hidden flex flex-col relative rounded-[48px] border-[10px] border-gray-900 shadow-2xl">
<!-- Status Bar & Nav Bar Container -->
<div class="bg-[#1A73E8] flex-none">
<!-- Status Bar -->
<div class="h-[44px] flex justify-between items-center px-8 text-white">
<span class="text-[15px] font-semibold">09:41</span>
<div class="flex items-center gap-1.5">
<iconify-icon class="text-lg" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-lg" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-xl" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- Navigation Bar -->
<div class="h-[44px] flex items-center px-4 relative">
<div class="flex items-center text-white cursor-pointer" onclick="window.location.href='main.html'">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span class="text-sm">返回</span>
</div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<h1 class="text-white text-lg font-bold">补充信息</h1>
</div>
<div class="ml-auto w-10"></div> <!-- Placeholder for right side -->
</div>
</div>
<!-- Content Area -->
<div class="flex-1 overflow-y-auto no-scrollbar p-4 space-y-4">
<!-- 收货人信息 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50">
<div class="flex items-center gap-2 mb-4">
<div class="w-1.5 h-4 bg-[#1A73E8] rounded-full"></div>
<h3 class="text-sm font-bold text-gray-900">收货人信息</h3>
</div>
<div class="space-y-4">
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">收货人名称</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 focus-within:border-blue-500 focus-within:bg-white transition-all">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入收货人名称" type="text"/>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">国家代码</label>
<div class="relative">
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none focus:border-blue-500 focus:bg-white transition-all">
<option disabled="" value="">请选择</option>
<option selected="" value="US">US - 美国</option>
<option value="CN">CN - 中国</option>
<option value="JP">JP - 日本</option>
<option value="UK">UK - 英国</option>
<option value="DE">DE - 德国</option>
<option value="FR">FR - 法国</option>
</select>
<iconify-icon class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none text-xl" icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">通讯方式</label>
<div class="relative">
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none focus:border-blue-500 focus:bg-white transition-all">
<option disabled="" selected="" value="">请选择</option>
<option value="phone">电话</option>
<option value="email">邮件</option>
<option value="fax">传真</option>
</select>
<iconify-icon class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none text-xl" icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">联系号码</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 focus-within:border-blue-500 focus-within:bg-white transition-all">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入联系号码" type="text"/>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">地址</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 focus-within:border-blue-500 focus-within:bg-white transition-all">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入地址" type="text"/>
</div>
</div>
</div>
</div>
<!-- 发货人信息 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50">
<div class="flex items-center gap-2 mb-4">
<div class="w-1.5 h-4 bg-[#1A73E8] rounded-full"></div>
<h3 class="text-sm font-bold text-gray-900">发货人信息</h3>
</div>
<div class="space-y-4">
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">发货人名称</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 focus-within:border-blue-500 focus-within:bg-white transition-all">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入发货人名称" type="text"/>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">国家代码</label>
<div class="relative">
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none focus:border-blue-500 focus:bg-white transition-all">
<option disabled="" value="">请选择</option>
<option selected="" value="US">US - 美国</option>
<option value="CN">CN - 中国</option>
<option value="JP">JP - 日本</option>
<option value="UK">UK - 英国</option>
<option value="DE">DE - 德国</option>
<option value="FR">FR - 法国</option>
</select>
<iconify-icon class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none text-xl" icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">通讯方式</label>
<div class="relative">
<select class="w-full bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 text-[14px] text-gray-800 outline-none appearance-none focus:border-blue-500 focus:bg-white transition-all">
<option disabled="" selected="" value="">请选择</option>
<option value="phone">电话</option>
<option value="email">邮件</option>
<option value="fax">传真</option>
</select>
<iconify-icon class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none text-xl" icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">联系号码</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 focus-within:border-blue-500 focus-within:bg-white transition-all">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入联系号码" type="text"/>
</div>
</div>
<div>
<label class="text-[13px] text-gray-500 font-medium mb-1.5 block">地址</label>
<div class="flex items-center bg-gray-50 border border-gray-200 rounded-xl h-10 px-3 focus-within:border-blue-500 focus-within:bg-white transition-all">
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none" placeholder="请输入地址" type="text"/>
</div>
</div>
</div>
</div>
</div>
<!-- 底部确定按钮 -->
<div class="px-5 py-4 shrink-0">
<button class="w-full bg-[#2563EB] text-white text-[15px] font-bold py-3.5 rounded-xl shadow-lg shadow-blue-500/30 active:scale-[0.98] transition-all" onclick="alert('补充信息已提交'); window.location.href='main.html'">
确定
</button>
<!-- Home Indicator -->
<div class="mt-4 flex justify-center">
<div class="w-32 h-1.5 bg-gray-200 rounded-full"></div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,668 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>出港移库 - 移动端原型</title>
<!-- Tailwind CSS -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
/* 同步参考页面body背景色 */
background-color: #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
/* 模拟手机容器 - 同步参考底色 */
.phone-container {
width: 375px;
height: 812px;
background-color: #f5f6fa;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 8px solid #1a1a1a;
border-radius: 40px;
}
/* 隐藏滚动条但保留滚动 */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* 页面切换动画 */
.page {
display: none;
flex-direction: column;
height: 100%;
}
.page.active {
display: flex;
}
/* 筛选面板弹出动画 */
#filter-panel {
transform: translateY(100%);
transition: transform 0.3s ease-out;
}
#filter-panel.show {
transform: translateY(0);
}
.overlay {
background-color: rgba(0,0,0,0.5);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.overlay.show {
opacity: 1;
pointer-events: auto;
}
/* 按钮按压效果 */
.btn-active:active {
opacity: 0.7;
transform: scale(0.98);
}
/* 复选框样式 */
.custom-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.custom-checkbox-round {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 50%;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox-round:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox-round:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
</style>
</head>
<body>
<div class="phone-container">
<!-- 状态栏 -->
<div class="h-10 w-full flex justify-between items-center px-8 flex-none bg-[#2563EB] text-white">
<span class="text-xs font-semibold">09:41</span>
<div class="flex gap-1 items-center">
<iconify-icon icon="mdi:signal" width="14"></iconify-icon>
<iconify-icon icon="mdi:wifi" width="14"></iconify-icon>
<iconify-icon icon="mdi:battery" width="18"></iconify-icon>
</div>
</div>
<!-- 列表首页 -->
<div class="page active" id="page-home">
<!-- 顶部导航栏 -->
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="alert('返回首页')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">出港移库</h1>
<div class="w-16"></div>
</div>
<!-- 滚动内容区域 -->
<div class="flex-1 overflow-y-auto no-scrollbar pb-24">
<!-- 搜索栏 -->
<section class="px-4 pt-4 pb-3 bg-white">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索运单号" type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="toggleFilterDrawer(true)">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- Tab切换 -->
<div class="flex bg-white border-b sticky top-0 z-20">
<button class="flex-1 py-3 text-sm font-bold text-[#1F2937] border-b-2 border-[#1F2937] transition-all" id="tab-pending" onclick="switchTab('pending')">
待移库 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
<button class="flex-1 py-3 text-sm font-bold text-gray-400 border-b-2 border-transparent transition-all" id="tab-shipped" onclick="switchTab('shipped')">
已移库 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
</div>
<!-- 待移库列表 -->
<div class="p-3 pt-4 space-y-3" id="list-pending">
<!-- 卡片1 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363573</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">NOR</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">100件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">200kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363573', status: '待移库', agent: 'NOR', pieces: '100', weight: '200', goodsName: '电子元器件', carrier: 'MU', route: 'HFE-NKG-LAX', location: 'A区-03货位', code: 'NOR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片2 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363574</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">CTA</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">85件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">150kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-PEK-LAX</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363574', status: '待移库', agent: 'CTA', pieces: '85', weight: '150', goodsName: '服装鞋帽', carrier: 'CA', route: 'HFE-PEK-LAX', location: 'B区-05货位', code: 'NOR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片3 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363575</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">DHL</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">200件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">450kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363575', status: '待移库', agent: 'DHL', pieces: '200', weight: '450', goodsName: '精密模具', carrier: '3U', route: 'HFE-NKG-LAX', location: 'C区-01货位', code: 'NOR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
</div>
<!-- 已移库列表 -->
<div class="p-3 pt-4 space-y-3 hidden" id="list-shipped">
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363001</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">NOR</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">50件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">80kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363001', status: '已移库', agent: 'NOR', pieces: '50', weight: '80', pickupNo: 'TH20260701001', goodsName: '精密仪器', carrier: 'MU', route: 'HFE-NKG-LAX', location: 'B区-05货位', code: 'NOR', counterTime: '2026-07-05 10:30', counterPerson: '张三', outboundTime: '2026-07-05 11:45', outboundPerson: '张三'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363002</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">CTA</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">120件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">300kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-PEK-LAX</span>
</div>
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363002', status: '已移库', agent: 'CTA', pieces: '120', weight: '300', pickupNo: 'TH20260702002', goodsName: '鲜花绿植', carrier: 'CA', route: 'HFE-PEK-LAX', location: 'A区-08货位', code: 'NOR', counterTime: '2026-07-06 09:15', counterPerson: '王五', outboundTime: '2026-07-06 10:30', outboundPerson: '赵六'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363003</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">DHL</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">75件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">160kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363003', status: '已移库', agent: 'DHL', pieces: '75', weight: '160', pickupNo: 'TH20260703003', goodsName: '生鲜食品', carrier: '3U', route: 'HFE-NKG-LAX', location: 'D区-02货位', code: 'NOR', counterTime: '2026-07-06 14:00', counterPerson: '钱七', outboundTime: '2026-07-06 15:20', outboundPerson: '孙八'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
</div>
</div>
<!-- 底部操作栏 -->
<div class="absolute bottom-0 w-full bg-white border-t border-gray-200 z-40 flex flex-col pt-4 pb-4" id="bottom-toolbar">
<div class="px-4 flex items-center w-full">
<div class="flex items-center gap-2">
<input class="custom-checkbox-round" id="select-all-cb" onclick="toggleSelectAll(this.checked)" type="checkbox"/>
<label class="text-[13px] text-[#1F2937] font-medium cursor-pointer" for="select-all-cb">全选</label>
</div>
<div class="flex-1"></div>
<div class="flex items-center gap-3">
<span class="text-[12px] text-[#9CA3AF]">已选 <span id="selected-count">0</span> 项</span>
<button class="bg-[#2563EB] text-white text-[15px] font-bold px-5 py-2 rounded-[10px] shadow-lg shadow-blue-500/25 btn-active" onclick="alert('移库操作')">
移库
</button>
</div>
</div>
</div>
</div>
<!-- 运单详情页面 -->
<div class="page" id="page-detail">
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="switchPage('page-home')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">运单详情</h1>
<div class="w-16"></div>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar pb-8 p-4">
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100">
<div class="bg-[#2563EB] px-4 py-3 flex justify-between items-center text-white">
<span class="font-bold text-[17px] leading-tight" id="detail-awb">781-22222620</span>
<div class="flex items-center text-[13px] font-normal opacity-90">
<span class="w-2 h-2 bg-green-400 rounded-full mr-1.5"></span>
<span id="detail-time">2026-05-10 08:30</span>
</div>
</div>
<div class="p-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<span class="bg-[#EF4444] text-white text-[11px] px-1.5 py-0.5 rounded leading-tight font-medium">国际进港</span>
<span class="text-[13px] font-normal text-[#666666] border-l border-gray-300 pl-2">普通货物运输</span>
</div>
</div>
<div class="text-[22px] font-bold text-[#111827] mb-3" id="detail-goods-name">电子元器件</div>
<div class="grid grid-cols-2 gap-x-8 gap-y-3">
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">件数</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-count">55</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">重量</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-weight">622.00 kg</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">特码</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-code">NOR</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">代理</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-agent">NOR</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">出运路径</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-route">HFE-PEK-LAX</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">承运人</div>
<div class="text-[15px] text-[#1F2937] font-bold" id="detail-carrier">MU</div>
</div>
</div>
</div>
</div>
<!-- 移库信息卡片 -->
<div class="mt-4 bg-white rounded-xl shadow-sm overflow-hidden border border-gray-200 hidden" id="detail-outbound-card">
<div class="bg-[#10B981] px-4 py-2.5">
<span class="text-white font-semibold text-[15px]">移库信息</span>
</div>
<div class="bg-white px-4 py-3 grid grid-cols-2 gap-y-3">
<div class="flex flex-col" id="detail-outbound-person-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">移库人</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-person">-</span>
</div>
<div class="flex flex-col" id="detail-outbound-time-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">移库时间</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-time">-</span>
</div>
</div>
</div>
</div>
</div>
<!-- 底部筛选遮罩 -->
<div class="overlay absolute inset-0 z-30" id="filter-overlay" onclick="toggleFilterDrawer(false)"></div>
<!-- 筛选弹窗 -->
<div class="absolute bottom-0 w-full bg-white rounded-t-3xl z-50 p-6 shadow-2xl overflow-hidden" id="filter-panel">
<div class="relative flex items-center justify-center mb-6">
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
<button class="absolute right-0 top-0 text-gray-400" onclick="toggleFilterDrawer(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
</div>
<div class="space-y-4">
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">承运人</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-800">
<span class="text-sm font-medium">MU</span>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">运单类型</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择运单类型</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择特码</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="flex gap-4 pt-2">
<button class="flex-1 bg-white border border-gray-300 text-gray-600 py-3.5 rounded-xl font-bold btn-active" onclick="alert('已重置')">
重置
</button>
<button class="flex-1 bg-[#2563EB] text-white py-3.5 rounded-xl font-bold shadow-lg shadow-blue-500/20 btn-active" onclick="toggleFilterDrawer(false)">
确认
</button>
</div>
</div>
</div>
</div>
<script>
// 页面切换
function switchPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById(pageId).classList.add('active');
if(pageId === 'page-detail'){
document.querySelector('#page-detail .no-scrollbar').scrollTop = 0;
}
}
// Tab切换
function switchTab(type) {
const tabPending = document.getElementById('tab-pending');
const tabShipped = document.getElementById('tab-shipped');
const listPending = document.getElementById('list-pending');
const listShipped = document.getElementById('list-shipped');
const bottomToolbar = document.getElementById('bottom-toolbar');
if (type === 'pending') {
tabPending.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabPending.classList.remove('text-gray-400', 'border-transparent');
tabShipped.classList.add('text-gray-400', 'border-transparent');
tabShipped.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listPending.classList.remove('hidden');
listShipped.classList.add('hidden');
bottomToolbar.style.display = 'flex';
} else {
tabShipped.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabShipped.classList.remove('text-gray-400', 'border-transparent');
tabPending.classList.add('text-gray-400', 'border-transparent');
tabPending.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listShipped.classList.remove('hidden');
listPending.classList.add('hidden');
bottomToolbar.style.display = 'none';
}
}
// 全选
function toggleSelectAll(checked) {
const checkboxes = document.querySelectorAll('.item-checkbox');
checkboxes.forEach(cb => {
cb.checked = checked;
});
updateSelectionCount();
}
// 单选
function handleCheckboxClick(checkbox, event) {
event.stopPropagation();
updateSelectionCount();
const allBox = document.getElementById('select-all-cb');
const all = Array.from(document.querySelectorAll('.item-checkbox')).every(i => i.checked);
allBox.checked = all;
}
// 更新选中数量
function updateSelectionCount() {
const cnt = document.querySelectorAll('.item-checkbox:checked').length;
document.getElementById('selected-count').textContent = cnt;
}
// 查看详情
function showDetail(data) {
if (!data) return;
const setText = (id, val) => {
const el = document.getElementById(id);
if(el) el.textContent = val || '-';
};
setText('detail-awb', data.id);
setText('detail-goods-name', data.goodsName);
setText('detail-count', data.pieces);
setText('detail-weight', data.weight + (String(data.weight).includes('kg') ? '' : ' kg'));
setText('detail-code', data.code);
setText('detail-agent', data.agent);
setText('detail-carrier', data.carrier);
setText('detail-route', data.route || 'HFE-PEK-LAX');
setText('detail-location', data.location);
const outboundCard = document.getElementById('detail-outbound-card');
if(data.status === '待移库'){
outboundCard.classList.add('hidden');
}else{
outboundCard.classList.remove('hidden');
setText('detail-outbound-person', data.outboundPerson);
setText('detail-outbound-time', data.outboundTime);
}
switchPage('page-detail');
}
// 筛选抽屉
function toggleFilterDrawer(show) {
const panel = document.getElementById('filter-panel');
const overlay = document.getElementById('filter-overlay');
if(show){
panel.classList.add('show');
overlay.classList.add('show');
}else{
panel.classList.remove('show');
overlay.classList.remove('show');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,963 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>出港查询</title>
<!-- Tailwind CSS (本地) -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify 图标 -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #e5e7eb;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
/* 隐藏滚动条但保留功能 */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.tag-occupied { background-color: #dcfce7; color: #22c55e; } /* 已离港 - 绿色 */
.tag-available { background-color: #dcfce7; color: #22c55e; } /* 已离港 - 绿色 */
.tag-repair { background-color: #fce7e7; color: #ef4444; } /* 离港 - 红色 */
/* 页面切换动画 */
.page {
position: absolute;
inset: 0;
transition: transform 0.3s ease-in-out;
background-color: #f3f4f6;
display: flex;
flex-direction: column;
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
}
.page::-webkit-scrollbar {
display: none;
}
.page-hidden-right { transform: translateX(100%); }
.page-hidden-left { transform: translateX(-100%); }
/* 统一的深蓝色调 */
.bg-header-blue { background-color: #2563EB; }
/* 标签页样式 */
.tab-item {
position: relative;
padding: 12px 0;
color: #999;
font-size: 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex: 1;
text-align: center;
}
.tab-item.active {
color: #2563eb;
font-weight: bold;
}
.tab-item::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background-color: #2563eb;
transition: width 0.3s ease;
}
.tab-item.active::after {
width: 40%;
}
/* ========== 筛选弹窗样式 提升层级+动画稳定 ========== */
.filter-overlay {
position: absolute;
inset: 0;
background-color: rgba(0,0,0,0.4);
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s ease, visibility 0.25s ease;
}
.filter-overlay.show {
opacity: 1;
visibility: visible;
}
.filter-sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 20px;
z-index: 10000;
transform: translateY(100%);
transition: transform 0.25s ease-out;
max-height: 80vh;
overflow-y: auto;
}
.filter-sheet.show {
transform: translateY(0);
}
/* 筛选输入框统一样式 */
.filter-input {
width: 100%;
height: 40px;
padding: 0 12px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
outline: none;
box-sizing: border-box;
}
.filter-input:focus {
border-color: #2563EB;
}
/* 下拉框箭头浅灰色 + 全浏览器兼容修复 */
select.filter-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%239ca3af' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 36px;
}
/* 清除IE默认下拉箭头 */
select.filter-input::-ms-expand {
display: none;
}
</style>
</head>
<body>
<div class="w-[375px] h-[812px] bg-[#F3F4F6] overflow-hidden flex flex-col relative shadow-2xl rounded-[40px] border-[8px] border-gray-800">
<!-- 状态栏 -->
<div class="bg-header-blue flex-none z-50">
<div class="flex justify-between items-center px-6 pt-3 pb-1 text-[12px] font-medium text-white">
<span>09:41</span>
<div class="flex gap-1.5 items-center">
<iconify-icon icon="material-symbols:signal-cellular-4-bar-rounded"></iconify-icon>
<iconify-icon icon="material-symbols:wifi"></iconify-icon>
<iconify-icon icon="material-symbols:battery-full-rounded"></iconify-icon>
</div>
</div>
</div>
<!-- 筛选弹窗容器 移到最外层,层级提升 -->
<div class="filter-overlay" id="filterOverlay"></div>
<div class="filter-sheet" id="filterSheet">
<!-- 顶部标题栏 -->
<div class="relative flex items-center justify-center mb-6">
<button class="absolute right-0 top-0 text-gray-400" onclick="closeFilter()">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
</div>
<!-- 筛选表单区域 -->
<div class="space-y-4 mb-7">
<!-- 1.航班日期 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期起</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期止</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<!-- 2.航班号 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班号</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入航班号" type="text" id="filterFlightNo">
</div>
</div>
<!-- 3.代理 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">代理</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择代理" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 4.特码 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择特码" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 5.目的港 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">目的港</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入目的港" type="text" id="filterOrigin">
</div>
</div>
<!-- 6.运单类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">运单类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择运单类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 7.业务类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">业务类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择业务类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 8.品名(中) -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">品名(中)</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入中文品名" type="text" id="filterCnName">
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="flex gap-3">
<button class="flex-1 h-11 bg-gray-100 text-gray-600 rounded-lg text-sm font-medium" onclick="resetFilter()">重置</button>
<button class="flex-1 h-11 bg-[#2563EB] text-white rounded-lg text-sm font-medium" onclick="confirmFilter()">确定</button>
</div>
</div>
<!-- 页面主容器 -->
<div class="flex-1 relative overflow-hidden">
<!-- 列表页 -->
<div class="page z-10" id="listPage">
<!-- 头部 -->
<header class="bg-header-blue pt-2 pb-4 px-4 flex items-center relative flex-none">
<button class="flex items-center text-white text-sm absolute left-4">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="mx-auto text-white font-bold text-lg">出港查询</h1>
</header>
<!-- 搜索栏 筛选按钮已移除边框 -->
<div class="p-3 bg-transparent flex items-center gap-3 border-b border-gray-100 flex-none">
<div class="flex-1 bg-white rounded-xl flex items-center px-3 py-[10px] gap-2 border border-gray-50">
<iconify-icon class="text-[#9ca3af] text-lg" icon="mdi:magnify"></iconify-icon>
<input class="bg-transparent text-[14px] w-full outline-none text-gray-700 placeholder-[#9ca3af]" placeholder="搜索运单号" type="text"/>
<iconify-icon class="text-[#2563eb] text-lg" icon="mdi:qrcode-scan" onclick="alert('开启扫码功能')"></iconify-icon>
</div>
<!-- 筛选按钮 无圆形实线边框 -->
<button id="filterBtn" class="w-10 h-10 flex items-center justify-center bg-white rounded-full relative z-[99999]">
<iconify-icon class="text-[#2563eb] text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
<!-- 统计标签 -->
<div class="flex items-center gap-2 px-4 pb-3">
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#2563EB]">
<div class="text-[11px] text-gray-500">总票数</div>
<div class="text-[22px] font-bold text-[#2563EB] leading-tight mt-0.5">4</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-orange-500">
<div class="text-[11px] text-gray-500">已入库</div>
<div class="text-[22px] font-bold text-orange-500 leading-tight mt-0.5">2</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#10B981]">
<div class="text-[11px] text-gray-500">已离港</div>
<div class="text-[22px] font-bold text-[#10B981] leading-tight mt-0.5">2</div>
</div>
</div>
<!-- ULD 列表内容 -->
<div class="p-3 space-y-3 pb-24 overflow-y-auto hide-scrollbar">
<!-- 卡片 1 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363396')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363396</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已离港</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:444</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:555</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
</div>
<!-- 卡片 2 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363397')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363397</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已离港</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:120</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:320</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:徽远</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:PER</span>
</div>
</div>
</div>
<!-- 卡片 3 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363398')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363398</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已入库</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:268</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:780</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:NOR</span>
</div>
</div>
</div>
<!-- 卡片 4 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363399')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363399</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已离港</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:85</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:210</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:徽远</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
</div>
<!-- 卡片 5 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363400')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363400</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已离港</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:350</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:430</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:PER</span>
</div>
</div>
</div>
</div>
</div>
<!-- 详情页 -->
<div class="page z-20 page-hidden-right !bg-[#f0f2f5]" id="detailPage">
<!-- 头部 -->
<header class="bg-header-blue pt-2 pb-4 px-4 flex items-center relative flex-none">
<button class="flex items-center text-white text-sm absolute left-4 active:opacity-70" onclick="goBack()">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="mx-auto text-white font-bold text-lg">出港详情</h1>
<div class="absolute right-4 flex items-center">
<iconify-icon class="text-red-500 text-2xl" icon="mdi:file-document-edit-outline"></iconify-icon>
</div>
</header>
<!-- 标签页 -->
<div class="bg-white px-4 border-b border-gray-200 flex-none">
<div class="flex justify-around">
<div class="tab-item active cursor-pointer" onclick="switchTab(0)">运单信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(1)">仓库信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(2)">库位信息</div>
</div>
</div>
<!-- 详情内容区 -->
<div class="flex-1 overflow-y-auto hide-scrollbar">
<!-- 运单信息标签页 -->
<div class="tab-pane p-3 space-y-3" id="tab-content-0">
<!-- 卡片1:基本信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800" id="det-uldTitle">基本信息</span>
</div>
<div class="p-3 grid grid-cols-2 gap-2">
<!-- Row 1 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billNo"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">代理人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-agent"></span>
</div>
<!-- Row 2 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">特码</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-code"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">承运人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-carrier"></span>
</div>
<!-- Row 3 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-route"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">中转站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-outboundTime"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-dest"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">UN编号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-unNo"></span>
</div>
<!-- Row 5 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-lockStatus-container">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5" id="det-lockStatus-label">锁定状态</span>
<div id="det-lockStatus-wrapper">
<span class="text-xs font-semibold" id="det-lockStatus"></span>
</div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">包装类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-packType"></span>
</div>
<!-- Row 6 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">业务类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-type"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库时间</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-outboundTime"></span>
</div>
<!-- Row 4 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billType"></span>
</div>
<!-- Row 7 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(中)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameCn"></span>
</div>
<!-- Row 8 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(英)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameEn"></span>
</div>
<!-- Row 9 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">备注</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-remark"></span>
</div>
</div>
</div>
<!-- 卡片2:件重信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">件重信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-pieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-weight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">计费重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-calcWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inPieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center opacity-0">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">—</div>
<div class="text-xs text-gray-800 font-semibold">—</div>
</div>
</div>
</div>
<!-- 卡片3:回执信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">回执信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">预配舱单</div>
<div class="text-xs text-gray-800 font-semibold" id="det-originalManifest"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运抵报告</div>
<div class="text-xs text-gray-800 font-semibold" id="det-tallyReport"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-customsRelease-container">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">海关放行</div>
<div id="det-customsRelease-wrapper">
<div class="text-xs font-semibold" id="det-customsRelease"></div>
</div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">装载舱单</div>
<div class="text-xs text-gray-800 font-semibold" id="det-loadManifest"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">理货报告</div>
<div class="text-xs text-gray-800 font-semibold" id="det-cargoReport"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center opacity-0">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">—</div>
<div class="text-xs text-gray-800 font-semibold">—</div>
</div>
</div>
</div>
</div>
<!-- 仓库信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-1"></div>
<!-- 库位信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-2"></div>
</div>
</div>
</div>
</div>
<script>
const uldData = {
'78133363396': {
billNo: '78133363396', agent: '中外运', code: 'DGR',
pieces: '444', weight: '555.0', type: '普通货物运输',
inPieces: '444', inWeight: '555.0', calcWeight: '555.0',
carrier: 'CA', route: 'HFE', packType: '纸箱',
billType: '国际出港(国际直航)', unNo: '—', dest: 'LAX',
itemNameCn: '电子元器件', itemNameEn: 'Electronic Components',
lockStatus: '未锁定', outboundTime: '—', remark: '需冷藏存储',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '1', pieces: 200, weight: 300.0, flightDate: '2026-07-08', flightNo: 'CA1234',route: 'HFE',dest: 'LAX', time: '2026-07-08 14:30:00' },
{ batch: '2', pieces: 244, weight: 255.0, flightDate: '2026-07-09', flightNo: 'CA5678',route: 'HFE',dest: 'LAX', time: '2026-07-09 09:10:46' }
],
locations: [
{ id: 'A-01-001', inPerson: '张三', inTime: '2026-07-08 14:30', outPerson: '李四', outTime: '2026-07-09 10:00' },
{ id: 'B-02-005', inPerson: '王五', inTime: '2026-07-09 09:10', outPerson: '—', outTime: '—' }
]
},
'78133363397': {
billNo: '78133363397', agent: '徽远', code: 'PER',
pieces: '120', weight: '320.0', type: '普通货物运输',
inPieces: '120', inWeight: '320.0', calcWeight: '320.0',
carrier: 'MU', route: 'PVG-NRT', packType: '托盘',
billType: '国际出港(国际直航)', unNo: '—', dest: 'PVG',
itemNameCn: '服装', itemNameEn: 'Garments',
lockStatus: '未锁定', outboundTime: '—', remark: '易碎品,轻拿轻放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '3', pieces: 60, weight: 150.0, flightDate: '2026-07-07', flightNo: 'MU123',route: 'HFE',dest: 'LAX', time: '2026-07-07 16:20:00' },
{ batch: '4', pieces: 60, weight: 170.0, flightDate: '2026-07-08', flightNo: 'MU456',route: 'HFE',dest: 'LAX', time: '2026-07-08 11:00:00' }
],
locations: [
{ id: 'A-03-012', inPerson: '赵六', inTime: '2026-07-07 16:20', outPerson: '钱七', outTime: '2026-07-08 14:00' },
{ id: 'C-01-008', inPerson: '孙八', inTime: '2026-07-08 11:00', outPerson: '—', outTime: '—' }
]
},
'78133363398': {
billNo: '78133363398', agent: '中外运', code: 'NOR',
pieces: '268', weight: '780.0', type: '普通货物运输',
inPieces: '268', inWeight: '780.0', calcWeight: '780.0',
carrier: 'CZ', route: 'CAN-AMS', packType: '托盘',
billType: '国际出港(国际直航)', unNo: '—', dest: 'CAN',
itemNameCn: '机械设备', itemNameEn: 'Machinery Equipment',
lockStatus: '锁定', outboundTime: '2026-07-09 08:00', remark: '需叉车装卸',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '5', pieces: 150, weight: 420.0, flightDate: '2026-07-08', flightNo: 'CZ789',route: 'HFE',dest: 'LAX', time: '2026-07-08 22:15:00' },
{ batch: '6', pieces: 118, weight: 360.0, flightDate: '2026-07-09', flightNo: 'CZ012',route: 'HFE',dest: 'LAX', time: '2026-07-09 06:30:00' },
{ batch: '7', pieces: 100, weight: 200.0, flightDate: '2026-07-09', flightNo: 'CZ345',route: 'HFE',dest: 'LAX', time: '2026-07-09 13:00:00' }
],
locations: [
{ id: 'B-01-003', inPerson: '张三', inTime: '2026-07-08 22:15', outPerson: '李四', outTime: '2026-07-09 06:00' },
{ id: 'A-02-010', inPerson: '王五', inTime: '2026-07-09 06:30', outPerson: '—', outTime: '—' },
{ id: 'C-03-007', inPerson: '赵六', inTime: '2026-07-09 13:00', outPerson: '—', outTime: '—' }
]
},
'78133363399': {
billNo: '78133363399', agent: '徽远', code: 'DGR',
pieces: '85', weight: '210.0', type: '危险品运输',
inPieces: '85', inWeight: '210.0', calcWeight: '210.0',
carrier: '3U', route: 'CTU-SIN', packType: '铁箱',
billType: '国际出港(国际直航)', unNo: 'UN3480', dest: 'CTU',
itemNameCn: '锂电池', itemNameEn: 'Lithium Battery',
lockStatus: '锁定', outboundTime: '—', remark: '危险品,单独存放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '8', pieces: 45, weight: 110.0, flightDate: '2026-07-08', flightNo: '3U678',route: 'HFE',dest: 'LAX', time: '2026-07-08 18:00:00' },
{ batch: '9', pieces: 40, weight: 100.0, flightDate: '2026-07-09', flightNo: '3U901',route: 'HFE',dest: 'LAX', time: '2026-07-09 07:45:00' }
],
locations: [
{ id: 'D-01-002', inPerson: '钱七', inTime: '2026-07-08 18:00', outPerson: '孙八', outTime: '2026-07-09 08:00' },
{ id: 'A-04-015', inPerson: '张三', inTime: '2026-07-09 07:45', outPerson: '—', outTime: '—' }
]
},
'78133363400': {
billNo: '78133363400', agent: '中外运', code: 'PER',
pieces: '350', weight: '430.0', type: '普通货物运输',
inPieces: '350', inWeight: '430.0', calcWeight: '430.0',
carrier: 'HU', route: 'HKG-HFE', packType: '纸箱',
billType: '国际出港(国际直航)', unNo: '—', dest: 'HFE',
itemNameCn: '食品', itemNameEn: 'Food Products',
lockStatus: '未锁定', outboundTime: '2026-07-09 11:30', remark: '保质期至2026-12',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '10', pieces: 180, weight: 220.0, flightDate: '2026-07-07', flightNo: 'HU234',route: 'HFE',dest: 'LAX', time: '2026-07-07 20:00:00' },
{ batch: '11', pieces: 170, weight: 210.0, flightDate: '2026-07-09', flightNo: 'HU567',route: 'HFE',dest: 'LAX', time: '2026-07-09 10:30:00' }
],
locations: [
{ id: 'B-02-011', inPerson: '李四', inTime: '2026-07-07 20:00', outPerson: '王五', outTime: '2026-07-08 16:00' },
{ id: 'C-01-009', inPerson: '赵六', inTime: '2026-07-09 10:30', outPerson: '—', outTime: '—' }
]
}
};
// 核心修复:等待DOM全部渲染完成后再绑定点击事件,避免获取元素为null
document.addEventListener('DOMContentLoaded', function() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
const filterBtn = document.getElementById('filterBtn');
// 点击遮罩空白关闭,添加self防止弹窗内部点击穿透关闭
filterOverlay.addEventListener('click', function(e) {
if (e.target === filterOverlay) {
closeFilter();
}
});
// 筛选按钮点击打开弹窗
filterBtn.addEventListener('click', openFilter);
});
// 打开筛选弹窗
function openFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterOverlay.classList.add('show');
setTimeout(() => filterSheet.classList.add('show'), 10);
}
// 关闭筛选弹窗
function closeFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterSheet.classList.remove('show');
setTimeout(() => filterOverlay.classList.remove('show'), 250);
}
// 重置所有筛选输入框
function resetFilter() {
document.querySelectorAll('.filter-input').forEach(input => {
input.value = '';
});
}
// 确定筛选(模拟查询,可对接后端筛选逻辑)
function confirmFilter() {
const params = {
flightDate: document.getElementById('filterFlightDate').value,
flightNo: document.getElementById('filterFlightNo').value,
agent: document.getElementById('filterAgent').value,
code: document.getElementById('filterCode').value,
origin: document.getElementById('filterOrigin').value,
billType: document.getElementById('filterBillType').value,
bizType: document.getElementById('filterBizType').value,
cnName: document.getElementById('filterCnName').value
};
console.log('筛选参数:', params);
alert('筛选执行成功,控制台可查看筛选条件');
closeFilter();
}
function showDetail(id) {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
const data = uldData[id];
if (data) {
document.getElementById('det-uldTitle').innerText = '基本信息';
document.getElementById('det-billNo').innerText = data.billNo;
document.getElementById('det-agent').innerText = data.agent;
document.getElementById('det-code').innerText = data.code;
document.getElementById('det-carrier').innerText = data.carrier;
document.getElementById('det-route').innerText = data.route;
document.getElementById('det-dest').innerText = data.dest;
document.getElementById('det-billType').innerText = data.billType;
document.getElementById('det-unNo').innerText = data.unNo;
// 锁定状态渲染
const lockStatusEl = document.getElementById('det-lockStatus');
if (data.lockStatus === '锁定') {
lockStatusEl.innerHTML = '<iconify-icon icon="mdi:lock" class="mr-1"></iconify-icon>锁定';
lockStatusEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-red-50 text-red-600 inline-flex items-center';
} else {
lockStatusEl.innerHTML = '—';
lockStatusEl.className = 'text-xs font-semibold text-gray-800';
}
document.getElementById('det-packType').innerText = data.packType;
document.getElementById('det-type').innerText = data.type;
document.getElementById('det-outboundTime').innerText = data.outboundTime || '—';
document.getElementById('det-itemNameCn').innerText = data.itemNameCn || '—';
document.getElementById('det-itemNameEn').innerText = data.itemNameEn || '—';
document.getElementById('det-remark').innerText = data.remark || '—';
// 件重信息
document.getElementById('det-pieces').innerText = data.pieces;
document.getElementById('det-weight').innerText = data.weight;
document.getElementById('det-calcWeight').innerText = data.calcWeight;
document.getElementById('det-inPieces').innerText = data.inPieces;
document.getElementById('det-inWeight').innerText = data.inWeight;
// 海关放行标签
const customsReleaseEl = document.getElementById('det-customsRelease');
if (data.customsRelease === '11') {
customsReleaseEl.innerText = '11';
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-green-50 text-green-600 inline-block';
} else {
customsReleaseEl.innerText = data.customsRelease;
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-gray-100 text-gray-600 inline-block';
}
// 渲染仓库批次列表
const warehouseContainer = document.getElementById('tab-content-1');
warehouseContainer.innerHTML = '';
data.warehouse.forEach(item => {
warehouseContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-semibold text-gray-900">批次号: ${item.batch}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">件数</div>
<div class="text-xs font-semibold text-gray-800">${item.pieces}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">重量</div>
<div class="text-xs font-semibold text-gray-800">${parseFloat(item.weight).toFixed(1)} kg</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班日期</div>
<div class="text-xs font-semibold text-gray-800">${item.flightDate}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班号</div>
<div class="text-xs font-semibold text-gray-800">${item.flightNo}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发站</div>
<div class="text-xs font-semibold text-gray-800">${item.route}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</div>
<div class="text-xs font-semibold text-gray-800">${item.dest}</div>
</div>
</div>
</div>`;
});
// 渲染库位信息
const locationContainer = document.getElementById('tab-content-2');
locationContainer.innerHTML = '';
data.locations.forEach(loc => {
locationContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-bold text-gray-900">${loc.id}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库人</div>
<div class="text-xs font-semibold text-gray-800">${loc.inPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库时间</div>
<div class="text-xs font-semibold text-gray-800">${loc.inTime}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库人</div>
<div class="text-xs ${loc.outPerson === '—' ? 'text-gray-500 text-[14px]' : 'text-gray-800'} font-semibold">${loc.outPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库时间</div>
<div class="text-xs ${loc.outTime === '—' ? 'text-gray-500 text-[14px]' : 'text-gray-800'} font-semibold">${loc.outTime}</div>
</div>
</div>
</div>`;
});
}
switchTab(0);
listPage.classList.add('page-hidden-left');
detailPage.classList.remove('page-hidden-right');
}
function goBack() {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
listPage.classList.remove('page-hidden-left');
detailPage.classList.add('page-hidden-right');
}
function switchTab(index) {
const tabs = document.querySelectorAll('.tab-item');
const contents = document.querySelectorAll('.tab-pane');
tabs.forEach((tab, i) => {
if (i === index) {
tab.classList.add('active');
contents[i].classList.remove('hidden');
} else {
tab.classList.remove('active');
contents[i].classList.add('hidden');
}
});
}
// 搜索框聚焦监听
document.querySelector('input[placeholder="搜索ULD编号"]').addEventListener('focus', () => {
console.log('搜索框已激活');
});
</script>
</div>
</body>
</html>

View File

@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 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>
@font-face {
font-family: 'Inter';
src: local('sans-serif');
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* 模拟手机容器阴影 */
.phone-shell {
box-shadow: 0 50px 100px -20px rgba(0,0,0,0.25), 0 30px 60px -30px rgba(0,0,0,0.3);
}
</style>
</head>
<body class="bg-gray-200 flex justify-center items-center min-h-screen p-4">
<!-- 模拟手机屏幕容器 -->
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] overflow-hidden flex flex-col relative rounded-[48px] border-[10px] border-gray-900">
<!-- 顶部蓝色区域 (包含状态栏和导航栏) -->
<div class="bg-[#1A73E8] flex-none">
<!-- 系统状态栏模拟 -->
<div class="h-11 w-full flex justify-between items-center px-8 text-white">
<span class="text-[15px] font-bold">09:41</span>
<div class="flex gap-1.5 items-center">
<iconify-icon class="text-sm" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-base" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-xl" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="h-[44px] px-4 flex items-center justify-between">
<button class="flex items-center text-white space-x-0.5">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span class="text-base">返回</span>
</button>
<h1 class="text-lg font-bold text-white absolute left-1/2 -translate-x-1/2">运单追踪</h1>
<div class="w-10"></div> <!-- 占位保持平衡 -->
</div>
</div>
<!-- 可滚动内容区 -->
<div class="flex-1 overflow-y-auto no-scrollbar px-5 pt-6 pb-12">
<!-- 流转状态卡片 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-5">
<!-- 运单号 Header -->
<div class="flex items-center justify-between pt-2 pb-5 mb-8 border-b border-gray-100">
<div class="text-xl font-black text-gray-900 leading-none tracking-tight">
78133358743
</div>
<span class="text-[11px] font-medium text-blue-600 bg-blue-50 px-2.5 py-1 rounded-full border border-blue-100">国际进港</span>
</div>
<!-- 流转状态标题 -->
<div class="mb-6">
<h2 class="text-base font-semibold text-gray-700 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-700 inline-block" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
流转状态
</h2>
</div>
<div class="relative pl-7 pb-2">
<div class="absolute left-[9px] top-2 bottom-4 w-[2px] bg-gray-100"></div>
<!-- 状态 1: 当前 -->
<div class="relative mb-10">
<div class="absolute -left-[27px] top-1 w-[20px] h-[20px] rounded-full bg-green-600 border-4 border-green-100 flex items-center justify-center ring-1 ring-green-600/20">
<div class="w-2 h-2 rounded-full bg-white"></div>
</div>
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-green-700">原始舱单</span>
<span class="text-green-600 text-xs font-normal bg-green-50 px-2 py-0.5 rounded-full">放行状态:01</span>
</div>
<div class="text-gray-400 text-xs font-normal flex items-center gap-1.5">
<span>2026-07-10 11:03:38</span>
</div>
</div>
</div>
<!-- 状态 2 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">分拣理货</span>
</div>
</div>
<!-- 状态 3 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">理货申报</span>
</div>
</div>
<!-- 状态 4 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">海关放行</span>
</div>
</div>
<!-- 状态 5 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">柜台办结</span>
</div>
</div>
<!-- 状态 6 -->
<div class="relative mb-2">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">提取出库</span>
</div>
</div>
</div>
</div>
<!-- 模块 4: 操作详情卡片 -> 已删除 -->
<!-- 底部占位 -->
<div class="text-center py-6">
<p class="text-[10px] text-gray-300 uppercase tracking-[0.2em] font-bold">查询信息已触底</p>
</div>
</div>
<!-- 底部 Home Indicator 模拟 -->
<div class="h-8 w-full flex justify-center items-center flex-none bg-white/60 backdrop-blur-md">
<div class="w-32 h-1.5 bg-black rounded-full opacity-20"></div>
</div>
</div>
</body>
</html>ml&gt;

View File

@@ -0,0 +1,932 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" 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>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #e5e7eb;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
/* 隐藏滚动条但保留功能 */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.tag-occupied { background-color: #dcfce7; color: #22c55e; } /* 已离港 - 绿色 */
.tag-available { background-color: #dcfce7; color: #22c55e; } /* 已离港 - 绿色 */
.tag-repair { background-color: #fce7e7; color: #ef4444; } /* 离港 - 红色 */
/* 页面切换动画 */
.page {
position: absolute;
inset: 0;
transition: transform 0.3s ease-in-out;
background-color: #f3f4f6;
display: flex;
flex-direction: column;
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
}
.page::-webkit-scrollbar {
display: none;
}
.page-hidden-right { transform: translateX(100%); }
.page-hidden-left { transform: translateX(-100%); }
/* 统一的深蓝色调 */
.bg-header-blue { background-color: #2563EB; }
/* 标签页样式 */
.tab-item {
position: relative;
padding: 12px 0;
color: #999;
font-size: 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex: 1;
text-align: center;
}
.tab-item.active {
color: #2563eb;
font-weight: bold;
}
.tab-item::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background-color: #2563eb;
transition: width 0.3s ease;
}
.tab-item.active::after {
width: 40%;
}
/* ========== 筛选弹窗样式 提升层级+动画稳定 ========== */
.filter-overlay {
position: absolute;
inset: 0;
background-color: rgba(0,0,0,0.4);
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s ease, visibility 0.25s ease;
}
.filter-overlay.show {
opacity: 1;
visibility: visible;
}
.filter-sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 20px;
z-index: 10000;
transform: translateY(100%);
transition: transform 0.25s ease-out;
max-height: 80vh;
overflow-y: auto;
}
.filter-sheet.show {
transform: translateY(0);
}
/* 筛选输入框统一样式 */
.filter-input {
width: 100%;
height: 40px;
padding: 0 12px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
outline: none;
box-sizing: border-box;
}
.filter-input:focus {
border-color: #2563EB;
}
/* 下拉框箭头浅灰色 + 全浏览器兼容修复 */
select.filter-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%239ca3af' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 36px;
}
/* 清除IE默认下拉箭头 */
select.filter-input::-ms-expand {
display: none;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100%; }
}
.scan-line {
animation: scan 2s linear infinite;
}
.custom-scrollbar::-webkit-scrollbar {
display: none;
}
.bottom-drawer {
transition: transform 0.3s ease-in-out;
}
</style>
</head>
<body>
<div class="w-[375px] h-[812px] bg-[#F3F4F6] overflow-hidden flex flex-col relative shadow-2xl rounded-[40px] border-[8px] border-gray-800">
<!-- 状态栏 -->
<div class="h-6 w-full bg-[#2563EB] flex justify-between items-center px-6 text-white text-xs pt-1">
<span>09:41</span>
<div class="flex items-center gap-1">
<iconify-icon icon="material-symbols:signal-cellular-4-bar-rounded"></iconify-icon>
<iconify-icon icon="material-symbols:wifi"></iconify-icon>
<iconify-icon icon="material-symbols:battery-full-rounded"></iconify-icon>
</div>
</div>
<!-- 筛选弹窗容器 移到最外层,层级提升 -->
<div class="filter-overlay" id="filterOverlay"></div>
<div class="filter-sheet" id="filterSheet">
<!-- 顶部标题栏 -->
<div class="relative flex items-center justify-center mb-6">
<button class="absolute right-0 top-0 text-gray-400" onclick="closeFilter()">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
</div>
<!-- 筛选表单区域 -->
<div class="space-y-4 mb-7">
<!-- 1.航班日期 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<!-- 2.航班号 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班号</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入航班号" type="text" id="filterFlightNo">
</div>
</div>
<!-- 3.代理 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">代理</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择代理" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 4.特码 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择特码" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 5.目的站 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">目的站</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入目的站" type="text" id="filterOrigin">
</div>
</div>
<!-- 6.运单类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">运单类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择运单类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 7.业务类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">业务类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择业务类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 8.品名(中) -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">品名(中)</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入中文品名" type="text" id="filterCnName">
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="flex gap-3">
<button class="flex-1 h-11 bg-gray-100 text-gray-600 rounded-lg text-sm font-medium" onclick="resetFilter()">重置</button>
<button class="flex-1 h-11 bg-[#2563EB] text-white rounded-lg text-sm font-medium" onclick="confirmFilter()">确定</button>
</div>
</div>
<!-- 页面主容器 -->
<div class="flex-1 relative overflow-hidden">
<!-- 列表首页:出港仓库卡片页面 -->
<div class="page z-10" id="listPage">
<!-- 顶部导航栏 -->
<header class="bg-[#2563EB] h-14 flex items-center justify-between px-4 flex-none">
<button class="flex items-center text-white text-sm" onclick="showToast('返回上一级')">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="text-white font-bold text-lg">出港仓库</h1>
<div class="w-12"></div>
</header>
<!-- 搜索区 -->
<section class="bg-[#F3F4F6] p-3 flex-none">
<div class="flex items-center gap-2">
<div class="flex-1 relative">
<iconify-icon class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-lg" icon="mdi:magnify"></iconify-icon>
<input class="w-full bg-white rounded-full pl-10 pr-10 py-2.5 text-sm shadow-sm border border-gray-200 outline-none" onkeypress="if(event.keyCode==13) showToast('执行搜索')" placeholder="搜索运单号" type="text"/>
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-[#2563EB]" onclick="toggleScanOverlay(true)">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button id="filterBtn" class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="openFilter()">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- 统计栏 -->
<section class="px-3 pb-3 flex justify-between gap-2 flex-none">
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#2563EB]">
<div class="text-[11px] text-gray-500">总票数</div>
<div class="text-lg font-bold text-[#2563EB]">8</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-orange-500">
<div class="text-[11px] text-gray-500">未清仓</div>
<div class="text-lg font-bold text-orange-500">4</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#10B981]">
<div class="text-[11px] text-gray-500">已清仓</div>
<div class="text-lg font-bold text-[#10B981]">4</div>
</div>
</section>
<!-- 卡片列表容器 -->
<main class="flex-1 overflow-y-auto px-3 pb-4 custom-scrollbar" id="cardListContainer">
</main>
<!-- 扫码层 -->
<div class="absolute inset-0 z-50 bg-black hidden flex flex-col" id="scanOverlay">
<div class="p-6 flex justify-between items-center text-white">
<button onclick="toggleScanOverlay(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<span class="text-sm font-medium">扫描运单条码</span>
<button><iconify-icon class="text-2xl" icon="mdi:flashlight"></iconify-icon></button>
</div>
<div class="flex-1 flex flex-col items-center justify-center p-12">
<div class="w-64 h-64 border-2 border-[#2563EB] relative overflow-hidden rounded-lg">
<div class="absolute top-0 left-0 w-full h-0.5 bg-[#2563EB] shadow-[0_0_10px_#2563EB] scan-line"></div>
<div class="absolute top-0 left-0 w-6 h-6 border-t-4 border-l-4 border-[#2563EB]"></div>
<div class="absolute top-0 right-0 w-6 h-6 border-t-4 border-r-4 border-[#2563EB]"></div>
<div class="absolute bottom-0 left-0 w-6 h-6 border-b-4 border-l-4 border-[#2563EB]"></div>
<div class="absolute bottom-0 right-0 w-6 h-6 border-b-4 border-r-4 border-[#2563EB]"></div>
</div>
<p class="text-white text-xs mt-6 opacity-60 text-center px-4">将条码/二维码放入框内,即可自动扫描</p>
</div>
<div class="h-32 bg-black flex justify-center items-center gap-12 text-white">
<div class="flex flex-col items-center opacity-60">
<iconify-icon class="text-3xl" icon="mdi:image-outline"></iconify-icon>
<span class="text-[10px] mt-1">相册</span>
</div>
<div class="flex flex-col items-center">
<iconify-icon class="text-3xl" icon="mdi:keyboard-outline"></iconify-icon>
<span class="text-[10px] mt-1">手动输入</span>
</div>
</div>
</div>
<!-- Toast -->
<div class="absolute bottom-20 left-1/2 -translate-x-1/2 bg-black bg-opacity-80 text-white text-xs px-4 py-2 rounded-full hidden z-[60] whitespace-nowrap" id="toast">提示内容</div>
</div>
<!-- 出港货物页面(右滑弹出) -->
<div class="page z-20 page-hidden-right !bg-[#f0f2f5]" id="detailPage">
<!-- 头部 -->
<header class="bg-header-blue pt-2 pb-4 px-4 flex items-center relative flex-none">
<button class="flex items-center text-white text-sm absolute left-4 active:opacity-70" onclick="goBack()">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="mx-auto text-white font-bold text-lg">出港货物</h1>
<div class="absolute right-4 flex items-center">
<iconify-icon class="text-red-500 text-2xl" icon="mdi:file-document-edit-outline"></iconify-icon>
</div>
</header>
<!-- 标签页 -->
<div class="bg-white px-4 border-b border-gray-200 flex-none">
<div class="flex justify-around">
<div class="tab-item active cursor-pointer" onclick="switchTab(0)">运单信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(1)">仓库信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(2)">库位信息</div>
</div>
</div>
<!-- 详情内容区 -->
<div class="flex-1 overflow-y-auto hide-scrollbar">
<!-- 运单信息标签页 -->
<div class="tab-pane p-3 space-y-3" id="tab-content-0">
<!-- 卡片1:基本信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800" id="det-uldTitle">基本信息</span>
</div>
<div class="p-3 grid grid-cols-2 gap-2">
<!-- Row 1 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billNo"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">代理人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-agent"></span>
</div>
<!-- Row 2 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">特码</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-code"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">承运人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-carrier"></span>
</div>
<!-- Row 3 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-route"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">中转站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-outboundTime"></span>
</div>
<!-- Row 4 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-dest"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">UN编号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-unNo"></span>
</div>
<!-- Row 5 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-lockStatus-container">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5" id="det-lockStatus-label">锁定状态</span>
<div id="det-lockStatus-wrapper">
<span class="text-xs font-semibold" id="det-lockStatus"></span>
</div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">包装类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-packType"></span>
</div>
<!-- Row 6 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">业务类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-type"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库时间</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-ciTime"></span>
</div>
<!-- Row 7 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billType"></span>
</div>
<!-- Row 8 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(中)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameCn"></span>
</div>
<!-- Row 9 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(英)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameEn"></span>
</div>
<!-- Row 10 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">备注</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-remark"></span>
</div>
</div>
</div>
<!-- 卡片2:件重信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">件重信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-pieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-weight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">计费重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-calcWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inPieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center opacity-0">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">—</div>
<div class="text-xs text-gray-800 font-semibold">—</div>
</div>
</div>
</div>
<!-- 卡片3:回执信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">回执信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">预配舱单</div>
<div class="text-xs text-gray-800 font-semibold" id="det-originalManifest"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运抵报告</div>
<div class="text-xs text-gray-800 font-semibold" id="det-arriReport"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-customsRelease-container">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">海关放行</div>
<div id="det-customsRelease-wrapper">
<div class="text-xs font-semibold" id="det-customsRelease"></div>
</div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">装载舱单</div>
<div class="text-xs text-gray-800 font-semibold" id="det-mftReport"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">理货报告</div>
<div class="text-xs text-gray-800 font-semibold" id="det-tallyReport"></div>
</div>
</div>
</div>
</div>
<!-- 仓库信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-1"></div>
<!-- 库位信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-2"></div>
</div>
</div>
</div>
</div>
<script>
// 运单详情数据源(用于出港货物渲染)
const uldData = {
'78133363396': {
billNo: '78133363396', agent: '中外运', code: 'DGR',
pieces: '444', weight: '555.0', type: '普通货物运输',
inPieces: '444', inWeight: '555.0', calcWeight: '555.0',
carrier: 'CA', route: 'HFE', packType: '纸箱',
billType: '国际进港(国际直航)', unNo: '—', dest: 'LAX',
itemNameCn: '电子元器件', itemNameEn: 'Electronic Components',
lockStatus: '未锁定', outboundTime: '—', remark: '需冷藏存储',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',arriReport: '01',mftReport: '01',
warehouse: [
{ batch: '1', pieces: 200, weight: 300.0, flightDate: '2026-07-08', flightNo: 'CA1234',route: 'HFE',dest: 'LAX', time: '2026-07-08 14:30:00' },
{ batch: '2', pieces: 244, weight: 255.0, flightDate: '2026-07-09', flightNo: 'CA5678',route: 'HFE',dest: 'LAX', time: '2026-07-09 09:10:46' }
],
locations: [
{ id: 'A-01-001', inPerson: '张三', inTime: '2026-07-08 14:30', outPerson: '李四', outTime: '2026-07-09 10:00' },
{ id: 'B-02-005', inPerson: '王五', inTime: '2026-07-09 09:10', outPerson: '—', outTime: '—' }
]
},
'78133363397': {
billNo: '78133363397', agent: '徽远', code: 'PER',
pieces: '120', weight: '320.0', type: '普通货物运输',
inPieces: '120', inWeight: '320.0', calcWeight: '320.0',
carrier: 'MU', route: 'PVG-NRT', packType: '托盘',
billType: '国际进港(国际直航)', unNo: '—', dest: 'PVG',
itemNameCn: '服装', itemNameEn: 'Garments',
lockStatus: '未锁定', outboundTime: '—', remark: '易碎品,轻拿轻放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',arriReport: '01',mftReport: '01',
warehouse: [
{ batch: '3', pieces: 60, weight: 150.0, flightDate: '2026-07-07', flightNo: 'MU123', time: '2026-07-07 16:20:00' },
{ batch: '4', pieces: 60, weight: 170.0, flightDate: '2026-07-08', flightNo: 'MU456', time: '2026-07-08 11:00:00' }
],
locations: [
{ id: 'A-03-012', inPerson: '赵六', inTime: '2026-07-07 16:20', outPerson: '钱七', outTime: '2026-07-08 14:00' },
{ id: 'C-01-008', inPerson: '孙八', inTime: '2026-07-08 11:00', outPerson: '—', outTime: '—' }
]
},
'78133363398': {
billNo: '78133363398', agent: '中外运', code: 'NOR',
pieces: '268', weight: '780.0', type: '普通货物运输',
inPieces: '268', inWeight: '780.0', calcWeight: '780.0',
carrier: 'CZ', route: 'CAN-AMS', packType: '托盘',
billType: '国际进港(国际直航)', unNo: '—', dest: 'CAN',
itemNameCn: '机械设备', itemNameEn: 'Machinery Equipment',
lockStatus: '锁定', outboundTime: '2026-07-09 08:00', remark: '需叉车装卸',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',arriReport: '01',mftReport: '01',
warehouse: [
{ batch: '5', pieces: 150, weight: 420.0, flightDate: '2026-07-08', flightNo: 'CZ789', time: '2026-07-08 22:15:00' },
{ batch: '6', pieces: 118, weight: 360.0, flightDate: '2026-07-09', flightNo: 'CZ012', time: '2026-07-09 06:30:00' },
{ batch: '7', pieces: 100, weight: 200.0, flightDate: '2026-07-09', flightNo: 'CZ345', time: '2026-07-09 13:00:00' }
],
locations: [
{ id: 'B-01-003', inPerson: '张三', inTime: '2026-07-08 22:15', outPerson: '李四', outTime: '2026-07-09 06:00' },
{ id: 'A-02-010', inPerson: '王五', inTime: '2026-07-09 06:30', outPerson: '—', outTime: '—' },
{ id: 'C-03-007', inPerson: '赵六', inTime: '2026-07-09 13:00', outPerson: '—', outTime: '—' }
]
},
'78133363399': {
billNo: '78133363399', agent: '徽远', code: 'DGR',
pieces: '85', weight: '210.0', type: '危险品运输',
inPieces: '85', inWeight: '210.0', calcWeight: '210.0',
carrier: '3U', route: 'CTU-SIN', packType: '铁箱',
billType: '国际进港(国际直航)', unNo: 'UN3480', dest: 'CTU',
itemNameCn: '锂电池', itemNameEn: 'Lithium Battery',
lockStatus: '锁定', outboundTime: '—', remark: '危险品,单独存放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',arriReport: '01',mftReport: '01',
warehouse: [
{ batch: '8', pieces: 45, weight: 110.0, flightDate: '2026-07-08', flightNo: '3U678', time: '2026-07-08 18:00:00' },
{ batch: '9', pieces: 40, weight: 100.0, flightDate: '2026-07-09', flightNo: '3U901', time: '2026-07-09 07:45:00' }
],
locations: [
{ id: 'D-01-002', inPerson: '钱七', inTime: '2026-07-08 18:00', outPerson: '孙八', outTime: '2026-07-09 08:00' },
{ id: 'A-04-015', inPerson: '张三', inTime: '2026-07-09 07:45', outPerson: '—', outTime: '—' }
]
},
'78133363400': {
billNo: '78133363400', agent: '中外运', code: 'PER',
pieces: '350', weight: '430.0', type: '普通货物运输',
inPieces: '350', inWeight: '430.0', calcWeight: '430.0',
carrier: 'HU', route: 'HKG-LAX', packType: '纸箱',
billType: '国际进港(国际直航)', unNo: '—', dest: 'LAX',
itemNameCn: '食品', itemNameEn: 'Food Products',
lockStatus: '未锁定', outboundTime: '2026-07-09 11:30', remark: '保质期至2026-12',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',arriReport: '01',mftReport: '01',
warehouse: [
{ batch: '10', pieces: 180, weight: 220.0, flightDate: '2026-07-07', flightNo: 'HU234', time: '2026-07-07 20:00:00' },
{ batch: '11', pieces: 170, weight: 210.0, flightDate: '2026-07-09', flightNo: 'HU567', time: '2026-07-09 10:30:00' }
],
locations: [
{ id: 'B-02-011', inPerson: '李四', inTime: '2026-07-07 20:00', outPerson: '王五', outTime: '2026-07-08 16:00' },
{ id: 'C-01-009', inPerson: '赵六', inTime: '2026-07-09 10:30', outPerson: '—', outTime: '—' }
]
}
};
// 出港仓库卡片列表数据源
const data = [
{ id: "78133363396", status: "已清仓", flight: "20260702/MU1111", route: "HFE-LAX", pcs: 111, weight: 2300 },
{ id: "78133363397", status: "未清仓", flight: "20260702/MU2222", route: "NRT-LAX", pcs: 85, weight: 1560 },
{ id: "78133363398", status: "已清仓", flight: "20260702/MU3333", route: "FRA-LAX", pcs: 200, weight: 4200 },
{ id: "78133363399", status: "未清仓", flight: "20260702/MU4444", route: "HFE-PVG", pcs: 50, weight: 980 },
{ id: "78133363400", status: "未清仓", flight: "20260702/MU5555", route: "CDG-LAX", pcs: 168, weight: 3150 },
{ id: "78133363401", status: "已清仓", flight: "20260702/MU6666", route: "SIN-LAX", pcs: 92, weight: 1800 },
{ id: "78133363402", status: "未清仓", flight: "20260702/MU7777", route: "HFE-HFE", pcs: 145, weight: 2800 },
{ id: "78133363403", status: "已清仓", flight: "20260702/MU8888", route: "HND-LAX", pcs: 73, weight: 1420 }
];
// 渲染仓库卡片列表
function renderList() {
const container = document.getElementById('cardListContainer');
container.innerHTML = data.map((item) => {
const isCleared = item.status === '已清仓';
const departure = item.route.split('-')[0];
const flightNo = item.flight.split('/')[1];
return `
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer mb-3" onclick="showDetail('${item.id}')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">${item.id}</span>
</div>
<div class="flex items-center gap-2">
${getStatusTag(item.status)}
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:${item.pcs}</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:${item.weight}</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span>航班号:${flightNo}</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>目的站:${departure}</span>
</div>
</div>
<div class="grid grid-cols-4 gap-2 mt-3">
<button onclick="event.stopPropagation(); showToast('执行清仓操作:运单号 ${item.id}')"
class="text-xs py-1.5 px-2 rounded-[6px] font-medium h-8 ${isCleared ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-[#EFF6FF] text-[#2563EB]'}" ${isCleared ? 'disabled' : ''}>
清仓
</button>
<button onclick="event.stopPropagation(); showToast('修改库位:运单号 ${item.id}')"
class="bg-[#EFF6FF] text-[#2563EB] text-xs py-1.5 px-2 rounded-[6px] font-medium h-8">
修改库位
</button>
<button onclick="event.stopPropagation(); showToast('执行出库:运单号 ${item.id}')"
class="bg-[#EFF6FF] text-[#2563EB] text-xs py-1.5 px-2 rounded-[6px] font-medium h-8">
出库
</button>
<button onclick="event.stopPropagation(); showToast('执行入库:运单号 ${item.id}')"
class="bg-[#EFF6FF] text-[#2563EB] text-xs py-1.5 px-2 rounded-[6px] font-medium h-8">
入库
</button>
</div>
</div>`;
}).join('');
}
// 状态标签渲染
function getStatusTag(status) {
let color = '';
switch(status) {
case '已清仓': color = 'bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium'; break;
case '未清仓': color = 'bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium'; break;
}
return `<span class="${color}">${status}</span>`;
}
// 跳转至出港货物页面,填充数据
function showDetail(id) {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
const data = uldData[id];
if (!data) {
showToast("暂无该运单详情数据");
return;
}
// 填充基础信息
document.getElementById('det-billNo').innerText = data.billNo;
document.getElementById('det-agent').innerText = data.agent;
document.getElementById('det-code').innerText = data.code;
document.getElementById('det-carrier').innerText = data.carrier;
document.getElementById('det-route').innerText = data.route;
document.getElementById('det-dest').innerText = data.dest;
document.getElementById('det-billType').innerText = data.billType;
document.getElementById('det-unNo').innerText = data.unNo;
document.getElementById('det-ciTime').innerText = data.ciTime;
// 锁定状态样式
const lockStatusEl = document.getElementById('det-lockStatus');
if (data.lockStatus === '锁定') {
lockStatusEl.innerHTML = '<iconify-icon icon="mdi:lock" class="mr-1"></iconify-icon>锁定';
lockStatusEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-red-50 text-red-600 inline-flex items-center';
} else {
lockStatusEl.innerText = '—';
lockStatusEl.className = 'text-xs font-semibold text-gray-800';
}
document.getElementById('det-packType').innerText = data.packType;
document.getElementById('det-type').innerText = data.type;
document.getElementById('det-outboundTime').innerText = data.outboundTime || '—';
document.getElementById('det-itemNameCn').innerText = data.itemNameCn || '—';
document.getElementById('det-itemNameEn').innerText = data.itemNameEn || '—';
document.getElementById('det-remark').innerText = data.remark || '—';
// 件重信息
document.getElementById('det-pieces').innerText = data.pieces;
document.getElementById('det-weight').innerText = data.weight;
document.getElementById('det-calcWeight').innerText = data.calcWeight;
document.getElementById('det-inPieces').innerText = data.inPieces;
document.getElementById('det-inWeight').innerText = data.inWeight;
// 海关放行标签
const customsReleaseEl = document.getElementById('det-customsRelease');
if (data.customsRelease === '11') {
customsReleaseEl.innerText = '11';
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-green-50 text-green-600 inline-block';
} else {
customsReleaseEl.innerText = data.customsRelease;
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-gray-100 text-gray-600 inline-block';
}
document.getElementById('det-originalManifest').innerText = data.originalManifest;
document.getElementById('det-arriReport').innerText = data.arriReport;
document.getElementById('det-mftReport').innerText = data.mftReport;
document.getElementById('det-tallyReport').innerText = data.tallyReport;
// 渲染仓库批次
const warehouseContainer = document.getElementById('tab-content-1');
warehouseContainer.innerHTML = '';
data.warehouse.forEach(item => {
warehouseContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-semibold text-gray-900">批次号: ${item.batch}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">件数</div>
<div class="text-xs font-semibold text-gray-800">${item.pieces}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">重量</div>
<div class="text-xs font-semibold text-gray-800">${parseFloat(item.weight).toFixed(1)} kg</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班日期</div>
<div class="text-xs font-semibold text-gray-800">${item.flightDate}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班号</div>
<div class="text-xs font-semibold text-gray-800">${item.flightNo}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发站</div>
<div class="text-xs font-semibold text-gray-800">${item.route}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</div>
<div class="text-xs font-semibold text-gray-800">${item.dest}</div>
</div>
</div>
</div>`;
});
// 渲染库位信息
const locationContainer = document.getElementById('tab-content-2');
locationContainer.innerHTML = '';
data.locations.forEach(loc => {
locationContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-bold text-gray-900">${loc.id}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库人</div>
<div class="text-xs font-semibold text-gray-800">${loc.inPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库时间</div>
<div class="text-xs font-semibold text-gray-800">${loc.inTime}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库人</div>
<div class="text-xs ${loc.outPerson === '—' ? 'text-gray-500' : 'text-gray-800'} font-semibold">${loc.outPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库时间</div>
<div class="text-xs ${loc.outTime === '—' ? 'text-gray-500' : 'text-gray-800'} font-semibold">${loc.outTime}</div>
</div>
</div>
</div>`;
});
// 默认激活运单信息标签
switchTab(0);
// 页面切换动画:列表左滑隐藏,详情滑入
listPage.classList.add('page-hidden-left');
detailPage.classList.remove('page-hidden-right');
}
// 返回列表页
function goBack() {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
listPage.classList.remove('page-hidden-left');
detailPage.classList.add('page-hidden-right');
}
// 切换详情标签页
function switchTab(index) {
const tabs = document.querySelectorAll('.tab-item');
const contents = document.querySelectorAll('.tab-pane');
tabs.forEach((tab, i) => {
if (i === index) {
tab.classList.add('active');
contents[i].classList.remove('hidden');
} else {
tab.classList.remove('active');
contents[i].classList.add('hidden');
}
});
}
// Toast提示
function showToast(msg) {
const toast = document.getElementById('toast');
toast.innerText = msg;
toast.classList.remove('hidden');
setTimeout(() => toast.classList.add('hidden'), 2000);
}
// 筛选弹窗方法
function openFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterOverlay.classList.add('show');
setTimeout(() => filterSheet.classList.add('show'), 10);
}
function closeFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterSheet.classList.remove('show');
setTimeout(() => filterOverlay.classList.remove('show'), 250);
}
function resetFilter() {
document.querySelectorAll('.filter-input').forEach(input => {
input.value = '';
});
}
function confirmFilter() {
const params = {
flightDate: document.getElementById('filterFlightDate')?.value || '',
flightNo: document.getElementById('filterFlightNo')?.value || '',
agent: '',
code: '',
origin: document.getElementById('filterOrigin')?.value || '',
billType: '',
bizType: '',
cnName: document.getElementById('filterCnName')?.value || ''
};
console.log('筛选参数:', params);
alert('筛选执行成功,控制台可查看筛选条件');
closeFilter();
}
// 扫码弹窗
function toggleScanOverlay(show) {
const overlay = document.getElementById('scanOverlay');
if (show) overlay.classList.remove('hidden');
else overlay.classList.add('hidden');
}
// 页面加载完成初始化
document.addEventListener('DOMContentLoaded', function() {
renderList();
// 筛选遮罩点击关闭
const filterOverlay = document.getElementById('filterOverlay');
filterOverlay.addEventListener('click', function(e) {
if (e.target === filterOverlay) closeFilter();
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 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>
@font-face {
font-family: 'Inter';
src: local('sans-serif');
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* 模拟手机容器阴影 */
.phone-shell {
box-shadow: 0 50px 100px -20px rgba(0,0,0,0.25), 0 30px 60px -30px rgba(0,0,0,0.3);
}
</style>
</head>
<body class="bg-gray-200 flex justify-center items-center min-h-screen p-4">
<!-- 模拟手机屏幕容器 -->
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] overflow-hidden flex flex-col relative rounded-[48px] border-[10px] border-gray-900">
<!-- 顶部蓝色区域 (包含状态栏和导航栏) -->
<div class="bg-[#1A73E8] flex-none">
<!-- 系统状态栏模拟 -->
<div class="h-11 w-full flex justify-between items-center px-8 text-white">
<span class="text-[15px] font-bold">09:41</span>
<div class="flex gap-1.5 items-center">
<iconify-icon class="text-sm" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-base" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-xl" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="h-[44px] px-4 flex items-center justify-between">
<button class="flex items-center text-white space-x-0.5">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span class="text-base">返回</span>
</button>
<h1 class="text-lg font-bold text-white absolute left-1/2 -translate-x-1/2">运单追踪</h1>
<div class="w-10"></div> <!-- 占位保持平衡 -->
</div>
</div>
<!-- 可滚动内容区 -->
<div class="flex-1 overflow-y-auto no-scrollbar px-5 pt-6 pb-12">
<!-- 流转状态卡片 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-5">
<!-- 运单号 Header -->
<div class="flex items-center justify-between pt-2 pb-5 mb-8 border-b border-gray-100">
<div class="text-xl font-black text-gray-900 leading-none tracking-tight">
78133358743
</div>
<span class="text-[11px] font-medium text-blue-600 bg-blue-50 px-2.5 py-1 rounded-full border border-blue-100">国际进港</span>
</div>
<!-- 流转状态标题 -->
<div class="mb-6">
<h2 class="text-base font-semibold text-gray-700 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-700 inline-block" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
流转状态
</h2>
</div>
<div class="relative pl-7 pb-2">
<div class="absolute left-[9px] top-2 bottom-4 w-[2px] bg-gray-100"></div>
<!-- 状态 1: 当前 -->
<div class="relative mb-10">
<div class="absolute -left-[27px] top-1 w-[20px] h-[20px] rounded-full bg-green-600 border-4 border-green-100 flex items-center justify-center ring-1 ring-green-600/20">
<div class="w-2 h-2 rounded-full bg-white"></div>
</div>
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-green-700">原始舱单</span>
<span class="text-green-600 text-xs font-normal bg-green-50 px-2 py-0.5 rounded-full">放行状态:01</span>
</div>
<div class="text-gray-400 text-xs font-normal flex items-center gap-1.5">
<span>2026-07-10 11:03:38</span>
</div>
</div>
</div>
<!-- 状态 2 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">分拣理货</span>
</div>
</div>
<!-- 状态 3 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">理货申报</span>
</div>
</div>
<!-- 状态 4 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">海关放行</span>
</div>
</div>
<!-- 状态 5 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">柜台办结</span>
</div>
</div>
<!-- 状态 6 -->
<div class="relative mb-2">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">提取出库</span>
</div>
</div>
</div>
</div>
<!-- 模块 4: 操作详情卡片 -> 已删除 -->
<!-- 底部占位 -->
<div class="text-center py-6">
<p class="text-[10px] text-gray-300 uppercase tracking-[0.2em] font-bold">查询信息已触底</p>
</div>
</div>
<!-- 底部 Home Indicator 模拟 -->
<div class="h-8 w-full flex justify-center items-center flex-none bg-white/60 backdrop-blur-md">
<div class="w-32 h-1.5 bg-black rounded-full opacity-20"></div>
</div>
</div>
</body>
</html>ml&gt;

View File

@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 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>
@font-face {
font-family: 'Inter';
src: local('sans-serif');
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* 模拟手机容器阴影 */
.phone-shell {
box-shadow: 0 50px 100px -20px rgba(0,0,0,0.25), 0 30px 60px -30px rgba(0,0,0,0.3);
}
</style>
</head>
<body class="bg-gray-200 flex justify-center items-center min-h-screen p-4">
<!-- 模拟手机屏幕容器 -->
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] overflow-hidden flex flex-col relative rounded-[48px] border-[10px] border-gray-900">
<!-- 顶部蓝色区域 (包含状态栏和导航栏) -->
<div class="bg-[#1A73E8] flex-none">
<!-- 系统状态栏模拟 -->
<div class="h-11 w-full flex justify-between items-center px-8 text-white">
<span class="text-[15px] font-bold">09:41</span>
<div class="flex gap-1.5 items-center">
<iconify-icon class="text-sm" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-base" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-xl" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="h-[44px] px-4 flex items-center justify-between">
<button class="flex items-center text-white space-x-0.5">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span class="text-base">返回</span>
</button>
<h1 class="text-lg font-bold text-white absolute left-1/2 -translate-x-1/2">运单追踪</h1>
<div class="w-10"></div> <!-- 占位保持平衡 -->
</div>
</div>
<!-- 可滚动内容区 -->
<div class="flex-1 overflow-y-auto no-scrollbar px-5 pt-6 pb-12">
<!-- 流转状态卡片 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-5">
<!-- 运单号 Header -->
<div class="flex items-center justify-between pt-2 pb-5 mb-8 border-b border-gray-100">
<div class="text-xl font-black text-gray-900 leading-none tracking-tight">
78133358743
</div>
<span class="text-[11px] font-medium text-blue-600 bg-blue-50 px-2.5 py-1 rounded-full border border-blue-100">国际进港</span>
</div>
<!-- 流转状态标题 -->
<div class="mb-6">
<h2 class="text-base font-semibold text-gray-700 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-700 inline-block" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
流转状态
</h2>
</div>
<div class="relative pl-7 pb-2">
<div class="absolute left-[9px] top-2 bottom-4 w-[2px] bg-gray-100"></div>
<!-- 状态 1: 当前 -->
<div class="relative mb-10">
<div class="absolute -left-[27px] top-1 w-[20px] h-[20px] rounded-full bg-green-600 border-4 border-green-100 flex items-center justify-center ring-1 ring-green-600/20">
<div class="w-2 h-2 rounded-full bg-white"></div>
</div>
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-green-700">原始舱单</span>
<span class="text-green-600 text-xs font-normal bg-green-50 px-2 py-0.5 rounded-full">放行状态:01</span>
</div>
<div class="text-gray-400 text-xs font-normal flex items-center gap-1.5">
<span>2026-07-10 11:03:38</span>
</div>
</div>
</div>
<!-- 状态 2 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">分拣理货</span>
</div>
</div>
<!-- 状态 3 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">理货申报</span>
</div>
</div>
<!-- 状态 4 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">海关放行</span>
</div>
</div>
<!-- 状态 5 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">柜台办结</span>
</div>
</div>
<!-- 状态 6 -->
<div class="relative mb-2">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">提取出库</span>
</div>
</div>
</div>
</div>
<!-- 模块 4: 操作详情卡片 -> 已删除 -->
<!-- 底部占位 -->
<div class="text-center py-6">
<p class="text-[10px] text-gray-300 uppercase tracking-[0.2em] font-bold">查询信息已触底</p>
</div>
</div>
<!-- 底部 Home Indicator 模拟 -->
<div class="h-8 w-full flex justify-center items-center flex-none bg-white/60 backdrop-blur-md">
<div class="w-32 h-1.5 bg-black rounded-full opacity-20"></div>
</div>
</div>
</body>
</html>ml&gt;

View File

@@ -0,0 +1,924 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" 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>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #e5e7eb;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
/* 隐藏滚动条但保留功能 */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.tag-occupied { background-color: #dcfce7; color: #22c55e; } /* 已离港 - 绿色 */
.tag-available { background-color: #dcfce7; color: #22c55e; } /* 已离港 - 绿色 */
.tag-repair { background-color: #fce7e7; color: #ef4444; } /* 离港 - 红色 */
/* 页面切换动画 */
.page {
position: absolute;
inset: 0;
transition: transform 0.3s ease-in-out;
background-color: #f3f4f6;
display: flex;
flex-direction: column;
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
}
.page::-webkit-scrollbar {
display: none;
}
.page-hidden-right { transform: translateX(100%); }
.page-hidden-left { transform: translateX(-100%); }
/* 统一的深蓝色调 */
.bg-header-blue { background-color: #2563EB; }
/* 标签页样式 */
.tab-item {
position: relative;
padding: 12px 0;
color: #999;
font-size: 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex: 1;
text-align: center;
}
.tab-item.active {
color: #2563eb;
font-weight: bold;
}
.tab-item::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background-color: #2563eb;
transition: width 0.3s ease;
}
.tab-item.active::after {
width: 40%;
}
/* ========== 筛选弹窗样式 提升层级+动画稳定 ========== */
.filter-overlay {
position: absolute;
inset: 0;
background-color: rgba(0,0,0,0.4);
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s ease, visibility 0.25s ease;
}
.filter-overlay.show {
opacity: 1;
visibility: visible;
}
.filter-sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 20px;
z-index: 10000;
transform: translateY(100%);
transition: transform 0.25s ease-out;
max-height: 80vh;
overflow-y: auto;
}
.filter-sheet.show {
transform: translateY(0);
}
/* 筛选输入框统一样式 */
.filter-input {
width: 100%;
height: 40px;
padding: 0 12px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
outline: none;
box-sizing: border-box;
}
.filter-input:focus {
border-color: #2563EB;
}
/* 下拉框箭头浅灰色 + 全浏览器兼容修复 */
select.filter-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%239ca3af' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 36px;
}
/* 清除IE默认下拉箭头 */
select.filter-input::-ms-expand {
display: none;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100%; }
}
.scan-line {
animation: scan 2s linear infinite;
}
.custom-scrollbar::-webkit-scrollbar {
display: none;
}
.bottom-drawer {
transition: transform 0.3s ease-in-out;
}
</style>
</head>
<body>
<div class="w-[375px] h-[812px] bg-[#F3F4F6] overflow-hidden flex flex-col relative shadow-2xl rounded-[40px] border-[8px] border-gray-800">
<!-- 状态栏 -->
<div class="h-6 w-full bg-[#2563EB] flex justify-between items-center px-6 text-white text-xs pt-1">
<span>09:41</span>
<div class="flex items-center gap-1">
<iconify-icon icon="material-symbols:signal-cellular-4-bar-rounded"></iconify-icon>
<iconify-icon icon="material-symbols:wifi"></iconify-icon>
<iconify-icon icon="material-symbols:battery-full-rounded"></iconify-icon>
</div>
</div>
<!-- 筛选弹窗容器 移到最外层,层级提升 -->
<div class="filter-overlay" id="filterOverlay"></div>
<div class="filter-sheet" id="filterSheet">
<!-- 顶部标题栏 -->
<div class="relative flex items-center justify-center mb-6">
<button class="absolute right-0 top-0 text-gray-400" onclick="closeFilter()">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
</div>
<!-- 筛选表单区域 -->
<div class="space-y-4 mb-7">
<!-- 1.航班日期 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<!-- 2.航班号 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班号</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入航班号" type="text" id="filterFlightNo">
</div>
</div>
<!-- 3.代理 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">代理</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择代理" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 4.特码 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择特码" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 5.始发站 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">始发站</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入始发站" type="text" id="filterOrigin">
</div>
</div>
<!-- 6.运单类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">运单类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择运单类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 7.业务类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">业务类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择业务类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 8.品名(中) -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">品名(中)</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入中文品名" type="text" id="filterCnName">
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="flex gap-3">
<button class="flex-1 h-11 bg-gray-100 text-gray-600 rounded-lg text-sm font-medium" onclick="resetFilter()">重置</button>
<button class="flex-1 h-11 bg-[#2563EB] text-white rounded-lg text-sm font-medium" onclick="confirmFilter()">确定</button>
</div>
</div>
<!-- 页面主容器 -->
<div class="flex-1 relative overflow-hidden">
<!-- 列表首页:进港仓库卡片页面 -->
<div class="page z-10" id="listPage">
<!-- 顶部导航栏 -->
<header class="bg-[#2563EB] h-14 flex items-center justify-between px-4 flex-none">
<button class="flex items-center text-white text-sm" onclick="showToast('返回上一级')">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="text-white font-bold text-lg">进港仓库</h1>
<div class="w-12"></div>
</header>
<!-- 搜索区 -->
<section class="bg-[#F3F4F6] p-3 flex-none">
<div class="flex items-center gap-2">
<div class="flex-1 relative">
<iconify-icon class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-lg" icon="mdi:magnify"></iconify-icon>
<input class="w-full bg-white rounded-full pl-10 pr-10 py-2.5 text-sm shadow-sm border border-gray-200 outline-none" onkeypress="if(event.keyCode==13) showToast('执行搜索')" placeholder="搜索运单号/航班号..." type="text"/>
<button class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-[#2563EB]" onclick="toggleScanOverlay(true)">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button id="filterBtn" class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="openFilter()">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- 统计栏 -->
<section class="px-3 pb-3 flex justify-between gap-2 flex-none">
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#2563EB]">
<div class="text-[11px] text-gray-500">总票数</div>
<div class="text-lg font-bold text-[#2563EB]">8</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-orange-500">
<div class="text-[11px] text-gray-500">未清仓</div>
<div class="text-lg font-bold text-orange-500">4</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#10B981]">
<div class="text-[11px] text-gray-500">已清仓</div>
<div class="text-lg font-bold text-[#10B981]">4</div>
</div>
</section>
<!-- 卡片列表容器 -->
<main class="flex-1 overflow-y-auto px-3 pb-4 custom-scrollbar" id="cardListContainer">
</main>
<!-- 扫码层 -->
<div class="absolute inset-0 z-50 bg-black hidden flex flex-col" id="scanOverlay">
<div class="p-6 flex justify-between items-center text-white">
<button onclick="toggleScanOverlay(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<span class="text-sm font-medium">扫描运单条码</span>
<button><iconify-icon class="text-2xl" icon="mdi:flashlight"></iconify-icon></button>
</div>
<div class="flex-1 flex flex-col items-center justify-center p-12">
<div class="w-64 h-64 border-2 border-[#2563EB] relative overflow-hidden rounded-lg">
<div class="absolute top-0 left-0 w-full h-0.5 bg-[#2563EB] shadow-[0_0_10px_#2563EB] scan-line"></div>
<div class="absolute top-0 left-0 w-6 h-6 border-t-4 border-l-4 border-[#2563EB]"></div>
<div class="absolute top-0 right-0 w-6 h-6 border-t-4 border-r-4 border-[#2563EB]"></div>
<div class="absolute bottom-0 left-0 w-6 h-6 border-b-4 border-l-4 border-[#2563EB]"></div>
<div class="absolute bottom-0 right-0 w-6 h-6 border-b-4 border-r-4 border-[#2563EB]"></div>
</div>
<p class="text-white text-xs mt-6 opacity-60 text-center px-4">将条码/二维码放入框内,即可自动扫描</p>
</div>
<div class="h-32 bg-black flex justify-center items-center gap-12 text-white">
<div class="flex flex-col items-center opacity-60">
<iconify-icon class="text-3xl" icon="mdi:image-outline"></iconify-icon>
<span class="text-[10px] mt-1">相册</span>
</div>
<div class="flex flex-col items-center">
<iconify-icon class="text-3xl" icon="mdi:keyboard-outline"></iconify-icon>
<span class="text-[10px] mt-1">手动输入</span>
</div>
</div>
</div>
<!-- Toast -->
<div class="absolute bottom-20 left-1/2 -translate-x-1/2 bg-black bg-opacity-80 text-white text-xs px-4 py-2 rounded-full hidden z-[60] whitespace-nowrap" id="toast">提示内容</div>
</div>
<!-- 进港货物页面(右滑弹出) -->
<div class="page z-20 page-hidden-right !bg-[#f0f2f5]" id="detailPage">
<!-- 头部 -->
<header class="bg-header-blue pt-2 pb-4 px-4 flex items-center relative flex-none">
<button class="flex items-center text-white text-sm absolute left-4 active:opacity-70" onclick="goBack()">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="mx-auto text-white font-bold text-lg">进港货物</h1>
<div class="absolute right-4 flex items-center">
<iconify-icon class="text-red-500 text-2xl" icon="mdi:file-document-edit-outline"></iconify-icon>
</div>
</header>
<!-- 标签页 -->
<div class="bg-white px-4 border-b border-gray-200 flex-none">
<div class="flex justify-around">
<div class="tab-item active cursor-pointer" onclick="switchTab(0)">运单信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(1)">仓库信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(2)">库位信息</div>
</div>
</div>
<!-- 详情内容区 -->
<div class="flex-1 overflow-y-auto hide-scrollbar">
<!-- 运单信息标签页 -->
<div class="tab-pane p-3 space-y-3" id="tab-content-0">
<!-- 卡片1:基本信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800" id="det-uldTitle">基本信息</span>
</div>
<div class="p-3 grid grid-cols-2 gap-2">
<!-- Row 1 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billNo"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">代理人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-agent"></span>
</div>
<!-- Row 2 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">特码</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-code"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">承运人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-carrier"></span>
</div>
<!-- Row 3 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-route"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">中转站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-outboundTime"></span>
</div>
<!-- Row 4 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-dest"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">UN编号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-unNo"></span>
</div>
<!-- Row 5 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-lockStatus-container">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5" id="det-lockStatus-label">锁定状态</span>
<div id="det-lockStatus-wrapper">
<span class="text-xs font-semibold" id="det-lockStatus"></span>
</div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">包装类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-packType"></span>
</div>
<!-- Row 6 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">业务类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-type"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库时间</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-ciTime"></span>
</div>
<!-- Row 7 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billType"></span>
</div>
<!-- Row 8 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(中)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameCn"></span>
</div>
<!-- Row 9 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(英)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameEn"></span>
</div>
<!-- Row 10 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">备注</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-remark"></span>
</div>
</div>
</div>
<!-- 卡片2:件重信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">件重信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-pieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-weight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">计费重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-calcWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inPieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center opacity-0">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">—</div>
<div class="text-xs text-gray-800 font-semibold">—</div>
</div>
</div>
</div>
<!-- 卡片3:回执信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">回执信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">原始舱单</div>
<div class="text-xs text-gray-800 font-semibold" id="det-originalManifest"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">理货报告</div>
<div class="text-xs text-gray-800 font-semibold" id="det-tallyReport"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-customsRelease-container">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">海关放行</div>
<div id="det-customsRelease-wrapper">
<div class="text-xs font-semibold" id="det-customsRelease"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 仓库信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-1"></div>
<!-- 库位信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-2"></div>
</div>
</div>
</div>
</div>
<script>
// 运单详情数据源(用于进港货物渲染)
const uldData = {
'78133363396': {
billNo: '78133363396', agent: '中外运', code: 'DGR',
pieces: '444', weight: '555.0', type: '普通货物运输',
inPieces: '444', inWeight: '555.0', calcWeight: '555.0',
carrier: 'CA', route: 'LAX', packType: '纸箱',
billType: '国际进港(国际直航)', unNo: '—', dest: 'HFE',
itemNameCn: '电子元器件', itemNameEn: 'Electronic Components',
lockStatus: '未锁定', outboundTime: '—', remark: '需冷藏存储',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',
warehouse: [
{ batch: '1', pieces: 200, weight: 300.0, flightDate: '2026-07-08', flightNo: 'CA1234',route: 'LAX',dest: 'HFE', time: '2026-07-08 14:30:00' },
{ batch: '2', pieces: 244, weight: 255.0, flightDate: '2026-07-09', flightNo: 'CA5678',route: 'LAX',dest: 'HFE', time: '2026-07-09 09:10:46' }
],
locations: [
{ id: 'A-01-001', inPerson: '张三', inTime: '2026-07-08 14:30', outPerson: '李四', outTime: '2026-07-09 10:00' },
{ id: 'B-02-005', inPerson: '王五', inTime: '2026-07-09 09:10', outPerson: '—', outTime: '—' }
]
},
'78133363397': {
billNo: '78133363397', agent: '徽远', code: 'PER',
pieces: '120', weight: '320.0', type: '普通货物运输',
inPieces: '120', inWeight: '320.0', calcWeight: '320.0',
carrier: 'MU', route: 'PVG-NRT', packType: '托盘',
billType: '国际进港(国际直航)', unNo: '—', dest: 'PVG',
itemNameCn: '服装', itemNameEn: 'Garments',
lockStatus: '未锁定', outboundTime: '—', remark: '易碎品,轻拿轻放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',
warehouse: [
{ batch: '3', pieces: 60, weight: 150.0, flightDate: '2026-07-07', flightNo: 'MU123', time: '2026-07-07 16:20:00' },
{ batch: '4', pieces: 60, weight: 170.0, flightDate: '2026-07-08', flightNo: 'MU456', time: '2026-07-08 11:00:00' }
],
locations: [
{ id: 'A-03-012', inPerson: '赵六', inTime: '2026-07-07 16:20', outPerson: '钱七', outTime: '2026-07-08 14:00' },
{ id: 'C-01-008', inPerson: '孙八', inTime: '2026-07-08 11:00', outPerson: '—', outTime: '—' }
]
},
'78133363398': {
billNo: '78133363398', agent: '中外运', code: 'NOR',
pieces: '268', weight: '780.0', type: '普通货物运输',
inPieces: '268', inWeight: '780.0', calcWeight: '780.0',
carrier: 'CZ', route: 'CAN-AMS', packType: '托盘',
billType: '国际进港(国际直航)', unNo: '—', dest: 'CAN',
itemNameCn: '机械设备', itemNameEn: 'Machinery Equipment',
lockStatus: '锁定', outboundTime: '2026-07-09 08:00', remark: '需叉车装卸',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',
warehouse: [
{ batch: '5', pieces: 150, weight: 420.0, flightDate: '2026-07-08', flightNo: 'CZ789', time: '2026-07-08 22:15:00' },
{ batch: '6', pieces: 118, weight: 360.0, flightDate: '2026-07-09', flightNo: 'CZ012', time: '2026-07-09 06:30:00' },
{ batch: '7', pieces: 100, weight: 200.0, flightDate: '2026-07-09', flightNo: 'CZ345', time: '2026-07-09 13:00:00' }
],
locations: [
{ id: 'B-01-003', inPerson: '张三', inTime: '2026-07-08 22:15', outPerson: '李四', outTime: '2026-07-09 06:00' },
{ id: 'A-02-010', inPerson: '王五', inTime: '2026-07-09 06:30', outPerson: '—', outTime: '—' },
{ id: 'C-03-007', inPerson: '赵六', inTime: '2026-07-09 13:00', outPerson: '—', outTime: '—' }
]
},
'78133363399': {
billNo: '78133363399', agent: '徽远', code: 'DGR',
pieces: '85', weight: '210.0', type: '危险品运输',
inPieces: '85', inWeight: '210.0', calcWeight: '210.0',
carrier: '3U', route: 'CTU-SIN', packType: '铁箱',
billType: '国际进港(国际直航)', unNo: 'UN3480', dest: 'CTU',
itemNameCn: '锂电池', itemNameEn: 'Lithium Battery',
lockStatus: '锁定', outboundTime: '—', remark: '危险品,单独存放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',
warehouse: [
{ batch: '8', pieces: 45, weight: 110.0, flightDate: '2026-07-08', flightNo: '3U678', time: '2026-07-08 18:00:00' },
{ batch: '9', pieces: 40, weight: 100.0, flightDate: '2026-07-09', flightNo: '3U901', time: '2026-07-09 07:45:00' }
],
locations: [
{ id: 'D-01-002', inPerson: '钱七', inTime: '2026-07-08 18:00', outPerson: '孙八', outTime: '2026-07-09 08:00' },
{ id: 'A-04-015', inPerson: '张三', inTime: '2026-07-09 07:45', outPerson: '—', outTime: '—' }
]
},
'78133363400': {
billNo: '78133363400', agent: '中外运', code: 'PER',
pieces: '350', weight: '430.0', type: '普通货物运输',
inPieces: '350', inWeight: '430.0', calcWeight: '430.0',
carrier: 'HU', route: 'HKG-HFE', packType: '纸箱',
billType: '国际进港(国际直航)', unNo: '—', dest: 'HFE',
itemNameCn: '食品', itemNameEn: 'Food Products',
lockStatus: '未锁定', outboundTime: '2026-07-09 11:30', remark: '保质期至2026-12',
originalManifest: '01', tallyReport: '01', customsRelease: '11',ciTime: '2026-07-08 14:30:00',
warehouse: [
{ batch: '10', pieces: 180, weight: 220.0, flightDate: '2026-07-07', flightNo: 'HU234', time: '2026-07-07 20:00:00' },
{ batch: '11', pieces: 170, weight: 210.0, flightDate: '2026-07-09', flightNo: 'HU567', time: '2026-07-09 10:30:00' }
],
locations: [
{ id: 'B-02-011', inPerson: '李四', inTime: '2026-07-07 20:00', outPerson: '王五', outTime: '2026-07-08 16:00' },
{ id: 'C-01-009', inPerson: '赵六', inTime: '2026-07-09 10:30', outPerson: '—', outTime: '—' }
]
}
};
// 进港仓库卡片列表数据源
const data = [
{ id: "78133363396", status: "已清仓", flight: "20260702/MU1111", route: "LAX-HFE", pcs: 111, weight: 2300 },
{ id: "78133363397", status: "未清仓", flight: "20260702/MU2222", route: "NRT-HFE", pcs: 85, weight: 1560 },
{ id: "78133363398", status: "已清仓", flight: "20260702/MU3333", route: "FRA-HFE", pcs: 200, weight: 4200 },
{ id: "78133363399", status: "未清仓", flight: "20260702/MU4444", route: "LAX-PVG", pcs: 50, weight: 980 },
{ id: "78133363400", status: "未清仓", flight: "20260702/MU5555", route: "CDG-HFE", pcs: 168, weight: 3150 },
{ id: "78133363401", status: "已清仓", flight: "20260702/MU6666", route: "SIN-HFE", pcs: 92, weight: 1800 },
{ id: "78133363402", status: "未清仓", flight: "20260702/MU7777", route: "LAX-NKG", pcs: 145, weight: 2800 },
{ id: "78133363403", status: "已清仓", flight: "20260702/MU8888", route: "HND-HFE", pcs: 73, weight: 1420 }
];
// 渲染仓库卡片列表
function renderList() {
const container = document.getElementById('cardListContainer');
container.innerHTML = data.map((item) => {
const isCleared = item.status === '已清仓';
const departure = item.route.split('-')[0];
const flightNo = item.flight.split('/')[1];
return `
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer mb-3" onclick="showDetail('${item.id}')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">${item.id}</span>
</div>
<div class="flex items-center gap-2">
${getStatusTag(item.status)}
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:${item.pcs}</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:${item.weight}</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span>航班号:${flightNo}</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span>始发站:${departure}</span>
</div>
</div>
<div class="grid grid-cols-4 gap-2 mt-3">
<button onclick="event.stopPropagation(); showToast('执行清仓操作:运单号 ${item.id}')"
class="text-xs py-1.5 px-2 rounded-[6px] font-medium h-8 ${isCleared ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-[#EFF6FF] text-[#2563EB]'}" ${isCleared ? 'disabled' : ''}>
清仓
</button>
<button onclick="event.stopPropagation(); showToast('修改库位:运单号 ${item.id}')"
class="bg-[#EFF6FF] text-[#2563EB] text-xs py-1.5 px-2 rounded-[6px] font-medium h-8">
修改库位
</button>
<button onclick="event.stopPropagation(); showToast('执行出库:运单号 ${item.id}')"
class="bg-[#EFF6FF] text-[#2563EB] text-xs py-1.5 px-2 rounded-[6px] font-medium h-8">
出库
</button>
<button onclick="event.stopPropagation(); showToast('执行入库:运单号 ${item.id}')"
class="bg-[#EFF6FF] text-[#2563EB] text-xs py-1.5 px-2 rounded-[6px] font-medium h-8">
入库
</button>
</div>
</div>`;
}).join('');
}
// 状态标签渲染
function getStatusTag(status) {
let color = '';
switch(status) {
case '已清仓': color = 'bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium'; break;
case '未清仓': color = 'bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium'; break;
}
return `<span class="${color}">${status}</span>`;
}
// 跳转至进港货物页面,填充数据
function showDetail(id) {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
const data = uldData[id];
if (!data) {
showToast("暂无该运单详情数据");
return;
}
// 填充基础信息
document.getElementById('det-billNo').innerText = data.billNo;
document.getElementById('det-agent').innerText = data.agent;
document.getElementById('det-code').innerText = data.code;
document.getElementById('det-carrier').innerText = data.carrier;
document.getElementById('det-route').innerText = data.route;
document.getElementById('det-dest').innerText = data.dest;
document.getElementById('det-billType').innerText = data.billType;
document.getElementById('det-unNo').innerText = data.unNo;
document.getElementById('det-ciTime').innerText = data.ciTime;
// 锁定状态样式
const lockStatusEl = document.getElementById('det-lockStatus');
if (data.lockStatus === '锁定') {
lockStatusEl.innerHTML = '<iconify-icon icon="mdi:lock" class="mr-1"></iconify-icon>锁定';
lockStatusEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-red-50 text-red-600 inline-flex items-center';
} else {
lockStatusEl.innerText = '—';
lockStatusEl.className = 'text-xs font-semibold text-gray-800';
}
document.getElementById('det-packType').innerText = data.packType;
document.getElementById('det-type').innerText = data.type;
document.getElementById('det-outboundTime').innerText = data.outboundTime || '—';
document.getElementById('det-itemNameCn').innerText = data.itemNameCn || '—';
document.getElementById('det-itemNameEn').innerText = data.itemNameEn || '—';
document.getElementById('det-remark').innerText = data.remark || '—';
// 件重信息
document.getElementById('det-pieces').innerText = data.pieces;
document.getElementById('det-weight').innerText = data.weight;
document.getElementById('det-calcWeight').innerText = data.calcWeight;
document.getElementById('det-inPieces').innerText = data.inPieces;
document.getElementById('det-inWeight').innerText = data.inWeight;
// 海关放行标签
const customsReleaseEl = document.getElementById('det-customsRelease');
if (data.customsRelease === '11') {
customsReleaseEl.innerText = '11';
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-green-50 text-green-600 inline-block';
} else {
customsReleaseEl.innerText = data.customsRelease;
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-gray-100 text-gray-600 inline-block';
}
document.getElementById('det-originalManifest').innerText = data.originalManifest;
document.getElementById('det-tallyReport').innerText = data.tallyReport;
// 渲染仓库批次
const warehouseContainer = document.getElementById('tab-content-1');
warehouseContainer.innerHTML = '';
data.warehouse.forEach(item => {
warehouseContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-semibold text-gray-900">批次号: ${item.batch}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">件数</div>
<div class="text-xs font-semibold text-gray-800">${item.pieces}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">重量</div>
<div class="text-xs font-semibold text-gray-800">${parseFloat(item.weight).toFixed(1)} kg</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班日期</div>
<div class="text-xs font-semibold text-gray-800">${item.flightDate}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班号</div>
<div class="text-xs font-semibold text-gray-800">${item.flightNo}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发站</div>
<div class="text-xs font-semibold text-gray-800">${item.route}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</div>
<div class="text-xs font-semibold text-gray-800">${item.dest}</div>
</div>
</div>
</div>`;
});
// 渲染库位信息
const locationContainer = document.getElementById('tab-content-2');
locationContainer.innerHTML = '';
data.locations.forEach(loc => {
locationContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-bold text-gray-900">${loc.id}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库人</div>
<div class="text-xs font-semibold text-gray-800">${loc.inPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库时间</div>
<div class="text-xs font-semibold text-gray-800">${loc.inTime}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库人</div>
<div class="text-xs ${loc.outPerson === '—' ? 'text-gray-500' : 'text-gray-800'} font-semibold">${loc.outPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库时间</div>
<div class="text-xs ${loc.outTime === '—' ? 'text-gray-500' : 'text-gray-800'} font-semibold">${loc.outTime}</div>
</div>
</div>
</div>`;
});
// 默认激活运单信息标签
switchTab(0);
// 页面切换动画:列表左滑隐藏,详情滑入
listPage.classList.add('page-hidden-left');
detailPage.classList.remove('page-hidden-right');
}
// 返回列表页
function goBack() {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
listPage.classList.remove('page-hidden-left');
detailPage.classList.add('page-hidden-right');
}
// 切换详情标签页
function switchTab(index) {
const tabs = document.querySelectorAll('.tab-item');
const contents = document.querySelectorAll('.tab-pane');
tabs.forEach((tab, i) => {
if (i === index) {
tab.classList.add('active');
contents[i].classList.remove('hidden');
} else {
tab.classList.remove('active');
contents[i].classList.add('hidden');
}
});
}
// Toast提示
function showToast(msg) {
const toast = document.getElementById('toast');
toast.innerText = msg;
toast.classList.remove('hidden');
setTimeout(() => toast.classList.add('hidden'), 2000);
}
// 筛选弹窗方法
function openFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterOverlay.classList.add('show');
setTimeout(() => filterSheet.classList.add('show'), 10);
}
function closeFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterSheet.classList.remove('show');
setTimeout(() => filterOverlay.classList.remove('show'), 250);
}
function resetFilter() {
document.querySelectorAll('.filter-input').forEach(input => {
input.value = '';
});
}
function confirmFilter() {
const params = {
flightDate: document.getElementById('filterFlightDate')?.value || '',
flightNo: document.getElementById('filterFlightNo')?.value || '',
agent: '',
code: '',
origin: document.getElementById('filterOrigin')?.value || '',
billType: '',
bizType: '',
cnName: document.getElementById('filterCnName')?.value || ''
};
console.log('筛选参数:', params);
alert('筛选执行成功,控制台可查看筛选条件');
closeFilter();
}
// 扫码弹窗
function toggleScanOverlay(show) {
const overlay = document.getElementById('scanOverlay');
if (show) overlay.classList.remove('hidden');
else overlay.classList.add('hidden');
}
// 页面加载完成初始化
document.addEventListener('DOMContentLoaded', function() {
renderList();
// 筛选遮罩点击关闭
const filterOverlay = document.getElementById('filterOverlay');
filterOverlay.addEventListener('click', function(e) {
if (e.target === filterOverlay) closeFilter();
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,773 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>提取出库 - 移动端原型</title>
<!-- Tailwind CSS -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
/* 模拟手机容器 */
.phone-container {
width: 375px;
height: 812px;
background-color: #f5f6fa;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 8px solid #1a1a1a;
border-radius: 40px;
}
/* 隐藏滚动条但保留功能 */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* 页面切换动画 */
.page {
display: none;
flex-direction: column;
height: 100%;
}
.page.active {
display: flex;
}
/* 筛选面板动画 */
#filter-panel {
transform: translateY(100%);
transition: transform 0.3s ease-out;
}
#filter-panel.show {
transform: translateY(0);
}
.overlay {
background-color: rgba(0,0,0,0.5);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.overlay.show {
opacity: 1;
pointer-events: auto;
}
/* 按钮点击态 */
.btn-active:active {
opacity: 0.7;
transform: scale(0.98);
}
/* 复选框样式 */
.custom-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.custom-checkbox-round {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 50%;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox-round:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox-round:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* 卡片选中样式已移除 */
</style>
</head>
<body>
<div class="phone-container">
<!-- 状态栏模拟 -->
<div class="h-10 w-full flex justify-between items-center px-8 flex-none bg-[#2563EB] text-white">
<span class="text-xs font-semibold">09:41</span>
<div class="flex space-x-1 items-center">
<iconify-icon icon="mdi:signal" width="14"></iconify-icon>
<iconify-icon icon="mdi:wifi" width="14"></iconify-icon>
<iconify-icon icon="mdi:battery" width="18"></iconify-icon>
</div>
</div>
<!-- 页面1: 货运列表首页 -->
<div class="page active" id="page-home">
<!-- 顶部导航 -->
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="alert('返回首页')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">提取出库</h1>
<div class="w-16"></div>
</div>
<!-- 内容滚动区 -->
<div class="flex-1 overflow-y-auto no-scrollbar pb-24">
<!-- 搜索栏 -->
<section class="px-4 pt-4 pb-3 bg-white">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索运单号" type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="toggleFilterDrawer(true)">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- 子Tab切换栏 -->
<div class="flex bg-white border-b sticky top-0 z-20">
<button class="flex-1 py-3 text-sm font-bold text-[#1F2937] border-b-2 border-[#1F2937] transition-all" id="tab-pending" onclick="switchTab('pending')">
待出库 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
<button class="flex-1 py-3 text-sm font-bold text-gray-400 border-b-2 border-transparent transition-all" id="tab-shipped" onclick="switchTab('shipped')">
已出库 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
</div>
<!-- 列表内容 -->
<div class="p-3 pt-4 space-y-3" id="list-pending">
<!-- 卡片 1 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363573</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待出库</span>
</div>
</div>
<!-- 提货车辆行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">提货车辆:皖A88909</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">代理人</span>
<span class="text-[#1F2937] text-[14px] font-semibold">ATR</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">100件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">200kg</span>
</div>
</div>
<!-- 库位信息行 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">库位:A区-03货位</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363573', status: '待出库', agent: 'ATR', flightDate: '20260706', flightNo: 'MU1111', pieces: '100', weight: '200', goodsName: '电子元器件', carrier: 'MU', origin: 'LAX', location: 'A区-03货位', code: 'NOR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 2 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363574</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待出库</span>
</div>
</div>
<!-- 提货车辆行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">提货车辆:皖A78223</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">代理人</span>
<span class="text-[#1F2937] text-[14px] font-semibold">CTA</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">85件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">150kg</span>
</div>
</div>
<!-- 库位信息行 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">库位:B区-05货位</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363574', status: '待出库', agent: 'CTA', flightDate: '20260706', flightNo: 'CA2222', pieces: '85', weight: '150', goodsName: '服装鞋帽', carrier: 'CA', origin: 'PEK', location: 'B区-05货位', code: 'ATR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 3 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363575</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待出库</span>
</div>
</div>
<!-- 提货车辆行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">提货车辆:皖A67651</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">代理人</span>
<span class="text-[#1F2937] text-[14px] font-semibold">DHL</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">200件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">450kg</span>
</div>
</div>
<!-- 库位信息行 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">库位:C区-01货位</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363575', status: '待出库', agent: 'DHL', flightDate: '20260707', flightNo: '3U3333', pieces: '200', weight: '450', goodsName: '精密模具', carrier: '3U', origin: 'CTU', location: 'C区-01货位', code: 'ATR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
</div>
<!-- 已出库列表内容 -->
<div class="p-3 pt-4 space-y-3 hidden" id="list-shipped">
<!-- 卡片 1 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363001</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已出库</span>
</div>
</div>
<!-- 操作信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">提货车辆:皖A23422</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">代理人</span>
<span class="text-[#1F2937] text-[14px] font-semibold">ATR</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">50件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">80kg</span>
</div>
</div>
<!-- 操作人信息 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出库人:张三 · 出库时间:2026-07-05 11:45</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer" onclick="showDetail({id: '78133363001', status: '已出库', agent: 'ATR', flightDate: '20260704', flightNo: 'MU1111', pieces: '50', weight: '80', pickupNo: 'TH20260701001', goodsName: '精密仪器', carrier: 'MU', origin: 'LAX', location: 'B区-05货位', code: 'ATR', counterTime: '2026-07-05 10:30', counterPerson: '张三', outboundTime: '2026-07-05 11:45', outboundPerson: '张三'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 2 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363002</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已出库</span>
</div>
</div>
<!-- 操作信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">提货车辆:皖A79909</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">代理人</span>
<span class="text-[#1F2937] text-[14px] font-semibold">CTA</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">120件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">300kg</span>
</div>
</div>
<!-- 操作人信息 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出库人:赵六 · 出库时间:2026-07-06 10:30</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer" onclick="showDetail({id: '78133363002', status: '已出库', agent: 'CTA', flightDate: '20260705', flightNo: 'CA2222', pieces: '120', weight: '300', pickupNo: 'TH20260702002', goodsName: '鲜花绿植', carrier: 'CA', origin: 'SFO', location: 'A区-08货位', code: 'ATR', counterTime: '2026-07-06 09:15', counterPerson: '王五', outboundTime: '2026-07-06 10:30', outboundPerson: '赵六'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
<!-- 卡片 3 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-3">
<!-- 顶部行 -->
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363003</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已出库</span>
</div>
</div>
<!-- 操作信息行 -->
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:airplane"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">提货车辆:皖A66712</span>
</div>
<!-- 统计框 -->
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">代理人</span>
<span class="text-[#1F2937] text-[14px] font-semibold">DHL</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">75件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">160kg</span>
</div>
</div>
<!-- 操作人信息 -->
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出库人:孙八 · 出库时间:2026-07-06 15:20</span>
</div>
<!-- 查看详情链接 -->
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer" onclick="showDetail({id: '78133363003', status: '已出库', agent: 'DHL', flightDate: '20260705', flightNo: '3U3333', pieces: '75', weight: '160', pickupNo: 'TH20260703003', goodsName: '生鲜食品', carrier: '3U', origin: 'CDG', location: 'D区-02货位', code: 'NOR', counterTime: '2026-07-06 14:00', counterPerson: '钱七', outboundTime: '2026-07-06 15:20', outboundPerson: '孙八'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
</div>
</div>
</div>
<!-- 底部工具栏 -->
<div class="absolute bottom-0 w-full bg-white border-t border-gray-200 z-40 flex flex-col pt-4 pb-4" id="bottom-toolbar">
<!-- 操作区域 -->
<div class="px-4 flex items-center w-full">
<div class="flex items-center gap-2">
<input class="custom-checkbox-round" id="select-all-cb" onclick="toggleSelectAll(this.checked)" type="checkbox"/>
<label class="text-[13px] text-[#1F2937] font-medium cursor-pointer" for="select-all-cb">全选</label>
</div>
<div class="flex-1"></div>
<div class="flex items-center gap-3">
<span class="text-[12px] text-[#9CA3AF]">已选 <span id="selected-count">0</span> 项</span>
<button class="bg-[#2563EB] text-white text-[15px] font-bold px-5 py-2 rounded-[10px] shadow-lg shadow-blue-500/25 btn-active" onclick="alert('出库操作')">
出库
</button>
</div>
</div>
</div>
</div>
<!-- 页面2: 详情页 -->
<div class="page" id="page-detail">
<!-- 顶部导航 -->
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="switchPage('home')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">运单详情</h1>
<div class="w-16"></div>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar pb-8 p-4">
<!-- 货运单据样式卡片 -->
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100">
<!-- 1. 顶部蓝色条带 -->
<div class="bg-[#2563EB] px-4 py-3 flex justify-between items-center text-white">
<span class="font-bold text-[17px] leading-tight" id="detail-awb">781-22222620</span>
<div class="flex items-center text-[13px] font-normal opacity-90">
<span class="w-2 h-2 bg-green-400 rounded-full mr-1.5"></span>
<span id="detail-time">2026-05-10 08:30</span>
</div>
</div>
<div class="p-4">
<!-- 2. 标签区 -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<span class="bg-[#EF4444] text-white text-[11px] px-1.5 py-0.5 rounded leading-tight font-medium">国际进港</span>
<span class="text-[13px] font-normal text-[#666666] border-l border-gray-300 pl-2">普通货物运输</span>
</div>
</div>
<!-- 3. 货物名称 -->
<div class="text-[22px] font-bold text-[#111827] mb-3" id="detail-goods-name">电子元器件</div>
<!-- 4. 两列信息表格 -->
<div class="grid grid-cols-2 gap-x-8 gap-y-3">
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">件数</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-count">55</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">重量</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-weight">622.00 kg</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">特码</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-code">NOR</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">代理</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-agent">ATR</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">始发港</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-origin">LAX</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">目的港</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-carrier">MU</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">航班日期</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-flight-date">20260706</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">航班号</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-flight-no">MU1111</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">库位</div>
<div class="flex">
<span class="bg-orange-50 text-orange-600 px-1 rounded text-[15px] font-bold" id="detail-location">1111111</span>
</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">提货单号</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-delivery-no">TH20260706001</div>
</div>
</div>
</div>
</div>
<!-- 提货信息卡片 -->
<div class="mt-4 bg-white rounded-xl shadow-sm overflow-hidden border border-gray-200 hidden" id="detail-outbound-card">
<div class="bg-[#10B981] px-4 py-2.5">
<span class="text-white font-semibold text-[15px]">提货信息</span>
</div>
<div class="bg-white px-4 py-3 grid grid-cols-2 gap-y-3">
<div class="flex flex-col">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">柜台办理人</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-counter-person">-</span>
</div>
<div class="flex flex-col">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">柜台办理时间</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-counter-time">-</span>
</div>
<div class="flex flex-col" id="detail-outbound-person-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">出库人</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-person">-</span>
</div>
<div class="flex flex-col" id="detail-outbound-time-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">出库时间</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-time">-</span>
</div>
</div>
</div>
</div>
</div>
<!-- 底部筛选面板蒙层 -->
<div class="overlay absolute inset-0 z-40" id="filter-overlay" onclick="toggleFilterDrawer(false)"></div>
<!-- 筛选面板 -->
<div class="absolute bottom-0 w-full bg-white rounded-t-3xl z-50 p-6 shadow-2xl overflow-hidden" id="filter-panel">
<!-- 标题 + 关闭按钮 -->
<div class="relative flex items-center justify-center mb-6">
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
<button class="absolute right-0 top-0 text-gray-400" onclick="toggleFilterDrawer(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
</div>
<div class="space-y-4">
<!-- 航班日期 -->
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择航班日期</span>
<iconify-icon class="text-gray-400" icon="mdi:calendar-month-outline"></iconify-icon>
</div>
</div>
<!-- 航班号 -->
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">航班号</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between">
<input class="bg-transparent outline-none text-sm w-full text-gray-800" placeholder="请输入航班号" type="text"/>
</div>
</div>
<!-- 代理人 -->
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">代理人</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择代理人</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<!-- 特码 -->
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择特码</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="flex gap-4 pt-2">
<button class="flex-1 bg-white border border-gray-300 text-gray-600 py-3.5 rounded-xl font-bold btn-active" onclick="alert('已重置')">
重置
</button>
<button class="flex-1 bg-[#2563EB] text-white py-3.5 rounded-xl font-bold shadow-lg shadow-blue-500/20 btn-active" onclick="toggleFilterDrawer(false)">
确认
</button>
</div>
</div>
</div>
</div>
<script>
// 页面切换逻辑
function switchPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById('page-' + pageId).classList.add('active');
// 如果进入详情页,可以重置滚动
if(pageId === 'detail') {
document.querySelector('#page-detail .overflow-y-auto').scrollTop = 0;
}
}
// 子Tab切换逻辑
function switchTab(type) {
const tabPending = document.getElementById('tab-pending');
const tabShipped = document.getElementById('tab-shipped');
const listPending = document.getElementById('list-pending');
const listShipped = document.getElementById('list-shipped');
const bottomToolbar = document.getElementById('bottom-toolbar');
if (type === 'pending') {
tabPending.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabPending.classList.remove('text-gray-400', 'border-transparent');
tabShipped.classList.add('text-gray-400', 'border-transparent');
tabShipped.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listPending.classList.remove('hidden');
listShipped.classList.add('hidden');
bottomToolbar.style.display = 'flex';
} else {
tabShipped.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabShipped.classList.remove('text-gray-400', 'border-transparent');
tabPending.classList.add('text-gray-400', 'border-transparent');
tabPending.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listShipped.classList.remove('hidden');
listPending.classList.add('hidden');
bottomToolbar.style.display = 'none';
}
}
// 全选/取消全选
function toggleSelectAll(checked) {
const checkboxes = document.querySelectorAll('.item-checkbox');
checkboxes.forEach(cb => {
cb.checked = checked;
updateCardStyle(cb);
});
updateSelectionCount();
}
// 处理卡片复选框点击
function handleCheckboxClick(checkbox, event) {
event.stopPropagation(); // 阻止触发卡片详情点击
updateCardStyle(checkbox);
updateSelectionCount();
// 更新全选状态
const checkboxes = document.querySelectorAll('.item-checkbox');
const selectAllCb = document.getElementById('select-all-cb');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
selectAllCb.checked = allChecked;
}
// 更新卡片选中样式 (已根据需求移除背景和边框变化,仅保留复选框状态)
function updateCardStyle(checkbox) {
// 卡片不再随选中状态改变背景和边框颜色
}
// 更新选中计数
function updateSelectionCount() {
const checkedCount = document.querySelectorAll('.item-checkbox:checked').length;
document.getElementById('selected-count').textContent = checkedCount;
}
// 显示详情页并填充数据
function showDetail(data) {
if (!data) return;
const setText = (id, text) => {
const el = document.getElementById(id);
if (el) el.textContent = text || '-';
};
setText('detail-awb', data.id);
setText('detail-time', new Date().toISOString().replace('T', ' ').substring(0, 16));
setText('detail-goods-name', data.goodsName);
setText('detail-count', data.pieces);
setText('detail-weight', data.weight + (data.weight && data.weight.toString().includes('kg') ? '' : ' kg'));
setText('detail-code', data.code);
setText('detail-agent', data.agent);
setText('detail-carrier', data.carrier);
setText('detail-origin', data.origin);
setText('detail-flight-date', data.flightDate);
setText('detail-flight-no', data.flightNo);
setText('detail-location', data.location);
setText('detail-delivery-no', data.pickupNo);
const outboundCard = document.getElementById('detail-outbound-card');
if (outboundCard) {
// 始终显示提货信息卡片
outboundCard.classList.remove('hidden');
setText('detail-counter-person', data.counterPerson);
setText('detail-counter-time', data.counterTime);
setText('detail-outbound-person', data.outboundPerson);
setText('detail-outbound-time', data.outboundTime);
const outboundPersonRow = document.getElementById('detail-outbound-person-row');
const outboundTimeRow = document.getElementById('detail-outbound-time-row');
if (data.status === '已出库') {
// 已出库显示全部
if (outboundPersonRow) outboundPersonRow.classList.remove('hidden');
if (outboundTimeRow) outboundTimeRow.classList.remove('hidden');
} else {
// 待出库隐藏出库人信息
if (outboundPersonRow) outboundPersonRow.classList.add('hidden');
if (outboundTimeRow) outboundTimeRow.classList.add('hidden');
}
}
switchPage('detail');
}
// 筛选面板显示隐藏
function toggleFilterDrawer(show) {
const panel = document.getElementById('filter-panel');
const overlay = document.getElementById('filter-overlay');
if (show) {
panel.classList.add('show');
overlay.classList.add('show');
} else {
panel.classList.remove('show');
overlay.classList.remove('show');
}
}
// 初始化显示日期
const today = new Date().toISOString().split('T')[0];
console.log("System date initialized:", today);
</script>
</body>
</html>

View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" 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>
@font-face {
font-family: 'Inter';
src: local('sans-serif');
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* 模拟手机容器阴影 */
.phone-shell {
box-shadow: 0 50px 100px -20px rgba(0,0,0,0.25), 0 30px 60px -30px rgba(0,0,0,0.3);
}
</style>
</head>
<body class="bg-gray-200 flex justify-center items-center min-h-screen p-4">
<!-- 模拟手机屏幕容器 -->
<div class="phone-shell w-[375px] h-[812px] bg-[#F5F5F7] overflow-hidden flex flex-col relative rounded-[48px] border-[10px] border-gray-900">
<!-- 顶部蓝色区域 (包含状态栏和导航栏) -->
<div class="bg-[#1A73E8] flex-none">
<!-- 系统状态栏模拟 -->
<div class="h-11 w-full flex justify-between items-center px-8 text-white">
<span class="text-[15px] font-bold">09:41</span>
<div class="flex gap-1.5 items-center">
<iconify-icon class="text-sm" icon="mdi:signal-cellular-3"></iconify-icon>
<iconify-icon class="text-base" icon="mdi:wifi"></iconify-icon>
<iconify-icon class="text-xl" icon="mdi:battery"></iconify-icon>
</div>
</div>
<!-- 导航栏 -->
<div class="h-[44px] px-4 flex items-center justify-between">
<button class="flex items-center text-white space-x-0.5">
<iconify-icon class="text-2xl" icon="mdi:chevron-left"></iconify-icon>
<span class="text-base">返回</span>
</button>
<h1 class="text-lg font-bold text-white absolute left-1/2 -translate-x-1/2">运单追踪</h1>
<div class="w-10"></div> <!-- 占位保持平衡 -->
</div>
</div>
<!-- 可滚动内容区 -->
<div class="flex-1 overflow-y-auto no-scrollbar px-5 pt-6 pb-12">
<!-- 流转状态卡片 -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100/50 mb-5">
<!-- 运单号 Header -->
<div class="flex items-center justify-between pt-2 pb-5 mb-8 border-b border-gray-100">
<div class="text-xl font-black text-gray-900 leading-none tracking-tight">
78133358743
</div>
<span class="text-[11px] font-medium text-blue-600 bg-blue-50 px-2.5 py-1 rounded-full border border-blue-100">国际进港</span>
</div>
<!-- 流转状态标题 -->
<div class="mb-6">
<h2 class="text-base font-semibold text-gray-700 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-700 inline-block" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
流转状态
</h2>
</div>
<div class="relative pl-7 pb-2">
<div class="absolute left-[9px] top-2 bottom-4 w-[2px] bg-gray-100"></div>
<!-- 状态 1: 当前 -->
<div class="relative mb-10">
<div class="absolute -left-[27px] top-1 w-[20px] h-[20px] rounded-full bg-green-600 border-4 border-green-100 flex items-center justify-center ring-1 ring-green-600/20">
<div class="w-2 h-2 rounded-full bg-white"></div>
</div>
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-green-700">原始舱单</span>
<span class="text-green-600 text-xs font-normal bg-green-50 px-2 py-0.5 rounded-full">放行状态:01</span>
</div>
<div class="text-gray-400 text-xs font-normal flex items-center gap-1.5">
<span>2026-07-10 11:03:38</span>
</div>
</div>
</div>
<!-- 状态 2 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">航班落地</span>
</div>
</div>
<!-- 状态 2 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">分拣理货</span>
</div>
</div>
<!-- 状态 3 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">理货申报</span>
</div>
</div>
<!-- 状态 4 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">海关放行</span>
</div>
</div>
<!-- 状态 5 -->
<div class="relative mb-10">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">柜台办结</span>
</div>
</div>
<!-- 状态 6 -->
<div class="relative mb-2">
<div class="absolute -left-[22px] top-1.5 w-[10px] h-[10px] rounded-full bg-gray-200 border-2 border-white"></div>
<div class="flex flex-col">
<span class="text-sm font-bold text-gray-400">提取出库</span>
</div>
</div>
</div>
</div>
<!-- 模块 4: 操作详情卡片 -> 已删除 -->
<!-- 底部占位 -->
<div class="text-center py-6">
<p class="text-[10px] text-gray-300 uppercase tracking-[0.2em] font-bold">查询信息已触底</p>
</div>
</div>
<!-- 底部 Home Indicator 模拟 -->
<div class="h-8 w-full flex justify-center items-center flex-none bg-white/60 backdrop-blur-md">
<div class="w-32 h-1.5 bg-black rounded-full opacity-20"></div>
</div>
</div>
</body>
</html>ml&gt;

View File

@@ -0,0 +1,951 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>进港查询</title>
<!-- Tailwind CSS (本地) -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify 图标 -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #e5e7eb;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
/* 隐藏滚动条但保留功能 */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.tag-occupied { background-color: #dcfce7; color: #22c55e; } /* 已出库 - 绿色 */
.tag-available { background-color: #dcfce7; color: #22c55e; } /* 已出库 - 绿色 */
.tag-repair { background-color: #fce7e7; color: #ef4444; } /* 离港 - 红色 */
/* 页面切换动画 */
.page {
position: absolute;
inset: 0;
transition: transform 0.3s ease-in-out;
background-color: #f3f4f6;
display: flex;
flex-direction: column;
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
}
.page::-webkit-scrollbar {
display: none;
}
.page-hidden-right { transform: translateX(100%); }
.page-hidden-left { transform: translateX(-100%); }
/* 统一的深蓝色调 */
.bg-header-blue { background-color: #2563EB; }
/* 标签页样式 */
.tab-item {
position: relative;
padding: 12px 0;
color: #999;
font-size: 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex: 1;
text-align: center;
}
.tab-item.active {
color: #2563eb;
font-weight: bold;
}
.tab-item::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background-color: #2563eb;
transition: width 0.3s ease;
}
.tab-item.active::after {
width: 40%;
}
/* ========== 筛选弹窗样式 提升层级+动画稳定 ========== */
.filter-overlay {
position: absolute;
inset: 0;
background-color: rgba(0,0,0,0.4);
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s ease, visibility 0.25s ease;
}
.filter-overlay.show {
opacity: 1;
visibility: visible;
}
.filter-sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 20px;
z-index: 10000;
transform: translateY(100%);
transition: transform 0.25s ease-out;
max-height: 80vh;
overflow-y: auto;
}
.filter-sheet.show {
transform: translateY(0);
}
/* 筛选输入框统一样式 */
.filter-input {
width: 100%;
height: 40px;
padding: 0 12px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
outline: none;
box-sizing: border-box;
}
.filter-input:focus {
border-color: #2563EB;
}
/* 下拉框箭头浅灰色 + 全浏览器兼容修复 */
select.filter-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%239ca3af' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 36px;
}
/* 清除IE默认下拉箭头 */
select.filter-input::-ms-expand {
display: none;
}
</style>
</head>
<body>
<div class="w-[375px] h-[812px] bg-[#F3F4F6] overflow-hidden flex flex-col relative shadow-2xl rounded-[40px] border-[8px] border-gray-800">
<!-- 状态栏 -->
<div class="bg-header-blue flex-none z-50">
<div class="flex justify-between items-center px-6 pt-3 pb-1 text-[12px] font-medium text-white">
<span>09:41</span>
<div class="flex gap-1.5 items-center">
<iconify-icon icon="material-symbols:signal-cellular-4-bar-rounded"></iconify-icon>
<iconify-icon icon="material-symbols:wifi"></iconify-icon>
<iconify-icon icon="material-symbols:battery-full-rounded"></iconify-icon>
</div>
</div>
</div>
<!-- 筛选弹窗容器 移到最外层,层级提升 -->
<div class="filter-overlay" id="filterOverlay"></div>
<div class="filter-sheet" id="filterSheet">
<!-- 顶部标题栏 -->
<div class="relative flex items-center justify-center mb-6">
<button class="absolute right-0 top-0 text-gray-400" onclick="closeFilter()">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
<h2 class="text-[18px] font-bold text-gray-900">筛选条件</h2>
</div>
<!-- 筛选表单区域 -->
<div class="space-y-4 mb-7">
<!-- 1.航班日期 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期起</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<!-- 1.航班日期 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期止</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<!-- 2.航班号 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班号</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入航班号" type="text" id="filterFlightNo">
</div>
</div>
<!-- 3.代理 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">代理</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择代理" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 4.特码 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择特码" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 5.始发站 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">始发站</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入始发站" type="text" id="filterOrigin">
</div>
</div>
<!-- 6.运单类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">运单类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择运单类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 7.业务类型 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">业务类型</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择业务类型" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:chevron-down" width="18"></iconify-icon>
</div>
</div>
<!-- 8.品名(中) -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">品名(中)</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入中文品名" type="text" id="filterCnName">
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="flex gap-3">
<button class="flex-1 h-11 bg-gray-100 text-gray-600 rounded-lg text-sm font-medium" onclick="resetFilter()">重置</button>
<button class="flex-1 h-11 bg-[#2563EB] text-white rounded-lg text-sm font-medium" onclick="confirmFilter()">确定</button>
</div>
</div>
<!-- 页面主容器 -->
<div class="flex-1 relative overflow-hidden">
<!-- 列表页 -->
<div class="page z-10" id="listPage">
<!-- 头部 -->
<header class="bg-header-blue pt-2 pb-4 px-4 flex items-center relative flex-none">
<button class="flex items-center text-white text-sm absolute left-4">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="mx-auto text-white font-bold text-lg">进港查询</h1>
</header>
<!-- 搜索栏 筛选按钮已移除边框 -->
<div class="p-3 bg-transparent flex items-center gap-3 border-b border-gray-100 flex-none">
<div class="flex-1 bg-white rounded-xl flex items-center px-3 py-[10px] gap-2 border border-gray-50">
<iconify-icon class="text-[#9ca3af] text-lg" icon="mdi:magnify"></iconify-icon>
<input class="bg-transparent text-[14px] w-full outline-none text-gray-700 placeholder-[#9ca3af]" placeholder="搜索运单号" type="text"/>
<iconify-icon class="text-[#2563eb] text-lg" icon="mdi:qrcode-scan" onclick="alert('开启扫码功能')"></iconify-icon>
</div>
<!-- 筛选按钮 无圆形实线边框 -->
<button id="filterBtn" class="w-10 h-10 flex items-center justify-center bg-white rounded-full relative z-[99999]">
<iconify-icon class="text-[#2563eb] text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
<!-- 统计标签 -->
<div class="flex items-center gap-2 px-4 pb-3">
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#2563EB]">
<div class="text-[11px] text-gray-500">总票数</div>
<div class="text-[22px] font-bold text-[#2563EB] leading-tight mt-0.5">4</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-orange-500">
<div class="text-[11px] text-gray-500">已入库</div>
<div class="text-[22px] font-bold text-orange-500 leading-tight mt-0.5">2</div>
</div>
<div class="flex-1 bg-white rounded-lg px-3 py-2.5 shadow-sm border-l-[4px] border-l-[#10B981]">
<div class="text-[11px] text-gray-500">已出库</div>
<div class="text-[22px] font-bold text-[#10B981] leading-tight mt-0.5">2</div>
</div>
</div>
<!-- ULD 列表内容 -->
<div class="p-3 space-y-3 pb-24 overflow-y-auto hide-scrollbar">
<!-- 卡片 1 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363396')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363396</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已出库</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:444</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:555</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
</div>
<!-- 卡片 2 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363397')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363397</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已出库</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:120</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:320</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:徽远</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:PER</span>
</div>
</div>
</div>
<!-- 卡片 3 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363398')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363398</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已入库</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:268</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:780</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:NOR</span>
</div>
</div>
</div>
<!-- 卡片 4 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363399')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363399</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已出库</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:85</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:210</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:徽远</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:DGR</span>
</div>
</div>
</div>
<!-- 卡片 5 -->
<div class="bg-white rounded-xl p-4 shadow-sm active:bg-gray-50 transition-colors border border-gray-50 cursor-pointer" onclick="showDetail('78133363400')">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<span class="font-bold text-[#1a1a1a]">78133363400</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-green-100 text-green-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">已出库</span>
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:chevron-right"></iconify-icon>
</div>
</div>
<div class="grid grid-cols-2 gap-y-2 text-[13px] text-gray-500">
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:package-variant-closed"></iconify-icon>
<span>件数:350</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:weight-kilogram"></iconify-icon>
<span>重量:430</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-tie"></iconify-icon>
<span>代理人:中外运</span>
</div>
<div class="flex items-center gap-2">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:tag-outline"></iconify-icon>
<span>特码:PER</span>
</div>
</div>
</div>
</div>
</div>
<!-- 详情页 -->
<div class="page z-20 page-hidden-right !bg-[#f0f2f5]" id="detailPage">
<!-- 头部 -->
<header class="bg-header-blue pt-2 pb-4 px-4 flex items-center relative flex-none">
<button class="flex items-center text-white text-sm absolute left-4 active:opacity-70" onclick="goBack()">
<iconify-icon class="text-xl" icon="mdi:chevron-left"></iconify-icon>
<span>返回</span>
</button>
<h1 class="mx-auto text-white font-bold text-lg">进港货物</h1>
<div class="absolute right-4 flex items-center">
<iconify-icon class="text-red-500 text-2xl" icon="mdi:file-document-edit-outline"></iconify-icon>
</div>
</header>
<!-- 标签页 -->
<div class="bg-white px-4 border-b border-gray-200 flex-none">
<div class="flex justify-around">
<div class="tab-item active cursor-pointer" onclick="switchTab(0)">运单信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(1)">仓库信息</div>
<div class="tab-item cursor-pointer" onclick="switchTab(2)">库位信息</div>
</div>
</div>
<!-- 详情内容区 -->
<div class="flex-1 overflow-y-auto hide-scrollbar">
<!-- 运单信息标签页 -->
<div class="tab-pane p-3 space-y-3" id="tab-content-0">
<!-- 卡片1:基本信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800" id="det-uldTitle">基本信息</span>
</div>
<div class="p-3 grid grid-cols-2 gap-2">
<!-- Row 1 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billNo"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">代理人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-agent"></span>
</div>
<!-- Row 2 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">特码</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-code"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">承运人</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-carrier"></span>
</div>
<!-- Row 3 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发港</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-route"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">中转站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-outboundTime"></span>
</div>
<!-- Row 4 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-dest"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">UN编号</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-unNo"></span>
</div>
<!-- Row 5 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-lockStatus-container">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5" id="det-lockStatus-label">锁定状态</span>
<div id="det-lockStatus-wrapper">
<span class="text-xs font-semibold" id="det-lockStatus"></span>
</div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">包装类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-packType"></span>
</div>
<!-- Row 6 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">业务类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-type"></span>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库时间</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-outboundTime"></span>
</div>
<!-- Row 7 运单类型单独一行 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单类型</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-billType"></span>
</div>
<!-- Row 8 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(中)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameCn"></span>
</div>
<!-- Row 9 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">品名(英)</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-itemNameEn"></span>
</div>
<!-- Row 10 -->
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center col-span-2">
<span class="text-[10px] text-gray-500 text-[14px] mb-0.5">备注</span>
<span class="text-xs text-gray-800 font-semibold truncate" id="det-remark"></span>
</div>
</div>
</div>
<!-- 卡片2:件重信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">件重信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-pieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">运单重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-weight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">计费重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-calcWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">已出库件数</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inPieces"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">已出库重量</div>
<div class="text-xs text-gray-800 font-semibold" id="det-inWeight"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center opacity-0">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">—</div>
<div class="text-xs text-gray-800 font-semibold">—</div>
</div>
</div>
</div>
<!-- 卡片3:回执信息 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-4 py-2.5 flex items-center border-b border-gray-100">
<div class="w-1 h-4 bg-blue-500 rounded-full inline-block mr-2"></div>
<span class="text-xs font-semibold text-gray-800">回执信息</span>
</div>
<div class="p-3 grid grid-cols-3 gap-2">
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">原始舱单</div>
<div class="text-xs text-gray-800 font-semibold" id="det-originalManifest"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">理货报告</div>
<div class="text-xs text-gray-800 font-semibold" id="det-tallyReport"></div>
</div>
<div class="bg-gray-50/30 rounded-lg p-2.5 flex flex-col justify-center" id="det-customsRelease-container">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">海关放行</div>
<div id="det-customsRelease-wrapper">
<div class="text-xs font-semibold" id="det-customsRelease"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 仓库信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-1"></div>
<!-- 库位信息标签页 -->
<div class="tab-pane p-3 space-y-3 hidden" id="tab-content-2"></div>
</div>
</div>
</div>
</div>
<script>
const uldData = {
'78133363396': {
billNo: '78133363396', agent: '中外运', code: 'DGR',
pieces: '444', weight: '555.0', type: '普通货物运输',
inPieces: '444', inWeight: '555.0', calcWeight: '555.0',
carrier: 'CA', route: 'LAX', packType: '纸箱',
billType: '国际进港(国际直航)', unNo: '—', dest: 'HFE',
itemNameCn: '电子元器件', itemNameEn: 'Electronic Components',
lockStatus: '未锁定', outboundTime: '—', remark: '需冷藏存储',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '1', pieces: 200, weight: 300.0, flightDate: '2026-07-08', flightNo: 'CA1234',route: 'LAX',dest: 'HFE', time: '2026-07-08 14:30:00' },
{ batch: '2', pieces: 244, weight: 255.0, flightDate: '2026-07-09', flightNo: 'CA5678',route: 'LAX',dest: 'HFE', time: '2026-07-09 09:10:46' }
],
locations: [
{ id: 'A-01-001', inPerson: '张三', inTime: '2026-07-08 14:30', outPerson: '李四', outTime: '2026-07-09 10:00' },
{ id: 'B-02-005', inPerson: '王五', inTime: '2026-07-09 09:10', outPerson: '—', outTime: '—' }
]
},
'78133363397': {
billNo: '78133363397', agent: '徽远', code: 'PER',
pieces: '120', weight: '320.0', type: '普通货物运输',
inPieces: '120', inWeight: '320.0', calcWeight: '320.0',
carrier: 'MU', route: 'PVG-NRT', packType: '托盘',
billType: '国际进港(国际直航)', unNo: '—', dest: 'PVG',
itemNameCn: '服装', itemNameEn: 'Garments',
lockStatus: '未锁定', outboundTime: '—', remark: '易碎品,轻拿轻放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '3', pieces: 60, weight: 150.0, flightDate: '2026-07-07', flightNo: 'MU123',route: 'LAX',dest: 'HFE', time: '2026-07-07 16:20:00' },
{ batch: '4', pieces: 60, weight: 170.0, flightDate: '2026-07-08', flightNo: 'MU456',route: 'LAX',dest: 'HFE', time: '2026-07-08 11:00:00' }
],
locations: [
{ id: 'A-03-012', inPerson: '赵六', inTime: '2026-07-07 16:20', outPerson: '钱七', outTime: '2026-07-08 14:00' },
{ id: 'C-01-008', inPerson: '孙八', inTime: '2026-07-08 11:00', outPerson: '—', outTime: '—' }
]
},
'78133363398': {
billNo: '78133363398', agent: '中外运', code: 'NOR',
pieces: '268', weight: '780.0', type: '普通货物运输',
inPieces: '268', inWeight: '780.0', calcWeight: '780.0',
carrier: 'CZ', route: 'CAN-AMS', packType: '托盘',
billType: '国际进港(国际直航)', unNo: '—', dest: 'CAN',
itemNameCn: '机械设备', itemNameEn: 'Machinery Equipment',
lockStatus: '锁定', outboundTime: '2026-07-09 08:00', remark: '需叉车装卸',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '5', pieces: 150, weight: 420.0, flightDate: '2026-07-08', flightNo: 'CZ789',route: 'LAX',dest: 'HFE', time: '2026-07-08 22:15:00' },
{ batch: '6', pieces: 118, weight: 360.0, flightDate: '2026-07-09', flightNo: 'CZ012',route: 'LAX',dest: 'HFE', time: '2026-07-09 06:30:00' },
{ batch: '7', pieces: 100, weight: 200.0, flightDate: '2026-07-09', flightNo: 'CZ345',route: 'LAX',dest: 'HFE', time: '2026-07-09 13:00:00' }
],
locations: [
{ id: 'B-01-003', inPerson: '张三', inTime: '2026-07-08 22:15', outPerson: '李四', outTime: '2026-07-09 06:00' },
{ id: 'A-02-010', inPerson: '王五', inTime: '2026-07-09 06:30', outPerson: '—', outTime: '—' },
{ id: 'C-03-007', inPerson: '赵六', inTime: '2026-07-09 13:00', outPerson: '—', outTime: '—' }
]
},
'78133363399': {
billNo: '78133363399', agent: '徽远', code: 'DGR',
pieces: '85', weight: '210.0', type: '危险品运输',
inPieces: '85', inWeight: '210.0', calcWeight: '210.0',
carrier: '3U', route: 'CTU-SIN', packType: '铁箱',
billType: '国际进港(国际直航)', unNo: 'UN3480', dest: 'CTU',
itemNameCn: '锂电池', itemNameEn: 'Lithium Battery',
lockStatus: '锁定', outboundTime: '—', remark: '危险品,单独存放',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '8', pieces: 45, weight: 110.0, flightDate: '2026-07-08', flightNo: '3U678',route: 'LAX',dest: 'HFE', time: '2026-07-08 18:00:00' },
{ batch: '9', pieces: 40, weight: 100.0, flightDate: '2026-07-09', flightNo: '3U901',route: 'LAX',dest: 'HFE', time: '2026-07-09 07:45:00' }
],
locations: [
{ id: 'D-01-002', inPerson: '钱七', inTime: '2026-07-08 18:00', outPerson: '孙八', outTime: '2026-07-09 08:00' },
{ id: 'A-04-015', inPerson: '张三', inTime: '2026-07-09 07:45', outPerson: '—', outTime: '—' }
]
},
'78133363400': {
billNo: '78133363400', agent: '中外运', code: 'PER',
pieces: '350', weight: '430.0', type: '普通货物运输',
inPieces: '350', inWeight: '430.0', calcWeight: '430.0',
carrier: 'HU', route: 'HKG-HFE', packType: '纸箱',
billType: '国际进港(国际直航)', unNo: '—', dest: 'HFE',
itemNameCn: '食品', itemNameEn: 'Food Products',
lockStatus: '未锁定', outboundTime: '2026-07-09 11:30', remark: '保质期至2026-12',
originalManifest: '01', tallyReport: '01', customsRelease: '11',
warehouse: [
{ batch: '10', pieces: 180, weight: 220.0, flightDate: '2026-07-07', flightNo: 'HU234',route: 'LAX',dest: 'HFE', time: '2026-07-07 20:00:00' },
{ batch: '11', pieces: 170, weight: 210.0, flightDate: '2026-07-09', flightNo: 'HU567',route: 'LAX',dest: 'HFE', time: '2026-07-09 10:30:00' }
],
locations: [
{ id: 'B-02-011', inPerson: '李四', inTime: '2026-07-07 20:00', outPerson: '王五', outTime: '2026-07-08 16:00' },
{ id: 'C-01-009', inPerson: '赵六', inTime: '2026-07-09 10:30', outPerson: '—', outTime: '—' }
]
}
};
// 核心修复:等待DOM全部渲染完成后再绑定点击事件,避免获取元素为null
document.addEventListener('DOMContentLoaded', function() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
const filterBtn = document.getElementById('filterBtn');
// 点击遮罩空白关闭,添加self防止弹窗内部点击穿透关闭
filterOverlay.addEventListener('click', function(e) {
if (e.target === filterOverlay) {
closeFilter();
}
});
// 筛选按钮点击打开弹窗
filterBtn.addEventListener('click', openFilter);
});
// 打开筛选弹窗
function openFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterOverlay.classList.add('show');
setTimeout(() => filterSheet.classList.add('show'), 10);
}
// 关闭筛选弹窗
function closeFilter() {
const filterOverlay = document.getElementById('filterOverlay');
const filterSheet = document.getElementById('filterSheet');
filterSheet.classList.remove('show');
setTimeout(() => filterOverlay.classList.remove('show'), 250);
}
// 重置所有筛选输入框
function resetFilter() {
document.querySelectorAll('.filter-input').forEach(input => {
input.value = '';
});
}
// 确定筛选(模拟查询,可对接后端筛选逻辑)
function confirmFilter() {
const params = {
flightDate: document.getElementById('filterFlightDate').value,
flightNo: document.getElementById('filterFlightNo').value,
agent: document.getElementById('filterAgent').value,
code: document.getElementById('filterCode').value,
origin: document.getElementById('filterOrigin').value,
billType: document.getElementById('filterBillType').value,
bizType: document.getElementById('filterBizType').value,
cnName: document.getElementById('filterCnName').value
};
console.log('筛选参数:', params);
alert('筛选执行成功,控制台可查看筛选条件');
closeFilter();
}
function showDetail(id) {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
const data = uldData[id];
if (data) {
document.getElementById('det-uldTitle').innerText = '基本信息';
document.getElementById('det-billNo').innerText = data.billNo;
document.getElementById('det-agent').innerText = data.agent;
document.getElementById('det-code').innerText = data.code;
document.getElementById('det-carrier').innerText = data.carrier;
document.getElementById('det-route').innerText = data.route;
document.getElementById('det-dest').innerText = data.dest;
document.getElementById('det-billType').innerText = data.billType;
document.getElementById('det-unNo').innerText = data.unNo;
// 锁定状态渲染
const lockStatusEl = document.getElementById('det-lockStatus');
if (data.lockStatus === '锁定') {
lockStatusEl.innerHTML = '<iconify-icon icon="mdi:lock" class="mr-1"></iconify-icon>锁定';
lockStatusEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-red-50 text-red-600 inline-flex items-center';
} else {
lockStatusEl.innerHTML = '—';
lockStatusEl.className = 'text-xs font-semibold text-gray-800';
}
document.getElementById('det-packType').innerText = data.packType;
document.getElementById('det-type').innerText = data.type;
document.getElementById('det-outboundTime').innerText = data.outboundTime || '—';
document.getElementById('det-itemNameCn').innerText = data.itemNameCn || '—';
document.getElementById('det-itemNameEn').innerText = data.itemNameEn || '—';
document.getElementById('det-remark').innerText = data.remark || '—';
// 件重信息
document.getElementById('det-pieces').innerText = data.pieces;
document.getElementById('det-weight').innerText = data.weight;
document.getElementById('det-calcWeight').innerText = data.calcWeight;
document.getElementById('det-inPieces').innerText = data.inPieces;
document.getElementById('det-inWeight').innerText = data.inWeight;
// 海关放行标签
const customsReleaseEl = document.getElementById('det-customsRelease');
if (data.customsRelease === '11') {
customsReleaseEl.innerText = '已放行';
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-green-50 text-green-600 inline-block';
} else {
customsReleaseEl.innerText = data.customsRelease || '未放行';
customsReleaseEl.className = 'text-xs font-semibold px-2 py-0.5 rounded-md bg-gray-100 text-gray-600 inline-block';
}
// 渲染仓库批次列表
const warehouseContainer = document.getElementById('tab-content-1');
warehouseContainer.innerHTML = '';
data.warehouse.forEach(item => {
warehouseContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-semibold text-gray-900">批次号: ${item.batch}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">件数</div>
<div class="text-xs font-semibold text-gray-800">${item.pieces}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">重量</div>
<div class="text-xs font-semibold text-gray-800">${parseFloat(item.weight).toFixed(1)} kg</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班日期</div>
<div class="text-xs font-semibold text-gray-800">${item.flightDate}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">航班号</div>
<div class="text-xs font-semibold text-gray-800">${item.flightNo}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">始发站</div>
<div class="text-xs font-semibold text-gray-800">${item.route}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">目的站</div>
<div class="text-xs font-semibold text-gray-800">${item.dest}</div>
</div>
</div>
</div>`;
});
// 渲染库位信息
const locationContainer = document.getElementById('tab-content-2');
locationContainer.innerHTML = '';
data.locations.forEach(loc => {
locationContainer.innerHTML += `
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="flex justify-between items-center pt-3 pb-1.5 px-3 mb-2 border-b border-gray-100">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 inline-block"></span>
<span class="text-xs font-bold text-gray-900">${loc.id}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-1.5 px-3 pb-2">
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库人</div>
<div class="text-xs font-semibold text-gray-800">${loc.inPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">入库时间</div>
<div class="text-xs font-semibold text-gray-800">${loc.inTime}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库人</div>
<div class="text-xs ${loc.outPerson === '—' ? 'text-gray-500 text-[14px]' : 'text-gray-800'} font-semibold">${loc.outPerson}</div>
</div>
<div class="bg-gray-50/50 rounded-lg p-2">
<div class="text-[10px] text-gray-500 text-[14px] mb-0.5">出库时间</div>
<div class="text-xs ${loc.outTime === '—' ? 'text-gray-500 text-[14px]' : 'text-gray-800'} font-semibold">${loc.outTime}</div>
</div>
</div>
</div>`;
});
}
switchTab(0);
listPage.classList.add('page-hidden-left');
detailPage.classList.remove('page-hidden-right');
}
function goBack() {
const listPage = document.getElementById('listPage');
const detailPage = document.getElementById('detailPage');
listPage.classList.remove('page-hidden-left');
detailPage.classList.add('page-hidden-right');
}
function switchTab(index) {
const tabs = document.querySelectorAll('.tab-item');
const contents = document.querySelectorAll('.tab-pane');
tabs.forEach((tab, i) => {
if (i === index) {
tab.classList.add('active');
contents[i].classList.remove('hidden');
} else {
tab.classList.remove('active');
contents[i].classList.add('hidden');
}
});
}
// 搜索框聚焦监听
document.querySelector('input[placeholder="搜索ULD编号"]').addEventListener('focus', () => {
console.log('搜索框已激活');
});
</script>
</div>
</body>
</html>

View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<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>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.phone-container {
width: 375px;
height: 812px;
background-color: #f5f6fa;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 8px solid #1a1a1a;
border-radius: 40px;
}
</style>
</head>
<body>
<div class="phone-container">
<!-- 状态栏 -->
<div class="h-10 w-full flex justify-between items-center px-8 flex-none bg-[#2563EB] text-white">
<span class="text-xs font-semibold">09:41</span>
<div class="flex gap-1 items-center">
<iconify-icon icon="mdi:signal" width="14"></iconify-icon>
<iconify-icon icon="mdi:wifi" width="14"></iconify-icon>
<iconify-icon icon="mdi:battery" width="18"></iconify-icon>
</div>
</div>
<!-- 顶部导航栏 -->
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center" onclick="closeWin()">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">拍照上传</h1>
<div class="w-16"></div>
</div>
<!-- 内容区域 -->
<div class="flex-1 overflow-y-auto p-4 bg-[#f5f6fa]">
<div class="bg-white rounded-xl p-5 shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-bold">运单照片上传</h2>
</div>
<div id="previewWrap" class="grid grid-cols-3 gap-2 mb-4 min-h-[100px]"></div>
<input type="file" id="fileInput" accept="image/*" capture="camera" multiple class="hidden">
<div class="flex gap-3">
<button class="flex-1 bg-blue-600 text-white py-2 rounded" onclick="fileInput.click()">
拍照/上传
</button>
<button class="flex-1 bg-green-600 text-white py-2 rounded" onclick="submitPhotos()">
确认提交
</button>
</div>
</div>
</div>
</div>
<script>
const awb = sessionStorage.getItem('currentAwb');
const fileInput = document.getElementById('fileInput');
const previewWrap = document.getElementById('previewWrap');
let fileList = [];
// 选择图片
fileInput.onchange = function () {
for (let file of this.files) {
const url = URL.createObjectURL(file);
fileList.push({ file, url });
renderPreview();
}
};
// 渲染预览
function renderPreview() {
previewWrap.innerHTML = '';
fileList.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'relative border rounded h-20 overflow-hidden';
div.innerHTML = `
<img src="${item.url}" class="w-full h-full object-cover">
<button onclick="delImg(${idx})"
class="absolute top-1 right-1 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center">
×
</button>
`;
previewWrap.appendChild(div);
});
}
// 删除
function delImg(index) {
fileList.splice(index, 1);
renderPreview();
}
// 提交
function submitPhotos() {
const urls = fileList.map(item => item.url);
sessionStorage.setItem('uploadedPhotos', JSON.stringify(urls));
closeWin();
}
function closeWin() {
window.close();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,831 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>进港移库 - 移动端原型</title>
<!-- Tailwind CSS -->
<script src="https://modao.cc/agent-py/static/source/js/tailwindcss.js"></script>
<!-- Iconify -->
<script src="https://modao.cc/agent-py/static/source/js/iconify-icon.min.js"></script>
<style>
@font-face {
font-family: 'PingFang SC';
src: local('PingFang SC');
}
body {
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.phone-container {
width: 375px;
height: 812px;
background-color: #f5f6fa;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 8px solid #1a1a1a;
border-radius: 40px;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.page {
display: none;
flex-direction: column;
height: 100%;
}
.page.active {
display: flex;
}
#filter-panel {
transform: translateY(100%);
transition: transform 0.3s ease-out;
}
#filter-panel.show {
transform: translateY(0);
}
.overlay {
background-color: rgba(0,0,0,0.5);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.overlay.show {
opacity: 1;
pointer-events: auto;
}
.btn-active:active {
opacity: 0.7;
transform: scale(0.98);
}
.custom-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.custom-checkbox-round {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 50%;
outline: none;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
background-color: white;
}
.custom-checkbox-round:checked {
background-color: #2563EB;
border-color: #2563EB;
}
.custom-checkbox-round:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* 长按拍照浮层核心样式 */
.card-action-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.5);
border-radius: 20px;
z-index: 10;
display: none;
align-items: center;
justify-content: center;
pointer-events: none;
}
.card-action-overlay.show {
display: flex;
pointer-events: auto;
}
/* 卡片必须相对定位,浮层才能盖住自身 */
.pending-card, #list-shipped > div {
position: relative;
}
</style>
</head>
<body>
<div class="phone-container">
<!-- 状态栏 -->
<div class="h-10 w-full flex justify-between items-center px-8 flex-none bg-[#2563EB] text-white">
<span class="text-xs font-semibold">09:41</span>
<div class="flex gap-1 items-center">
<iconify-icon icon="mdi:signal" width="14"></iconify-icon>
<iconify-icon icon="mdi:wifi" width="14"></iconify-icon>
<iconify-icon icon="mdi:battery" width="18"></iconify-icon>
</div>
</div>
<!-- 列表首页 -->
<div class="page active" id="page-home">
<!-- 顶部导航栏 -->
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="alert('返回首页')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">进港移库</h1>
<div class="w-16"></div>
</div>
<!-- 滚动内容区域 -->
<div class="flex-1 overflow-y-auto no-scrollbar pb-24">
<!-- 搜索栏 -->
<section class="px-4 pt-4 pb-3 bg-white">
<div class="flex items-center gap-2">
<div class="flex items-center bg-white border border-gray-200 shadow-sm rounded-[12px] h-10 px-3 gap-2 flex-1">
<iconify-icon class="text-gray-400 text-lg flex-none" icon="mdi:magnify"></iconify-icon>
<input class="flex-1 bg-transparent text-[14px] text-gray-800 placeholder-gray-400 outline-none border-none rounded-lg px-2" placeholder="搜索运单号" type="text"/>
<button class="flex-none text-blue-500">
<iconify-icon class="text-lg" icon="mdi:qrcode-scan"></iconify-icon>
</button>
</div>
<button class="bg-white p-2.5 rounded-full shadow-sm border border-gray-200 text-[#2563EB] flex items-center justify-center flex-none" onclick="toggleFilterDrawer(true)">
<iconify-icon class="text-xl" icon="mdi:filter-variant"></iconify-icon>
</button>
</div>
</section>
<!-- Tab切换 -->
<div class="flex bg-white border-b sticky top-0 z-20">
<button class="flex-1 py-3 text-sm font-bold text-[#1F2937] border-b-2 border-[#1F2937] transition-all" id="tab-pending" onclick="switchTab('pending')">
待移库 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
<button class="flex-1 py-3 text-sm font-bold text-gray-400 border-b-2 border-transparent transition-all" id="tab-shipped" onclick="switchTab('shipped')">
已移库 <span class="bg-[#DBEAF6] text-[#2563EB] text-[11px] font-medium px-1.5 py-0.5 rounded-md ml-1.5">3</span>
</button>
</div>
<!-- 待移库列表 -->
<div class="p-3 pt-4 space-y-3" id="list-pending">
<!-- 卡片1 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden"
onmousedown="startLongPress(this, event)"
onmouseup="endLongPress()"
onmouseleave="endLongPress()"
ontouchstart="startLongPress(this, event)"
ontouchmove="cancelLongPress()"
ontouchend="endLongPress()">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363573</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">NOR</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">100件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">200kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363573', status: '待移库', agent: 'NOR', pieces: '100', weight: '200', goodsName: '电子元器件', carrier: 'MU', route: 'HFE-NKG-LAX', location: 'A区-03货位', code: 'NOR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<!-- 拍照浮层 -->
<div class="card-action-overlay" onclick="closeSelf(this)">
<button class="bg-purple-500 text-white w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center text-[10px] font-bold leading-tight" onclick="event.stopPropagation();goToPhoto('78133363573')">拍照</button>
</div>
</div>
<!-- 卡片2 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden"
onmousedown="startLongPress(this, event)"
onmouseup="endLongPress()"
onmouseleave="endLongPress()"
ontouchstart="startLongPress(this, event)"
ontouchmove="cancelLongPress()"
ontouchend="endLongPress()">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363574</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">PER</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">85件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">150kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-PEK-LAX</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363574', status: '待移库', agent: 'PER', pieces: '85', weight: '150', goodsName: '服装鞋帽', carrier: 'CA', route: 'HFE-PEK-LAX', location: 'B区-05货位', code: 'NOR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="card-action-overlay" onclick="closeSelf(this)">
<button class="bg-purple-500 text-white w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center text-[10px] font-bold leading-tight" onclick="event.stopPropagation();goToPhoto('78133363574')">拍照</button>
</div>
</div>
<!-- 卡片3 -->
<div class="pending-card bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden"
onmousedown="startLongPress(this, event)"
onmouseup="endLongPress()"
onmouseleave="endLongPress()"
ontouchstart="startLongPress(this, event)"
ontouchmove="cancelLongPress()"
ontouchend="endLongPress()">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<input class="item-checkbox custom-checkbox-round flex-none" onclick="handleCheckboxClick(this, event)" type="checkbox"/>
<div class="text-[#2563EB] text-[15px] font-bold">78133363575</div>
</div>
<div class="flex items-center gap-1">
<span class="bg-orange-100 text-orange-600 text-[11px] px-2 h-5 flex items-center rounded-[6px] font-medium">待移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">DHL</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">200件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">450kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:map-marker"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-4 flex justify-end border-t border-gray-100 pt-3">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363575', status: '待移库', agent: 'DHL', pieces: '200', weight: '450', goodsName: '精密模具', carrier: '3U', route: 'HFE-NKG-LAX', location: 'C区-01货位', code: 'NOR', counterPerson: '--', counterTime: '--', outboundPerson: '', outboundTime: ''})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="card-action-overlay" onclick="closeSelf(this)">
<button class="bg-purple-500 text-white w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center text-[10px] font-bold leading-tight" onclick="event.stopPropagation();goToPhoto('78133363575')">拍照</button>
</div>
</div>
</div>
<!-- 已移库列表 -->
<div class="p-3 pt-4 space-y-3 hidden" id="list-shipped">
<div class="bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden"
onmousedown="startLongPress(this, event)"
onmouseup="endLongPress()"
onmouseleave="endLongPress()"
ontouchstart="startLongPress(this, event)"
ontouchmove="cancelLongPress()"
ontouchend="endLongPress()">
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363001</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">NOR</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">50件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">80kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363001', status: '已移库', agent: 'NOR', pieces: '50', weight: '80', pickupNo: 'TH20260701001', goodsName: '精密仪器', carrier: 'MU', route: 'HFE-NKG-LAX', location: 'B区-05货位', code: 'NOR', counterTime: '2026-07-05 10:30', counterPerson: '张三', outboundTime: '2026-07-05 11:45', outboundPerson: '张三'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="card-action-overlay" onclick="closeSelf(this)">
<button class="bg-purple-500 text-white w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center text-[10px] font-bold leading-tight" onclick="event.stopPropagation();goToPhoto('78133363001')">拍照</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden"
onmousedown="startLongPress(this, event)"
onmouseup="endLongPress()"
onmouseleave="endLongPress()"
ontouchstart="startLongPress(this, event)"
ontouchmove="cancelLongPress()"
ontouchend="endLongPress()">
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363002</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">PER</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">120件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">300kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-PEK-LAX</span>
</div>
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363002', status: '已移库', agent: 'PER', pieces: '120', weight: '300', pickupNo: 'TH20260702002', goodsName: '鲜花绿植', carrier: 'CA', route: 'HFE-PEK-LAX', location: 'A区-08货位', code: 'NOR', counterTime: '2026-07-06 09:15', counterPerson: '王五', outboundTime: '2026-07-06 10:30', outboundPerson: '赵六'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="card-action-overlay" onclick="closeSelf(this)">
<button class="bg-purple-500 text-white w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center text-[10px] font-bold leading-tight" onclick="event.stopPropagation();goToPhoto('78133363002')">拍照</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-4 mb-3 overflow-hidden"
onmousedown="startLongPress(this, event)"
onmouseup="endLongPress()"
onmouseleave="endLongPress()"
ontouchstart="startLongPress(this, event)"
ontouchmove="cancelLongPress()"
ontouchend="endLongPress()">
<div class="flex justify-between items-center">
<div class="text-[#1F2937] text-[15px] font-bold">78133363003</div>
<div class="flex items-center gap-1 text-[#1F2937] text-[13px] font-medium">
<iconify-icon class="text-[#10B981]" icon="mdi:check-circle" width="16"></iconify-icon>
<span>已移库</span>
</div>
</div>
<div class="mt-3 mb-3 flex items-center gap-1">
<iconify-icon icon="mdi:airplane" class="text-gray-500 text-[14px]"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">航班信息:20260709/MU2311</span>
</div>
<div class="bg-[#F3F4F6] rounded-lg p-3 grid grid-cols-3 text-center">
<div class="flex flex-col">
<span class="text-[#9CA3AF] text-[11px]">特码</span>
<span class="text-[#1F2937] text-[14px] font-semibold">DHL</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">件数</span>
<span class="text-[#1F2937] text-[14px] font-semibold">75件</span>
</div>
<div class="flex flex-col border-l border-gray-200">
<span class="text-[#9CA3AF] text-[11px]">重量</span>
<span class="text-[#1F2937] text-[14px] font-semibold">160kg</span>
</div>
</div>
<div class="mt-3 flex items-center gap-1.5">
<iconify-icon class="text-gray-500 text-[14px]" icon="mdi:account-outline"></iconify-icon>
<span class="text-[#6B7280] text-[13px]">出运路径:HFE-NKG-LAX</span>
</div>
<div class="mt-2 flex justify-end border-t border-gray-50 pt-2">
<div class="text-[#2563EB] text-[14px] font-medium flex items-center cursor-pointer btn-active" onclick="showDetail({id: '78133363003', status: '已移库', agent: 'DHL', pieces: '75', weight: '160', pickupNo: 'TH20260703003', goodsName: '生鲜食品', carrier: '3U', route: 'HFE-NKG-LAX', location: 'D区-02货位', code: 'NOR', counterTime: '2026-07-06 14:00', counterPerson: '钱七', outboundTime: '2026-07-06 15:20', outboundPerson: '孙八'})">
查看详情 <iconify-icon icon="mdi:chevron-right" width="16"></iconify-icon>
</div>
</div>
<div class="card-action-overlay" onclick="closeSelf(this)">
<button class="bg-purple-500 text-white w-14 h-14 rounded-full shadow-lg shadow-purple-500/30 flex items-center justify-center text-[10px] font-bold leading-tight" onclick="event.stopPropagation();goToPhoto('78133363003')">拍照</button>
</div>
</div>
</div>
</div>
<!-- 底部操作栏 -->
<div class="absolute bottom-0 w-full bg-white border-t border-gray-200 z-40 flex flex-col pt-4 pb-4" id="bottom-toolbar">
<div class="px-4 flex items-center w-full">
<div class="flex items-center gap-2">
<input class="custom-checkbox-round" id="select-all-cb" onclick="toggleSelePERll(this.checked)" type="checkbox"/>
<label class="text-[13px] text-[#1F2937] font-medium cursor-pointer" for="select-all-cb">全选</label>
</div>
<div class="flex-1"></div>
<div class="flex items-center gap-3">
<span class="text-[12px] text-[#9CA3AF]">已选 <span id="selected-count">0</span> 项</span>
<button class="bg-[#2563EB] text-white text-[15px] font-bold px-5 py-2 rounded-[10px] shadow-lg shadow-blue-500/25 btn-active" onclick="alert('移库操作')">
移库
</button>
</div>
</div>
</div>
</div>
<!-- 运单详情页面 -->
<div class="page" id="page-detail">
<div class="bg-[#2563EB] text-white h-12 flex items-center justify-between px-4 flex-none">
<div class="flex items-center btn-active" onclick="switchPage('page-home')">
<iconify-icon icon="mdi:chevron-left" width="28"></iconify-icon>
<span class="text-sm font-bold -ml-1">返回</span>
</div>
<h1 class="text-lg font-medium">运单详情</h1>
<div class="w-16"></div>
</div>
<div class="flex-1 overflow-y-auto no-scrollbar pb-8 p-4">
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100">
<div class="bg-[#2563EB] px-4 py-3 flex justify-between items-center text-white">
<span class="font-bold text-[17px] leading-tight" id="detail-awb">781-22222620</span>
<div class="flex items-center text-[13px] font-normal opacity-90">
<span class="w-2 h-2 bg-green-400 rounded-full mr-1.5"></span>
<span id="detail-time">2026-05-10 08:30</span>
</div>
</div>
<div class="p-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<span class="bg-[#EF4444] text-white text-[11px] px-1.5 py-0.5 rounded leading-tight font-medium">国际进港</span>
<span class="text-[13px] font-normal text-[#666666] border-l border-gray-300 pl-2">普通货物运输</span>
</div>
</div>
<div class="text-[22px] font-bold text-[#111827] mb-3" id="detail-goods-name">电子元器件</div>
<div class="grid grid-cols-2 gap-x-8 gap-y-3">
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">件数</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-count">55</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">重量</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-weight">622.00 kg</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">特码</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-code">NOR</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">代理</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-agent">NOR</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">出运路径</div>
<div class="text-[15px] text-[#1F2937] font-medium" id="detail-route">HFE-PEK-LAX</div>
</div>
<div class="flex flex-col gap-y-[2px]">
<div class="text-[12px] text-[#9CA3AF] font-normal">承运人</div>
<div class="text-[15px] text-[#1F2937] font-bold" id="detail-carrier">MU</div>
</div>
</div>
</div>
</div>
<!-- 移库信息卡片-->
<div class="mt-4 bg-white rounded-xl shadow-sm overflow-hidden border border-gray-200 hidden" id="detail-outbound-card">
<div class="bg-[#10B981] px-4 py-2.5">
<span class="text-white font-semibold text-[15px]">移库信息</span>
</div>
<div class="bg-white px-4 py-3 grid grid-cols-2 gap-y-3">
<div class="flex flex-col" id="detail-outbound-person-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">移库人</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-person">-</span>
</div>
<div class="flex flex-col" id="detail-outbound-time-row">
<span class="text-[12px] text-[#9CA3AF] font-normal mb-[2px]">移库时间</span>
<span class="text-[14px] text-[#1F2937] font-medium" id="detail-outbound-time">-</span>
</div>
</div>
</div>
<!-- 异常照片-->
<div class="mt-4 bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100">
<div class="bg-[#F9971E] px-4 py-3 flex items-center">
<span class="text-white font-semibold text-[15px]">异常照片</span>
</div>
<div class="p-4">
<div id="image-preview-list" class="grid grid-cols-3 gap-3">
<div class="border border-dashed border-gray-300 rounded-lg h-24 flex items-center justify-center text-gray-400 text-lg">+</div>
<div class="border border-dashed border-gray-300 rounded-lg h-24 flex items-center justify-center text-gray-400 text-lg">+</div>
<div class="border border-dashed border-gray-300 rounded-lg h-24 flex items-center justify-center text-gray-400 text-lg">+</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部筛选遮罩 -->
<div class="overlay absolute inset-0 z-30" id="filter-overlay" onclick="toggleFilterDrawer(false)"></div>
<!-- 筛选弹窗 -->
<div class="absolute bottom-0 w-full bg-white rounded-t-3xl z-50 p-6 shadow-2xl overflow-hidden" id="filter-panel">
<div class="relative flex items-center justify-center mb-6">
<h2 class="text-center text-[18px] font-bold text-gray-900">筛选条件</h2>
<button class="absolute right-0 top-0 text-gray-400" onclick="toggleFilterDrawer(false)">
<iconify-icon class="text-2xl" icon="mdi:close"></iconify-icon>
</button>
</div>
<div class="space-y-4">
<!-- 1.航班日期 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班日期</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3 cursor-pointer">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请选择航班日期" readonly="" type="text"/>
<iconify-icon class="text-gray-500 text-[14px] flex-none" icon="mdi:calendar" width="18"></iconify-icon>
</div>
</div>
<!-- 2.航班号 -->
<div>
<label class="block text-[14px] text-gray-700 mb-1.5">航班号</label>
<div class="flex items-center bg-white border border-gray-200 rounded-lg h-10 px-3">
<input class="flex-1 text-[14px] text-gray-800 placeholder-gray-400 outline-none bg-transparent" placeholder="请输入航班号" type="text" id="filterFlightNo">
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">特码</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择特码</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="space-y-2">
<label class="block text-[14px] text-gray-700 mb-1.5">运单类型</label>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center justify-between text-gray-400">
<span class="text-sm">请选择特码</span>
<iconify-icon icon="mdi:chevron-down"></iconify-icon>
</div>
</div>
<div class="flex gap-4 pt-2">
<button class="flex-1 bg-white border border-gray-300 text-gray-600 py-3.5 rounded-xl font-bold btn-active" onclick="alert('已重置')">
重置
</button>
<button class="flex-1 bg-[#2563EB] text-white py-3.5 rounded-xl font-bold shadow-lg shadow-blue-500/20 btn-active" onclick="toggleFilterDrawer(false)">
确认
</button>
</div>
</div>
</div>
</div>
<script>
// 页面切换
function switchPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById(pageId).classList.add('active');
if(pageId === 'page-detail'){
document.querySelector('#page-detail .no-scrollbar').scrollTop = 0;
}
}
// Tab切换
function switchTab(type) {
const tabPending = document.getElementById('tab-pending');
const tabShipped = document.getElementById('tab-shipped');
const listPending = document.getElementById('list-pending');
const listShipped = document.getElementById('list-shipped');
const bottomToolbar = document.getElementById('bottom-toolbar');
if (type === 'pending') {
tabPending.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabPending.classList.remove('text-gray-400', 'border-transparent');
tabShipped.classList.add('text-gray-400', 'border-transparent');
tabShipped.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listPending.classList.remove('hidden');
listShipped.classList.add('hidden');
bottomToolbar.style.display = 'flex';
} else {
tabShipped.classList.add('text-[#1F2937]', 'border-[#1F2937]');
tabShipped.classList.remove('text-gray-400', 'border-transparent');
tabPending.classList.add('text-gray-400', 'border-transparent');
tabPending.classList.remove('text-[#1F2937]', 'border-[#1F2937]');
listShipped.classList.remove('hidden');
listPending.classList.add('hidden');
bottomToolbar.style.display = 'none';
}
}
// 全选
function toggleSelePERll(checked) {
const checkboxes = document.querySelectorAll('.item-checkbox');
checkboxes.forEach(cb => {
cb.checked = checked;
});
updateSelectionCount();
}
// 单选
function handleCheckboxClick(checkbox, event) {
event.stopPropagation();
updateSelectionCount();
const allBox = document.getElementById('select-all-cb');
const all = Array.from(document.querySelectorAll('.item-checkbox')).every(i => i.checked);
allBox.checked = all;
}
// 更新选中数量
function updateSelectionCount() {
const cnt = document.querySelectorAll('.item-checkbox:checked').length;
document.getElementById('selected-count').textContent = cnt;
}
// 图片存储全局对象
let photoStore = {};
// 查看详情
function showDetail(data) {
if (!data) return;
const setText = (id, val) => {
const el = document.getElementById(id);
if(el) el.textContent = val || '-';
};
setText('detail-awb', data.id);
setText('detail-goods-name', data.goodsName);
setText('detail-count', data.pieces);
setText('detail-weight', data.weight + (String(data.weight).includes('kg') ? '' : ' kg'));
setText('detail-code', data.code);
setText('detail-agent', data.agent);
setText('detail-carrier', data.carrier);
setText('detail-route', data.route || 'HFE-PEK-LAX');
// 渲染图片:最多3格,有图显示图片,空位虚线+占位
const list = document.getElementById('image-preview-list');
list.innerHTML = '';
const imgs = photoStore[data.id] || [];
const maxSlot = 3;
for(let i = 0; i < maxSlot; i++){
if(imgs[i]){
// 已有图片
const imgWrap = document.createElement('div');
imgWrap.className = 'border border-gray-200 rounded-lg h-24 overflow-hidden';
imgWrap.innerHTML = `<img src="${imgs[i]}" class="w-full h-full object-cover">`;
list.appendChild(imgWrap);
}else{
// 空虚线占位框
const emptyBox = document.createElement('div');
emptyBox.className = 'border border-dashed border-gray-300 rounded-lg h-24 flex items-center justify-center text-gray-400 text-lg';
emptyBox.textContent = '+';
list.appendChild(emptyBox);
}
}
const outboundCard = document.getElementById('detail-outbound-card');
if(data.status === '待移库'){
outboundCard.classList.add('hidden');
}else{
outboundCard.classList.remove('hidden');
setText('detail-outbound-person', data.outboundPerson);
setText('detail-outbound-time', data.outboundTime);
}
switchPage('page-detail');
}
// 筛选抽屉
function toggleFilterDrawer(show) {
const panel = document.getElementById('filter-panel');
const overlay = document.getElementById('filter-overlay');
if(show){
panel.classList.add('show');
overlay.classList.add('show');
}else{
panel.classList.remove('show');
overlay.classList.remove('show');
}
}
// ===================== 长按拍照完整逻辑 =====================
let longPressTimer = null;
const LONG_PRESS_DELAY = 600;
let isShowOverlay = false;
// 关闭单个遮罩
function closeSelf(dom) {
dom.classList.remove('show');
isShowOverlay = false;
}
// 关闭全部遮罩
function closeAllOverlay() {
document.querySelectorAll('.card-action-overlay').forEach(item => {
item.classList.remove('show');
})
isShowOverlay = false;
}
// 开始长按
function startLongPress(cardDom, e) {
if (e) e.preventDefault();
if (isShowOverlay) return;
closeAllOverlay();
clearTimeout(longPressTimer);
longPressTimer = setTimeout(() => {
cardDom.querySelector('.card-action-overlay').classList.add('show');
isShowOverlay = true;
}, LONG_PRESS_DELAY)
}
// 滑动取消长按
function cancelLongPress() {
if (!isShowOverlay) clearTimeout(longPressTimer);
}
// 松开鼠标/手指
function endLongPress() {
clearTimeout(longPressTimer);
}
// 跳转拍照页,传递运单号
function goToPhoto(awb) {
sessionStorage.setItem('currentAwb', awb);
window.open('photo.html', '_blank');
}
// 页面加载读取上传图片
window.onload = function() {
const uploadData = sessionStorage.getItem('uploadedPhotos');
const awb = sessionStorage.getItem('currentAwb');
if(uploadData && awb) {
photoStore[awb] = JSON.parse(uploadData);
sessionStorage.removeItem('uploadedPhotos');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,312 @@
# 5.5 寸手持端适配技术方案
> 适用范围:AirLogistics(航空物流信息管理系统)在现有 10 寸平板横屏基础上,增加 5.5 寸手机竖屏形态。
> 状态:核心技术已打通并双端实机验证,参考页面「国际出港移库」已完成。
> 最后更新:2026-07-28
---
## 一、背景与结论
设计稿位于 `documents/5.5寸手持端设计文件/`,共 11 个业务入口、约 66 个界面(37 主页面 + 17 弹层/筛选面板 + 12 详情页 Tab 子页)。手机版是全新 UI,但**功能与交互保持不变**。
前置调研结论:**本项目的逻辑与 UI 分离度很好,适配成本可集中在 XML 布局重写。**
| 层 | 现状 | 手机版可复用性 |
|---|---|---|
| ViewModel(业务逻辑/网络/校验/统计) | 继承 `BaseViewModel`/`BasePageViewModel`,只持有 `MutableLiveData`,不引用 View | 完全复用 |
| Activity | 极薄。如 `IntExpStorageUseActivity` 195 行仅引用 `binding.srl`/`binding.rv`/`binding.viewModel` | 完全复用 |
| ViewHolder | 通过 id 访问控件(`binding.ivIcon` 等) | 保留同名 id 即复用 |
| Adapter | `CommonAdapter` 由 `itemLayoutId` 驱动 | 完全复用 |
| 布局 XML | Pad 横屏专用,三列表单、宽表格式 item | 需全部重写 |
| 公共 UI 组件 | `PadSearchLayout` / `PadDataLayoutNew` / `title_tool_bar` 均为 Pad 形态 | 需新增手机版 |
两个必须先解决的工程前提:
1. `app/src/main/AndroidManifest.xml` 中 **95 个 Activity 全部为 `screenOrientation="userLandscape"`**(强制横屏)
—— 现已统一改为 `unspecified`,见下文「屏幕方向」一节
2. 项目**没有任何资源限定符目录**,只有 `res/layout/`
---
## 二、核心机制:布局按资源限定符自动切换
```
res/layout-sw600dp/activity_xxx.xml ← 平板(最小宽度 >= 600dp,10 寸约 800dp)
res/layout/activity_xxx.xml ← 手机(兜底,5.5 寸约 360~411dp)
```
两个变体**同名**,DataBinding 会生成同一个 Binding 基类 + 两个变体实现:
```
ActivityIntExpMoveBinding.java 公共基类(Activity 中引用的就是它)
ActivityIntExpMoveBindingImpl.java 手机变体
ActivityIntExpMoveBindingSw600dpImpl.java 平板变体
```
因此 **Activity / ViewModel / ViewHolder 完全不需要设备判断**,`layoutId()` 与 `itemLayoutId` 保持原样。
### 为什么不用「不同文件名 + 代码判断」
这是本次实施中被否掉的第一版方案,务必不要走回去:
```kotlin
// ❌ 错误做法
override fun layoutId() =
if (DeviceUtil.isPhone()) R.layout.activity_xxx_phone else R.layout.activity_xxx
```
`activity_xxx_phone.xml` 生成的是 `ActivityXxxPhoneBinding`,而 `binding` 字段声明的类型是 `ActivityXxxBinding`。
泛型擦除让**编译期完全不报错**,但一进入该页面就 `ClassCastException` 崩溃。
### 变体的强制约束
| 约束 | 说明 | 保障方式 |
|---|---|---|
| DataBinding 变量必须完全一致 | 两变体 `<data>` 中 `<variable>` 名称与类型相同 | 编译失败 |
| 共用控件 id 双方都要有 | Activity/ViewHolder 引用的 id(`srl`/`rv`/`checkIcon`/`iv_icon` 等) | 见下 |
| 单侧独有 id 自动 `@Nullable` | 只在一个变体存在的 id,基类中为可空字段 | Kotlin 强制 null 检查 |
实测生成的基类字段可空性(出港移库):
```java
@NonNull public final TextView btnMove; // 两变体都有
@NonNull public final ImageView checkIcon; // 两变体都有
@NonNull public final RecyclerView rv; // 两变体都有
@NonNull public final SmartRefreshLayout srl; // 两变体都有
@Nullable public final EditText etWaybillNo; // 仅手机变体
@Nullable public final PadSearchLayout searchWaybillNo; // 仅平板变体
```
即:**约束由编译器静态保障**,不依赖人工检查。
---
## 三、已实现的基础设施
### 1. `DeviceUtil`(新增)
`module_base/src/main/java/com/lukouguoji/module_base/util/DeviceUtil.kt`
按 `smallestScreenWidthDp >= 600` 区分平板/手机。关键点:读取 `Resources.getSystem()`(系统资源)而非应用 Resources,以绕开 AndroidAutoSize 对 density 的动态改写,保证判定稳定。
```kotlin
DeviceUtil.isPhone() // 是否手机
DeviceUtil.isTablet() // 是否平板
DeviceUtil.describe() // "PHONE(sw=411dp, force=AUTO)",日志排查用
DeviceUtil.forceMode = DeviceUtil.ForceMode.PHONE // 调试:平板上强制走手机布局
```
`DeviceUtil.layout(padId, phoneId)` 仅用于极少数确实需要代码级判断的场景(如非 DataBinding 布局),**常规页面一律用资源限定符**。
### 2. 屏幕方向:Manifest 写 `unspecified` + `BaseActivity` 按形态锁定
方案演进过了两轮,两次失败都有明确的实测证据,记录如下以免重蹈。
**方案 A(一期,已废弃)**:Manifest 保持 `userLandscape`,靠 `BaseActivity` 在 `onCreate` 里纠正。
缺陷:手机上「先横屏、约 1 秒后转竖屏」+ Activity 重建。因为 `screenOrientation` 由系统在
**创建 Activity 窗口时**生效,那时业务代码还没跑,代码只能事后纠正。
**方案 B(二期,已废弃)**:Manifest 改 `@integer/screen_orientation`,
`values/`=portrait、`values-sw600dp/`=userLandscape。**看似优雅,实测是错的**:
> 系统解析 Manifest 属性时用的是**默认配置,不套用设备限定符**,`values-sw600dp/` 永远不会被选中。
> 于是所有 Activity 都拿到了 `values/` 的竖屏值,平板端出现两个回归:
> 1. 先竖屏、再被 `BaseActivity` 纠正成横屏(把闪屏从手机搬到了平板)
> 2. **AutoSize 在竖屏配置下选了平板竖屏基准 720dp**(density = 1280/720 ≈ 1.78,
> 正常应为 1280/1152 ≈ 1.11)→ 交互几次后整个 UI 放大约 1.6 倍
>
> 验证方法(可复现):把 `values/` 与 `values-sw600dp/` 的取值**互换**,并临时让
> `lockOrientationByDevice()` 返回 false 以排除运行时纠正,装到平板上看首屏方向 ——
> 结果平板取的是 `values/` 的值,证明限定符被忽略。
**方案 C(现行)**:Manifest 一律写 `unspecified`,系统按设备**当前物理方向**建窗口
(平板横屏摆放 → 横屏起;手机 → 竖屏起,两端起始方向本就正确),
`BaseActivity.applyDeviceOrientation()` 再按形态锁定,因方向一致故不产生二次纠正与重建。
```xml
<activity android:name="…"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="unspecified" />
```
全项目 192 处已统一(`module_p` 等 24 处本就是 `portrait` 的 PDA 页面保持不变)。
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
applyDeviceOrientation() // 必须在 super 之前
super.onCreate(savedInstanceState)
...
}
protected open fun applyDeviceOrientation() {
if (!lockOrientationByDevice()) return
requestedOrientation = if (DeviceUtil.isPhone(this))
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
protected open fun lockOrientationByDevice(): Boolean = true // 子类可覆写跳过
```
**已知边界**:若平板在启动瞬间被竖持且系统自动旋转开启,仍会先竖后横。
现场平板为横屏固定安装,实测无此现象。
**验证**:手机冷启动连拍 6 帧全为竖屏;平板冷启动连拍 6 帧全为横屏(1280×800),
登录后多次切换菜单与进出页面,UI 尺寸正常无放大。
### 3. AutoSize 设计基准(易漏,后果严重)
`MyApplication.initAutoSizeConfig()` 原先只按方向切换,现增加手机分支:
| 场景 | 设计基准 |
|---|---|
| 横屏(平板) | 1152 × 720 dp |
| 竖屏 + 手机 | **390 × 844 dp** ← 本次新增 |
| 竖屏 + 平板 | 720 × 1280 dp |
| PictureSelector | 360 × 480 dp |
**若手机沿用平板竖屏的 720dp 基准,手机上所有 dp 会被压缩约一半,布局全部错位。**
### 4. 手机端公共资源
| 资源 | 内容 |
|---|---|
| `colors.xml` | `phone_primary`(#2563EB)、`phone_primary_light`、`phone_bg`、`phone_text_title/body/hint`、`phone_divider`、`phone_tag_*` |
| `bg_phone_card.xml` | 列表卡片:白底 + 12dp 圆角 + 0.5dp 边框 |
| `bg_phone_search.xml` | 搜索框:白底 + 12dp 圆角 + 1dp 边框 |
| `bg_phone_btn_primary.xml` | 主按钮:主色实底 + 10dp 圆角 + 按下态 |
| `bg_phone_tag_gray/green.xml` | 状态标签底色 |
| `layout/title_tool_bar.xml` | 手机版标题栏(平板版已移至 `layout-sw600dp/`),保留 `toolbar`/`tool_back`/`title_name` id,`setBackArrow()` 零改动复用 |
---
## 四、手机版 UI 规范
| 部位 | 规范 |
|---|---|
| 画布基准 | 390 × 844 dp |
| 顶栏 | 高 48dp,`phone_primary`,左「‹ 返回」,标题居中 18sp 加粗 |
| 页面背景 | `phone_bg` (#F3F4F6) |
| 列表卡片 | `bg_phone_card`,`marginHorizontal=16dp`,`marginTop=12dp`,`padding=14dp` |
| 卡片头行 | 圆形勾选框 20dp(`radiobtn_checked_style`/`radiobtn_unchecked_style`)+ 主字段 15sp 加粗 + 状态标签 |
| 卡片字段 | 两列网格,13sp,`phone_text_body`,行距 8dp |
| 状态标签 | 绿底=已完成 / 灰底=未完成,11sp,高 20dp |
| 搜索行 | 搜索框高 40dp(放大镜 + 输入 + 扫码)+ 右侧筛选按钮 40dp |
| 状态 Tab | 高 42dp,选中文字 `phone_primary` + 底部 28×3dp 指示条 |
| 底部操作条 | 白底 + `elevation=8dp`,全选 + 统计(13sp/11sp 两行)+ 主按钮 |
| 筛选弹层 | 遮罩 `#80000000` + 底部白面板,重置 / 确定各占一半 |
---
## 五、页面改造步骤
1. **移动平板布局**
```bash
mkdir -p module_xxx/src/main/res/layout-sw600dp
git mv module_xxx/src/main/res/layout/activity_xxx.xml module_xxx/src/main/res/layout-sw600dp/activity_xxx.xml
git mv module_xxx/src/main/res/layout/item_xxx.xml module_xxx/src/main/res/layout-sw600dp/item_xxx.xml
```
2. **新建手机布局**:在 `res/layout/` 下创建**同名**文件,保持相同 `<variable>` 与共用控件 id
3. **Kotlin 侧不动**:`layoutId()`、`itemLayoutId`、ViewHolder 全部保持原样
4. **纯 UI 交互写在 Activity**,不污染 ViewModel;状态过滤优先复用 ViewModel 既有字段
5. 手机布局顶部 `<include layout="@layout/title_tool_bar" />` 不变(变体自动切换)
### 示例:状态 Tab 零 ViewModel 改动
出港移库的 `moveState` 字段(`""` 全部 / `"0"` 未移库 / `"1"` 已移库)与查询参数本就存在,Tab 直接复用:
```kotlin
// IntExpMoveActivity 中新增纯 UI 方法
fun switchTab(state: String) {
if (viewModel.moveState.value == state) return
viewModel.moveState.value = state
viewModel.searchClick() // 复用原有查询链路
}
```
```xml
<!-- 判等把字面量放前面,避免 LiveData 为 null 时崩溃 -->
android:textColor="@{`0`.equals(viewModel.moveState) ? @color/phone_primary : @color/phone_text_body}"
```
---
## 六、参考页面实施结果:国际出港移库
| 文件 | 改动 |
|---|---|
| `IntExpMoveViewModel.kt` | **0 行业务改动**(仅补注释) |
| `IntExpMoveViewHolder.kt` | **完全未改** |
| `IntExpMoveActivity.kt` | 仅新增 4 个纯 UI 方法:`switchTab()` / `toggleFilterPanel()` / `resetFilter()` / `confirmFilter()`,以及 `filterPanelVisible` 状态 |
| `layout-sw600dp/activity_int_exp_move.xml` | 原平板布局,内容未变,仅移动目录 |
| `layout-sw600dp/item_int_exp_move.xml` | 同上 |
| `layout/activity_int_exp_move.xml` | 新增手机版 |
| `layout/item_int_exp_move.xml` | 新增手机版 |
---
## 七、双端验证
| | 手机模拟器 | 平板模拟器 |
|---|---|---|
| AVD | `Medium_Phone_API_36.1` | `Aerologic_Tablet` |
| 屏幕 | 1080×2400 @420dpi | 1280×800 @160dpi |
| 形态识别 | `PHONE(sw=411dp, force=AUTO)` | `TABLET(sw=800dp, force=AUTO)` |
| 方向 | 竖屏(`unspecified` + BaseActivity 锁定,无闪屏) | 横屏 |
| 加载布局 | 手机版 | 原平板布局,**行为无变化** |
| 交互 | Tab 切换选中态迁移并触发查询;筛选弹层正常弹出/关闭 | — |
APK 内两套变体均正确打包:
```
res/layout-sw600dp-v13/activity_int_exp_move.xml
res/layout-sw600dp-v13/item_int_exp_move.xml
res/layout-sw600dp-v13/title_tool_bar.xml
res/layout/activity_int_exp_move.xml
res/layout/item_int_exp_move.xml
res/layout/title_tool_bar.xml
```
### 验证命令
```bash
~/Library/Android/sdk/emulator/emulator -list-avds
~/Library/Android/sdk/emulator/emulator -avd Medium_Phone_API_36.1 -no-snapshot-load -no-boot-anim &
adb -s <设备> install -r -t app/build/outputs/apk/debug/app-debug.apk
adb -s <设备> shell wm size && adb -s <设备> shell wm density
adb -s <设备> logcat -d -s BaseActivity:D # 查看形态识别结果
adb -s <设备> exec-out screencap -p > /tmp/shot.png
```
### 调试便利
`app/src/debug/AndroidManifest.xml`(**仅 Debug 生效,不进 Release 包**)用 `tools:replace` 将适配调试页临时置为 `exported="true"`,免去每次登录再逐级点入:
```bash
adb shell am start -n com.lukouguoji.aerologic/com.lukouguoji.gjc.page.move.IntExpMoveActivity
```
新增调试页时在该文件追加对应 `<activity>` 即可。
---
## 八、待办
| 项 | 说明 |
|---|---|
| `PhoneDataLayout` 手机版表单组件 | 对标 `PadDataLayoutNew`(319 行 kt)+ `DataLayoutKtx`(404 行)+ `AutoQueryManager`(179 行),支持 INPUT/SPINNER/DATE + required + AutoQuery |
| `PhoneSearchBar` 手机版搜索组件 | 当前手机版搜索框为原生 `EditText`,组件化后恢复运单号 AutoQuery 联想能力 |
| 筛选弹层内控件替换 | 当前仍复用 `PadSearchLayout`(功能完整、视觉待统一) |
| 首页 / 登录 / 我的 | **设计稿缺失**,是手机端使用闭环的前置条件 |
---
## 九、注意事项
1. 改动 `BaseActivity` / `BaseBindingActivity` / 公共组件后,**必须在平板上回归**,确认现有平板版行为不变
2. 未适配手机的页面,布局保留在 `res/layout/` 即可(两端共用),手机上视觉不佳但不会崩溃——支持渐进式适配
3. 手机版资源统一使用 `phone_*` 颜色与 `bg_phone_*` drawable,禁止直接写死色值
4. 两个 git 仓库(`aerologic-app` 与 `air-cargo`)为同一份源码的独立副本,适配改动需同步

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.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

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