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>
This commit is contained in:
YANG JIANKUAN
2026-10-11 21:20:03 -07:00
356 changed files with 44711 additions and 13385 deletions

View File

@@ -1,33 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lukouguoji.module_base">
<application
android:allowBackup="true"
android:icon="@mipmap/logot"
android:label="@string/app_name"
android:roundIcon="@mipmap/logot"
android:supportsRtl="true"
android:theme="@style/Theme.Aerologic">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="userLandscape"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 屏幕适配 -->
<meta-data
android:name="design_width_in_dp"
android:value="1152" />
<meta-data
android:name="design_height_in_dp"
android:value="720" />
</application>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lukouguoji.module_base">
<application
android:allowBackup="true"
android:icon="@mipmap/logot"
android:label="@string/app_name"
android:roundIcon="@mipmap/logot"
android:supportsRtl="true"
android:theme="@style/Theme.Aerologic">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="unspecified"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 屏幕适配 -->
<meta-data
android:name="design_width_in_dp"
android:value="1152" />
<meta-data
android:name="design_height_in_dp"
android:value="720" />
</application>
</manifest>

View File

@@ -2,6 +2,7 @@ package com.lukouguoji.module_base
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.net.Uri
@@ -26,6 +27,7 @@ import com.yanzhenjie.permission.runtime.Permission
import com.yzq.zxinglibrary.android.CaptureActivity
import com.yzq.zxinglibrary.bean.ZxingConfig
import com.yzq.zxinglibrary.common.Constant
import com.lukouguoji.module_base.util.DeviceUtil
import dev.utils.app.HandlerUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -49,15 +51,48 @@ open class BaseActivity : AppCompatActivity(), CoroutineScope {
private var timerTask: TimerTask? = null
override fun onCreate(savedInstanceState: Bundle?) {
// 必须在 super.onCreate 之前设置,避免进入页面后再旋转造成闪屏与重建
applyDeviceOrientation()
super.onCreate(savedInstanceState)
// requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
setContentView(R.layout.activity_base)
Log.d("BaseActivity", javaClass.simpleName)
Log.d("BaseActivity", "${javaClass.simpleName} ${DeviceUtil.describe()}")
ActivityCollector.addActivity(this)
ARouter.getInstance().inject(this); /////////必须要写这行,自动注入解析
loadingDialog = LoadingDialog(this)
}
/**
* 按设备形态锁定屏幕方向 —— **兜底手段**
*
* 方向的主控权在 Manifest:各 Activity 统一写
* `android:screenOrientation="@integer/screen_orientation"`,
* 由资源限定符取值(`values/` = portrait,`values-sw600dp/` = userLandscape)。
* 这样方向在**窗口创建阶段**就已确定,不会出现"先横屏、约 1 秒后转竖屏"的闪屏。
*
* 本方法只作为安全网:万一某个 Activity 的 Manifest 漏配(仍写死 userLandscape),
* 至少还能在手机上纠正为竖屏——但那种情况下闪屏依旧存在,应当去补 Manifest。
*
* 子类如需特殊方向(如扫码页),覆写 [lockOrientationByDevice] 返回 false 即可跳过。
*/
protected open fun applyDeviceOrientation() {
if (!lockOrientationByDevice()) return
try {
requestedOrientation = if (DeviceUtil.isPhone(this)) {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} else {
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
} catch (e: Exception) {
// 半透明 / 悬浮类 Activity 不允许指定方向,忽略即可
Log.w("BaseActivity", "setRequestedOrientation failed: ${e.message}")
}
}
/**
* 是否由基类按设备形态自动锁定方向,默认开启
*/
protected open fun lockOrientationByDevice(): Boolean = true
override fun onDestroy() {
super.onDestroy()
job.cancel()

View File

@@ -13,6 +13,7 @@ import com.cretin.www.cretinautoupdatelibrary.model.UpdateConfig
import com.cretin.www.cretinautoupdatelibrary.utils.AppUpdateUtils
import com.cretin.www.cretinautoupdatelibrary.utils.SSLUtils.TrustAllHostnameVerifier
import com.lukouguoji.module_base.handler.MyCrashHandler
import com.lukouguoji.module_base.util.DeviceUtil
import com.lukouguoji.module_base.util.OkHttp3Connection
import com.scwang.smart.refresh.footer.ClassicsFooter
import com.scwang.smart.refresh.header.ClassicsHeader
@@ -79,20 +80,26 @@ class MyApplication : Application() {
.setScreenHeight(activity.resources.displayMetrics.heightPixels)
//根据屏幕方向,设置设计尺寸
if (activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
//设置横屏设计尺寸
//设置横屏设计尺寸(平板)
AutoSizeConfig.getInstance()
.setDesignWidthInDp(1152)
.setDesignHeightInDp(720)
.setDesignWidthInDp(DeviceUtil.TABLET_DESIGN_WIDTH_DP_LAND)
.setDesignHeightInDp(DeviceUtil.TABLET_DESIGN_HEIGHT_DP_LAND)
} else {
if(activity.javaClass.simpleName.startsWith("PictureSelectorSupporterActivity")){
AutoSizeConfig.getInstance()
.setDesignWidthInDp(360)
.setDesignHeightInDp(480)
}else{
//设置竖屏设计尺寸
} else if (DeviceUtil.isPhone(activity)) {
//手机竖屏:按 5.5 寸手持端设计稿画布 390 × 844 换算
//若沿用平板竖屏的 720 × 1280 基准,手机上所有 dp 会被压缩约一半,布局全部错位
AutoSizeConfig.getInstance()
.setDesignWidthInDp(720)
.setDesignHeightInDp(1280)
.setDesignWidthInDp(DeviceUtil.PHONE_DESIGN_WIDTH_DP)
.setDesignHeightInDp(DeviceUtil.PHONE_DESIGN_HEIGHT_DP)
} else {
//设置竖屏设计尺寸(平板)
AutoSizeConfig.getInstance()
.setDesignWidthInDp(DeviceUtil.TABLET_DESIGN_WIDTH_DP_PORT)
.setDesignHeightInDp(DeviceUtil.TABLET_DESIGN_HEIGHT_DP_PORT)
}
}
}

View File

@@ -21,7 +21,17 @@ abstract class BaseBindingActivity<T : ViewDataBinding, VM : ViewModel> : BaseAc
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//dataBinding初始化
/*
* dataBinding初始化
*
* 平板 / 手机双形态的布局切换由 Android 资源限定符自动完成,此处无需分支:
* res/layout-sw600dp/xxx.xml → 平板(最小宽度 >= 600dp)
* res/layout/xxx.xml → 手机(兜底)
* 两个变体同名,DataBinding 会生成同一个 Binding 类,因此 Activity 与 ViewModel 无需改动。
*
* 注意:不要按「不同文件名 + 代码判断」的方式切换布局,
* 那样会生成两个不同的 Binding 类,泛型擦除下编译期无法发现,运行时必然 ClassCastException。
*/
binding = DataBindingUtil.setContentView(this, layoutId())
binding.lifecycleOwner = this
@@ -37,6 +47,9 @@ abstract class BaseBindingActivity<T : ViewDataBinding, VM : ViewModel> : BaseAc
/**
* 布局ID
*
* 双形态适配时保持同一个 id 即可,具体加载哪份布局由资源限定符决定:
* res/layout-sw600dp/ 平板 res/layout/ 手机
*/
abstract fun layoutId(): Int

View File

@@ -1,8 +1,19 @@
package com.lukouguoji.module_base.bean
class ActionBean(
val title: String,
val icon: Int,
val key: String = ""
) {
}
package com.lukouguoji.module_base.bean
/**
* 手持端(手机版)首页菜单项
*
* @param title 菜单显示名称
* @param icon 菜单图标
* @param key 权限串(取 [com.lukouguoji.module_base.common.Constant.AuthName] 中的常量)。
* 菜单按登录返回的 authList 是否包含该串来决定显不显示
* @param route ARouter 路由路径。填了就由通用逻辑直接跳转,无需再写 when 分支;
* 留空表示由所在 ViewModel 的 onItemClick 自行处理(module_p 内的 PDA 页面走这条)
*/
class ActionBean(
val title: String,
val icon: Int,
val key: String = "",
val route: String = ""
) {
}

View File

@@ -25,9 +25,14 @@ data class GjcCheckInRecord(
var whId: Long = 0, // GJC_WAREHOUSE.ID
var carWeight: String = "", // 托盘车重
var trayNumber: Int = 0, // 托盘数量
var trayTotalWeight: Double = 0.0 // 托盘重量(托盘自重)
var trayTotalWeight: Double = 0.0, // 托盘重量(托盘自重)
var opUserName: String = "" // 计重人姓名(opId 关联查询)
) : BaseObservable() {
/** 计重人:优先姓名,为空回退操作员编码 */
val opUserDisplay: String
get() = opUserName.ifEmpty { opId }
// 数据变化回调
var onDataChanged: (() -> Unit)? = null

View File

@@ -2,6 +2,7 @@ package com.lukouguoji.module_base.bean
import androidx.databinding.ObservableBoolean
import com.lukouguoji.module_base.ktx.toPlainText
import java.io.Serializable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -130,6 +131,8 @@ data class GjcMaWb(
val checked: ObservableBoolean = ObservableBoolean(false) // 选中状态
@Transient
val showMore: ObservableBoolean = ObservableBoolean(false) // 展开状态
@Transient
val actionMenuVisible: ObservableBoolean = ObservableBoolean(false) // 5.5寸手机版:卡片长按操作浮层显隐
// 兼容现有API的isSelected属性
var isSelected: Boolean
@@ -143,6 +146,44 @@ data class GjcMaWb(
val canCheckAllHaWb: Boolean
get() = allTally == "1"
/**
* 5.5寸手机版:申报状态三色标签的着色分类(01=绿/W=靛蓝/其余非空=琥珀)。
*/
val declareTagColor: String
get() = when (declareStatus) {
"01" -> "green"
"W" -> "indigo"
else -> if (declareStatus.isNullOrEmpty()) "" else "amber"
}
/**
* 5.5寸手机版-出港查询:卡片状态标签。
* 已离港 = 航班关闭时间(fclose)非空;已入库 = 入库时间(opDate)非空;都为空 = 未入库(灰)。
* 接口无独立状态出参,此判定为前端推断(与卡片「入库时间/离港时间」两列同源)。
*/
val isQueryDeparted: Boolean
get() = !fclose.isNullOrEmpty()
val isQueryStored: Boolean
get() = !opDate.isNullOrEmpty()
/** 5.5寸手机版:空值统一占位 "--"(空串与 null 都兜底,布局绑定用) */
fun orDash(value: String?): String = if (value.isNullOrEmpty()) "--" else value
/**
* 5.5寸手机版:代理人展示值(名称优先,空则回退代码)。
* 不能在 XML 里用 `??` 兜底——转换器会把 JSON null 归一成空串,`??` 只判 null 不判空串。
*/
val agentDisplay: String?
get() = if (agentName.isNullOrEmpty()) agentCode else agentName
/**
* 5.5寸手机版-出港仓库:卡片状态标签(已清仓=绿 / 未清仓=橙,clearNormal 为空视作未清仓)。
* 已清仓时卡片「清仓」按钮置灰不可点。
*/
val isCleared: Boolean
get() = clearNormal == "1"
// ==================== 状态转换扩展属性 ====================
/**
* 计重状态中文
@@ -292,15 +333,36 @@ data class GjcHaWb(
var tallyOpId: String? = null, // 理货操作人ID
var declareCount: Long? = null, // 申报次数
var activeId: Long? = null // 活动ID
) {
) : Serializable {
// ==================== UI扩展字段 ====================
@Transient
val checked: ObservableBoolean = ObservableBoolean(false) // 选中状态
@Transient
val actionMenuVisible: ObservableBoolean = ObservableBoolean(false) // 5.5寸手机版:卡片长按操作浮层显隐
// 兼容现有API的isSelected属性
var isSelected: Boolean
get() = checked.get()
set(value) = checked.set(value)
/**
* 5.5寸手机版:申报状态三色标签的着色分类(01=绿/W=靛蓝/其余非空=琥珀)。
* 与 GjcMaWb.declareTagColor 同源逻辑,作用于分单自身的 arrivalStatus。
*/
val haWbTagColor: String
get() = when (arrivalStatus) {
"01" -> "green"
"W" -> "indigo"
else -> if (arrivalStatus.isNullOrEmpty()) "" else "amber"
}
/**
* 5.5寸手机版「回执」页:运抵操作时间(格式化)。
*/
val arrivalOpDateText: String
get() = arrivalOpDate?.let {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(it)
} ?: ""
}
/**

View File

@@ -44,6 +44,9 @@ data class GjcMove(
var likeNo: String = "", // 部分运单号no(模糊查询)
var range: String = "",
var flight: String = "", // 航班信息(航班日期/航班号,如 20260924/MU2222)
var businessName: String = "", // 业务类型名称(中)
var opUserName: String = "", // 操作人(移库人)姓名
// UI扩展字段 - 使用ObservableBoolean实现自动UI更新
val checked: ObservableBoolean = ObservableBoolean(false)
@@ -56,4 +59,29 @@ data class GjcMove(
var isSelected: Boolean
get() = checked.get()
set(value) = checked.set(value)
// ========== 5.5 寸手持端展示辅助字段(仅用于手机版布局,不参与序列化) ==========
/** 是否已移库(moveState:0-未移交,1-已移交) */
val isMoved: Boolean
get() = moveState == 1
/** 品名:优先中文,为空回退英文 */
val goodsName: String
get() = goodsCn.ifEmpty { goods }
/** 业务类型:优先中文名称,为空回退编码 */
val businessDisplay: String
get() = businessName.ifEmpty { businessType }
/** 移库人:优先姓名,为空回退操作人ID */
val opUserDisplay: String
get() = opUserName.ifEmpty { opId }
/**
* 详情页字段统一空值占位。
* 接口对 businessType / opdate / opId 等字段常返回 null,
* 直接绑会显示成 "null" 或空白,这里统一成 "-"。
*/
fun orDash(value: String?): String = if (value.isNullOrEmpty()) "-" else value
}

View File

@@ -122,4 +122,19 @@ class GjcUldUseBean : Serializable {
// 复磅状态文本
val wtStatusText: String
get() = if (wtDate.isNotEmpty()) "已复磅" else "未复磅"
// ========== 5.5 寸手持端展示辅助字段(仅用于手机版布局,不参与序列化) ==========
/** 是否已交接:有交接时间即视为已交接 */
val isHandovered: Boolean
get() = hoDate.isNotEmpty()
/**
* IMP 代码是否属于危险品类 —— 决定手机版卡片上 IMP 代码标签是红底还是绿底。
*
* 依据 IATA IMP 特货代码惯例:以 R 开头(RFL/RRY/REX 等)及 ICE 归为危险品。
* 若后端另有判定口径,只需改这一处。
*/
val isDgrDangerous: Boolean
get() = dgrCode.startsWith("R", ignoreCase = true) || dgrCode.equals("ICE", ignoreCase = true)
}

View File

@@ -73,6 +73,7 @@ class GjcWeighingRecordBean : Serializable {
var paperTime: String = "" // 单据时间
var userName: String = "" // 收运人
var passageWay: String = "" // 通道号(计重时选定的通道号名称)
var userId: String = "" // 收运人ID
var remark: String = "" // 备注

View File

@@ -10,4 +10,6 @@ class GjcWeighingStatisticsBean {
var totalPc: Long = 0 // 总件数
var totalWeight: Double = 0.0 // 总重量
var cargoWeight: Double = 0.0 // 总货重
var waitCheckInNumber: Int = 0 // 待计重票数(IntExpCheckIn/pageQueryTotal)
var checkingNumber: Int = 0 // 计重中票数(IntExpCheckIn/pageQueryTotal)
}

View File

@@ -46,6 +46,7 @@ class IntImpPickUpDLVBean : Serializable {
var dlvId: String = ""
var dlvIdType: String = ""
var dlvName: String = ""
var refUserName: String = "" // 出库人姓名(refId 关联查询)
var dlvPhone: String = ""
var locFlag: String = ""
var origin: String = ""
@@ -74,4 +75,11 @@ class IntImpPickUpDLVBean : Serializable {
var isSelected: Boolean
get() = checked.get()
set(value) = checked.set(value)
/**
* 5.5寸手机版:是否已出库(出库时间非空)。
* 卡片按此切两种形态:待出库(勾选框+橙标签+库位行)/已出库(绿标签+出库人行)。
*/
val isPickedUp: Boolean
get() = dlvTime.isNotEmpty()
}

View File

@@ -54,4 +54,26 @@ data class IntImpQueryBean(
// ==================== 其他 ====================
var remark: String? = null, // 备注
var activeId: Long? = null // 有效值
)
) {
/**
* 5.5寸手机版-进港查询:卡片状态标签。
* 已出库 = 出库时间(dlvTime)非空;已入库 = 入库时间(inDate)非空;都为空 = 未入库(灰)。
* 接口无独立状态出参,此判定为前端推断(与平板列表「入库时间/出库时间」两列同源)。
*/
val isQueryOutbound: Boolean
get() = !dlvTime.isNullOrEmpty()
val isQueryStored: Boolean
get() = !inDate.isNullOrEmpty()
/** 5.5寸手机版:空值统一占位 "--"(空串与 null 都兜底,布局绑定用) */
fun orDash(value: String?): String = if (value.isNullOrEmpty()) "--" else value
/**
* 5.5寸手机版:代理人展示值(名称优先,空则回退代码)。
* 不能在 XML 里用 `??` 兜底——转换器会把 JSON null 归一成空串,`??` 只判 null 不判空串。
*/
val agentDisplay: String?
get() = if (agentName.isNullOrEmpty()) agentCode else agentName
}

View File

@@ -9,5 +9,14 @@ data class ManifestTotalDto(
var totalPc: Long = 0, // 总件数
var totalWeight: Double = 0.0, // 总重量
var cargoWeight: Double = 0.0, // 总货重
var flightNumber: Int = 0 // 航班航次
// ===== 后端统一统计 DTO 的分项计数(2026-09 后端补齐,按接口各取所需) =====
var flightNumber: Int = 0, // 航班航次
var uldNumber: Int = 0, // 板箱数(出库交接 Tab 角标)
var storedNumber: Int = 0, // 已入库票数(出港/进港查询)
var departedNumber: Int = 0, // 已离港(出港)/ 已出库(进港)票数
var notClearNumber: Int = 0, // 未清仓票数(出港/进港仓库)
var clearNumber: Int = 0, // 已清仓票数(出港/进港仓库)
var waitCheckInNumber: Int = 0, // 待计重票数(出港计重)
var checkingNumber: Int = 0 // 计重中票数(出港计重)
)

View File

@@ -6,6 +6,7 @@ import com.lukouguoji.module_base.interfaces.ICheck
class MoveStashBean : ICheck {
var activeId: String = "" // null
var agentCode: String = "" // HFEXB
var agentName: String = "" // 代理人名称(search 出参有定义)
var awbType: String = "" // 国际进港(国内中转)
var businessType: String = "" // null
var by1: String = "" // null
@@ -26,7 +27,9 @@ class MoveStashBean : ICheck {
var flight: String = "" // 20240711/MU7766
var fno: String = "" // MU7766
var goods: String = "" // 国际货
var mawbId: String = "" // 996422
var mawbId: String = "" // 996422(searchMoved 出参不含此字段)
var moveId: String = "" // CIII260930141150(已移库,movedDetail 入参)
var opUserName: String = "" // 移库人姓名(searchMoved / movedDetail 出参)
var no: String = "" // 44445074
var opDate: String = "" // null
var opId: String = "" // null
@@ -49,4 +52,23 @@ class MoveStashBean : ICheck {
override fun getCheckObservable(): ObservableBoolean {
return checked
}
// ========== 5.5 寸手持端展示辅助字段(仅前端使用,不参与提交) ==========
/** 是否已移库:由列表加载端点决定(search=待移库 / searchMoved=已移库),载入时打标 */
var isMoved: Boolean = false
/** 卡片长按「拍照」操作浮层显隐(仅 UI 状态) */
val actionMenuVisible = ObservableBoolean(false)
/** 代理显示:网络层会把 null 归一成空串,须 isNullOrEmpty 双兜底(沿用进港/出港查询口径) */
val agentDisplay: String
get() = if (agentName.isNotEmpty()) agentName else agentCode
/** 移库人:优先姓名,为空回退操作人 ID */
val opUserDisplay: String
get() = opUserName.ifEmpty { opId }
/** 详情页/卡片字段统一空值占位 */
fun orDash(value: String?): String = if (value.isNullOrEmpty()) "-" else value
}

View File

@@ -19,9 +19,16 @@ data class ULDBean(
var status: String = "",
// 备注
var remark: String = "",
// ========== 以下字段接口出参已有(api-doc /eqm/uld/pageQuery) ==========
// 进港航班号
var ifNo: String = "",
// 出港航班号
var efNo: String = "",
var checkInDate: String = "",
var checkOutDate: String = "",
// 来源(空运、陆运)
var source: String = "",
// 入库时间 yyyy-MM-dd HH:mm:ss
// 入库时间 yyyy-MM-dd HH:mm:ss(后端确认「入库时间」取 inStoreTime,与平板端一致)
var inStoreTime: String = "",
// 所在港
var nowPort: String = "",
@@ -35,4 +42,19 @@ data class ULDBean(
"1" -> "故障"
else -> status
}
// ========== 手机版(5.5 寸手持端)状态展示 ==========
// 后端确认状态枚举维持两态(0 正常 / 1 故障),不新增「占用」;
// 手机版按设计稿文案映射 0→可用(绿)、1→维修(红)。
/** 手机版状态文案 */
val statusPhoneName: String
get() = when (status) {
"0" -> "可用"
"1" -> "维修"
else -> statusName()
}
/** 可用(绿色标签) */
val isStatusAvailable: Boolean get() = status == "0"
}

View File

@@ -272,6 +272,8 @@ interface Constant {
const val IntImpTally = "AppIntImpTally" //理货报告
const val IntImpPickUpRecord = "AppIntImpPickUpRecord" //提取记录
const val IntImpPickUpDLV = "AppIntImpPickUpDLV" //提取出库
// 进港移库:后端暂无该模块与权限串(2026-07-31 api-doc 核对),按 AppIntImp* 命名约定预定义,
// 权限串下发前手机首页菜单不会显示该入口(已登记后端接口对接问题清单)
const val IntImpAccidentVisa = "AppIntImpAccidentVisa" //事故签证
const val GjjManifestListActivity = "AppIntExpManifest" //舱单
const val GjjTallyListActivity = "AppIntExpTally" //理货

View File

@@ -899,6 +899,16 @@ interface Api {
@POST("IntExpArrive/deleteDeclare")
suspend fun deleteArriveDeclare(@Body data: RequestBody): BaseResultBean<Boolean>
/**
* 国际出港运抵-分单补充收发货人信息(5.5寸手机版新增)
*
* 后端暂无此接口(api-doc 搜索 IntExpArrive、HaWb 均无匹配),路径为参照 Pad 端国际进港
* IntImpAirManifest/complete 命名习惯拟定的建议值,见 documents/后端接口对接问题清单.xlsx。
* 请求体为分单数组,仅需 haWbId 与收发货人相关字段。
*/
@POST("IntExpArrive/haWb/supplement")
suspend fun supplementArriveHaWb(@Body data: RequestBody): BaseResultBean<Any>
/**
* 国际出港移库-分页查询
* 接口路径: /IntExpMove/pageQuery
@@ -1836,6 +1846,12 @@ interface Api {
@POST("DomImpMove/search")
suspend fun getGnjMoveStashList(@Body data: RequestBody): BaseListBean<MoveStashBean>
/**
* 获取-国内进港移库-已移库列表(移库完成分页搜索,5.5 寸手机端「已移库」Tab)
*/
@POST("DomImpMove/searchMoved")
suspend fun getGnjMoveStashMovedList(@Body data: RequestBody): BaseListBean<MoveStashBean>
/**
* 获取-国内进港移库-列表--运单列表
*/
@@ -1860,6 +1876,13 @@ interface Api {
@GET("DomImpMove/detail")
suspend fun getGnjMoveStashDetail(@Query("mawbId") mawbId: String): BaseResultBean<GnjYiKuBean>
/**
* 国内进港移库-移库完成详情(已移库运单无 mawbId,按 moveId 查,含 pic 三字段与移库人)
* 接口路径: /DomImpMove/movedDetail
*/
@GET("DomImpMove/movedDetail")
suspend fun getGnjMoveStashMovedDetail(@Query("moveId") moveId: String): BaseResultBean<GnjYiKuBean>
/**
* 编辑-国内进港移库(新接口)
*/
@@ -2173,6 +2196,13 @@ interface Api {
@POST("eqm/uld/deleteUld")
suspend fun deleteUld(@Query("uld") uld: String): BaseResultBean<Any>
/**
* 批量删除-ULD-信息(请求体为 ULD 编号字符串数组)
* 接口路径: /eqm/uld/deleteUldBatch
*/
@POST("eqm/uld/deleteUldBatch")
suspend fun deleteUldBatch(@Body data: RequestBody): BaseResultBean<Any>
///////////////////////////////////////////////////////////////////////////
// 冷库登记
///////////////////////////////////////////////////////////////////////////

View File

@@ -104,6 +104,7 @@ object ARouterConstants {
// 国内进港移库(新版本)
const val ACTIVITY_URL_GNJ_YIKU_LIST = "/gnj/GnjYiKuListActivity" //国内进港 移库列表
const val ACTIVITY_URL_GNJ_MOVE_STASH_LIST = "/app/GnjMoveStashListActivity" //国内进港 移库(首页菜单实际跳转页,双形态)
const val ACTIVITY_URL_GNJ_YIKU_DETAILS = "/gnj/GnjYiKuDetailsActivity" //国内进港 移库详情
const val ACTIVITY_URL_GNJ_YIKU_EDIT = "/gnj/GnjYiKuEditActivity" //国内进港 移库编辑
const val ACTIVITY_URL_GNJ_YIKU_HANDOVER = "/gnj/GnjYiKuHandoverActivity" //国内进港 移交编辑/详情
@@ -145,6 +146,8 @@ object ARouterConstants {
const val ACTIVITY_URL_INT_EXP_MOVE_EDIT = "/gjc/IntExpMoveEditActivity" //国际出港 出港移库编辑/详情
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

@@ -0,0 +1,136 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import android.util.SparseArray
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.lukouguoji.module_base.R
/**
* 手机版底部操作条(5.5 寸手持端公共组件)
*
* 结构:全选(圆形勾选框 + 文字)+ 已选统计 + 次操作按钮(可选,最多 2 个)+ 主操作按钮。
* 平板底部有多个批量操作按钮的页面,用 subAction1Text/subAction2Text 承载其余操作(如出库交接「允许装机」「配运」)。
* 对应设计稿各多选列表页底部的白色操作条。
*
* XML 用法:
* ```xml
* <com.lukouguoji.module_base.ui.weight.phone.PhoneBottomBar
* android:layout_width="match_parent"
* android:layout_height="wrap_content"
* allChecked="@{viewModel.isAllChecked}"
* countText='@{"已选 " + viewModel.selectedCount + " 项"}'
* actionText="@{`交接`}"
* setOnAllCheckClick="@{(v)-> viewModel.checkAllClick()}"
* setOnActionClick="@{(v)-> viewModel.completeHandover()}" />
* ```
*/
class PhoneBottomBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
val llCheckAll: LinearLayout
val ivCheckAll: ImageView
val tvCount: TextView
val tvAction: TextView
val tvSubAction1: TextView
val tvSubAction2: TextView
/** 全选状态(驱动勾选框图标切换) */
var allChecked: Boolean = false
set(v) {
field = v
ivCheckAll.setImageResource(
if (v) R.drawable.ic_phone_check_round_checked
else R.drawable.ic_phone_check_round_unchecked
)
}
/** 已选统计文案,如「已选 3 项」 */
var countText: String = ""
set(v) {
field = v
tvCount.text = v
}
/** 主按钮文字 */
var actionText: String = ""
set(v) {
field = v
tvAction.text = v
}
/** 次操作按钮 1 文字(空则隐藏) */
var subAction1Text: String = ""
set(v) {
field = v
tvSubAction1.text = v
tvSubAction1.visibility = if (v.isEmpty()) GONE else VISIBLE
}
/** 次操作按钮 2 文字(空则隐藏) */
var subAction2Text: String = ""
set(v) {
field = v
tvSubAction2.text = v
tvSubAction2.visibility = if (v.isEmpty()) GONE else VISIBLE
}
/**
* 危险操作样式:浅红底 + 红字 + 垃圾桶图标(如 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
setBackgroundColor(ContextCompat.getColor(context, R.color.white))
elevation = 8 * resources.displayMetrics.density
val padH = (16 * resources.displayMetrics.density).toInt()
val padV = (14 * resources.displayMetrics.density).toInt()
setPadding(padH, padV, padH, padV)
inflate(context, R.layout.layout_phone_bottom_bar, this)
llCheckAll = findViewById(R.id.ll_check_all)
ivCheckAll = findViewById(R.id.iv_check_all)
tvCount = findViewById(R.id.tv_count)
tvAction = findViewById(R.id.tv_action)
tvSubAction1 = findViewById(R.id.tv_sub_action1)
tvSubAction2 = findViewById(R.id.tv_sub_action2)
}
/** 只保存/恢复自身状态,避免内部子控件 id 与其他复合组件实例串档 */
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
}

View File

@@ -0,0 +1,238 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import android.util.SparseArray
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.doOnTextChanged
import androidx.databinding.InverseBindingListener
import com.lukouguoji.module_base.R
import com.lukouguoji.module_base.adapter.bindAdapter
import com.lukouguoji.module_base.adapter.bindOnSelected
import com.lukouguoji.module_base.interfaces.IOnSpinnerSelected
import com.lukouguoji.module_base.ktx.formatDate
import com.lukouguoji.module_base.ktx.getActivity
import com.lukouguoji.module_base.util.Common
import dev.utils.app.info.KeyValue
import java.util.Calendar
/**
* 手机版表单 / 筛选项(5.5 寸手持端公共组件)
*
* 结构:上方标题(14sp #374151)+ 下方 40dp 高控件框(白底 + 1px 边框 + 8dp 圆角)。
* 支持三种形态,由 [type] 决定:
*
* | type | 控件 | 右侧图标 |
* |-----------|-----------|----------|
* | `INPUT` | EditText | 无 |
* | `DATE` | TextView(点击弹日历)| 日历 |
* | `SPINNER` | Spinner | 下箭头 |
*
* XML 用法:
* ```xml
* <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}" />
* ```
*
* 手机端专用,与平板端 PadDataLayoutNew 并列存在,互不影响。
*/
class PhoneDataLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
val tvTitle: TextView
val tvMust: TextView
val llContent: LinearLayout
val et: EditText
val tv: TextView
val spinner: Spinner
val iv: ImageView
/** 双向绑定回调 */
var onChangeListener: InverseBindingListener? = null
/** 输入完成 / 选择完成回调 */
var refreshCallBack: (() -> Unit)? = null
var type: PhoneDataLayoutType = PhoneDataLayoutType.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
bindAdapter(spinner, list, v, R.layout.item_spinner_list_14sp)
}
var value: String = ""
set(v) {
if (field == v) return
field = v
applyValue()
onChangeListener?.onChange()
}
var list: List<KeyValue> = emptyList()
set(v) {
field = v
bindAdapter(spinner, v, hint, R.layout.item_spinner_list_14sp)
applyValue()
}
var required: Boolean = false
set(v) {
field = v
tvMust.visibility = if (v) VISIBLE else GONE
}
var enable: Boolean = true
set(v) {
field = v
llContent.isEnabled = v
et.isEnabled = v
spinner.isEnabled = v
}
init {
orientation = VERTICAL
inflate(context, R.layout.layout_phone_data_layout, this)
tvTitle = findViewById(R.id.tv_title)
tvMust = findViewById(R.id.tv_must)
llContent = findViewById(R.id.ll_content)
et = findViewById(R.id.et)
tv = findViewById(R.id.tv)
spinner = findViewById(R.id.spinner)
iv = findViewById(R.id.iv)
et.doOnTextChanged { text, _, _, _ ->
// 仅 INPUT 形态才回写,避免隐藏的 EditText 被系统状态恢复时误清空值
if (type == PhoneDataLayoutType.INPUT) {
value = text.toString()
}
}
bindOnSelected(spinner, object : IOnSpinnerSelected {
override fun onSelected(position: Int) {
value = list.getOrNull(position)?.value ?: ""
refreshCallBack?.invoke()
}
})
applyType()
}
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) {
PhoneDataLayoutType.INPUT -> {
et.visibility = VISIBLE
tv.visibility = GONE
spinner.visibility = GONE
iv.visibility = GONE
llContent.setOnClickListener(null)
llContent.isClickable = false
}
PhoneDataLayoutType.DATE -> {
et.visibility = GONE
tv.visibility = VISIBLE
spinner.visibility = GONE
iv.visibility = VISIBLE
iv.setImageResource(R.drawable.ic_phone_calendar)
llContent.setOnClickListener(dateClick)
}
PhoneDataLayoutType.SPINNER -> {
et.visibility = GONE
tv.visibility = GONE
spinner.visibility = VISIBLE
iv.visibility = VISIBLE
iv.setImageResource(R.drawable.ic_phone_chevron_down)
llContent.setOnClickListener(null)
llContent.isClickable = false
}
}
}
private fun applyValue() {
when (type) {
PhoneDataLayoutType.INPUT -> {
if (et.text.toString() != value) et.setText(value)
}
PhoneDataLayoutType.DATE -> {
tv.text = value
}
PhoneDataLayoutType.SPINNER -> {
if (value.isNotEmpty()) {
val position = list.indexOfFirst { it.value == value }
if (position >= 0) spinner.setSelection(position)
}
}
}
}
/**
* 同一页面往往放置多个本组件实例,而它们的内部子控件(et / tv / spinner)共用同一套 id。
* 系统按 id 保存/恢复视图状态会发生跨实例串档(典型表现:日期项被邻近输入框的空文本覆盖)。
* 这里只保存/恢复自身状态、不下发给子控件;控件取值本就由 ViewModel + DataBinding 持有,无需视图级恢复。
*/
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
/** 重置为空值(筛选弹层「重置」按钮使用) */
fun reset() {
value = ""
if (type == PhoneDataLayoutType.SPINNER && list.isNotEmpty()) {
spinner.setSelection(if (hint.isEmpty()) list.size - 1 else list.size)
}
}
}
/** [PhoneDataLayout] 支持的控件形态 */
enum class PhoneDataLayoutType {
INPUT, // 文本输入
DATE, // 日期选择
SPINNER, // 下拉选择
}

View File

@@ -0,0 +1,134 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import android.util.SparseArray
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.lukouguoji.module_base.R
/**
* 手机版底部筛选弹层(5.5 寸手持端公共组件)
*
* 半透明遮罩 + 底部白色面板(顶部 20dp 圆角),面板内含标题行、内容插槽、重置/确认按钮行。
* 对应设计稿各列表页的「筛选条件」弹层。
*
* **内容插槽**:直接把筛选项(一般是 [PhoneDataLayout])写在本组件标签内即可,
* [onFinishInflate] 会自动把它们移入面板中部,无需关心内部层级:
*
* ```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 ... />
* <com.lukouguoji.module_base.ui.weight.phone.PhoneDataLayout ... />
*
* </com.lukouguoji.module_base.ui.weight.phone.PhoneFilterPanel>
* ```
*
* 相邻筛选项之间的 20dp 间距由本组件统一施加,使用方无需逐个写 marginTop。
*/
class PhoneFilterPanel @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
/**
* 同一页面可能出现多个含内部 id 的手机版复合组件,系统按 id 保存/恢复视图状态会跨实例串档。
* 这里只保存/恢复自身状态、不下发给子控件;筛选值由 ViewModel + DataBinding 持有,无需视图级恢复。
*/
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
val llPanel: LinearLayout
val llContent: LinearLayout
val tvTitle: TextView
val ivClose: ImageView
val tvReset: TextView
val tvConfirm: TextView
/** 遮罩点击 / 关闭图标点击回调 */
private var dismissListener: OnClickListener? = null
var title: String = ""
set(v) {
field = v
if (v.isNotEmpty()) tvTitle.text = v
}
/** 弹层显隐 */
var panelVisible: Boolean = false
set(v) {
field = v
visibility = if (v) VISIBLE else GONE
}
init {
setBackgroundColor(ContextCompat.getColor(context, R.color.black_tran50))
visibility = GONE
inflate(context, R.layout.layout_phone_filter_panel, this)
llPanel = findViewById(R.id.ll_panel)
llContent = findViewById(R.id.ll_content)
tvTitle = findViewById(R.id.tv_title)
ivClose = findViewById(R.id.iv_close)
tvReset = findViewById(R.id.tv_reset)
tvConfirm = findViewById(R.id.tv_confirm)
// 遮罩点击关闭;面板自身已 clickable,不会穿透
setOnClickListener { dismissListener?.onClick(this) }
ivClose.setOnClickListener { dismissListener?.onClick(it) }
}
/**
* 把 XML 中声明在本组件内的子 View 移入面板内容区,并统一施加 20dp 项间距。
*/
override fun onFinishInflate() {
super.onFinishInflate()
// index 0 是内部面板(init 中 inflate 得到),其后才是使用方声明的子 View
val declared = mutableListOf<View>()
for (i in childCount - 1 downTo 1) {
declared.add(0, getChildAt(i))
removeViewAt(i)
}
declared.forEachIndexed { index, child ->
llContent.addView(child)
if (index > 0) {
(child.layoutParams as? LinearLayout.LayoutParams)?.let { lp ->
lp.topMargin = (20 * resources.displayMetrics.density).toInt()
child.layoutParams = lp
}
}
}
}
fun setOnDismiss(listener: OnClickListener?) {
dismissListener = listener
}
/** 遍历内容区所有 [PhoneDataLayout] 并重置为空值 */
fun resetAllFields() {
for (i in 0 until llContent.childCount) {
(llContent.getChildAt(i) as? PhoneDataLayout)?.reset()
}
}
}

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

@@ -0,0 +1,112 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.lukouguoji.module_base.R
/**
* 手机版详情页字段项(5.5 寸手持端公共组件)
*
* 竖排「小号灰标签(12sp)+ 数值(15sp)」,用于详情页两列信息网格。
* 对应设计稿「板箱详情」等详情卡片内的字段。
*
* XML 用法:
* ```xml
* <com.lukouguoji.module_base.ui.weight.phone.PhoneKvItem
* android:layout_width="0dp"
* android:layout_height="wrap_content"
* android:layout_weight="1"
* title="@{`装机重`}"
* value="@{bean.netWeight}" />
* ```
*
* 需要突出的字段(如库位)设 `tag="@{true}"`,数值会渲染为橙色高亮标签。
*/
class PhoneKvItem @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
val tvTitle: TextView
val tvValue: TextView
var title: String = ""
set(v) {
field = v
tvTitle.text = v
}
var value: String = ""
set(v) {
field = v
// 详情页空值统一显示为 "-",与设计稿一致
tvValue.text = v.ifEmpty { "-" }
applyStyle()
}
/** 是否以橙色高亮标签样式展示数值 */
var tag: Boolean = false
set(v) {
field = v
applyStyle()
}
/** 高亮标签配色:"orange"(默认,如库位)/ "green"(如海关放行正常态) */
var tagColor: String = "orange"
set(v) {
field = v
applyStyle()
}
/**
* 空值不套高亮标签底 —— 否则占位符 "-"/"--" 会显示成一个孤零零的色块。
*/
private fun applyStyle() {
if (tag && value.isNotEmpty() && value != "-" && value != "--") {
if (tagColor == "green") {
tvValue.setBackgroundResource(R.drawable.bg_phone_tag_green)
tvValue.setTextColor(ContextCompat.getColor(context, R.color.phone_tag_green_text))
} else {
tvValue.setBackgroundResource(R.drawable.bg_phone_value_orange)
tvValue.setTextColor(ContextCompat.getColor(context, R.color.phone_orange_text))
}
tvValue.setTypeface(tvValue.typeface, android.graphics.Typeface.BOLD)
val padH = (4 * resources.displayMetrics.density).toInt()
tvValue.setPadding(padH, 0, padH, 0)
} else {
tvValue.background = null
tvValue.setTextColor(ContextCompat.getColor(context, R.color.phone_text_title))
tvValue.setPadding(0, 0, 0, 0)
}
}
init {
orientation = VERTICAL
tvTitle = TextView(context).apply {
textSize = 12f
setTextColor(ContextCompat.getColor(context, R.color.phone_text_hint))
}
addView(
tvTitle,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
)
tvValue = TextView(context).apply {
textSize = 15f
setTextColor(ContextCompat.getColor(context, R.color.phone_text_title))
}
addView(
tvValue,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
topMargin = (2 * resources.displayMetrics.density).toInt()
gravity = Gravity.START
}
)
}
}

View File

@@ -0,0 +1,139 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import android.util.SparseArray
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.widget.doOnTextChanged
import androidx.databinding.InverseBindingListener
import com.lukouguoji.module_base.R
import com.lukouguoji.module_base.ktx.setUpperCaseAlphanumericFilter
/**
* 手机版搜索栏(5.5 寸手持端公共组件)
*
* 结构:圆角搜索框(放大镜 + 输入框 + 扫码图标)+ 右侧圆形筛选按钮。
* 对应设计稿「5.5寸手持端设计文件」各列表页顶部搜索区。
*
* XML 用法:
* ```xml
* <com.lukouguoji.module_base.ui.weight.phone.PhoneSearchBar
* android:layout_width="match_parent"
* android:layout_height="wrap_content"
* hint="@{`搜索ULD编号`}"
* value="@={viewModel.uldNo}"
* upperCase="@{true}"
* setOnScanClickListener="@{(v)-> viewModel.scanUld()}"
* setOnFilterClickListener="@{(v)-> activity.toggleFilterPanel()}"
* setOnSearchListener="@{()-> viewModel.searchClick()}" />
* ```
*
* 说明:与平板端 [com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout] 并列,
* 二者互不影响;手机布局用本组件,平板布局继续用 PadSearchLayout。
*/
class PhoneSearchBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
val llSearch: LinearLayout
val ivSearch: ImageView
val et: EditText
val ivScan: ImageView
val ivFilter: ImageView
/** 双向绑定回调 */
var onChangeListener: InverseBindingListener? = null
/** 键盘「搜索」键 / 扫码完成后触发的查询回调 */
var searchCallBack: (() -> Unit)? = null
var value: String = ""
set(v) {
if (field == v) return
field = v
if (et.text.toString() != v) et.setText(v)
onChangeListener?.onChange()
}
var hint: String = ""
set(v) {
field = v
et.hint = v
}
/** 是否显示扫码图标(默认显示) */
var showScan: Boolean = true
set(v) {
field = v
ivScan.visibility = if (v) VISIBLE else GONE
}
/** 是否显示右侧筛选按钮(默认显示) */
var showFilter: Boolean = true
set(v) {
field = v
ivFilter.visibility = if (v) VISIBLE else GONE
}
/** 输入内容强制大写字母数字(运单号 / ULD 编号等场景) */
var upperCase: Boolean = false
set(v) {
field = v
if (v) et.setUpperCaseAlphanumericFilter()
}
init {
orientation = HORIZONTAL
gravity = android.view.Gravity.CENTER_VERTICAL
inflate(context, R.layout.layout_phone_search_bar, this)
llSearch = findViewById(R.id.ll_search)
ivSearch = findViewById(R.id.iv_search)
et = findViewById(R.id.et)
ivScan = findViewById(R.id.iv_scan)
ivFilter = findViewById(R.id.iv_filter)
et.doOnTextChanged { text, _, _, _ ->
value = text.toString()
}
// 软键盘「搜索」键触发查询
et.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchCallBack?.invoke()
true
} else {
false
}
}
}
/**
* 只保存/恢复自身状态,不下发给内部子控件。
* 内部 et / iv_* 的 id 在每个实例中相同,交由系统按 id 恢复会跨实例串档;
* 取值本就由 ViewModel + DataBinding 持有,无需视图级恢复。
*/
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
/** 供外部(如扫码回调)复用的图标点击入口 */
fun setOnScanClick(listener: View.OnClickListener?) {
ivScan.setOnClickListener(listener)
}
fun setOnFilterClick(listener: View.OnClickListener?) {
ivFilter.setOnClickListener(listener)
}
}

View File

@@ -0,0 +1,108 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import android.util.SparseArray
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import com.lukouguoji.module_base.R
/**
* 手机版卡片统计框(5.5 寸手持端公共组件)
*
* 浅灰底 + 8dp 圆角,横向等宽 2~4 列,每列为「小号灰标签 + 加粗数值」,列间带竖分隔线。
* 对应设计稿列表卡片中部的统计区(如「架子车号 / 目的港 / 装机重」)。
*
* XML 用法(列数由是否传 label 决定,未传的列自动隐藏):
* ```xml
* <com.lukouguoji.module_base.ui.weight.phone.PhoneStatBox
* android:layout_width="match_parent"
* android:layout_height="wrap_content"
* label1="@{`架子车号`}" value1="@{bean.carId}"
* label2="@{`目的港`}" value2="@{bean.fdest}"
* label3="@{`装机重`}" value3='@{bean.netWeight + "kg"}' />
* ```
*/
class PhoneStatBox @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val columns: List<LinearLayout>
private val labels: List<TextView>
private val values: List<TextView>
/** 分隔线(下标 0 对应第 2 列左侧,依次类推) */
private val dividers: List<View>
init {
orientation = HORIZONTAL
setBackgroundResource(R.drawable.bg_phone_stat_box)
val pad = (12 * resources.displayMetrics.density).toInt()
setPadding(pad, pad, pad, pad)
inflate(context, R.layout.layout_phone_stat_box, this)
columns = listOf(
findViewById(R.id.ll_col1),
findViewById(R.id.ll_col2),
findViewById(R.id.ll_col3),
findViewById(R.id.ll_col4)
)
labels = listOf(
findViewById(R.id.tv_label1),
findViewById(R.id.tv_label2),
findViewById(R.id.tv_label3),
findViewById(R.id.tv_label4)
)
values = listOf(
findViewById(R.id.tv_value1),
findViewById(R.id.tv_value2),
findViewById(R.id.tv_value3),
findViewById(R.id.tv_value4)
)
dividers = listOf(
findViewById(R.id.divider2),
findViewById(R.id.divider3),
findViewById(R.id.divider4)
)
// 默认只展示第 1 列,其余待 label 赋值后再显示
for (i in 1..3) setColumnVisible(i, false)
}
/**
* 只保存/恢复自身状态。列表中每个卡片都含一个本组件,内部子控件 id 相同,
* 交由系统按 id 恢复会跨实例串档。
*/
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
/** 设置第 [index] 列(0 起)的标签,传空则隐藏该列 */
fun setLabel(index: Int, text: String?) {
if (index !in labels.indices) return
labels[index].text = text.orEmpty()
setColumnVisible(index, !text.isNullOrEmpty())
}
/** 设置第 [index] 列(0 起)的数值 */
fun setValue(index: Int, text: String?) {
if (index !in values.indices) return
values[index].text = text.orEmpty()
}
private fun setColumnVisible(index: Int, visible: Boolean) {
val flag = if (visible) VISIBLE else GONE
columns[index].visibility = flag
// 第 2~4 列连带其左侧分隔线一起显隐
if (index in 1..3) dividers[index - 1].visibility = flag
}
}

View File

@@ -0,0 +1,182 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import android.util.SparseArray
import android.view.Gravity
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.databinding.InverseBindingListener
import com.lukouguoji.module_base.R
import dev.utils.app.info.KeyValue
/**
* 手机版状态 Tab 条(5.5 寸手持端公共组件)
*
* 每个 Tab = 文字 + 可选角标数字,选中态为「深色加粗文字 + 底部 2dp 全宽指示条」。
* 对应设计稿各列表页的「待交接 / 已交接」「全部 / 待移库 / 已移库」等状态切换栏。
*
* XML 用法:
* ```xml
* <com.lukouguoji.module_base.ui.weight.phone.PhoneStatusTab
* android:layout_width="match_parent"
* android:layout_height="wrap_content"
* tabs="@{viewModel.handoverStateTabs}"
* counts="@{viewModel.handoverStateCounts}"
* value="@={viewModel.handoverState}"
* setOnTabChanged="@{()-> activity.onTabChanged()}" />
* ```
*
* [tabs] 用 [KeyValue]:`key` 为显示文字,`value` 为写回 [value] 的业务值。
* [counts] 为与 tabs 等长的角标文案列表,传 null 或空串则该 Tab 不显示角标。
*/
class PhoneStatusTab @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
/**
* 同一页面可能出现多个含内部 id 的手机版复合组件,系统按 id 保存/恢复视图状态会跨实例串档。
* 这里只保存/恢复自身状态、不下发给子控件;选中值由 ViewModel + DataBinding 持有,无需视图级恢复。
*/
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
/** 双向绑定回调 */
var onChangeListener: InverseBindingListener? = null
/** Tab 切换回调(用户点击引起的变化才触发,外部赋值不触发) */
var tabChangedCallBack: (() -> Unit)? = null
var tabs: List<KeyValue> = emptyList()
set(v) {
field = v
buildTabs()
}
var counts: List<String> = emptyList()
set(v) {
field = v
refreshState()
}
var value: String = ""
set(v) {
if (field == v) return
field = v
refreshState()
onChangeListener?.onChange()
}
init {
orientation = HORIZONTAL
setBackgroundColor(ContextCompat.getColor(context, R.color.white))
}
private fun buildTabs() {
removeAllViews()
tabs.forEachIndexed { index, tab ->
addView(createTab(tab, index))
}
refreshState()
}
private fun createTab(tab: KeyValue, index: Int): View {
val column = LinearLayout(context).apply {
orientation = VERTICAL
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
setOnClickListener {
val target = tab.value ?: ""
if (value != target) {
value = target
tabChangedCallBack?.invoke()
}
}
}
// 文字 + 角标
val row = LinearLayout(context).apply {
orientation = HORIZONTAL
gravity = Gravity.CENTER
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(42))
}
row.addView(TextView(context).apply {
id = titleIdOf(index)
text = tab.key
textSize = 14f
setTypeface(typeface, android.graphics.Typeface.BOLD)
})
row.addView(TextView(context).apply {
id = badgeIdOf(index)
textSize = 11f
gravity = Gravity.CENTER
setBackgroundResource(R.drawable.bg_phone_badge)
setTextColor(ContextCompat.getColor(context, R.color.phone_primary))
setPadding(dp(6), dp(1), dp(6), dp(1))
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
).apply { marginStart = dp(6) }
})
column.addView(row)
// 底部指示条
column.addView(View(context).apply {
id = indicatorIdOf(index)
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(2))
})
return column
}
private fun refreshState() {
tabs.forEachIndexed { index, tab ->
val selected = (tab.value ?: "") == value
findViewById<TextView>(titleIdOf(index))?.setTextColor(
ContextCompat.getColor(
context,
if (selected) R.color.phone_text_title else R.color.phone_text_body
)
)
findViewById<View>(indicatorIdOf(index))?.setBackgroundColor(
if (selected) ContextCompat.getColor(context, R.color.phone_text_title)
else android.graphics.Color.TRANSPARENT
)
findViewById<TextView>(badgeIdOf(index))?.apply {
val count = counts.getOrNull(index).orEmpty()
if (count.isEmpty()) {
visibility = GONE
} else {
visibility = VISIBLE
text = count
}
}
}
}
private fun dp(value: Int): Int =
(value * resources.displayMetrics.density).toInt()
// 动态生成的子 View 用固定偏移量作为 id,避免与资源 id 冲突
private fun titleIdOf(index: Int) = ID_BASE_TITLE + index
private fun badgeIdOf(index: Int) = ID_BASE_BADGE + index
private fun indicatorIdOf(index: Int) = ID_BASE_INDICATOR + index
companion object {
private const val ID_BASE_TITLE = 0x7E100000
private const val ID_BASE_BADGE = 0x7E200000
private const val ID_BASE_INDICATOR = 0x7E300000
}
}

View File

@@ -0,0 +1,310 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.view.View
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import dev.utils.app.info.KeyValue
/**
* 5.5 寸手持端公共组件的 DataBinding 适配器集合
*
* 所有属性均为无命名空间写法(与平板端 PadSearchLayout / PadDataLayoutNew 的用法一致)。
* 同名属性(hint / value / title / list 等)按控件类型解析,与平板端组件互不冲突。
*/
///////////////////////////////////////////////////////////////////////////
// PhoneSearchBar
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("hint", "showScan", "showFilter", "upperCase", requireAll = false)
fun setPhoneSearchBarAttrs(
bar: PhoneSearchBar,
hint: String?,
showScan: Boolean?,
showFilter: Boolean?,
upperCase: Boolean?
) {
hint?.let { bar.hint = it }
showScan?.let { bar.showScan = it }
showFilter?.let { bar.showFilter = it }
upperCase?.let { bar.upperCase = it }
}
@BindingAdapter("value", requireAll = false)
fun setPhoneSearchBarValue(bar: PhoneSearchBar, value: String?) {
value?.let { bar.value = it }
}
@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged")
fun getPhoneSearchBarValue(bar: PhoneSearchBar): String = bar.value
@BindingAdapter("valueAttrChanged", requireAll = false)
fun setPhoneSearchBarValueAttrChanged(bar: PhoneSearchBar, listener: InverseBindingListener) {
bar.onChangeListener = listener
}
@BindingAdapter("setOnScanClickListener", requireAll = false)
fun setPhoneSearchBarScanClick(bar: PhoneSearchBar, listener: View.OnClickListener?) {
bar.setOnScanClick(listener)
}
@BindingAdapter("setOnFilterClickListener", requireAll = false)
fun setPhoneSearchBarFilterClick(bar: PhoneSearchBar, listener: View.OnClickListener?) {
bar.setOnFilterClick(listener)
}
@BindingAdapter("setRefreshCallBack", requireAll = false)
fun setPhoneSearchBarRefreshCallBack(bar: PhoneSearchBar, callBack: (() -> Unit)?) {
bar.searchCallBack = callBack
}
///////////////////////////////////////////////////////////////////////////
// PhoneStatusTab
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("tabs", requireAll = false)
fun setPhoneStatusTabTabs(tab: PhoneStatusTab, tabs: List<KeyValue>?) {
tabs?.let { tab.tabs = it }
}
@BindingAdapter("counts", requireAll = false)
fun setPhoneStatusTabCounts(tab: PhoneStatusTab, counts: List<String>?) {
tab.counts = counts ?: emptyList()
}
@BindingAdapter("value", requireAll = false)
fun setPhoneStatusTabValue(tab: PhoneStatusTab, value: String?) {
value?.let { tab.value = it }
}
@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged")
fun getPhoneStatusTabValue(tab: PhoneStatusTab): String = tab.value
@BindingAdapter("valueAttrChanged", requireAll = false)
fun setPhoneStatusTabValueAttrChanged(tab: PhoneStatusTab, listener: InverseBindingListener) {
tab.onChangeListener = listener
}
@BindingAdapter("setOnTabChanged", requireAll = false)
fun setPhoneStatusTabChanged(tab: PhoneStatusTab, callBack: (() -> Unit)?) {
tab.tabChangedCallBack = callBack
}
///////////////////////////////////////////////////////////////////////////
// PhoneDataLayout
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("title", "hint", "type", "required", "enable", requireAll = false)
fun setPhoneDataLayoutAttrs(
layout: PhoneDataLayout,
title: String?,
hint: String?,
type: PhoneDataLayoutType?,
required: Boolean?,
enable: Boolean?
) {
// type 必须先于 value/hint 生效,否则控件形态与取值分支不匹配
type?.let { layout.type = it }
title?.let { layout.title = it }
hint?.let { layout.hint = it }
required?.let { layout.required = it }
enable?.let { layout.enable = it }
}
@BindingAdapter("value", requireAll = false)
fun setPhoneDataLayoutValue(layout: PhoneDataLayout, value: String?) {
value?.let { layout.value = it }
}
@InverseBindingAdapter(attribute = "value", event = "valueAttrChanged")
fun getPhoneDataLayoutValue(layout: PhoneDataLayout): String = layout.value
@BindingAdapter("valueAttrChanged", requireAll = false)
fun setPhoneDataLayoutValueAttrChanged(
layout: PhoneDataLayout,
listener: InverseBindingListener
) {
layout.onChangeListener = listener
}
@BindingAdapter("list", requireAll = false)
fun setPhoneDataLayoutList(layout: PhoneDataLayout, list: List<KeyValue>?) {
list?.let { layout.list = it }
}
@BindingAdapter("setRefreshCallBack", requireAll = false)
fun setPhoneDataLayoutRefreshCallBack(layout: PhoneDataLayout, callBack: (() -> Unit)?) {
layout.refreshCallBack = callBack
}
///////////////////////////////////////////////////////////////////////////
// PhoneFilterPanel
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("title", "panelVisible", requireAll = false)
fun setPhoneFilterPanelAttrs(panel: PhoneFilterPanel, title: String?, panelVisible: Boolean?) {
title?.let { panel.title = it }
panelVisible?.let { panel.panelVisible = it }
}
@BindingAdapter("setOnResetClick", requireAll = false)
fun setPhoneFilterPanelResetClick(panel: PhoneFilterPanel, listener: View.OnClickListener?) {
panel.tvReset.setOnClickListener(listener)
}
@BindingAdapter("setOnConfirmClick", requireAll = false)
fun setPhoneFilterPanelConfirmClick(panel: PhoneFilterPanel, listener: View.OnClickListener?) {
panel.tvConfirm.setOnClickListener(listener)
}
@BindingAdapter("setOnDismissClick", requireAll = false)
fun setPhoneFilterPanelDismissClick(panel: PhoneFilterPanel, listener: View.OnClickListener?) {
panel.setOnDismiss(listener)
}
///////////////////////////////////////////////////////////////////////////
// PhoneBottomBar
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("allChecked", "countText", "actionText", requireAll = false)
fun setPhoneBottomBarAttrs(
bar: PhoneBottomBar,
allChecked: Boolean?,
countText: String?,
actionText: String?
) {
allChecked?.let { bar.allChecked = it }
countText?.let { bar.countText = it }
actionText?.let { bar.actionText = it }
}
@BindingAdapter("setOnAllCheckClick", requireAll = false)
fun setPhoneBottomBarAllCheckClick(bar: PhoneBottomBar, listener: View.OnClickListener?) {
bar.llCheckAll.setOnClickListener(listener)
}
@BindingAdapter("setOnActionClick", requireAll = false)
fun setPhoneBottomBarActionClick(bar: PhoneBottomBar, listener: View.OnClickListener?) {
bar.tvAction.setOnClickListener(listener)
}
///////////////////////////////////////////////////////////////////////////
// PhoneStatBox
///////////////////////////////////////////////////////////////////////////
@BindingAdapter(
"label1", "value1", "label2", "value2",
"label3", "value3", "label4", "value4",
requireAll = false
)
fun setPhoneStatBoxAttrs(
box: PhoneStatBox,
label1: String?, value1: String?,
label2: String?, value2: String?,
label3: String?, value3: String?,
label4: String?, value4: String?
) {
box.setLabel(0, label1)
box.setValue(0, value1)
box.setLabel(1, label2)
box.setValue(1, value2)
box.setLabel(2, label3)
box.setValue(2, value3)
box.setLabel(3, label4)
box.setValue(3, value4)
}
///////////////////////////////////////////////////////////////////////////
// PhoneKvItem
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("title", "value", "tag", "tagColor", requireAll = false)
fun setPhoneKvItemAttrs(
item: PhoneKvItem,
title: String?,
value: String?,
tag: Boolean?,
tagColor: String?
) {
title?.let { item.title = it }
// tag/tagColor 需先于 value 生效,确保样式切换后再填内容
tagColor?.let { item.tagColor = it }
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 }
}
///////////////////////////////////////////////////////////////////////////
// PhoneBottomBar(追加:次操作按钮,承载平板底部其余批量操作)
///////////////////////////////////////////////////////////////////////////
@BindingAdapter("subAction1Text", "subAction2Text", requireAll = false)
fun setPhoneBottomBarSubActions(bar: PhoneBottomBar, subAction1Text: String?, subAction2Text: String?) {
subAction1Text?.let { bar.subAction1Text = it }
subAction2Text?.let { bar.subAction2Text = it }
}
@BindingAdapter("setOnSubAction1Click", requireAll = false)
fun setPhoneBottomBarSubAction1Click(bar: PhoneBottomBar, listener: View.OnClickListener?) {
bar.tvSubAction1.setOnClickListener(listener)
}
@BindingAdapter("setOnSubAction2Click", requireAll = false)
fun setPhoneBottomBarSubAction2Click(bar: PhoneBottomBar, listener: View.OnClickListener?) {
bar.tvSubAction2.setOnClickListener(listener)
}

View File

@@ -0,0 +1,116 @@
package com.lukouguoji.module_base.util
import android.content.Context
import android.content.res.Resources
import androidx.annotation.LayoutRes
import com.lukouguoji.module_base.MyApplication
/**
* 设备形态判定工具
*
* 用于「平板(10 寸横屏)」与「手机(5.5 寸竖屏)」双形态适配:
* 1. 识别当前设备是平板还是手机
* 2. 提供按形态选择布局的入口(Activity 布局 / RecyclerView item 布局通用)
* 3. 提供手机端 AutoSize 设计基准尺寸常量
*
* 判定依据:`smallestScreenWidthDp`(最小屏幕宽度,与横竖屏无关,是 Android 官方推荐的设备分类维度)
* - 5.5 寸手机 ≈ 360dp
* - 10 寸平板 ≈ 800dp
* - 分界线取官方标准 600dp
*
* 注意:这里读取的是 [Resources.getSystem](系统资源)而非应用 Resources,
* 目的是绕开 AndroidAutoSize 对 density 的动态改写,保证判定结果稳定。
*/
object DeviceUtil {
/** 平板判定阈值:最小屏幕宽度 >= 600dp 视为平板 */
private const val TABLET_MIN_WIDTH_DP = 600
/** 手机端设计稿基准尺寸(对应 5.5 寸手持端设计稿画布 390 × 844) */
const val PHONE_DESIGN_WIDTH_DP = 390
const val PHONE_DESIGN_HEIGHT_DP = 844
/** 平板端设计稿基准尺寸(沿用现有配置) */
const val TABLET_DESIGN_WIDTH_DP_LAND = 1152
const val TABLET_DESIGN_HEIGHT_DP_LAND = 720
const val TABLET_DESIGN_WIDTH_DP_PORT = 720
const val TABLET_DESIGN_HEIGHT_DP_PORT = 1280
/**
* 调试用:强制指定设备形态
*
* 开发阶段可在平板上强制走手机布局做验证,正式发布保持 [ForceMode.AUTO]。
* 用法:`DeviceUtil.forceMode = DeviceUtil.ForceMode.PHONE`
*/
enum class ForceMode { AUTO, PHONE, TABLET }
@JvmStatic
var forceMode: ForceMode = ForceMode.AUTO
/** 缓存自动判定结果,避免每次都读 Configuration */
private var cachedIsTablet: Boolean? = null
/**
* 是否平板
* @param context 可不传,默认用 Application 上下文(判定与具体 Activity 无关)
*/
@JvmStatic
@JvmOverloads
fun isTablet(context: Context? = null): Boolean = when (forceMode) {
ForceMode.PHONE -> false
ForceMode.TABLET -> true
ForceMode.AUTO -> cachedIsTablet ?: (smallestWidthDp(context) >= TABLET_MIN_WIDTH_DP)
.also { cachedIsTablet = it }
}
/** 是否手机 */
@JvmStatic
@JvmOverloads
fun isPhone(context: Context? = null): Boolean = !isTablet(context)
/**
* 获取最小屏幕宽度(dp)
*
* 优先读系统资源(不受 AutoSize 影响);异常时回退到应用资源。
*/
@JvmStatic
@JvmOverloads
fun smallestWidthDp(context: Context? = null): Int {
val fromSystem = Resources.getSystem()?.configuration?.smallestScreenWidthDp ?: 0
if (fromSystem > 0) return fromSystem
val res = (context ?: runCatching { MyApplication.context }.getOrNull())?.resources
return res?.configuration?.smallestScreenWidthDp ?: TABLET_MIN_WIDTH_DP
}
/**
* 按设备形态选择布局
*
* @param tabletLayout 平板布局(现有布局,必填)
* @param phoneLayout 手机布局;传 0 或未提供时,手机上回退使用平板布局
* @return 当前设备应加载的布局 id
*
* 用法(ViewModel 中选择 item 布局):
* ```
* val itemLayoutId = DeviceUtil.layout(R.layout.item_xxx, R.layout.item_xxx_phone)
* ```
*/
@JvmStatic
@JvmOverloads
fun layout(
@LayoutRes tabletLayout: Int,
@LayoutRes phoneLayout: Int = 0,
context: Context? = null
): Int = if (phoneLayout != 0 && isPhone(context)) phoneLayout else tabletLayout
/** 配置发生变化时清空缓存(如分屏、折叠屏形态切换) */
@JvmStatic
fun clearCache() {
cachedIsTablet = null
}
/** 设备形态描述,用于日志排查 */
@JvmStatic
fun describe(): String =
"${if (isTablet()) "TABLET" else "PHONE"}(sw=${smallestWidthDp()}dp, force=$forceMode)"
}

View File

@@ -1,53 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lukouguoji.module_base">
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:icon="@mipmap/logot"
android:roundIcon="@mipmap/logot_round">
<activity
android:name=".ui.signature.SignatureActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="userLandscape" />
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"
android:screenOrientation="userLandscape" />
<activity
android:name=".ui.page.preview.PreviewActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"/>
<!-- PDF预览Activity -->
<activity
android:name=".ui.page.preview.PdfPreviewActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="userLandscape" />
<!-- 海关回执(只读) -->
<activity
android:name=".ui.page.customs.CustomsResponseActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="userLandscape" />
<!-- 屏幕适配 -->
<meta-data
android:name="design_width_in_dp"
android:value="1152" />
<meta-data
android:name="design_height_in_dp"
android:value="720" />
</application>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lukouguoji.module_base">
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:icon="@mipmap/logot"
android:roundIcon="@mipmap/logot_round">
<activity
android:name=".ui.signature.SignatureActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="unspecified" />
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"
android:screenOrientation="unspecified" />
<activity
android:name=".ui.page.preview.PreviewActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"/>
<!-- PDF预览Activity -->
<activity
android:name=".ui.page.preview.PdfPreviewActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="unspecified" />
<!-- 海关回执(只读) -->
<activity
android:name=".ui.page.customs.CustomsResponseActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="unspecified" />
<!-- 屏幕适配 -->
<meta-data
android:name="design_width_in_dp"
android:value="1152" />
<meta-data
android:name="design_height_in_dp"
android:value="720" />
</application>
</manifest>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版 Tab 角标:主色浅底 + 6dp 圆角 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_badge_bg" />
<corners android:radius="6dp" />
</shape>

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,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版禁用态操作按钮:灰底 + 10dp 圆角(出港仓库卡片「清仓」已清仓时置灰) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_tag_gray_bg" />
<corners android:radius="10dp" />
</shape>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版绿色操作按钮:完成态绿实底 + 10dp 圆角(进港移库-拍照上传「确认提交」) -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#0E9F6E" />
<corners android:radius="10dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/phone_green" />
<corners android:radius="10dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版次要按钮(重置):白底 + 浅边框 + 10dp 圆角 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/phone_stat_bg" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/phone_divider" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/phone_divider" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版主操作按钮:主色实底 + 10dp 圆角 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#1D4FD8" />
<corners android:radius="10dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/phone_primary" />
<corners android:radius="10dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版弱化操作按钮:浅蓝底 + 10dp 圆角(出港计重卡片内「提前运抵」) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_primary_light" />
<corners android:radius="10dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版列表卡片背景:白底 + 12dp 圆角 + 浅边框 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="12dp" />
<stroke
android:width="0.5dp"
android:color="@color/phone_divider" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版详情卡片表头:主色底 + 顶部 12dp 圆角(与 bg_phone_card 圆角一致) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_primary" />
<corners
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版详情卡片表头:完成态绿底 + 顶部 12dp 圆角 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_green" />
<corners
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版详情卡片表头:橙色底 + 顶部 12dp 圆角(进港移库-异常照片卡片) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_amber" />
<corners
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版卡片顶部主色描边条(配合 bg_phone_card 使用,出港计重头部卡片) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_primary" />
<corners
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态圆点:主色蓝(出港查询卡片运单号前的指示点) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/phone_primary" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态圆点:灰色(运单追踪时间线未到达节点) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/phone_text_hint" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态圆点:绿色(详情页表头的在途/正常指示点) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/phone_green" />
</shape>

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,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版圆形操作按钮:琥珀色(出港运抵卡片长按菜单「重置」) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#F59E0B" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版圆形操作按钮:翠绿色(出港运抵卡片长按菜单「回执」) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#10B981" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版圆形操作按钮:紫色(出港运抵卡片长按菜单「补充」) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#A855F7" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版圆形操作按钮:红色(出港运抵卡片长按菜单「删除申报」) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/phone_danger_text" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版圆形图标按钮(搜索行右侧筛选按钮):白底 + 浅边框 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/white" />
<stroke
android:width="1dp"
android:color="@color/phone_divider" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版表单/筛选输入框:白底 + 1px 边框 + 8dp 圆角 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/phone_divider" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版底部弹层面板:白底 + 顶部 20dp 圆角 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners
android:topLeftRadius="20dp"
android:topRightRadius="20dp" />
</shape>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版照片加号槽位:灰色虚线边框 + 8dp 圆角(进港移库-异常照片) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/phone_divider"
android:dashGap="4dp"
android:dashWidth="4dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版搜索框背景:白底 + 12dp 圆角 + 浅边框 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="12dp" />
<stroke
android:width="1dp"
android:color="@color/phone_divider" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版卡片内统计框:浅灰底 + 8dp 圆角 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_stat_bg" />
<corners android:radius="8dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态标签:琥珀底(出港运抵申报状态 E),与 bg_phone_tag_green/indigo/gray 同属状态标签家族 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_tag_amber_bg" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态标签:灰底(未完成态) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_tag_gray_bg" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态标签:绿底(已完成态) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_tag_green_bg" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态标签:靛蓝底(出港运抵申报状态 W),与 bg_phone_tag_green/orange/gray 同属状态标签家族 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_tag_indigo_bg" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版状态标签:橙底(待处理态,如「待移库」)
与 bg_phone_tag_green / gray / red 同属状态标签家族,统一 6dp 圆角。
数值高亮(详情页库位那种小色块)请用 bg_phone_value_orange。
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_tag_orange_bg" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版状态标签:红底(危险品等强提示) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_tag_red_bg" />
<corners android:radius="6dp" />
</shape>

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"?>
<!--
手机版数值高亮底:浅橙 + 4dp 圆角
用于详情页需要突出的字段值(如库位),由 PhoneKvItem 的 tag=true 使用。
状态标签(待移库/已完成那类)请用 bg_phone_tag_* 家族。
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/phone_orange_bg" />
<corners android:radius="4dp" />
</shape>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:飞机(航班信息行) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_text_body">
<path
android:fillColor="@android:color/white"
android:pathData="M21,16V14L13,9V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V9L2,14V16L10,13.5V19L8,20.5V22L11.5,21L15,22V20.5L13,19V13.5L21,16Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:日历(日期选择) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_text_body">
<path
android:fillColor="@android:color/white"
android:pathData="M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1H16Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:圆形对勾(已完成状态) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_green">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z" />
</vector>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版圆形复选框 —— 选中态
主色实心圆 + 白色对勾(对应设计稿 .custom-checkbox-round:checked)
-->
<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_primary"
android:pathData="M12,1A11,11 0 1,0 12,23A11,11 0 1,0 12,1Z" />
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.6"
android:scaleY="0.6">
<path
android:fillColor="@color/white"
android:pathData="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z" />
</group>
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版圆形复选框 —— 未选中态
白色填充 + 2dp 灰边圆环(对应设计稿 .custom-checkbox-round 默认态)
-->
<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/white"
android:pathData="M12,2A10,10 0 1,0 12,22A10,10 0 1,0 12,2Z"
android:strokeColor="@color/phone_check_border"
android:strokeWidth="2" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:下箭头(下拉选择) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_text_body">
<path
android:fillColor="@android:color/white"
android:pathData="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:右箭头(查看详情) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_primary">
<path
android:fillColor="@android:color/white"
android:pathData="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 剪贴板图标(MDI clipboard-text,出港待计重页 FAB → 计重记录) -->
<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="M17,9H7V7H17M17,13H7V11H17M14,17H7V15H14M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:关闭(弹层右上角) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_text_hint">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</vector>

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,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:定位(库位行) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_text_body">
<path
android:fillColor="@android:color/white"
android:pathData="M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版件数图标(MDI package-variant-closed),默认灰色,可 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="#9CA3AF"
android:pathData="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版图标:人像(操作人信息行) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/phone_text_body">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 15,14.9 12,14.9Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 手机版特码/标签图标(MDI tag-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="#9CA3AF"
android:pathData="M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.9,2 2,2.9 2,4V11C2,11.55 2.22,12.05 2.59,12.42L11.59,21.42C11.95,21.78 12.45,22 13,22C13.55,22 14.05,21.78 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.45 21.77,11.94 21.41,11.58M13,20L4,11V4H11L20,13M6.5,5C7.33,5 8,5.67 8,6.5C8,7.33 7.33,8 6.5,8C5.67,8 5,7.33 5,6.5C5,5.67 5.67,5 6.5,5Z" />
</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,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 砝码/称重图标(MDI weight,计重记录卡片右上角 → 计重明细),可 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="#374151"
android:pathData="M12,3A4,4 0 0,1 16,7C16,7.73 15.81,8.41 15.46,9H18.5C19.15,9 19.71,9.44 19.87,10.07L21.95,18.36C22.07,18.86 21.96,19.38 21.65,19.79C21.34,20.19 20.86,20.43 20.35,20.43H3.65C3.14,20.43 2.66,20.19 2.35,19.79C2.04,19.38 1.93,18.86 2.05,18.36L4.13,10.07C4.29,9.44 4.85,9 5.5,9H8.54C8.19,8.41 8,7.73 8,7A4,4 0 0,1 12,3M12,5A2,2 0 0,0 10,7A2,2 0 0,0 12,9A2,2 0 0,0 14,7A2,2 0 0,0 12,5Z" />
</vector>

View File

@@ -0,0 +1,35 @@
<?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 />
<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" />
<!-- 回执内容区域(只读,可滚动、可长按复制) -->
<EditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="20dp"
android:background="@drawable/bg_white_radius_8"
android:gravity="top|start"
android:inputType="textMultiLine"
android:lineSpacingExtra="4dp"
android:overScrollMode="always"
android:padding="20dp"
android:scrollbars="vertical"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>
</layout>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<LinearLayout
android:id="@+id/tool_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<ImageView
android:id="@+id/tool_iv_back"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@mipmap/left_icon"
app:tint="@color/white" />
<TextView
android:id="@+id/tool_tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|left"
android:text="返回"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<TextView
android:id="@+id/title_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="主页"
android:textColor="@color/white"
android:textSize="18sp" />
</androidx.appcompat.widget.Toolbar>

View File

@@ -1,35 +1,39 @@
<?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 />
<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" />
<!-- 回执内容区域(只读,可滚动、可长按复制) -->
<EditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="20dp"
android:background="@drawable/bg_white_radius_8"
android:gravity="top|start"
android:inputType="textMultiLine"
android:lineSpacingExtra="4dp"
android:overScrollMode="always"
android:padding="20dp"
android:scrollbars="vertical"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<!--
海关回执详情页(手机版,5.5 寸手持端):只读回执文本卡片。
平板变体:layout-sw600dp/activity_customs_response.xml,两者共用 CustomsResponseActivity 与 etContent。
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<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" />
<!-- 回执内容区域(只读,可滚动、可长按复制) -->
<EditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp"
android:background="@drawable/bg_phone_card"
android:gravity="top|start"
android:inputType="textMultiLine"
android:lineSpacingExtra="4dp"
android:overScrollMode="always"
android:padding="14dp"
android:scrollbars="vertical"
android:textColor="@color/phone_text_body"
android:textSize="13sp" />
</LinearLayout>
</layout>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版下拉选项条目(14sp)
与 PhoneDataLayout 输入框文字大小保持一致,供 5.5 寸手持端 SPINNER 使用。
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:textSize="14sp" />

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhoneBottomBar 内部布局(手机版底部操作条)
结构:全选 + 已选统计 + 次操作按钮(可选,最多 2 个,未设置文字时隐藏)+ 主操作按钮
对应设计稿各多选列表页底部的白色操作条
-->
<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_check_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_check_all"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_phone_check_round_unchecked" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="全选"
android:textColor="@color/phone_text_title"
android:textSize="13sp" />
</LinearLayout>
<!-- 中间留白 -->
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<!-- 已选统计 -->
<TextView
android:id="@+id/tv_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:textColor="@color/phone_text_hint"
android:textSize="12sp" />
<!-- 次操作按钮 1(浅蓝弱按钮,subAction1Text 为空时隐藏) -->
<TextView
android:id="@+id/tv_sub_action1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_phone_btn_weak"
android:gravity="center"
android:paddingHorizontal="12dp"
android:paddingVertical="9dp"
android:textColor="@color/phone_primary"
android:textSize="14sp"
android:visibility="gone" />
<!-- 次操作按钮 2(浅蓝弱按钮,subAction2Text 为空时隐藏) -->
<TextView
android:id="@+id/tv_sub_action2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_phone_btn_weak"
android:gravity="center"
android:paddingHorizontal="12dp"
android:paddingVertical="9dp"
android:textColor="@color/phone_primary"
android:textSize="14sp"
android:visibility="gone" />
<!-- 主操作按钮 -->
<TextView
android:id="@+id/tv_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_phone_btn_primary"
android:gravity="center"
android:paddingHorizontal="20dp"
android:paddingVertical="9dp"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
</merge>

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhoneDataLayout 内部布局(手机版表单 / 筛选项)
结构:上方标题 + 下方控件框(输入 / 日期 / 下拉三态互斥显示)
对应设计稿:5.5 寸手持端筛选弹层内的条件项
-->
<merge 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"
tools:parentTag="android.widget.LinearLayout">
<!-- 标题行:标题 + 必填星号 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:gravity="center_vertical"
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="14sp" />
<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_tag_red_bg"
android:textSize="14sp"
android:visibility="gone" />
</LinearLayout>
<!-- 控件框 -->
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_phone_input"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp">
<!-- 文本输入 -->
<EditText
android:id="@+id/et"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:inputType="text"
android:maxLines="1"
android:textColor="@color/phone_text_title"
android:textColorHint="@color/phone_text_hint"
android:textSize="14sp" />
<!-- 日期显示(点击弹日历) -->
<TextView
android:id="@+id/tv"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:textColor="@color/phone_text_title"
android:textColorHint="@color/phone_text_hint"
android:textSize="14sp"
android:visibility="gone" />
<!-- 下拉选择 -->
<Spinner
android:id="@+id/spinner"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:visibility="gone" />
<!-- 右侧指示图标(日期=日历 / 下拉=箭头 / 输入=隐藏) -->
<ImageView
android:id="@+id/iv"
android:layout_width="18dp"
android:layout_height="18dp"
android:visibility="gone"
app:tint="@color/phone_text_body" />
</LinearLayout>
</merge>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhoneFilterPanel 内部布局(手机版底部筛选弹层)
结构:底部白色面板(顶部 20dp 圆角)= 标题行 + 内容插槽 + 重置/确认按钮行
外层遮罩由 PhoneFilterPanel 自身(FrameLayout)承担。
XML 中声明在 PhoneFilterPanel 内的子 View 会被自动移入 ll_content。
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/bg_phone_panel_top"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingHorizontal="20dp"
android:paddingTop="24dp"
android:paddingBottom="32dp">
<!-- 标题行:标题居中 + 右上关闭 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="筛选条件"
android:textColor="@color/phone_text_strong"
android:textSize="18sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/iv_close"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="end|center_vertical"
android:src="@drawable/ic_phone_close" />
</FrameLayout>
<!-- 内容插槽:筛选条件项由使用方在 XML 中声明。
外包 ScrollView + weight=1:筛选项少时行为不变(wrap 高度),
筛选项多到超屏时内容区收缩滚动,保证底部「重置/确认」按钮始终可见
(出港查询 9 个筛选项时曾把按钮挤出屏幕) -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:overScrollMode="never"
android:scrollbars="none">
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
<!-- 底部按钮行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_reset"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_btn_outline"
android:gravity="center"
android:text="重置"
android:textColor="@color/phone_text_weak_btn"
android:textSize="15sp" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_btn_primary"
android:gravity="center"
android:text="确认"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>

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

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhoneSearchBar 内部布局(手机版搜索行)
结构:圆角搜索框(放大镜 + 输入 + 扫码) + 右侧圆形筛选按钮
对应设计稿:5.5 寸手持端各列表页顶部搜索区
-->
<merge 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"
tools:parentTag="android.widget.LinearLayout">
<!-- 搜索框 -->
<LinearLayout
android:id="@+id/ll_search"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@drawable/bg_phone_search"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp">
<ImageView
android:id="@+id/iv_search"
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@drawable/img_search"
app:tint="@color/phone_text_hint" />
<EditText
android:id="@+id/et"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingHorizontal="8dp"
android:textColor="@color/phone_text_title"
android:textColorHint="@color/phone_text_hint"
android:textSize="14sp" />
<ImageView
android:id="@+id/iv_scan"
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@drawable/img_scan"
app:tint="@color/phone_primary" />
</LinearLayout>
<!-- 筛选按钮 -->
<ImageView
android:id="@+id/iv_filter"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="10dp"
android:background="@drawable/bg_phone_icon_round"
android:padding="10dp"
android:src="@drawable/img_filter"
app:tint="@color/phone_primary" />
</merge>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhoneStatBox 内部布局(手机版卡片统计框)
最多 4 列,未设置 label 的列自动隐藏(含其左侧分隔线)。
对应设计稿列表卡片中间的浅灰统计区。
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.LinearLayout">
<!-- 第 1 列 -->
<LinearLayout
android:id="@+id/ll_col1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_label1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/phone_text_hint"
android:textSize="11sp" />
<TextView
android:id="@+id/tv_value1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/phone_text_title"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:id="@+id/divider2"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/phone_divider" />
<!-- 第 2 列 -->
<LinearLayout
android:id="@+id/ll_col2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_label2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/phone_text_hint"
android:textSize="11sp" />
<TextView
android:id="@+id/tv_value2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/phone_text_title"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:id="@+id/divider3"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/phone_divider" />
<!-- 第 3 列 -->
<LinearLayout
android:id="@+id/ll_col3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_label3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/phone_text_hint"
android:textSize="11sp" />
<TextView
android:id="@+id/tv_value3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/phone_text_title"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:id="@+id/divider4"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/phone_divider" />
<!-- 第 4 列 -->
<LinearLayout
android:id="@+id/ll_col4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_label4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/phone_text_hint"
android:textSize="11sp" />
<TextView
android:id="@+id/tv_value4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/phone_text_title"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</merge>

View File

@@ -1,42 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<LinearLayout
android:id="@+id/tool_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<ImageView
android:id="@+id/tool_iv_back"
android:layout_width="15dp"
android:layout_height="15dp"
android:src="@mipmap/left_icon"
app:tint="@color/white" />
<TextView
android:id="@+id/tool_tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|left"
android:text="返回"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<TextView
android:id="@+id/title_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="主页"
android:textColor="@color/white"
android:textSize="18sp" />
</androidx.appcompat.widget.Toolbar>
<?xml version="1.0" encoding="utf-8"?>
<!--
手机版公共标题栏(5.5 寸手持端)
与平板版 title_tool_bar.xml 保持相同的根类型(Toolbar)与控件 id:
toolbar / tool_back / tool_iv_back / tool_tv_back / title_name
因此 BaseActivity.setBackArrow() 无需任何改动即可复用。
-->
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/phone_primary"
android:contentInsetStart="0dp"
android:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:contentInsetLeft="0dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<!-- 整条标题栏:左侧返回 + 居中标题 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 居中标题(先声明,保证在返回区下层,不抢占点击) -->
<TextView
android:id="@+id/title_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:maxLines="1"
android:ellipsize="end"
android:text="标题"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<!-- 左侧返回区 -->
<LinearLayout
android:id="@+id/tool_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start|center_vertical"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:id="@+id/tool_iv_back"
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@mipmap/left_icon"
app:tint="@color/white" />
<TextView
android:id="@+id/tool_tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:text="返回"
android:textColor="@color/white"
android:textSize="15sp" />
</LinearLayout>
</FrameLayout>
</androidx.appcompat.widget.Toolbar>

View File

@@ -47,4 +47,38 @@
<color name="black_tran50">#80000000</color> <!-- 50%透明度 -->
<color name="black_tran70">#B3000000</color> <!-- 70%透明度 -->
<!-- ==================== 5.5 寸手持端(手机版)配色 ==================== -->
<color name="phone_primary">#2563EB</color> <!-- 手机版主色(顶栏、主按钮) -->
<color name="phone_primary_light">#EAF1FE</color> <!-- 主色浅底(选中态背景) -->
<color name="phone_bg">#F3F4F6</color> <!-- 页面背景 -->
<color name="phone_text_title">#1F2937</color> <!-- 标题/主要文字 -->
<color name="phone_text_body">#6B7280</color> <!-- 次要文字 -->
<color name="phone_text_hint">#9CA3AF</color> <!-- 占位文字 -->
<color name="phone_divider">#E5E7EB</color> <!-- 分割线/边框 -->
<color name="phone_tag_gray_bg">#F3F4F6</color> <!-- 状态标签-灰底 -->
<color name="phone_tag_gray_text">#6B7280</color>
<color name="phone_tag_green_bg">#DCFCE7</color> <!-- 状态标签-绿底 -->
<color name="phone_tag_green_text">#16A34A</color>
<color name="phone_tag_red_bg">#DC2626</color> <!-- 状态标签-红底(危险品等强提示) -->
<color name="phone_tag_red_text">#FFFFFF</color>
<color name="phone_page_bg">#F5F6FA</color> <!-- 列表页滚动区背景 -->
<color name="phone_stat_bg">#F3F4F6</color> <!-- 卡片内统计框底色 -->
<color name="phone_badge_bg">#DBEAF6</color> <!-- Tab 角标底色 -->
<color name="phone_text_label">#374151</color> <!-- 表单/筛选项标题 -->
<color name="phone_text_strong">#111827</color> <!-- 强调标题(弹层标题、详情主字段) -->
<color name="phone_text_weak_btn">#4B5563</color> <!-- 次要按钮文字(重置) -->
<color name="phone_green">#10B981</color> <!-- 完成态强调色(对勾、信息卡表头) -->
<color name="phone_check_border">#CBD5E1</color> <!-- 未选中复选框边框 -->
<color name="phone_orange_bg">#FFF7ED</color> <!-- 数值高亮-浅橙底(详情页库位等) -->
<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>
<color name="phone_tag_indigo_bg">#E0E7FF</color> <!-- 状态标签-靛蓝底(出港运抵申报状态 W) -->
<color name="phone_tag_indigo_text">#6366F1</color>
<color name="phone_tag_amber_bg">#FEF3C7</color> <!-- 状态标签-琥珀底(出港运抵申报状态 E) -->
<color name="phone_tag_amber_text">#D97706</color>
<color name="phone_amber">#F59E0B</color> <!-- 橙色信息卡表头(进港移库-异常照片) -->
</resources>