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>
This commit is contained in:
2026-07-29 16:30:42 +08:00
parent bbc29ca2e1
commit 5a019a55c4
33 changed files with 1848 additions and 422 deletions

View File

@@ -19,6 +19,18 @@ data class ULDBean(
var status: String = "",
// 备注
var remark: String = "",
// ========== 以下字段接口出参已有api-doc /eqm/uld/pageQuery此前 Bean 未解析 ==========
// 进港航班号(语义待后端确认,见 documents/后端接口对接问题清单.xlsx
var ifNo: String = "",
// 出港航班号
var efNo: String = "",
// 入库时间
var checkInDate: String = "",
// 出库时间
var checkOutDate: String = "",
// ========== 后端暂无对应字段,前端按设计稿预留(提交后端忽略,无副作用) ==========
// 来源:空运 / 陆运(后端字段需求已登记清单)
var source: String = "",
){
val checked = ObservableBoolean(false)
@@ -27,4 +39,23 @@ data class ULDBean(
"1" -> "故障"
else -> status
}
// ========== 手机版5.5 寸手持端)三态状态展示 ==========
// 设计稿状态为 可用(绿)/占用(橙)/维修(红) 三态后端当前仅两态0/1
// 按语义映射 0→可用、1→维修"2"→占用为后端扩展枚举后的预留(需求已登记清单)。
/** 手机版状态文案 */
val statusPhoneName: String
get() = when (status) {
"0" -> "可用"
"1" -> "维修"
"2" -> "占用"
else -> statusName()
}
/** 可用(绿色标签) */
val isStatusAvailable: Boolean get() = status == "0"
/** 占用(橙色标签,后端扩展枚举后的预留) */
val isStatusOccupied: Boolean get() = status == "2"
}

View File

@@ -144,6 +144,8 @@ object ARouterConstants {
const val ACTIVITY_URL_INT_EXP_MOVE = "/gjc/IntExpMoveActivity" //国际出港 出港移库
const val ACTIVITY_URL_GJC_ASSEMBLE_ALLOCATE = "/gjc/GjcAssembleAllocateActivity" //国际出港 组装分配
const val ACTIVITY_URL_INT_EXP_OUT_HANDOVER = "/gjc/IntExpOutHandoverActivity" //国际出港 出库交接
const val ACTIVITY_URL_ULD_LIST = "/app/UldListActivity" //综合管理 ULD管理手机端首页入口用
const val ACTIVITY_URL_INT_EXP_LOAD = "/gjc/IntExpLoadActivity" //国际出港 出港装载
const val ACTIVITY_URL_INT_EXP_TALLY = "/gjc/IntExpTallyActivity" //国际出港 出港理货
const val ACTIVITY_URL_INT_EXP_ARRIVE = "/gjc/IntExpArriveActivity" //国际出港 出港运抵

View File

@@ -64,6 +64,29 @@ class PhoneBottomBar @JvmOverloads constructor(
tvAction.text = v
}
/**
* 危险操作样式:浅红底 + 红字 + 垃圾桶图标(如 ULD 管理的批量删除)。
* 默认 false 为主色实底按钮,既有页面不受影响。
*/
var actionDanger: Boolean = false
set(v) {
field = v
if (v) {
tvAction.setBackgroundResource(R.drawable.bg_phone_btn_danger)
tvAction.setTextColor(ContextCompat.getColor(context, R.color.phone_danger_text))
val icon = ContextCompat.getDrawable(context, R.drawable.ic_phone_trash)?.apply {
val size = (16 * resources.displayMetrics.density).toInt()
setBounds(0, 0, size, size)
}
tvAction.setCompoundDrawables(icon, null, null, null)
tvAction.compoundDrawablePadding = (4 * resources.displayMetrics.density).toInt()
} else {
tvAction.setBackgroundResource(R.drawable.bg_phone_btn_primary)
tvAction.setTextColor(ContextCompat.getColor(context, R.color.white))
tvAction.setCompoundDrawables(null, null, null, null)
}
}
init {
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL

View File

@@ -0,0 +1,232 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.os.Parcelable
import android.text.InputType
import android.util.AttributeSet
import android.util.SparseArray
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.widget.doOnTextChanged
import androidx.databinding.InverseBindingListener
import com.lukouguoji.module_base.R
import com.lukouguoji.module_base.ktx.formatDate
import com.lukouguoji.module_base.ktx.getActivity
import com.lukouguoji.module_base.util.Common
import com.lxj.xpopup.XPopup
import dev.utils.app.info.KeyValue
import java.util.Calendar
/**
* 手机版行式表单项5.5 寸手持端公共组件)
*
* 结构:左标签(固定 110dp+ 必填星)+ 右值区 + 可选箭头 + 底部 1px 分隔线。
* 对应设计稿中新增 / 修改 / 详情三态共用的行式表单(如 01_ULD管理
* 与 [PhoneDataLayout](上下结构,用于筛选弹层)互补,形态见 [PhoneFormRowType]。
*
* XML 用法type 按页面状态用三元切换即可覆盖三态页面):
* ```xml
* <com.lukouguoji.module_base.ui.weight.phone.PhoneFormRow
* android:layout_width="match_parent"
* android:layout_height="wrap_content"
* title='@{"所属航司"}'
* hint='@{"请选择所属航司"}'
* list="@{viewModel.carrierList}"
* type="@{viewModel.pageType == DetailsPageType.Details ? PhoneFormRowType.TEXT : PhoneFormRowType.SELECT}"
* value="@={viewModel.uldBean.carrier}" />
* ```
*/
class PhoneFormRow @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
val tvTitle: TextView
val tvMust: TextView
val et: EditText
val tv: TextView
val ivArrow: ImageView
val llRow: LinearLayout
/** 双向绑定回调 */
var onChangeListener: InverseBindingListener? = null
/** 选择 / 日期确定后的回调 */
var refreshCallBack: (() -> Unit)? = null
var type: PhoneFormRowType = PhoneFormRowType.INPUT
set(v) {
field = v
applyType()
applyValue()
}
var title: String = ""
set(v) {
field = v
tvTitle.text = v
}
var hint: String = ""
set(v) {
field = v
et.hint = v
tv.hint = v
}
var value: String = ""
set(v) {
if (field == v) return
field = v
applyValue()
onChangeListener?.onChange()
}
/** SELECT 形态的选项key 显示文案 / value 业务值) */
var list: List<KeyValue> = emptyList()
set(v) {
field = v
applyValue()
}
var required: Boolean = false
set(v) {
field = v
tvMust.visibility = if (v) VISIBLE else GONE
}
var enable: Boolean = true
set(v) {
field = v
et.isEnabled = v
applyType()
}
/** INPUT 形态是否数字键盘(自重等数值项) */
var numeric: Boolean = false
set(v) {
field = v
et.inputType = if (v) {
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
} else {
InputType.TYPE_CLASS_TEXT
}
}
init {
orientation = VERTICAL
inflate(context, R.layout.layout_phone_form_row, this)
llRow = findViewById(R.id.ll_row)
tvTitle = findViewById(R.id.tv_title)
tvMust = findViewById(R.id.tv_must)
et = findViewById(R.id.et)
tv = findViewById(R.id.tv)
ivArrow = findViewById(R.id.iv_arrow)
et.doOnTextChanged { text, _, _, _ ->
// 仅 INPUT 形态才回写,避免隐藏的 EditText 被系统状态恢复时误清空值
if (type == PhoneFormRowType.INPUT) {
value = text.toString()
}
}
applyType()
}
private val selectClick: (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()
}
}
private val dateClick: (v: View) -> Unit = {
if (enable) {
Common.onYearMonthDay(context.getActivity(), value) { year, month, day ->
val calendar = Calendar.getInstance()
calendar.set(year, month - 1, day)
value = calendar.time.formatDate()
refreshCallBack?.invoke()
}
}
}
private fun applyType() {
when (type) {
PhoneFormRowType.INPUT -> {
et.visibility = VISIBLE
tv.visibility = GONE
ivArrow.visibility = GONE
llRow.setOnClickListener(null)
llRow.isClickable = false
}
PhoneFormRowType.SELECT -> {
et.visibility = GONE
tv.visibility = VISIBLE
tv.gravity = Gravity.START or Gravity.CENTER_VERTICAL
ivArrow.visibility = if (enable) VISIBLE else GONE
llRow.setOnClickListener(selectClick)
}
PhoneFormRowType.DATE -> {
et.visibility = GONE
tv.visibility = VISIBLE
tv.gravity = Gravity.START or Gravity.CENTER_VERTICAL
ivArrow.visibility = GONE
llRow.setOnClickListener(dateClick)
}
PhoneFormRowType.TEXT -> {
et.visibility = GONE
tv.visibility = VISIBLE
tv.gravity = Gravity.END or Gravity.CENTER_VERTICAL
tv.hint = ""
ivArrow.visibility = GONE
llRow.setOnClickListener(null)
llRow.isClickable = false
}
}
}
private fun applyValue() {
when (type) {
PhoneFormRowType.INPUT -> {
if (et.text.toString() != value) et.setText(value)
}
PhoneFormRowType.SELECT -> {
// 显示选中项的文案;列表里找不到就按原值显示(如后端返回的历史值)
tv.text = list.firstOrNull { it.value == value }?.key ?: value
}
PhoneFormRowType.DATE -> {
tv.text = value
}
PhoneFormRowType.TEXT -> {
// 只读展示:空值统一占位 "--"(与设计稿一致)
tv.text = value.ifEmpty { "--" }
}
}
}
/** 只保存/恢复自身状态,避免内部子控件 id 与其他实例串档(公共组件必做,见 components.md */
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
}

View File

@@ -0,0 +1,18 @@
package com.lukouguoji.module_base.ui.weight.phone
/**
* [PhoneFormRow] 的形态。
*
* | 形态 | 值区控件 | 交互 |
* |----------|--------------------|------|
* | `INPUT` | EditText左对齐 | 直接输入 |
* | `SELECT` | TextView + 右箭头 | 点击弹底部选择面板list 提供选项) |
* | `DATE` | TextView | 点击弹日历 |
* | `TEXT` | TextView右对齐 | 只读展示(详情态) |
*/
enum class PhoneFormRowType {
INPUT,
SELECT,
DATE,
TEXT,
}

View File

@@ -226,3 +226,58 @@ fun setPhoneKvItemAttrs(item: PhoneKvItem, title: String?, value: String?, tag:
tag?.let { item.tag = it }
item.value = value.orEmpty()
}
///////////////////////////////////////////////////////////////////////////
// PhoneFormRow
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("title", "hint", "type", "required", "enable", "numeric", requireAll = false)
fun setPhoneFormRowAttrs(
row: PhoneFormRow,
title: String?,
hint: String?,
type: PhoneFormRowType?,
required: Boolean?,
enable: Boolean?,
numeric: Boolean?
) {
// type 先于其余属性生效,确保 value/hint 写进正确的子控件
type?.let { row.type = it }
title?.let { row.title = it }
hint?.let { row.hint = it }
required?.let { row.required = it }
enable?.let { row.enable = it }
numeric?.let { row.numeric = it }
}
@BindingAdapter("value", requireAll = false)
fun setPhoneFormRowValue(row: PhoneFormRow, value: String?) {
if (row.value != value.orEmpty()) row.value = value.orEmpty()
}
@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged")
fun getPhoneFormRowValue(row: PhoneFormRow): String = row.value
@BindingAdapter("valueAttrChanged", requireAll = false)
fun setPhoneFormRowValueAttrChanged(row: PhoneFormRow, listener: InverseBindingListener) {
row.onChangeListener = listener
}
@BindingAdapter("list", requireAll = false)
fun setPhoneFormRowList(row: PhoneFormRow, list: List<KeyValue>?) {
row.list = list ?: emptyList()
}
@BindingAdapter("setRefreshCallBack", requireAll = false)
fun setPhoneFormRowRefreshCallBack(row: PhoneFormRow, callBack: (() -> Unit)?) {
row.refreshCallBack = callBack
}
///////////////////////////////////////////////////////////////////////////
// PhoneBottomBar追加危险操作样式
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("actionDanger", requireAll = false)
fun setPhoneBottomBarActionDanger(bar: PhoneBottomBar, danger: Boolean?) {
danger?.let { bar.actionDanger = it }
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版危险操作按钮:浅红底 + 10dp 圆角(如 ULD 管理的批量删除) -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#FECACA" />
<corners android:radius="10dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/phone_danger_bg" />
<corners android:radius="10dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版右下角浮动新增按钮:主色圆形 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="#1D4FD8" />
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="@color/phone_primary" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态标签浅红底红字ULD管理「维修」态配文字色 phone_danger_text -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_danger_bg" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版编辑铅笔图标MDI pencil可 tint 染色 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版删除图标MDI delete-outline可 tint 染色 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/phone_danger_text"
android:pathData="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</vector>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhoneFormRow 内部布局(手机版行式表单项)
结构:左标签(+必填星)+ 右值区(输入 / 只读文本互斥显示)+ 可选箭头 + 底部分隔线
对应设计稿01_ULD管理 的新增 / 修改 / 详情三个表单页的行式布局
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.LinearLayout">
<LinearLayout
android:id="@+id/ll_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="54dp"
android:orientation="horizontal">
<!-- 左标签 + 必填星minWidth 保证编辑态值列对齐,长标签自适应不折行) -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minWidth="110dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/phone_text_label"
android:textSize="15sp" />
<TextView
android:id="@+id/tv_must"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:text="*"
android:textColor="@color/phone_danger_text"
android:textSize="15sp"
android:visibility="gone" />
</LinearLayout>
<!-- 值区:输入 -->
<EditText
android:id="@+id/et"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@null"
android:inputType="text"
android:maxLines="1"
android:paddingVertical="15dp"
android:textColor="@color/phone_text_title"
android:textColorHint="@color/phone_text_hint"
android:textSize="15sp" />
<!-- 值区:只读 / 选择 / 日期文本 -->
<TextView
android:id="@+id/tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:paddingVertical="16dp"
android:textColor="@color/phone_text_title"
android:textColorHint="@color/phone_text_hint"
android:textSize="15sp"
android:visibility="gone" />
<!-- SELECT 态右箭头 -->
<ImageView
android:id="@+id/iv_arrow"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="6dp"
android:src="@drawable/ic_phone_chevron_right"
android:visibility="gone" />
</LinearLayout>
<View
android:id="@+id/v_divider"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/phone_divider" />
</merge>

View File

@@ -73,5 +73,7 @@
<color name="phone_orange_text">#EA580C</color>
<color name="phone_tag_orange_bg">#FFEDD5</color> <!-- 状态标签-橙底(待处理态) -->
<color name="phone_tag_orange_text">#EA580C</color>
<color name="phone_danger_bg">#FEE2E2</color> <!-- 危险操作按钮-浅红底ULD管理批量删除 -->
<color name="phone_danger_text">#DC2626</color>
</resources>