Compare commits

..

14 Commits

Author SHA1 Message Date
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
35 changed files with 1101 additions and 304 deletions

View File

@@ -0,0 +1,57 @@
---
name: zentao-bug
description: 读取禅道(njlkairport.chandao.net,开源版 18.8)中的 Bug 单——我的 Bug 列表、单个 Bug 的标题/描述/截图/评论/修改历史/附件——并按 AirLogistics 项目规范定位代码完成修复。禅道 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 <账号>
```
密码交互输入、不回显;默认存入 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>`。不确定截图意图时先问用户,禁止臆测。
## 写回禅道
暂未实现(写操作会改动线上 Bug 状态,需用户确认后再加)。修复完成后由用户在禅道手动"解决",
可附上提交号与改动摘要供用户粘贴。
## 实现说明
- 认证:`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>`。
- REST v1 的 `/products/<id>/bugs` 只返回激活中的 Bug,不能替代"我的 Bug"页面。
- 凭据:Token 在 `~/.config/zentao-lite/config.json`(0600),密码只在钥匙串(服务名 `zentao-lite`)。`logout` 一并清除。

View File

@@ -0,0 +1,528 @@
#!/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 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 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)
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()

View File

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

View File

@@ -177,6 +177,13 @@ data class GjcMaWb(
val agentDisplay: String? val agentDisplay: String?
get() = if (agentName.isNullOrEmpty()) agentCode else agentName get() = if (agentName.isNullOrEmpty()) agentCode else agentName
/**
* 5.5寸手机版-进港仓库:始发站展示值。IntImpStorage/pageQuery 不返回 dep/fdep,
* 始发站在 origin(货源地)中(Bug #635 实测)。
*/
val depDisplay: String?
get() = if (dep.isNullOrEmpty()) origin else dep
/** /**
* 5.5寸手机版-出港仓库:卡片状态标签(已清仓=绿 / 未清仓=橙,clearNormal 为空视作未清仓)。 * 5.5寸手机版-出港仓库:卡片状态标签(已清仓=绿 / 未清仓=橙,clearNormal 为空视作未清仓)。
* 已清仓时卡片「清仓」按钮置灰不可点。 * 已清仓时卡片「清仓」按钮置灰不可点。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -235,43 +235,35 @@
</LinearLayout> </LinearLayout>
<!-- ==================== 卡片三:备注 + 交接图片(编辑态可改) ==================== --> <!-- ==================== 卡片三:交接图片(Bug #632:样式与进港移库「异常照片」一致——橙色表头 + 三列虚线加号槽位,长按图片出删除角标) ==================== -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="@drawable/bg_phone_card" android:background="@drawable/bg_phone_card"
android:orientation="vertical" android:orientation="vertical">
android:paddingHorizontal="16dp"
android:paddingBottom="12dp">
<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.remark}" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:background="@drawable/bg_phone_card_header_orange"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp"
android:text="交接图片" android:text="交接图片"
android:textColor="@color/phone_text_label" android:textColor="@color/white"
android:textSize="15sp" /> android:textSize="15sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_images" android:id="@+id/rv_images"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:overScrollMode="never" android:overScrollMode="never"
android:padding="10dp"
itemLayoutId="@{viewModel.imageItemLayoutId}" itemLayoutId="@{viewModel.imageItemLayoutId}"
viewHolder="@{viewModel.imageItemViewHolder}" viewHolder="@{viewModel.imageItemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="4" /> app:spanCount="3" />
<!-- 详情模式下无图片时的占位提示 --> <!-- 详情模式下无图片时的占位提示 -->
<TextView <TextView
@@ -286,6 +278,39 @@
</LinearLayout> </LinearLayout>
<!-- ==================== 卡片四:补充信息(Bug #632:备注录入与保存,表头底色 #F3F4F6) ==================== -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/bg_phone_card"
android:orientation="vertical">
<!-- 表头文字色:Bug 单写的是白色,但白字在 #F3F4F6 上不可读,暂用标题深色,待确认 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_card_header_gray"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp"
android:text="补充信息"
android:textColor="@color/phone_text_title"
android:textSize="15sp"
android:textStyle="bold" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingBottom="4dp"
enable="@{viewModel.pageType != DetailsPageType.Details}"
hint='@{"请输入备注"}'
title='@{"备注"}'
type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.INPUT}"
value="@={viewModel.remark}" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

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

View File

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

View File

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

View File

@@ -185,12 +185,13 @@
</LinearLayout> </LinearLayout>
<!-- 字段网格:海关放行 / 运单类型(与平板卡片第二行一致) --> <!-- 字段网格:海关放行 / 运单类型(Bug #626:待运抵不显示,仅已运抵显示,与原型一致) -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:orientation="horizontal"> android:orientation="horizontal"
android:visibility="@{`shipped`.equals(viewModel.currentTab) ? View.VISIBLE : View.GONE}">
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"

View File

@@ -79,19 +79,6 @@
android:textSize="11sp" android:textSize="11sp"
android:visibility="@{!bean.isHandovered &amp;&amp; !bean.dgrCode.empty ? View.VISIBLE : View.GONE}" /> android:visibility="@{!bean.isHandovered &amp;&amp; !bean.dgrCode.empty ? View.VISIBLE : View.GONE}" />
<!-- 装机状态标签(与平板「装机状态」一致:允许=绿 / 不允许=浅红,无值不显示) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:background="@{bean.isAllowLoad ? @drawable/bg_phone_tag_green : @drawable/bg_phone_tag_red_light}"
android:gravity="center"
android:paddingHorizontal="8dp"
android:text="@{bean.allowLoadText}"
android:textColor="@{bean.isAllowLoad ? @color/phone_tag_green_text : @color/phone_danger_text}"
android:textSize="11sp"
android:visibility="@{bean.allowLoadText.empty ? View.GONE : View.VISIBLE}" />
<!-- 已交接:对勾 + 状态文字 --> <!-- 已交接:对勾 + 状态文字 -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -211,14 +198,34 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="@color/phone_stat_bg" /> android:background="@color/phone_stat_bg" />
<!-- 底部行:左侧装机状态标签(Bug #634:从头部移下来,头部右侧只留 IMP 代码)+ 右侧查看详情 -->
<LinearLayout <LinearLayout
android:id="@+id/ll_detail"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical|end" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="12dp"> android:paddingTop="12dp">
<!-- 装机状态标签(与平板「装机状态」一致:允许=绿 / 不允许=浅红,无值不显示) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:background="@{bean.isAllowLoad ? @drawable/bg_phone_tag_green : @drawable/bg_phone_tag_red_light}"
android:gravity="center"
android:paddingHorizontal="8dp"
android:text="@{bean.allowLoadText}"
android:textColor="@{bean.isAllowLoad ? @color/phone_tag_green_text : @color/phone_danger_text}"
android:textSize="11sp"
android:visibility="@{bean.allowLoadText.empty ? View.GONE : View.VISIBLE}" />
<LinearLayout
android:id="@+id/ll_detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical|end"
android:orientation="horizontal">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -233,6 +240,8 @@
</LinearLayout> </LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
</layout> </layout>

View File

@@ -212,16 +212,32 @@
</LinearLayout> </LinearLayout>
<!-- 清仓备注(与平板列表第三行一致,无备注时不显示) --> <!-- 清仓备注(与平板列表第三行一致,无备注时不显示;图标与上方字段行同规格) -->
<TextView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text='@{"备注:" + bean.clearRemark}' android:gravity="center_vertical"
android:textColor="@color/phone_text_body" android:orientation="horizontal"
android:textSize="13sp" android:visibility="@{bean.clearRemark == null || bean.clearRemark.empty ? View.GONE : View.VISIBLE}">
android:visibility="@{bean.clearRemark == null || bean.clearRemark.empty ? View.GONE : View.VISIBLE}"
tools:text="备注:正常清仓" /> <ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_phone_clipboard"
android:tint="@color/phone_text_body" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text='@{"备注:" + bean.clearRemark}'
android:textColor="@color/phone_text_body"
android:textSize="13sp"
tools:text="备注:正常清仓" />
</LinearLayout>
<!-- ==================== 操作按钮行:清仓 / 修改库位 / 出库 / 入库 ==================== --> <!-- ==================== 操作按钮行:清仓 / 修改库位 / 出库 / 入库 ==================== -->
<LinearLayout <LinearLayout

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
取值统一走 viewModel.str()/dec() 帮助方法(空值兜底 "-")。 取值统一走 viewModel.str()/dec() 帮助方法(空值兜底 "-")。
字段 key 以平板变体既有绑定为准(awbPc/awbWeight/cashWeight/inPc/inWeight/lockStateText/ 字段 key 以平板变体既有绑定为准(awbPc/awbWeight/cashWeight/inPc/inWeight/lockStateText/
mftStatus/tallyStatus/command);后端确认按平板端展示(问题清单 #27): mftStatus/tallyStatus/command);后端确认按平板端展示(问题清单 #27):
航程 range(无经停站,替代始发站/中转站/目的站)、UN编号 unNumber、出库时间 dlvTime。 航程 range(无经停站,替代始发站/中转站/目的站)、UN编号 unNumber;出库时间按 Bug #635 不再展示。
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <layout xmlns:android="http://schemas.android.com/apk/res/android">
@@ -167,7 +167,7 @@
</LinearLayout> </LinearLayout>
<!-- 业务类型 | 出库时间 --> <!-- 业务类型(Bug #635:去掉出库时间,右半格留空保持两列对齐) -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -181,12 +181,10 @@
title='@{"业务类型"}' title='@{"业务类型"}'
value='@{viewModel.str(viewModel.maWbData, "businessName")}' /> value='@{viewModel.str(viewModel.maWbData, "businessName")}' />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem <Space
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1" />
title='@{"出库时间"}'
value='@{viewModel.str(viewModel.maWbData, "dlvTime")}' />
</LinearLayout> </LinearLayout>

View File

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

View File

@@ -121,7 +121,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
title='@{"始发站"}' title='@{"始发站"}'
value="@{bean.dep}" /> value="@{bean.depDisplay}" />
<com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem <com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
android:layout_width="0dp" android:layout_width="0dp"

View File

@@ -203,7 +203,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="6dp" android:layout_marginStart="6dp"
android:text='@{"始发站:" + bean.orDash(bean.dep)}' android:text='@{"始发站:" + bean.orDash(bean.depDisplay)}'
android:textColor="@color/phone_text_body" android:textColor="@color/phone_text_body"
android:textSize="13sp" android:textSize="13sp"
tools:text="始发站:LAX" /> tools:text="始发站:LAX" />
@@ -212,16 +212,32 @@
</LinearLayout> </LinearLayout>
<!-- 清仓备注(与平板列表第三行一致,无备注时不显示) --> <!-- 清仓备注(与平板列表第三行一致,无备注时不显示;图标与上方字段行同规格) -->
<TextView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text='@{"备注:" + bean.clearRemark}' android:gravity="center_vertical"
android:textColor="@color/phone_text_body" android:orientation="horizontal"
android:textSize="13sp" android:visibility="@{bean.clearRemark == null || bean.clearRemark.empty ? View.GONE : View.VISIBLE}">
android:visibility="@{bean.clearRemark == null || bean.clearRemark.empty ? View.GONE : View.VISIBLE}"
tools:text="备注:正常清仓" /> <ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/ic_phone_clipboard"
android:tint="@color/phone_text_body" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text='@{"备注:" + bean.clearRemark}'
android:textColor="@color/phone_text_body"
android:textSize="13sp"
tools:text="备注:正常清仓" />
</LinearLayout>
<!-- ==================== 操作按钮行:清仓 / 修改库位 / 出库 / 入库 ==================== --> <!-- ==================== 操作按钮行:清仓 / 修改库位 / 出库 / 入库 ==================== -->
<LinearLayout <LinearLayout