feat: 完成国际出港移库/出库交接页面5.5寸手机端适配

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 14:18:29 +08:00
parent 9c4588e47e
commit db062c8ee3
134 changed files with 19602 additions and 2965 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

@@ -56,4 +56,21 @@ data class GjcMove(
var isSelected: Boolean
get() = checked.get()
set(value) = checked.set(value)
// ========== 5.5 寸手持端展示辅助字段(仅用于手机版布局,不参与序列化) ==========
/** 是否已移库moveState0-未移交1-已移交) */
val isMoved: Boolean
get() = moveState == 1
/** 品名:优先中文,为空回退英文 */
val goodsName: String
get() = goodsCn.ifEmpty { goods }
/**
* 详情页字段统一空值占位。
* 接口对 businessType / opdate / opId 等字段常返回 null
* 直接绑会显示成 "null" 或空白,这里统一成 "-"。
*/
fun orDash(value: String?): String = if (value.isNullOrEmpty()) "-" else value
}

View File

@@ -108,4 +108,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

@@ -0,0 +1,92 @@
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 寸手持端公共组件)
*
* 结构:全选(圆形勾选框 + 文字)+ 已选统计 + 主操作按钮。
* 对应设计稿各多选列表页底部的白色操作条。
*
* 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
/** 全选状态(驱动勾选框图标切换) */
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
}
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)
}
/** 只保存/恢复自身状态,避免内部子控件 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,120 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.util.AttributeSet
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) {
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,100 @@
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()
}
/**
* 空值不套高亮标签底 —— 否则占位符 "-" 会显示成一个孤零零的橙色小方块。
*/
private fun applyStyle() {
if (tag && value.isNotEmpty()) {
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,168 @@
package com.lukouguoji.module_base.ui.weight.phone
import android.content.Context
import android.util.AttributeSet
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) {
/** 双向绑定回调 */
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,228 @@
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", requireAll = false)
fun setPhoneKvItemAttrs(item: PhoneKvItem, title: String?, value: String?, tag: Boolean?) {
title?.let { item.title = it }
// tag 需先于 value 生效,确保样式切换后再填内容
tag?.let { item.tag = it }
item.value = value.orEmpty()
}

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,46 +1,46 @@
<?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" />
<!-- 屏幕适配 -->
<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" />
<!-- 屏幕适配 -->
<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,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,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,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,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,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"?>
<!-- 手机版状态标签:灰底(未完成态) -->
<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,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,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,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,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,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,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

@@ -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,64 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhoneBottomBar 内部布局(手机版底部操作条)
结构:全选 + 已选统计 + 主操作按钮
对应设计稿各多选列表页底部的白色操作条
-->
<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" />
<!-- 主操作按钮 -->
<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,87 @@
<?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 中声明 -->
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<!-- 底部按钮行 -->
<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,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,31 @@
<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>
</resources>