18034727885
2026-04-17 bddc945a6d0a6ac33300bddde6b4afe6461741fa
个人中心增加巡游打卡任务模块
22个文件已添加
20个文件已修改
3174 ■■■■■ 已修改文件
app/build.gradle.kts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/Constants.kt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/MyApp.kt 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/model/CheckInMissionPointInfo.kt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/model/CruiseCheckInMissionInfo.kt 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/WorkApi.kt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/WorkRepository.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/MainActivity.kt 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/dialog/ApplyForReassignDialog.kt 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/dialog/ApplyForReassignReasonDialog.kt 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/task/accident/detail/AccidentDetailVM.kt 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/task/normal/NormalTaskDetailVM.kt 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/MineFragment.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/MineVM.kt 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/widget/CircleProgressView.kt 219 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/CheckInMissionPointsListActivity.kt 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/adapter/CheckInMissionPointAdapter.kt 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/viewmodel/CheckInMissionPointsListViewModel.kt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/websocket/IWebSocketListener.kt 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/websocket/WebSocketClient.kt 412 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/websocket/WebSocketConfig.kt 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/websocket/WebSocketManager.kt 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/circle_gray_6.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/layer_list_green_progress_bar.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/layer_list_orange_progress_bar.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/shape_red_5_line_gray_3_bg_4.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_check_in_mission_points_list.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_apply_for_reassign.xml 390 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/dialog_apply_for_reassign_reason.xml 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/fragment_mine.xml 843 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/item_check_in_mission_point.xml 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_apply_reassign_bg.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/ic_clock_note.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/mipmap-xhdpi/icon_note.png 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/attrs.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/colors.xml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/build.gradle.kts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradle/libs.versions.toml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle.kts
@@ -86,6 +86,7 @@
            manifestPlaceholders["icon"] = "@mipmap/ic_launcher"
            buildConfigField("String", "API_HOST", "\"${API_UAT}\"")
            buildConfigField("String", "WS_URL", "\"ws://192.168.110.188:8888/websocket\"")
        }
        create("prd") { // 正式环境
@@ -96,6 +97,7 @@
            manifestPlaceholders["icon"] = "@mipmap/ic_launcher"
            buildConfigField("String", "API_HOST", "\"${API_PRD}\"")
            buildConfigField("String", "WS_URL", "\"ws://192.168.110.188:8888/websocket\"")
        }
    }
@@ -119,7 +121,7 @@
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    implementation("com.aliyun.ams:alicloud-android-push:3.10.1")
    implementation(libs.alicloud)
    implementation(project(":common"))
    implementation(libs.dialog.pickerview)
app/src/main/AndroidManifest.xml
@@ -40,6 +40,9 @@
        android:supportsRtl="true"
        android:theme="@style/Theme.Yunwei">
        <activity
            android:name=".ui.work.checkInMission.CheckInMissionPointsListActivity"
            android:exported="false" />
        <activity
            android:name=".ui.work.site.SiteErrorUploadActivity"
            android:exported="false" />
        <activity
app/src/main/java/com/xw/yunwei/Constants.kt
@@ -18,4 +18,13 @@
            return field
        }
    //改派原因
    val reasons = listOf<String>(
        "任务分派冲突",
        "个人原因",
        "任务距离太远",
        "临时处理紧急情况",
        "交通拥堵",
        "其他"
    )
}
app/src/main/java/com/xw/yunwei/MyApp.kt
@@ -16,6 +16,7 @@
import com.alibaba.sdk.android.push.noonesdk.PushServiceFactory
import com.amap.api.maps.MapsInitializer
import com.scwang.smart.refresh.layout.SmartRefreshLayout
import com.tencent.bugly.crashreport.CrashReport
import com.xw.common.custom.ClassicsFooter11
import com.xw.common.custom.ClassicsHeader11
import com.xw.common.utils.ActivityManager
@@ -24,14 +25,18 @@
import com.xw.common.utils.MMKVManager
import com.xw.yunwei.di.networkModule
import com.xw.yunwei.model.AppPermissionModel
import com.xw.yunwei.websocket.WebSocketManager
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext.startKoin
import com.tencent.bugly.crashreport.CrashReport
class MyApp : Application() {
    companion object {
        private const val TAG = "MyApp"
        lateinit var myApp: MyApp
        var pageShowNum = 0 // 可见页面数
@@ -99,6 +104,7 @@
        SmartRefreshLayout.setDefaultRefreshFooterCreator { context, _ ->
            ClassicsFooter11(context)
        }
        initWebSocket()
    }
    fun initLog() {
@@ -132,7 +138,7 @@
        registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                ActivityManager.addActivity(activity)
                LogUtils.i("zmc-pageA", "onActivityCreated: " + activity.javaClass.name)
                LogUtils.i(TAG, "onActivityCreated: " + activity.javaClass.name)
                if (activity is AppCompatActivity) {
                    activity.supportFragmentManager.registerFragmentLifecycleCallbacks(object :
                        FragmentManager.FragmentLifecycleCallbacks() {
@@ -154,31 +160,31 @@
            }
            override fun onActivityStarted(activity: Activity) {
                LogUtils.d("zmc-pageA", "onActivityStarted: " + activity.javaClass.name)
                LogUtils.d(TAG, "onActivityStarted: " + activity.javaClass.name)
            }
            override fun onActivityResumed(activity: Activity) {
                LogUtils.i("zmc-pageA", "onActivityResumed: " + activity.javaClass.name)
                LogUtils.i(TAG, "onActivityResumed: " + activity.javaClass.name)
                currentShowActivity!!.complete(activity)
                pageShowNum++
            }
            override fun onActivityPaused(activity: Activity) {
                LogUtils.e("zmc-pageA", "onActivityPaused: " + activity.javaClass.name)
                LogUtils.e(TAG, "onActivityPaused: " + activity.javaClass.name)
                pageShowNum--
                currentShowActivity = null
            }
            override fun onActivityStopped(activity: Activity) {
                LogUtils.e("zmc-pageA", "onActivityStopped: " + activity.javaClass.name)
                LogUtils.e(TAG, "onActivityStopped: " + activity.javaClass.name)
            }
            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
                LogUtils.d("zmc-pageA", "onActivitySaveInstanceState: " + activity.javaClass.name)
                LogUtils.d(TAG, "onActivitySaveInstanceState: " + activity.javaClass.name)
            }
            override fun onActivityDestroyed(activity: Activity) {
                LogUtils.e("zmc-pageA", "onActivityDestroyed: " + activity.javaClass.name)
                LogUtils.e(TAG, "onActivityDestroyed: " + activity.javaClass.name)
                ActivityManager.removeActivity(activity)
            }
        })
@@ -266,4 +272,44 @@
        })
    }
    //初始化websocket
    private fun initWebSocket() {
        WebSocketManager.initialize(this)
        CoroutineScope(Dispatchers.IO).launch {
            WebSocketManager.eventFlow.collect { event ->
                when (event) {
                    is WebSocketManager.WebSocketEvent.Connected -> {
                    }
                    is WebSocketManager.WebSocketEvent.ConnectFailed -> {
                    }
                    is WebSocketManager.WebSocketEvent.Message -> {
                        val currentShowActivity = MyApp.myApp.getCurrentShowActivity()
                        LogUtils.d(TAG, "显示activity ${currentShowActivity.javaClass.name}")
                    }
                    is WebSocketManager.WebSocketEvent.BinaryMessage -> {
                    }
                    is WebSocketManager.WebSocketEvent.Disconnected -> {
                    }
                }
            }
        }
    }
    //连接websocket
    fun connectWebSocket() {
        WebSocketManager.connect()
    }
    //断连websocket
    fun disconnectWebSocket() {
        WebSocketManager.disconnect()
    }
}
app/src/main/java/com/xw/yunwei/model/CheckInMissionPointInfo.kt
New file
@@ -0,0 +1,10 @@
package com.xw.yunwei.model
//巡游任务点信息
data class CheckInMissionPointInfo(
    val id: Int,
    val timeRange: String?, // 打卡限时,如"08:00~09:00"
    val address: String?,   // 地址,如"蜀山区华侨中心A座2301"
    val status: String?, // 打卡状态
    val checkTime: String? = null // 打卡时间,仅已打卡/超时打卡时有值,
)
app/src/main/java/com/xw/yunwei/model/CruiseCheckInMissionInfo.kt
New file
@@ -0,0 +1,21 @@
package com.xw.yunwei.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
//巡游打卡任务信息
@Parcelize
data class CruiseCheckInMissionInfo(
    val checkInPointCount: Int,
    val checkInPointFinishCount: Int,
    val checkInPointUnFinishCount: Int,
    val cruiseCheckInTaskTime: String,
    val id: String
) : Parcelable {
    //数据是否有效
    fun isEffective() = (id.toLongOrNull() ?: 0L) > 0L
    //获取完成进度
    fun getFinishProgress() =
        (checkInPointFinishCount.toFloat() / checkInPointCount.toFloat()) * 100f
}
app/src/main/java/com/xw/yunwei/net/WorkApi.kt
@@ -13,6 +13,7 @@
import com.xw.yunwei.model.CarReportModel
import com.xw.yunwei.model.ChargeRecordModel
import com.xw.yunwei.model.CheckRecordModel
import com.xw.yunwei.model.CruiseCheckInMissionInfo
import com.xw.yunwei.model.FenceInfoModel
import com.xw.yunwei.model.MaintainRecordModel
import com.xw.yunwei.model.RepairRecordModel
@@ -138,4 +139,8 @@
    /* 现场情况上报 */
    @POST("operation/maintenance-app/api/v1/pointSituationReport/addPointSituationReport")
    fun addPointSituationReport(@Body params: RequestBody): Call<HttpResponse<Any>>
    /* 获取个人中心巡游打卡任务 */
    @GET("maintenance/maintenance-app/api/v1/cruiseCheckInRule/getMyXpCruiseCheckInRule")
    fun getMyCruiseCheckInMissionInfo(): Call<HttpResponse<CruiseCheckInMissionInfo>>
}
app/src/main/java/com/xw/yunwei/net/WorkRepository.kt
@@ -4,6 +4,7 @@
import com.xw.common.net.BaseHttpManager.Companion.getBody
import com.xw.common.net.interceptAwait
import com.xw.yunwei.model.CheckRecordModel
import com.xw.yunwei.model.CruiseCheckInMissionInfo
import com.xw.yunwei.model.MaintainRecordModel
import com.xw.yunwei.model.SiteModel
import com.xw.yunwei.model.UploadModel
@@ -74,4 +75,9 @@
        api.addPointSituationReport(getBody(map)).interceptAwait()
        return true
    }
    /* 获取个人中心巡游打卡任务 */
    suspend fun getMyCruiseCheckInMissionInfo(): CruiseCheckInMissionInfo? {
        return api.getMyCruiseCheckInMissionInfo().interceptAwait()
    }
}
app/src/main/java/com/xw/yunwei/ui/MainActivity.kt
@@ -17,13 +17,13 @@
import com.xw.yunwei.MyApp
import com.xw.yunwei.R
import com.xw.yunwei.databinding.ActivityMainBinding
import com.xw.yunwei.model.LocationData
import com.xw.yunwei.model.SwitchTabMain
import com.xw.yunwei.model.UserInfo
import com.xw.yunwei.push.MessageHandler.showProtocolDialog
import com.xw.yunwei.ui.task.TaskFragment
import com.xw.yunwei.ui.user.mine.MineFragment
import com.xw.yunwei.ui.work.WorkFragment
import com.xw.yunwei.websocket.WebSocketManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
@@ -44,6 +44,7 @@
        super.init()
        val user = MMKVManager.getInstance().getModel<UserInfo>(KeyPreferences.USER)
        if (user == null) {
            MyApp.myApp.disconnectWebSocket()
            MMKVManager.getInstance().clear()
            ToastUtilCommon.showWarn("登录已过期,请重新登录")
            lifecycleScope.launch {
@@ -62,7 +63,7 @@
    override fun setObservable() {
        super.setObservable()
        viewModel.selectIndex.observe(this) { showFragment(it) }
        MyApp.permissions.observe(this){
        MyApp.permissions.observe(this) {
            viewModel.workPermissions.value = MyApp.findPermissionByName("工作台")?.visible == "0"
            viewModel.taskPermissions.value = MyApp.findPermissionByName("任务中心")?.visible == "0"
@@ -90,7 +91,7 @@
    }
    // 检测是否有新协议
    private fun checkNewProtocol(){
    private fun checkNewProtocol() {
        val needRead = MMKVManager.getInstance().getBoolean(KeyPreferences.NEED_READ, false)
        if (needRead) {
            showProtocolDialog(this, true)
@@ -98,7 +99,7 @@
    }
    // 检测是否有强制升级版本
    private fun checkNewForcedVersion(){
    private fun checkNewForcedVersion() {
        viewModel.getVersion {
            if (it.forceUpdateFlag == 1) {
                UpdateManager.instance!!
@@ -118,7 +119,11 @@
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val permissionList: MutableList<String> = ArrayList()
            for (permission in listOf("android.permission.POST_NOTIFICATIONS")) {
                if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                if (ContextCompat.checkSelfPermission(
                        this,
                        permission
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    permissionList.add(permission)
                }
            }
@@ -126,10 +131,16 @@
            if (!result) { // 无权限 申请
                val pl = arrayOf("android.permission.POST_NOTIFICATIONS")
                XXPermissions.with(this).permission(pl).request(object : OnPermissionCallback {
                    override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) { // 权限同意
                    override fun onGranted(
                        permissions: MutableList<String>,
                        allGranted: Boolean
                    ) { // 权限同意
                    }
                    override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
                    override fun onDenied(
                        permissions: MutableList<String>,
                        doNotAskAgain: Boolean
                    ) {
                        super.onDenied(permissions, doNotAskAgain)
                        ToastUtilCommon.showWarn("您未授权通知权限,可能会导致无法及时收到消息")
                    }
app/src/main/java/com/xw/yunwei/ui/dialog/ApplyForReassignDialog.kt
@@ -1,46 +1,220 @@
package com.xw.yunwei.ui.dialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isVisible
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.xw.common.R
import com.xw.common.utils.BindUtils.getResColor
import com.xw.yunwei.R
import com.xw.yunwei.model.TaskModel
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import kotlin.math.max
//申请改派弹窗
object ApplyForReassignDialog {
    fun show(context: Context){
    // 倒计时Handler
    private val countdownHandler = Handler(Looper.getMainLooper())
    //显示
    fun show(
        context: Context, item: TaskModel,
        applyForReassignBlock: (dialog: MaterialDialog) -> Unit,
        receive: () -> Unit
    ) {
        var runnable: Runnable? = null
        val dialog = MaterialDialog(context).apply {
            customView(
                R.layout.dialog_confirm, noVerticalPadding = true, dialogWrapContent = false
                R.layout.dialog_apply_for_reassign,
                noVerticalPadding = true,
                dialogWrapContent = false
            )
            cancelable(true)
            cornerRadius(15f)
            val tvContent = findViewById<AppCompatTextView>(R.id.content)
            val btnLeft = findViewById<Button>(R.id.btnLeft)
            val btnRight = findViewById<Button>(R.id.btnRight)
            tvContent.text = content
            btnLeft.text = leftText ?: ""
            btnRight.text = rightText ?: ""
            btnLeft.isVisible = !leftText.isNullOrEmpty()
            btnRight.isVisible = !rightText.isNullOrEmpty()
            btnLeft.setOnClickListener {
                if (leftBlock()) {
                    dismiss()
                }
            cancelable(false)
            cancelOnTouchOutside(false)
            cornerRadius(4f)
            val levelId = when (item.taskUrgencyLevel) {
                0 -> R.mipmap.ic_task_item_left_one
                1 -> R.mipmap.ic_task_item_left_two
                2 -> R.mipmap.ic_task_item_left_three
                3 -> R.mipmap.ic_task_item_left_four
                else -> null
            }
            btnRight.setOnClickListener {
                if (rightBlock()) {
                    dismiss()
            val level = findViewById<AppCompatImageView>(R.id.level)
            val countdownLayout = findViewById<LinearLayout>(R.id.countdownLayout)
            val countdownTips = findViewById<AppCompatTextView>(R.id.countdownTips)
            val hour = findViewById<AppCompatTextView>(R.id.hour)
            val colon1 = findViewById<AppCompatImageView>(R.id.colon1)
            val minutes = findViewById<AppCompatTextView>(R.id.minutes)
            val colon2 = findViewById<AppCompatImageView>(R.id.colon2)
            val second = findViewById<AppCompatTextView>(R.id.second)
            val type = findViewById<AppCompatTextView>(R.id.type)
            val plateNumber = findViewById<AppCompatTextView>(R.id.plateNumber)
            val status = findViewById<AppCompatTextView>(R.id.status)
            val address = findViewById<AppCompatTextView>(R.id.address)
            val tvApplyForReassignment =
                findViewById<AppCompatTextView>(R.id.tv_apply_for_reassignment)
            val tvControl = findViewById<AppCompatTextView>(R.id.tvControl)
            levelId?.let { level.setImageResource(levelId) }
            if (item.status == 0 && item.taskCategory == 2) {
                countdownLayout.isVisible = true
                val list = calculateTimeDifference(item.endTime)
                updateTime(
                    hour,
                    colon1,
                    minutes,
                    colon2,
                    second,
                    countdownTips,
                    countdownLayout,
                    list
                )
                if (list.all { it == 0L }) { // 全0
                    countdownTips.text = "已超时"
                } else { // 注册到倒计时列表
                    countdownTips.text = "倒计时结束前,务必到达现场"
                    runnable = object : Runnable {
                        override fun run() {
                            val list = calculateTimeDifference(item.endTime)
                            if (list.count { it == 0L } == list.size) {
                                countdownHandler.removeCallbacks(this)
                            } else {
                                updateTime(
                                    hour,
                                    colon1,
                                    minutes,
                                    colon2,
                                    second,
                                    countdownTips,
                                    countdownLayout,
                                    list
                                )
                                countdownHandler.postDelayed(this, 1000)
                            }
                        }
                    }
                    countdownHandler.postDelayed(runnable, 1000)
                }
            } else {
                countdownLayout.isVisible = false
            }
            if (item.isAccidentTask()) {
                type.text = "事故"
            } else if (item.isRoutineTask()) {
                type.text = item.getTaskType()
            }
            plateNumber.text = item.plateNumber
            val setStatus: (Int, Int) -> Unit = { text, bg ->
                status.setTextColor(text)
                status.setBackgroundResource(bg)
            }
            setStatus(
                getResColor(com.xw.common.R.color.gray_8),
                com.xw.common.R.color.gray_3
            )
            // 0 = 待到达 1 = 待处理 2 = 已完成 3 = 已取消 4 = 已取消
            when (item.status) {
                0 -> status.text = "待到达"
                1 -> status.text = "待处理"
                2 -> status.text = "已处理"
                3 -> status.text = "已取消"
                4 -> status.text = "改派中"
            }
            address.text = item.vehicleLocation
            tvApplyForReassignment.setOnClickListener {
                applyForReassignBlock.invoke(this)
            }
            tvControl.setOnClickListener {
                receive.invoke()
                dismiss()
            }
        }
        dialog.setOnDismissListener {
            runnable?.also {
                countdownHandler.removeCallbacks(it)
            }
        }
        dialog.show()
        dialog.window?.setGravity(Gravity.CENTER)
        dialog.window?.setWindowAnimations(R.style.AnimLeft)
        dialog.window?.setWindowAnimations(com.xw.common.R.style.AnimLeft)
    }
    //更新时间
    private fun updateTime(
        hour: AppCompatTextView,
        colon1: AppCompatImageView,
        minutes: AppCompatTextView,
        colon2: AppCompatImageView,
        second: AppCompatTextView,
        countdownTips: AppCompatTextView,
        countdownLayout: LinearLayout,
        list: List<Long>
    ) {
        hour.text = if (list[0] < 10) "0${list[0]}" else "${list[0]}"
        minutes.text = if (list[1] < 10) "0${list[1]}" else "${list[1]}"
        second.text = if (list[2] < 10) "0${list[2]}" else "${list[2]}"
        val showRed = list[0] == 0L && list[1] == 0L
        val countdownId =
            if (showRed) R.mipmap.img_task_item_top_bg_red else R.mipmap.img_task_item_top_bg_blue
        countdownLayout.setBackgroundResource(countdownId)
        val colorId =
            if (showRed) getResColor(com.xw.common.R.color.red_6) else getResColor(com.xw.common.R.color.blue_5)
        countdownTips.setTextColor(colorId)
        val bgId = if (showRed) R.drawable.shape_fa3e3e_bg_4 else R.drawable.shape_blue_5_bg_4
        hour.setBackgroundResource(bgId)
        minutes.setBackgroundResource(bgId)
        second.setBackgroundResource(bgId)
        val imgId = if (showRed) R.mipmap.ic_task_item_colon_r else R.mipmap.ic_task_item_colon_b
        colon1.setImageResource(imgId)
        colon2.setImageResource(imgId)
        val hideTime = list[0] == 0L && list[1] == 0L && list[2] == 0L
        hour.isVisible = !hideTime
        minutes.isVisible = !hideTime
        second.isVisible = !hideTime
        colon1.isVisible = !hideTime
        colon2.isVisible = !hideTime
    }
    /**
     * 计算目标时间与当前时间的差值,返回时分秒格式
     */
    private fun calculateTimeDifference(targetTimeStr: String?): List<Long> {
        targetTimeStr?.let {
            // 定义输入格式
            val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            // 解析目标时间
            val targetTime = LocalDateTime.parse(targetTimeStr, formatter)
            // 获取当前时间
            val now = LocalDateTime.now()
            // 计算时间差(毫秒)
            val duration = Duration.between(now, targetTime)
            val totalSeconds = max(duration.seconds, 0L)
            // 转换为时分秒
            val hours = TimeUnit.SECONDS.toHours(totalSeconds)
            val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
            val seconds = totalSeconds % 60
            return listOf(hours, minutes, seconds)
        }
        return listOf(0L, 0L, 0L)
    }
}
app/src/main/java/com/xw/yunwei/ui/dialog/ApplyForReassignReasonDialog.kt
New file
@@ -0,0 +1,139 @@
package com.xw.yunwei.ui.dialog
import android.content.Context
import android.util.TypedValue
import android.view.Gravity
import android.widget.EditText
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.google.android.flexbox.FlexboxLayout
import com.scwang.smart.refresh.layout.util.SmartUtil
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.R
//申请改派原因弹窗
object ApplyForReassignReasonDialog {
    fun show(
        context: Context,
        reasons: List<String>,
        submitBlock: (position: Int, input: String) -> Unit
    ) {
        val dialog = MaterialDialog(context).apply {
            customView(
                R.layout.dialog_apply_for_reassign_reason,
                noVerticalPadding = true,
                dialogWrapContent = false
            )
            cornerRadius(4f)
            val flexboxLayout = findViewById<FlexboxLayout>(R.id.fl_tags)
            val etInput = findViewById<EditText>(R.id.et_input)
            val tvOperation = findViewById<TextView>(R.id.tv_operation)
            val set = mutableSetOf<String>()
            fillTag(flexboxLayout, reasons, set) {
                if (set.first() == "其他" && etInput.text.isNullOrEmpty()) {
                    tvOperation.setTextColor(
                        ContextCompat.getColor(
                            context,
                            com.xw.common.R.color.gray_6
                        )
                    )
                } else {
                    tvOperation.setTextColor(
                        ContextCompat.getColor(
                            context,
                            com.xw.common.R.color.blue_5
                        )
                    )
                }
            }
            etInput.addTextChangedListener {
                if ((it?.length ?: 0) >= 200) {
                    ToastUtilCommon.showSuc("最多输入200字")
                }
            }
            tvOperation.setOnClickListener {
                if (set.isEmpty()) {
                    ToastUtilCommon.showSuc("请选择原因")
                    return@setOnClickListener
                }
                if (set.first() == "其他" && etInput.text.isNullOrEmpty()) {
                    ToastUtilCommon.showSuc("请输入改派原因")
                    return@setOnClickListener
                }
                submitBlock.invoke(reasons.indexOf(set.first()), etInput.text?.toString() ?: "")
                dismiss()
            }
        }
        dialog.show()
        dialog.window?.setGravity(Gravity.CENTER)
        dialog.window?.setWindowAnimations(com.xw.common.R.style.AnimLeft)
    }
    //填充标签
    private fun fillTag(
        flexboxLayout: FlexboxLayout,
        reasons: List<String>,
        set: MutableSet<String>,
        refreshListener: () -> Unit,
    ) {
        flexboxLayout.removeAllViews()
        reasons.map { it ->
            getTagItemView(flexboxLayout.context, it, set.contains(it)) {
                if (set.contains(it)) {
                    set.remove(it)
                } else {
                    set.clear()
                    set.add(it)
                }
                refreshListener.invoke()
                fillTag(flexboxLayout, reasons, set, refreshListener)
            }
        }.forEach {
            flexboxLayout.addView(it)
        }
    }
    //获取标签item控件
    private fun getTagItemView(
        context: Context,
        tag: String,
        isChecked: Boolean,
        clickListener: (tag: String) -> Unit
    ): TextView {
        return TextView(context).apply {
            layoutParams = FlexboxLayout.LayoutParams(
                FlexboxLayout.LayoutParams.WRAP_CONTENT,
                FlexboxLayout.LayoutParams.WRAP_CONTENT
            ).apply {
                topMargin = SmartUtil.dp2px(4f)
                bottomMargin = SmartUtil.dp2px(4f)
                leftMargin = SmartUtil.dp2px(4f)
                rightMargin = SmartUtil.dp2px(4f)
            }
            setPadding(
                SmartUtil.dp2px(8f),
                SmartUtil.dp2px(6f),
                SmartUtil.dp2px(8f),
                SmartUtil.dp2px(6f)
            )
            text = tag
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
            setTextColor(
                ContextCompat.getColor(
                    context,
                    if (isChecked) com.xw.common.R.color.blue_5 else com.xw.common.R.color.gray_9
                )
            )
            gravity = Gravity.CENTER
            setBackgroundResource(if (isChecked) R.drawable.shape_blue_1_bg_4 else R.drawable.shape_gray_3_bg_4)
            setOnClickListener {
                clickListener.invoke(tag)
            }
        }
    }
}
app/src/main/java/com/xw/yunwei/ui/task/accident/detail/AccidentDetailVM.kt
@@ -1,10 +1,10 @@
package com.xw.yunwei.ui.task.accident.detail
import android.app.Application
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import com.xw.common.utils.LogUtils
import android.view.View
import androidx.core.os.bundleOf
import androidx.lifecycle.MutableLiveData
@@ -13,13 +13,16 @@
import com.amap.api.maps.model.LatLng
import com.xw.common.base.BaseViewModel
import com.xw.common.utils.CalculatorLineDistance
import com.xw.common.utils.LogUtils
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.Constants
import com.xw.yunwei.MyApp
import com.xw.yunwei.R
import com.xw.yunwei.model.TaskAccidentModel
import com.xw.yunwei.net.TaskRepository
import com.xw.yunwei.net.UserRepository
import com.xw.yunwei.net.WorkRepository
import com.xw.yunwei.ui.dialog.ApplyForReassignReasonDialog
import com.xw.yunwei.ui.task.accident.handle.AccidentProcessActivity
import com.xw.yunwei.ui.task.list.TaskListAdapter.Companion.MSG_COUNTDOWN
import org.koin.core.component.inject
@@ -27,7 +30,6 @@
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import kotlin.getValue
import kotlin.math.max
@@ -39,11 +41,11 @@
    var taskId = 0L
    val taskDetail: MutableLiveData<TaskAccidentModel> = MutableLiveData()
    val title = taskDetail.map {
        if(it.status == 0) "待到达" else if(it.status == 1) "待处理" else ""
        if (it.status == 0) "待到达" else if (it.status == 1) "待处理" else ""
    }
    val levelId = taskDetail.map {
        LogUtils.e("zmc-L", "taskDetail ${it.taskUrgencyLevel}" )
        when(it.taskUrgencyLevel){
        LogUtils.e("zmc-L", "taskDetail ${it.taskUrgencyLevel}")
        when (it.taskUrgencyLevel) {
            0 -> R.mipmap.ic_accident_one
            1 -> R.mipmap.ic_accident_two
            2 -> R.mipmap.ic_accident_three
@@ -66,7 +68,7 @@
    val actionPermissions = MutableLiveData(false) // 按钮权限
    val actionName = taskDetail.map {
        if(it.status == 0) "到达现场" else if(it.status == 1) "事故处理" else ""
        if (it.status == 0) "到达现场" else if (it.status == 1) "事故处理" else ""
    }
    var canConfirmArrive = MutableLiveData(false) // 是否能提交到达操作
    var location: AMapLocation? = null
@@ -93,7 +95,7 @@
        }
    }
    fun confirmArrive(){
    fun confirmArrive() {
        launchRequest("操作中...") {
            taskRepository.confirmArrive("${taskDetail.value!!.id}")
            queryDetail()
@@ -113,7 +115,7 @@
        }
    }
    private fun handleAction(){
    private fun handleAction() {
        when (taskDetail.value?.status) {
            0 -> {
                if (canConfirmArrive.value == true) {
@@ -139,13 +141,18 @@
                val lat = location?.latitude ?: 0.0
                val lng = location?.longitude ?: 0.0
                if (lat != 0.0 && lng != 0.0 && (it.vehicleLat ?: 0.0) != 0.0 && (it.vehicleLon ?: 0.0) != 0.0) {
                if (lat != 0.0 && lng != 0.0 && (it.vehicleLat ?: 0.0) != 0.0 && (it.vehicleLon
                        ?: 0.0) != 0.0
                ) {
                    val dis = CalculatorLineDistance.calculate(
                        LatLng(lat, lng),
                        LatLng(it.vehicleLat!!, it.vehicleLon!!)
                    )
                    canConfirmArrive.postValue((it.canArriveRange ?: 0) > dis.toInt())
                    LogUtils.i("zmc-test-detail","${it.plateNumber} dis = $dis canArriveRange = ${it.canArriveRange} canClick = ${(it.canArriveRange ?: 0) > dis.toInt()}")
                    LogUtils.i(
                        "zmc-test-detail",
                        "${it.plateNumber} dis = $dis canArriveRange = ${it.canArriveRange} canClick = ${(it.canArriveRange ?: 0) > dis.toInt()}"
                    )
                }
            } else {
@@ -176,7 +183,7 @@
                return listOf(hours, minutes, seconds)
            }
        } catch (e: Exception) {
            LogUtils.e(TAG, "calculateTimeDifference: $e", )
            LogUtils.e(TAG, "calculateTimeDifference: $e")
        }
        return listOf(0L, 0L, 0L)
    }
@@ -232,4 +239,14 @@
            }
        }
    }
    //显示改派弹窗
    fun showApplyForReassignDialog(context: Context) {
        ApplyForReassignReasonDialog.show(
            context,
            Constants.reasons,
        ) { position, input ->
        }
    }
}
app/src/main/java/com/xw/yunwei/ui/task/normal/NormalTaskDetailVM.kt
@@ -1,6 +1,7 @@
package com.xw.yunwei.ui.task.normal
import android.app.Application
import android.content.Context
import android.util.Log
import android.view.View
import androidx.core.os.bundleOf
@@ -13,6 +14,7 @@
import com.xw.common.base.ImageEventListener
import com.xw.common.base.ImageSelectAdapter
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.Constants
import com.xw.yunwei.Constants.user
import com.xw.yunwei.R
import com.xw.yunwei.model.TaskNormalChargeModel
@@ -21,9 +23,10 @@
import com.xw.yunwei.model.TaskNormalMaintainModel
import com.xw.yunwei.model.TaskNormalModel
import com.xw.yunwei.model.TaskNormalRepairModel
import com.xw.yunwei.net.CommonRepository
import com.xw.yunwei.net.TaskRepository
import com.xw.yunwei.net.WorkRepository
import com.xw.yunwei.net.CommonRepository
import com.xw.yunwei.ui.dialog.ApplyForReassignReasonDialog
import com.xw.yunwei.ui.work.car.detail.StandardDescriptionActivity
import com.xw.yunwei.ui.work.car.detail.operations.charge.ChargeCarActivity
import com.xw.yunwei.ui.work.car.detail.operations.check.CheckCarActivity
@@ -34,7 +37,6 @@
import okhttp3.RequestBody.Companion.asRequestBody
import org.koin.core.component.inject
import java.io.File
import kotlin.getValue
class NormalTaskDetailVM(application: Application) : BaseViewModel(application) {
    val taskRepository: TaskRepository by inject()
@@ -46,16 +48,16 @@
    var taskId = 0L
    val taskDetail: MutableLiveData<TaskNormalModel> = MutableLiveData()
    val title = taskDetail.map {
        if(it.status == 1) "待处理" else if(it.status == 2) "已处理" else ""
        if (it.status == 1) "待处理" else if (it.status == 2) "已处理" else ""
    }
    val levelId = taskDetail.map {
       when(it.taskUrgencyLevel){
           0 -> R.mipmap.ic_accident_one
           1 -> R.mipmap.ic_accident_two
           2 -> R.mipmap.ic_accident_three
           3 -> R.mipmap.ic_accident_four
           else -> R.mipmap.ic_accident_one
       }
        when (it.taskUrgencyLevel) {
            0 -> R.mipmap.ic_accident_one
            1 -> R.mipmap.ic_accident_two
            2 -> R.mipmap.ic_accident_three
            3 -> R.mipmap.ic_accident_four
            else -> R.mipmap.ic_accident_one
        }
    }
    val btnDoingAction = MutableLiveData("")
    val showDoingBtn = MutableLiveData(true) // 显示按钮
@@ -107,7 +109,7 @@
    fun queryDetail() {
        launchRequest {
            Log.e(TAG, "queryDetail: $taskId", )
            Log.e(TAG, "queryDetail: $taskId")
            taskRepository.vehicleRoutineDetail("$taskId")?.let {
                runOnMainThread {
                    if (taskDetail.value == null || taskDetail.value!!.status != it.status) {
@@ -155,21 +157,25 @@
                            checkModel.postValue(it)
                        }
                    }
                    "2" -> { // 充电
                        taskRepository.chargingDetail("$taskId")?.let {
                            chargeModel.postValue(it)
                        }
                    }
                    "3" -> { // 整备
                        taskRepository.preparationDetail("$taskId")?.let {
                            maintainModel.postValue(it)
                        }
                    }
                    "4" -> { // 洗车
                        taskRepository.cleanDetail("$taskId")?.let {
                            cleanModel.postValue(it)
                        }
                    }
                    "5" -> { // 维修
                        taskRepository.repairDetail("$taskId")?.let {
                            repairModel.postValue(it)
@@ -180,7 +186,7 @@
        }
    }
    private fun submit(){
    private fun submit() {
        taskDetail.value?.let {
            when (it.taskTypeId) {
                "1" -> { // 车辆检测
@@ -189,28 +195,33 @@
                        bundleOf("carId" to (it.vehicleId ?: 0L), "taskId" to (it.id ?: 0L))
                    )
                }
                "2" -> { // 开始充电
                    startActivity(
                        ChargeCarActivity::class.java,
                        bundleOf("carId" to (it.vehicleId ?: 0L), "taskId" to (it.id ?: 0L))
                    )
                }
                "3" -> { // 开始整备
                    startActivity(
                        MaintainCarActivity::class.java,
                        bundleOf("carId" to (it.vehicleId ?: 0L), "taskId" to (it.id ?: 0L))
                    )
                }
                "4" -> { // 完成洗车任务
                    cleanCar()
                }
                "5" -> { // 去送修/取回
                    startActivity(
                        RepairCarActivity::class.java,
                        bundleOf("carId" to (it.vehicleId ?: 0L), "taskId" to (it.id ?: 0L))
                    )
                }
                "6","7","8","9" -> { // 完成保养 完成续保 完成年检 车辆运营资质到期
                "6", "7", "8", "9" -> { // 完成保养 完成续保 完成年检 车辆运营资质到期
                    saveBy()
                }
            }
@@ -245,7 +256,7 @@
    }
    /*完成保养/保险/年检*/
    private fun saveBy(){
    private fun saveBy() {
        if (taskDescription.value.isNullOrEmpty()) {
            ToastUtilCommon.showError("请录入完成任务情况说明")
            return
@@ -295,4 +306,14 @@
            }
        }
    }
    //显示改派弹窗
    fun showApplyForReassignDialog(context: Context) {
        ApplyForReassignReasonDialog.show(
            context,
            Constants.reasons,
        ) { position, input ->
        }
    }
}
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt
@@ -28,11 +28,12 @@
    override fun init() {
        super.init()
        if (Constants.token.isNotEmpty()) {
            MyApp.myApp.connectWebSocket()
            startActivity(MainActivity::class.java)
            finish()
        } else {
            MyApp.myApp.disconnectWebSocket()
            MyApp.myApp.unbindPushAccount()
            val tab1 = binding.tabLayout.newTab().apply { text = "验证码登录" }
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt
@@ -9,6 +9,7 @@
import com.xw.common.utils.KeyPreferences
import com.xw.common.utils.MMKVManager
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.MyApp
import com.xw.yunwei.R
import com.xw.yunwei.model.AgreeInfo
import com.xw.yunwei.net.UserRepository
@@ -77,6 +78,7 @@
            data?.let {
                Constants.token = it.token?.access_token ?: ""
                MMKVManager.getInstance().saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                MyApp.myApp.connectWebSocket()
                startActivity(MainActivity::class.java)
                finish()
            }
@@ -104,6 +106,7 @@
                    Constants.token = it.token?.access_token ?: ""
                    MMKVManager.getInstance()
                        .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                    MyApp.myApp.connectWebSocket()
                    startActivity(MainActivity::class.java)
                    finish()
                }
app/src/main/java/com/xw/yunwei/ui/user/mine/MineFragment.kt
@@ -24,6 +24,7 @@
        super.onResume()
        viewModel.getInfo()
        viewModel.querySchedule()
        viewModel.getMyCruiseCheckInMissionInfo()
    }
    @SuppressLint("UseKtx")
@@ -48,7 +49,9 @@
        binding.nav.setOnClickListener {
            viewModel.clockModel.value?.let {
                if ((it.xpCheckLocation?.locationLat ?: 0.0) != 0.0 && (it.xpCheckLocation?.locationLon ?: 0.0) != 0.0) {
                if ((it.xpCheckLocation?.locationLat
                        ?: 0.0) != 0.0 && (it.xpCheckLocation?.locationLon ?: 0.0) != 0.0
                ) {
                    navigate(
                        end = LatLng(
                            it.xpCheckLocation?.locationLat!!,
@@ -73,11 +76,13 @@
                    binding.status.setTextColor(getResColor(com.xw.common.R.color.orange_5))
                    binding.status.setBackgroundResource(R.drawable.shape_orange_1_bg_4)
                }
                2 -> {
                    binding.status.text = "上班中"
                    binding.status.setTextColor(getResColor(com.xw.common.R.color.green_5))
                    binding.status.setBackgroundResource(R.drawable.shape_green_1_bg_4)
                }
                3 -> {
                    binding.status.text = "休息"
                    binding.status.setTextColor(getResColor(com.xw.common.R.color.gray_9))
@@ -85,6 +90,14 @@
                }
            }
        }
        viewModel.cruiseCheckInMissionInfo.observe(this) {
            binding.tvMissionTime.text = viewModel.getFormateCruiseCheckInMissionTime()
            binding.cpvProgress.setProgress(viewModel.getCruiseCheckInProgress(), true)
            binding.tvMissionPointCount.text = it.checkInPointCount.toString()
            binding.tvHadReportNum.text = it.checkInPointFinishCount.toString()
            binding.tvNotReportNum.text = it.checkInPointUnFinishCount.toString()
        }
    }
}
app/src/main/java/com/xw/yunwei/ui/user/mine/MineVM.kt
@@ -14,22 +14,26 @@
import com.xw.common.viewAdapter.ImageDefault
import com.xw.yunwei.R
import com.xw.yunwei.model.ClockInStatusModel
import com.xw.yunwei.model.CruiseCheckInMissionInfo
import com.xw.yunwei.model.UserInfo
import com.xw.yunwei.net.UserRepository
import com.xw.yunwei.net.WorkRepository
import com.xw.yunwei.ui.user.mine.checkin.CheckInActivity
import com.xw.yunwei.ui.user.mine.info.UserInfoActivity
import com.xw.yunwei.ui.user.mine.scheduling.SchedulingActivity
import com.xw.yunwei.ui.user.mine.set.SetActivity
import com.xw.yunwei.ui.user.mine.upload.CertUploadActivity
import com.xw.yunwei.ui.work.checkInMission.CheckInMissionPointsListActivity
import org.json.JSONObject
import org.koin.core.component.inject
import kotlin.getValue
class MineVM(application: Application) : BaseViewModel(application) {
    val userRepository: UserRepository by inject()
    val workRepository: WorkRepository by inject()
    val userInfo = MutableLiveData(MMKVManager.getInstance().getModel<UserInfo>(KeyPreferences.USER))
    val userInfo =
        MutableLiveData(MMKVManager.getInstance().getModel<UserInfo>(KeyPreferences.USER))
    val avatar = MutableLiveData(ImageDefault("", com.xw.common.R.drawable.svg_avatar))
@@ -43,6 +47,9 @@
    }
    val workFlag = MutableLiveData(0)
    //巡游打卡任务
    val cruiseCheckInMissionInfo = MutableLiveData<CruiseCheckInMissionInfo>()
    var phone = "" // 客服电话
@@ -52,8 +59,12 @@
            R.id.uploadLayout -> startActivity(CertUploadActivity::class.java)
            R.id.setLayout -> startActivity(SetActivity::class.java)
            R.id.schedulingLayout -> startActivity(SchedulingActivity::class.java)
            R.id.goCheck -> startActivity(CheckInActivity::class.java,
                bundleOf("clockModel" to clockModel.value))
            R.id.goCheck -> startActivity(
                CheckInActivity::class.java,
                bundleOf("clockModel" to clockModel.value)
            )
            R.id.ll_cruise_check_in_mission -> startActivity(CheckInMissionPointsListActivity::class.java)
        }
    }
@@ -64,7 +75,7 @@
            flowBlock = { userRepository.getUserInfo() },
            onCollect = {
                avatar.postValue(avatar.value!!.apply { this.url = it?.avatar?.getUrl() ?: "" })
                workFlag.postValue(it?.workFlag?:0)
                workFlag.postValue(it?.workFlag ?: 0)
                roleName.postValue(it?.roles?.getOrNull(0)?.roleName ?: "未分配")
                fleetLeaderName.postValue(it?.fleet?.fleetLeaderName ?: "无")
                fleetName.postValue(it?.fleet?.fleetName ?: "无")
@@ -74,9 +85,10 @@
    fun querySchedule() {
        launchRequest(showLoading = clockModel.value == null) {
            userRepository.querySchedule(mapOf("schedulingDate" to getTime(System.currentTimeMillis())))?.let {
                clockModel.postValue(it)
            }
            userRepository.querySchedule(mapOf("schedulingDate" to getTime(System.currentTimeMillis())))
                ?.let {
                    clockModel.postValue(it)
                }
        }
    }
@@ -93,4 +105,22 @@
        }
    }
    //获取个人中心巡游打卡任务
    fun getMyCruiseCheckInMissionInfo() {
        launchRequest {
            workRepository.getMyCruiseCheckInMissionInfo()?.also {
                cruiseCheckInMissionInfo.postValue(it)
            }
        }
    }
    //获取个人中心巡游打卡任务时间
    fun getFormateCruiseCheckInMissionTime(): String {
        return cruiseCheckInMissionInfo.value?.cruiseCheckInTaskTime ?: ""
    }
    //获取巡游打卡进度
    fun getCruiseCheckInProgress(): Float {
        return cruiseCheckInMissionInfo.value?.getFinishProgress() ?: 0f
    }
}
app/src/main/java/com/xw/yunwei/ui/user/mine/set/SetActivity.kt
@@ -3,7 +3,8 @@
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.net.Uri
import android.provider.Settings
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
@@ -17,11 +18,10 @@
import com.xw.common.utils.MMKVManager
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.BR
import com.xw.yunwei.MyApp
import com.xw.yunwei.databinding.ActivitySetBinding
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import android.net.Uri
import android.provider.Settings
/**
 * 设置
@@ -58,11 +58,19 @@
            if (binding.locationTips.text == "去授权") {
                val pl = arrayOf(Permission.ACCESS_FINE_LOCATION, Permission.ACCESS_COARSE_LOCATION)
                XXPermissions.with(this).permission(pl).request(object : OnPermissionCallback {
                    override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) { // 权限同意
                        if(allGranted){ binding.locationTips.text = "已开启" }
                    override fun onGranted(
                        permissions: MutableList<String>,
                        allGranted: Boolean
                    ) { // 权限同意
                        if (allGranted) {
                            binding.locationTips.text = "已开启"
                        }
                    }
                    override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
                    override fun onDenied(
                        permissions: MutableList<String>,
                        doNotAskAgain: Boolean
                    ) {
                        super.onDenied(permissions, doNotAskAgain)
                        //权限禁止
                    }
@@ -100,6 +108,7 @@
        binding.logOut.setOnClickListener {
            CommonDialog.showTipsDialog(this, "确认退出当前账号登录吗?", rightBlock = {
                MMKVManager.getInstance().clear()
                MyApp.myApp.disconnectWebSocket()
                lifecycleScope.launch {
                    delay(100)
                    AppUtils.relaunchApp(true)
@@ -115,8 +124,13 @@
        binding.switchNotification.isChecked = isEnabled
    }
    private fun checkLocation(){
        val result = checkPermission(listOf(Permission.ACCESS_FINE_LOCATION, Permission.ACCESS_COARSE_LOCATION))
    private fun checkLocation() {
        val result = checkPermission(
            listOf(
                Permission.ACCESS_FINE_LOCATION,
                Permission.ACCESS_COARSE_LOCATION
            )
        )
        if (result) {
            binding.locationTips.text = "已开启"
        } else {
@@ -124,10 +138,14 @@
        }
    }
    private fun checkPermission(permission: List<String>): Boolean{
    private fun checkPermission(permission: List<String>): Boolean {
        val permissionList: MutableList<String> = ArrayList()
        for (permission in permission) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
            if (ContextCompat.checkSelfPermission(
                    this,
                    permission
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                permissionList.add(permission)
            }
        }
app/src/main/java/com/xw/yunwei/ui/widget/CircleProgressView.kt
New file
@@ -0,0 +1,219 @@
package com.xw.yunwei.ui.widget
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import com.xw.yunwei.R
import androidx.core.graphics.toColorInt
class CircularProgressView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    // 画笔
    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    // 颜色
    var backgroundProgressColor: Int = "#FF9800".toColorInt()
        set(value) {
            field = value
            backgroundPaint.color = value
            invalidate()
        }
    var progressColor: Int = "#4CAF50".toColorInt()
        set(value) {
            field = value
            progressPaint.color = value
            invalidate()
        }
    // 尺寸
    var ringWidth: Float = 40f
        set(value) {
            field = value
            backgroundPaint.strokeWidth = value
            progressPaint.strokeWidth = value
            requestLayout()
        }
    // 进度
    var progress: Float = 0f
        set(value) {
            field = value.coerceIn(0f, maxProgress)
            animateProgress()
        }
    var maxProgress: Float = 100f
        set(value) {
            field = value
            invalidate()
        }
    // 动画进度
    private var animatedProgress: Float = 0f
    private var progressAnimator: ValueAnimator? = null
    // 坐标
    private var centerX: Float = 0f
    private var centerY: Float = 0f
    private var radius: Float = 0f
    init {
        setupPaints()
        setupAttributes(attrs)
    }
    private fun setupAttributes(attrs: AttributeSet?) {
        attrs?.let {
            val typedArray = context.obtainStyledAttributes(it, R.styleable.CircularProgressView)
            try {
                ringWidth = typedArray.getDimension(
                    R.styleable.CircularProgressView_ringWidth,
                    ringWidth
                )
                backgroundProgressColor = typedArray.getColor(
                    R.styleable.CircularProgressView_backgroundProgressColor,
                    backgroundProgressColor
                )
                progressColor = typedArray.getColor(
                    R.styleable.CircularProgressView_progressColor,
                    progressColor
                )
                progress = typedArray.getFloat(
                    R.styleable.CircularProgressView_progress,
                    progress
                )
                maxProgress = typedArray.getFloat(
                    R.styleable.CircularProgressView_maxProgress,
                    maxProgress
                )
            } finally {
                typedArray.recycle()
            }
        }
    }
    private fun setupPaints() {
        // 背景环画笔
        backgroundPaint.style = Paint.Style.STROKE
        backgroundPaint.strokeCap = Paint.Cap.ROUND
        backgroundPaint.strokeWidth = ringWidth
        backgroundPaint.color = backgroundProgressColor
        // 进度环画笔
        progressPaint.style = Paint.Style.STROKE
        progressPaint.strokeCap = Paint.Cap.ROUND
        progressPaint.strokeWidth = ringWidth
        progressPaint.color = progressColor
    }
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // 计算中心点和半径
        centerX = w / 2f
        centerY = h / 2f
        radius = minOf(w, h) / 2f - ringWidth / 2f
        // 更新画笔宽度
        backgroundPaint.strokeWidth = ringWidth
        progressPaint.strokeWidth = ringWidth
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 绘制背景环
        canvas.drawCircle(centerX, centerY, radius, backgroundPaint)
        // 绘制进度环
        if (animatedProgress > 0) {
            val sweepAngle = 360f * (animatedProgress / maxProgress)
            val rect = RectF(
                centerX - radius,
                centerY - radius,
                centerX + radius,
                centerY + radius
            )
            canvas.drawArc(
                rect,
                -90f,  // 从顶部开始
                -sweepAngle,
                false,
                progressPaint
            )
        }
    }
    /**
     * 设置进度
     * @param progress 进度值
     * @param animate 是否开启动画
     */
    fun setProgress(progress: Float, animate: Boolean = true) {
        this.progress = progress.coerceIn(0f, maxProgress)
        if (animate) {
            animateProgress()
        } else {
            animatedProgress = this.progress
            invalidate()
        }
    }
    /**
     * 进度动画
     */
    private fun animateProgress() {
        progressAnimator?.cancel()
        progressAnimator = ValueAnimator.ofFloat(animatedProgress, progress).apply {
            duration = 800
            interpolator = AccelerateDecelerateInterpolator()
            addUpdateListener { animation ->
                animatedProgress = animation.animatedValue as Float
                invalidate()
            }
            start()
        }
    }
    /**
     * 获取当前动画进度
     */
    fun getAnimatedProgress(): Float = animatedProgress
    /**
     * 重置进度
     */
    fun resetProgress(animate: Boolean = true) {
        setProgress(0f, animate)
    }
    /**
     * 设置进度到最大值
     */
    fun setProgressToMax(animate: Boolean = true) {
        setProgress(maxProgress, animate)
    }
    /**
     * 释放资源
     */
    fun release() {
        progressAnimator?.cancel()
    }
}
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/CheckInMissionPointsListActivity.kt
New file
@@ -0,0 +1,14 @@
package com.xw.yunwei.ui.work.checkInMission
import com.xw.common.base.BaseActivity
import com.xw.yunwei.BR
import com.xw.yunwei.ui.work.checkInMission.viewmodel.CheckInMissionPointsListViewModel
class CheckInMissionPointsListActivity :
    BaseActivity<ActivityCheckInMissionPointsListBinding, CheckInMissionPointsListViewModel>() {
    override fun getBindingVariable() = BR.viewModel
}
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/adapter/CheckInMissionPointAdapter.kt
New file
@@ -0,0 +1,16 @@
package com.xw.yunwei.ui.work.checkInMission.adapter
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.xw.yunwei.R
import com.xw.yunwei.model.CheckInMissionPointInfo
//巡游任务点适配器
class CheckInMissionPointAdapter():BaseQuickAdapter<CheckInMissionPointInfo, BaseViewHolder>(R.layout.item_check_in_mission_point) {
    override fun convert(
        holder: BaseViewHolder,
        item: CheckInMissionPointInfo
    ) {
        TODO("Not yet implemented")
    }
}
app/src/main/java/com/xw/yunwei/ui/work/checkInMission/viewmodel/CheckInMissionPointsListViewModel.kt
New file
@@ -0,0 +1,7 @@
package com.xw.yunwei.ui.work.checkInMission.viewmodel
import android.app.Application
import com.xw.common.base.BaseViewModel
class CheckInMissionPointsListViewModel(application: Application): BaseViewModel(application) {
}
app/src/main/java/com/xw/yunwei/websocket/IWebSocketListener.kt
New file
@@ -0,0 +1,54 @@
package com.xw.yunwei.websocket
import okio.ByteString
interface IWebSocketListener {
    /**
     * WebSocket连接成功
     */
    fun onConnected()
    /**
     * WebSocket连接失败
     * @param t 异常信息
     */
    fun onConnectFailed(t: Throwable)
    /**
     * 接收到消息
     * @param text 文本消息
     */
    fun onMessage(text: String)
    /**
     * 接收到二进制消息
     * @param bytes 二进制数据
     */
    fun onMessage(bytes: ByteString)
    /**
     * 连接断开
     * @param code 断开码
     * @param reason 断开原因
     */
    fun onDisconnected(code: Int, reason: String)
    /**
     * 网络状态变化
     * @param isAvailable 网络是否可用
     */
    fun onNetworkStateChanged(isAvailable: Boolean)
    /**
     * 发送消息成功
     * @param message 消息内容
     */
    fun onSendMessageSuccess(message: String)
    /**
     * 发送消息失败
     * @param message 消息内容
     * @param t 异常
     */
    fun onSendMessageFailed(message: String, t: Throwable)
}
app/src/main/java/com/xw/yunwei/websocket/WebSocketClient.kt
New file
@@ -0,0 +1,412 @@
package com.xw.yunwei.websocket
import com.xw.common.utils.LogUtils
import com.xw.yunwei.BuildConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.logging.HttpLoggingInterceptor
import okio.ByteString
import org.json.JSONObject
import java.util.LinkedList
import java.util.concurrent.TimeUnit
//WebSocket客户端
class WebSocketClient(
    private val config: WebSocketConfig = WebSocketConfig
) {
    private var webSocket: WebSocket? = null
    private var okHttpClient: OkHttpClient? = null
    private var isConnecting = false
    private var isConnected = false
    private var reconnectCount = 0
    private val ioCoroutineScope = CoroutineScope(Dispatchers.IO)
    // 监听器列表
    private val listeners = mutableListOf<IWebSocketListener>()
    // 心跳相关
    private var heartbeatJob: Job? = null
    // 消息队列(连接失败时缓存)
    private val messageQueue = LinkedList<String>()
    private val maxQueueSize = 50
    companion object {
        private const val TAG = "WebSocketClient"
    }
    /**
     * 初始化WebSocket客户端
     */
    private fun initClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(config.WS_TIMEOUT, TimeUnit.SECONDS)
            .writeTimeout(config.WS_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(config.WS_TIMEOUT, TimeUnit.SECONDS)
            .pingInterval(config.PING_INTERVAL, TimeUnit.SECONDS)  // 自动Ping
            .retryOnConnectionFailure(true)
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = if (BuildConfig.DEBUG) {
                    HttpLoggingInterceptor.Level.BODY
                } else {
                    HttpLoggingInterceptor.Level.NONE
                }
            })
            .addInterceptor { chain ->
                val original = chain.request()
                val request = original.newBuilder()
                    .header("User-Agent", "Android-App/${BuildConfig.VERSION_NAME}")
                    .header("Connection", "Upgrade")
                    .header("Upgrade", "websocket")
                    .header("Sec-WebSocket-Version", "13")
                    .apply {
                        // 添加认证token
                        val token = getAuthToken()
                        if (token.isNotBlank()) {
                            header("Authorization", "Bearer $token")
                        }
                    }
                    .build()
                chain.proceed(request)
            }
            .build()
    }
    /**
     * 连接WebSocket
     */
    fun connect(isFirstConnect: Boolean) {
        if (isConnecting || isConnected) {
            LogUtils.d(TAG, "WebSocket正在连接或已连接")
            return
        }
        isConnecting = true
        reconnectCount = 0
        ioCoroutineScope.launch {
            try {
                val request = Request.Builder()
                    .url(config.WS_URL)
                    .build()
                okHttpClient = initClient()
                okHttpClient?.newWebSocket(request, createWebSocketListener())
                LogUtils.i(TAG, "开始连接WebSocket: ${config.WS_URL}")
            } catch (e: Exception) {
                isConnecting = false
                LogUtils.e(TAG, "连接WebSocket失败: ${e.message}", e)
                notifyConnectFailed(e)
            }
        }
    }
    /**
     * 创建WebSocket监听器
     */
    private fun createWebSocketListener(): WebSocketListener {
        return object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                LogUtils.i(TAG, "WebSocket连接成功")
                isConnecting = false
                isConnected = true
                reconnectCount = 0
                this@WebSocketClient.webSocket = webSocket
                // 启动心跳
                startHeartbeat()
                // 发送积压的消息
                flushMessageQueue()
                // 通知监听器
                notifyConnected()
            }
            override fun onMessage(webSocket: WebSocket, text: String) {
                LogUtils.d(TAG, "收到文本消息: $text")
                handleReceivedMessage(text)
            }
            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                LogUtils.d(TAG, "收到二进制消息, 长度: ${bytes.size}")
                handleReceivedMessage(bytes)
            }
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                LogUtils.w(TAG, "WebSocket正在关闭: code=$code, reason=$reason")
                isConnected = false
                stopHeartbeat()
            }
            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                LogUtils.w(TAG, "WebSocket已关闭: code=$code, reason=$reason")
                isConnected = false
                stopHeartbeat()
                notifyDisconnected(code, reason)
                // 根据关闭码决定是否重连
                if (shouldReconnect(code)) {
                    scheduleReconnect()
                }
            }
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                LogUtils.e(TAG, "WebSocket连接失败: ${t.message}")
                isConnecting = false
                isConnected = false
                stopHeartbeat()
                notifyConnectFailed(t)
                // 失败时尝试重连
                if (reconnectCount < config.MAX_RECONNECT_COUNT) {
                    scheduleReconnect()
                }
            }
        }
    }
    /**
     * 发送消息
     */
    fun sendMessage(message: String): Boolean {
        return if (isConnected) {
            try {
                webSocket?.send(message)
                LogUtils.d(TAG, "发送消息成功: $message")
                notifySendMessageSuccess(message)
                true
            } catch (e: Exception) {
                LogUtils.e(TAG, "发送消息失败: ${e.message}", e)
                cacheMessage(message)
                notifySendMessageFailed(message, e)
                false
            }
        } else {
            LogUtils.w(TAG, "WebSocket未连接,缓存消息: $message")
            cacheMessage(message)
            false
        }
    }
    /**
     * 发送JSON消息
     */
    fun sendJsonMessage(type: String, data: Any = emptyMap<String, Any>()): Boolean {
        val json = try {
            val jsonObject = JSONObject().apply {
                put("type", type)
                put("data", JSONObject(data.toString()))
                put("timestamp", System.currentTimeMillis())
            }
            jsonObject.toString()
        } catch (e: Exception) {
            LogUtils.e(TAG, "构建JSON失败: ${e.message}", e)
            return false
        }
        return sendMessage(json)
    }
    /**
     * 断开连接
     */
    fun disconnect(code: Int = 1000, reason: String = "normal closure") {
        LogUtils.i(TAG, "主动断开WebSocket连接")
        stopHeartbeat()
        isConnecting = false
        isConnected = false
        reconnectCount = config.MAX_RECONNECT_COUNT  // 阻止重连
        webSocket?.close(code, reason)
        webSocket = null
        try {
            okHttpClient?.dispatcher?.executorService?.shutdown()
            okHttpClient?.connectionPool?.evictAll()
        } catch (e: Exception) {
            LogUtils.e(TAG, "清理资源失败: ${e.message}", e)
        }
    }
    /**
     * 重新连接
     */
    fun reconnect() {
        if (isConnecting) {
            LogUtils.d(TAG, "已经在重连中...")
            return
        }
        disconnect()
        LogUtils.d(TAG, "开始重连...")
        ioCoroutineScope.launch {
            delay(1000)  // 延迟1秒重连
            connect(false)
        }
    }
    /**
     * 添加监听器
     */
    fun addListener(listener: IWebSocketListener) {
        if (!listeners.contains(listener)) {
            listeners.add(listener)
        }
    }
    /**
     * 移除监听器
     */
    fun removeListener(listener: IWebSocketListener) {
        listeners.remove(listener)
    }
    /**
     * 清除所有监听器
     */
    fun clearListeners() {
        listeners.clear()
    }
    /**
     * 获取连接状态
     */
    fun getConnectionState(): ConnectionState {
        return when {
            isConnected -> ConnectionState.CONNECTED
            isConnecting -> ConnectionState.CONNECTING
            else -> ConnectionState.DISCONNECTED
        }
    }
    // 私有方法
    private fun startHeartbeat() {
        stopHeartbeat()
        heartbeatJob = ioCoroutineScope.launch {
            while (isActive && isConnected) {
                delay(config.PING_INTERVAL * 1000)
                if (isConnected) {
                    sendJsonMessage(WebSocketConfig.MessageType.HEARTBEAT)
                }
            }
        }
    }
    private fun stopHeartbeat() {
        heartbeatJob?.cancel()
        heartbeatJob = null
    }
    private fun scheduleReconnect() {
        if (reconnectCount >= config.MAX_RECONNECT_COUNT) {
            LogUtils.w(TAG, "已达到最大重连次数: ${config.MAX_RECONNECT_COUNT}")
            return
        }
        reconnectCount++
        val delay = config.RECONNECT_DELAY * reconnectCount  // 退避策略
        LogUtils.i(TAG, "计划 ${reconnectCount} 秒后重连")
        ioCoroutineScope.launch {
            delay(delay * 1000)
            if (!isConnected && !isConnecting) {
                connect(false)
            }
        }
    }
    private fun cacheMessage(message: String) {
        synchronized(messageQueue) {
            if (messageQueue.size >= maxQueueSize) {
                messageQueue.poll()  // 移除最旧的消息
            }
            messageQueue.offer(message)
        }
    }
    private fun flushMessageQueue() {
        synchronized(messageQueue) {
            while (messageQueue.isNotEmpty()) {
                messageQueue.poll()?.also {
                    sendMessage(it)
                }
            }
        }
    }
    private fun handleReceivedMessage(text: String) {
        try {
            val json = JSONObject(text)
            val type = json.optString("type")
            when (type) {
                "pong" -> {
                    // 心跳响应
                    LogUtils.d(TAG, "收到心跳响应")
                }
                else -> {
                    listeners.forEach { it.onMessage(text) }
                }
            }
        } catch (e: Exception) {
            LogUtils.e(TAG, "解析消息失败: ${e.message}", e)
            listeners.forEach { it.onMessage(text) }
        }
    }
    private fun handleReceivedMessage(bytes: ByteString) {
        listeners.forEach { it.onMessage(bytes) }
    }
    private fun notifyConnected() {
        listeners.forEach { it.onConnected() }
    }
    private fun notifyConnectFailed(t: Throwable) {
        listeners.forEach { it.onConnectFailed(t) }
    }
    private fun notifyDisconnected(code: Int, reason: String) {
        listeners.forEach { it.onDisconnected(code, reason) }
    }
    private fun notifySendMessageSuccess(message: String) {
        listeners.forEach { it.onSendMessageSuccess(message) }
    }
    private fun notifySendMessageFailed(message: String, t: Throwable) {
        listeners.forEach { it.onSendMessageFailed(message, t) }
    }
    private fun shouldReconnect(code: Int): Boolean {
        // 正常关闭不重连
        return false
    }
    private fun getAuthToken(): String {
        // 从SharedPreferences或安全存储获取
        return ""
    }
    enum class ConnectionState {
        CONNECTING,
        CONNECTED,
        DISCONNECTED
    }
}
app/src/main/java/com/xw/yunwei/websocket/WebSocketConfig.kt
New file
@@ -0,0 +1,21 @@
package com.xw.yunwei.websocket
import com.xw.yunwei.BuildConfig
// WebSocket配置
object WebSocketConfig {
    const val WS_URL = BuildConfig.WS_URL
    const val WS_TIMEOUT = 10L  // 连接超时(秒)
    const val PING_INTERVAL = 25L  // 心跳间隔(秒)
    const val RECONNECT_DELAY = 3L  // 重连延迟(秒)
    const val MAX_RECONNECT_COUNT = Int.MAX_VALUE  // 最大重连次数
    // 消息类型
    object MessageType {
        const val CONNECT = "connect"
        const val HEARTBEAT = "heartbeat"
        const val MESSAGE = "message"
        const val ERROR = "error"
        const val CLOSE = "close"
    }
}
app/src/main/java/com/xw/yunwei/websocket/WebSocketManager.kt
New file
@@ -0,0 +1,237 @@
package com.xw.yunwei.websocket
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.xw.common.utils.LogUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import okio.ByteString
// WebSocket管理器(单例)
object WebSocketManager : IWebSocketListener {
    private const val TAG = "WebSocketManager"
    private lateinit var webSocketClient: WebSocketClient
    private var isInitialized = false
    private var enableConnect = false
    private val mainCoroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
    // 连接状态LiveData
    private val _connectionState = MutableLiveData<WebSocketClient.ConnectionState>()
    val connectionState: LiveData<WebSocketClient.ConnectionState> = _connectionState
    // 消息LiveData
    private val _messageEvent = MutableLiveData<Event<String>>()
    val messageEvent: LiveData<Event<String>> = _messageEvent
    // 事件Flow
    private val _eventFlow = MutableSharedFlow<WebSocketEvent>()
    val eventFlow: SharedFlow<WebSocketEvent> = _eventFlow.asSharedFlow()
    /**
     * 初始化
     */
    fun initialize(context: Context) {
        if (isInitialized) return
        webSocketClient = WebSocketClient()
        webSocketClient.addListener(this)
        // 监听网络状态
        observeNetworkState(context)
        isInitialized = true
        LogUtils.i(TAG, "WebSocket管理器初始化完成")
    }
    /**
     * 连接WebSocket
     */
    fun connect() {
        if (!isInitialized) {
            LogUtils.e(TAG, "请先调用initialize()初始化")
            return
        }
        enableConnect = true
        webSocketClient.connect(true)
    }
    /**
     * 断开连接
     */
    fun disconnect() {
        if (isInitialized) {
            enableConnect = false
            webSocketClient.disconnect()
        }
    }
    /**
     * 发送消息
     */
    fun sendMessage(message: String): Boolean {
        return if (isInitialized) {
            webSocketClient.sendMessage(message)
        } else {
            false
        }
    }
    /**
     * 发送JSON消息
     */
    fun sendJson(type: String, data: Map<String, Any> = emptyMap()): Boolean {
        return if (isInitialized) {
            webSocketClient.sendJsonMessage(type, data)
        } else {
            false
        }
    }
    /**
     * 重新连接
     */
    fun reconnect() {
        if (isInitialized && enableConnect) {
            webSocketClient.reconnect()
        }
    }
    /**
     * 获取连接状态
     */
    fun getConnectionState(): WebSocketClient.ConnectionState {
        return if (isInitialized) {
            webSocketClient.getConnectionState()
        } else {
            WebSocketClient.ConnectionState.DISCONNECTED
        }
    }
    /**
     * 清理资源
     */
    fun release() {
        if (isInitialized) {
            webSocketClient.disconnect()
            webSocketClient.clearListeners()
            isInitialized = false
        }
    }
    // 实现IWebSocketListener接口
    override fun onConnected() {
        LogUtils.i(TAG, "WebSocket已连接")
        _connectionState.postValue(WebSocketClient.ConnectionState.CONNECTED)
        mainCoroutineScope.launch {
            _eventFlow.emit(WebSocketEvent.Connected)
        }
    }
    override fun onConnectFailed(t: Throwable) {
        LogUtils.e(TAG, "连接失败: ${t.message}")
        _connectionState.postValue(WebSocketClient.ConnectionState.DISCONNECTED)
        mainCoroutineScope.launch {
            _eventFlow.emit(WebSocketEvent.ConnectFailed(t))
        }
    }
    override fun onMessage(text: String) {
        LogUtils.d(TAG, "收到消息: $text")
        _messageEvent.postValue(Event(text))
        mainCoroutineScope.launch {
            _eventFlow.emit(WebSocketEvent.Message(text))
        }
    }
    override fun onMessage(bytes: ByteString) {
        LogUtils.d(TAG, "收到二进制消息, 长度: ${bytes.size}")
        mainCoroutineScope.launch {
            _eventFlow.emit(WebSocketEvent.BinaryMessage(bytes))
        }
    }
    override fun onDisconnected(code: Int, reason: String) {
        LogUtils.w(TAG, "连接断开: code=$code, reason=$reason")
        _connectionState.postValue(WebSocketClient.ConnectionState.DISCONNECTED)
        mainCoroutineScope.launch {
            _eventFlow.emit(WebSocketEvent.Disconnected(code, reason))
        }
    }
    override fun onNetworkStateChanged(isAvailable: Boolean) {
        if (isAvailable && getConnectionState() == WebSocketClient.ConnectionState.DISCONNECTED) {
            // 网络恢复时重连
            reconnect()
        }
    }
    override fun onSendMessageSuccess(message: String) {
        LogUtils.d(TAG, "发送消息成功: $message")
    }
    override fun onSendMessageFailed(message: String, t: Throwable) {
        LogUtils.e(TAG, "发送消息失败: ${t.message}")
    }
    // 网络状态监听
    private fun observeNetworkState(context: Context) {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
                as ConnectivityManager
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                LogUtils.i(TAG, "网络已连接")
                onNetworkStateChanged(true)
            }
            override fun onLost(network: Network) {
                LogUtils.w(TAG, "网络已断开")
                onNetworkStateChanged(false)
            }
        }
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        connectivityManager.registerNetworkCallback(request, networkCallback)
    }
    // Event包装类(避免LiveData重复触发)
    class Event<T>(private val content: T) {
        private var hasBeenHandled = false
        fun getContentIfNotHandled(): T? {
            return if (hasBeenHandled) {
                null
            } else {
                hasBeenHandled = true
                content
            }
        }
    }
    // WebSocket事件密封类
    sealed class WebSocketEvent {
        object Connected : WebSocketEvent()
        data class ConnectFailed(val throwable: Throwable) : WebSocketEvent()
        data class Message(val text: String) : WebSocketEvent()
        data class BinaryMessage(val bytes: ByteString) : WebSocketEvent()
        data class Disconnected(val code: Int, val reason: String) : WebSocketEvent()
    }
}
app/src/main/res/drawable/circle_gray_6.xml
New 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"
    android:useLevel="false">
    <solid android:color="@color/gray_6" />
</shape>
app/src/main/res/drawable/layer_list_green_progress_bar.xml
New file
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 背景 -->
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="@dimen/dp_8" />
            <solid android:color="@color/gray_5" />
        </shape>
    </item>
    <!-- 主进度 -->
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="@dimen/dp_8" />
                <solid android:color="@color/green_5" />
            </shape>
        </clip>
    </item>
</layer-list>
app/src/main/res/drawable/layer_list_orange_progress_bar.xml
New file
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 背景 -->
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="@dimen/dp_8" />
            <solid android:color="@color/gray_5" />
        </shape>
    </item>
    <!-- 主进度 -->
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="@dimen/dp_8" />
                <solid android:color="@color/orange_5" />
            </shape>
        </clip>
    </item>
</layer-list>
app/src/main/res/drawable/shape_red_5_line_gray_3_bg_4.xml
New file
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <stroke
        android:width="@dimen/dp_1"
        android:color="@color/red_5" />
    <corners android:radius="@dimen/dp_4" />
    <solid android:color="@color/gray_3" />
</shape>
app/src/main/res/layout/activity_check_in_mission_points_list.xml
New file
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.work.checkInMission.CheckInMissionPointsListActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
app/src/main/res/layout/dialog_apply_for_reassign.xml
@@ -1,210 +1,234 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout 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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
    <ImageView
        android:id="@+id/iv_bg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/dp_4"
        android:layout_marginTop="@dimen/dp_4"
        android:background="@drawable/shape_white_bg_4"
        android:orientation="vertical"
        android:paddingBottom="@dimen/dp_20"
        app:layout_constraintTop_toTopOf="parent">
        android:scaleType="fitXY"
        android:src="@mipmap/ic_apply_reassign_bg" />
        <LinearLayout
            android:id="@+id/countdownLayout"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_36"
            android:background="@mipmap/img_task_item_top_bg_blue"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:paddingHorizontal="@dimen/dp_12">
    <ImageView
        android:id="@+id/iv_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:src="@mipmap/icon_note" />
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/countdownTips"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:ellipsize="end"
                android:gravity="end"
                android:maxLines="1"
                android:paddingEnd="@dimen/dp_8"
                android:singleLine="true"
                android:text="倒计时结束前,务必到达现场"
                android:textColor="@color/blue_5"
                android:textSize="@dimen/sp_12" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/dp_16"
        android:layout_marginTop="@dimen/dp_20"
        android:text="新任务提示"
        android:textColor="@color/gray_9"
        android:textSize="@dimen/sp_16"
        android:textStyle="bold" />
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/hour"
                android:layout_width="@dimen/dp_30"
                android:layout_height="@dimen/dp_23"
                android:layout_marginStart="@dimen/dp_8"
                android:background="@drawable/shape_blue_5_bg_4"
                android:gravity="center"
                android:text="11"
                android:textColor="@color/white"
                android:textSize="@dimen/sp_16"
                android:textStyle="bold" />
            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/colon1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginHorizontal="@dimen/dp_3"
                android:layout_marginTop="@dimen/dp_3"
                android:src="@mipmap/ic_task_item_colon_b" />
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/minutes"
                android:layout_width="@dimen/dp_30"
                android:layout_height="@dimen/dp_23"
                android:background="@drawable/shape_blue_5_bg_4"
                android:gravity="center"
                android:text="33"
                android:textColor="@color/white"
                android:textSize="@dimen/sp_16"
                android:textStyle="bold" />
            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/colon2"
                android:layout_width="@dimen/dp_3"
                android:layout_height="@dimen/dp_10"
                android:layout_marginHorizontal="@dimen/dp_3"
                android:layout_marginTop="@dimen/dp_3"
                android:src="@mipmap/ic_task_item_colon_b" />
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/second"
                android:layout_width="@dimen/dp_30"
                android:layout_height="@dimen/dp_23"
                android:background="@drawable/shape_blue_5_bg_4"
                android:gravity="center"
                android:text="09"
                android:textColor="@color/white"
                android:textSize="@dimen/sp_16"
                android:textStyle="bold" />
        </LinearLayout>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_alignStart="@id/iv_bg"
        android:layout_alignTop="@id/iv_bg"
        android:layout_alignEnd="@id/iv_bg"
        android:layout_marginHorizontal="@dimen/dp_4"
        android:layout_marginTop="@dimen/dp_60">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/dp_16"
            android:layout_marginTop="@dimen/dp_20"
            android:gravity="center_vertical"
            android:orientation="horizontal">
            android:layout_marginStart="@dimen/dp_4"
            android:layout_marginTop="@dimen/dp_4"
            android:orientation="vertical"
            android:paddingBottom="@dimen/dp_20"
            app:layout_constraintTop_toTopOf="parent">
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/type"
                android:layout_width="wrap_content"
            <LinearLayout
                android:id="@+id/countdownLayout"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp_36"
                android:layout_marginEnd="@dimen/dp_4"
                android:background="@mipmap/img_task_item_top_bg_blue"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingHorizontal="@dimen/dp_12">
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/countdownTips"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:ellipsize="end"
                    android:gravity="end"
                    android:maxLines="1"
                    android:paddingEnd="@dimen/dp_8"
                    android:singleLine="true"
                    android:text="倒计时结束前,务必到达现场"
                    android:textColor="@color/blue_5"
                    android:textSize="@dimen/sp_12" />
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/hour"
                    android:layout_width="@dimen/dp_30"
                    android:layout_height="@dimen/dp_23"
                    android:layout_marginStart="@dimen/dp_8"
                    android:background="@drawable/shape_blue_5_bg_4"
                    android:gravity="center"
                    android:text="11"
                    android:textColor="@color/white"
                    android:textSize="@dimen/sp_16"
                    android:textStyle="bold" />
                <androidx.appcompat.widget.AppCompatImageView
                    android:id="@+id/colon1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginHorizontal="@dimen/dp_3"
                    android:layout_marginTop="@dimen/dp_3"
                    android:src="@mipmap/ic_task_item_colon_b" />
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/minutes"
                    android:layout_width="@dimen/dp_30"
                    android:layout_height="@dimen/dp_23"
                    android:background="@drawable/shape_blue_5_bg_4"
                    android:gravity="center"
                    android:text="33"
                    android:textColor="@color/white"
                    android:textSize="@dimen/sp_16"
                    android:textStyle="bold" />
                <androidx.appcompat.widget.AppCompatImageView
                    android:id="@+id/colon2"
                    android:layout_width="@dimen/dp_3"
                    android:layout_height="@dimen/dp_10"
                    android:layout_marginHorizontal="@dimen/dp_3"
                    android:layout_marginTop="@dimen/dp_3"
                    android:src="@mipmap/ic_task_item_colon_b" />
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/second"
                    android:layout_width="@dimen/dp_30"
                    android:layout_height="@dimen/dp_23"
                    android:background="@drawable/shape_blue_5_bg_4"
                    android:gravity="center"
                    android:text="09"
                    android:textColor="@color/white"
                    android:textSize="@dimen/sp_16"
                    android:textStyle="bold" />
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_gray_7_r_4"
                android:paddingHorizontal="@dimen/dp_8"
                android:paddingVertical="@dimen/dp_3"
                android:text="事故"
                android:textColor="@color/gray_7"
                android:textSize="@dimen/sp_14" />
                android:layout_marginHorizontal="@dimen/dp_16"
                android:layout_marginTop="@dimen/dp_20"
                android:gravity="center_vertical"
                android:orientation="horizontal">
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/plateNumber"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_marginHorizontal="@dimen/dp_8"
                android:layout_weight="1"
                android:text="粤A 888WM试"
                android:textColor="@color/gray_9"
                android:textSize="@dimen/sp_18"
                android:textStyle="bold" />
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/type"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="@drawable/shape_gray_7_r_4"
                    android:paddingHorizontal="@dimen/dp_8"
                    android:paddingVertical="@dimen/dp_3"
                    android:text="事故"
                    android:textColor="@color/gray_7"
                    android:textSize="@dimen/sp_14" />
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/status"
                android:layout_width="wrap_content"
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/plateNumber"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_marginHorizontal="@dimen/dp_8"
                    android:layout_weight="1"
                    android:text="粤A 888WM试"
                    android:textColor="@color/gray_9"
                    android:textSize="@dimen/sp_18"
                    android:textStyle="bold" />
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/status"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="@drawable/shape_gray_3_bg_2"
                    android:paddingHorizontal="@dimen/dp_8"
                    android:paddingVertical="@dimen/dp_3"
                    android:text="待到达"
                    android:textColor="@color/gray_8"
                    android:textSize="@dimen/sp_12" />
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_gray_3_bg_2"
                android:paddingHorizontal="@dimen/dp_8"
                android:paddingVertical="@dimen/dp_3"
                android:text="待到达"
                android:textColor="@color/gray_8"
                android:textSize="@dimen/sp_12" />
        </LinearLayout>
                android:layout_marginHorizontal="@dimen/dp_16"
                android:layout_marginTop="@dimen/dp_16"
                android:background="@mipmap/img_map"
                android:minHeight="@dimen/dp_58"
                android:orientation="horizontal">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/dp_16"
            android:layout_marginTop="@dimen/dp_16"
            android:background="@mipmap/img_map"
            android:minHeight="@dimen/dp_58"
            android:orientation="horizontal">
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/address"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical"
                    android:layout_marginStart="@dimen/dp_12"
                    android:layout_weight="1"
                    android:textColor="@color/gray_9"
                    android:textSize="@dimen/sp_14"
                    android:textStyle="bold"
                    tools:text="蜀山区华侨中心A座2301" />
            </LinearLayout>
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/address"
                android:layout_width="0dp"
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginStart="@dimen/dp_12"
                android:layout_weight="1"
                android:textColor="@color/gray_9"
                android:textSize="@dimen/sp_14"
                android:textStyle="bold"
                tools:text="蜀山区华侨中心A座2301" />
                android:layout_marginHorizontal="@dimen/dp_16"
                android:layout_marginTop="@dimen/dp_16"
                android:orientation="horizontal">
            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/nav"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="end|center_vertical"
                android:layout_margin="@dimen/dp_12"
                android:src="@drawable/svg_map_nav" />
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_apply_for_reassignment"
                    android:layout_width="0dp"
                    android:layout_height="@dimen/dp_40"
                    android:layout_marginEnd="@dimen/dp_6"
                    android:layout_weight="1"
                    android:background="@drawable/shape_gray_5_line_4"
                    android:gravity="center"
                    android:text="申请改派"
                    android:textColor="@color/gray_9"
                    android:textSize="@dimen/sp_16"
                    android:textStyle="bold" />
                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tvControl"
                    android:layout_width="0dp"
                    android:layout_height="@dimen/dp_40"
                    android:layout_marginStart="@dimen/dp_6"
                    android:layout_weight="1"
                    android:background="@drawable/shape_blue_5_bg_4"
                    android:gravity="center"
                    android:text="接受任务"
                    android:textColor="@color/white"
                    android:textSize="@dimen/sp_16"
                    android:textStyle="bold" />
            </LinearLayout>
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/dp_16"
            android:layout_marginTop="@dimen/dp_16"
            android:orientation="horizontal">
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tv_apply_for_reassignment"
                android:layout_width="0dp"
                android:layout_height="@dimen/dp_40"
                android:layout_marginEnd="@dimen/dp_6"
                android:layout_weight="1"
                android:background="@drawable/shape_gray_5_line_4"
                android:gravity="center"
                android:text="申请改派"
                android:textColor="@color/gray_9"
                android:textSize="@dimen/sp_16"
                android:textStyle="bold" />
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tvControl"
                android:layout_width="0dp"
                android:layout_height="@dimen/dp_40"
                android:layout_marginStart="@dimen/dp_6"
                android:layout_weight="1"
                android:background="@drawable/shape_blue_5_bg_4"
                android:gravity="center"
                android:text="接受任务"
                android:textColor="@color/white"
                android:textSize="@dimen/sp_16"
                android:textStyle="bold" />
        </LinearLayout>
    </LinearLayout>
    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/level"
        android:layout_width="@dimen/dp_33"
        android:layout_height="@dimen/dp_24"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:src="@mipmap/ic_task_item_left_one" />
</androidx.constraintlayout.widget.ConstraintLayout>
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/level"
            android:layout_width="@dimen/dp_33"
            android:layout_height="@dimen/dp_24"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:src="@mipmap/ic_task_item_left_one" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
app/src/main/res/layout/dialog_apply_for_reassign_reason.xml
New file
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/shape_while_bg_5"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="@dimen/dp_20"
        android:layout_marginTop="@dimen/dp_16"
        android:gravity="center"
        android:text="请选择改派原因"
        android:textColor="@color/gray_10"
        android:textSize="@dimen/sp_18"
        android:textStyle="bold" />
    <com.google.android.flexbox.FlexboxLayout
        android:id="@+id/fl_tags"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="@dimen/dp_16"
        android:layout_marginVertical="@dimen/dp_12"
        app:alignContent="stretch"
        app:alignItems="stretch"
        app:flexWrap="wrap"
        app:justifyContent="flex_start" />
    <EditText
        android:id="@+id/et_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="@dimen/dp_20"
        android:layout_marginTop="@dimen/dp_4"
        android:layout_marginBottom="@dimen/dp_8"
        android:background="@drawable/shape_red_5_line_gray_3_bg_4"
        android:gravity="top|start"
        android:hint="请输入改派原因"
        android:maxLength="200"
        android:minHeight="@dimen/dp_95"
        android:padding="@dimen/dp_12"
        android:textColor="@color/gray_9"
        android:textColorLink="@color/gray_6"
        android:textSize="@dimen/sp_14" />
    <TextView
        android:id="@+id/tv_operation"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_60"
        android:gravity="center"
        android:text="提交"
        android:textColor="@color/blue_5"
        android:textSize="@dimen/sp_16"
        android:textStyle="bold" />
</LinearLayout>
app/src/main/res/layout/fragment_mine.xml
@@ -68,9 +68,9 @@
                    android:text="@{viewModel.userInfo.phoneNumber}"
                    android:textColor="@color/blue_5"
                    android:textSize="@dimen/sp_14"
                    app:layout_constraintTop_toTopOf="@+id/name"
                    app:layout_constraintBottom_toBottomOf="@+id/name"
                    app:layout_constraintStart_toEndOf="@+id/name"
                    app:layout_constraintTop_toTopOf="@+id/name"
                    tools:text="13488881203" />
                <androidx.appcompat.widget.AppCompatImageView
@@ -164,335 +164,596 @@
                    tools:text="未打卡" />
            </androidx.constraintlayout.widget.ConstraintLayout>
            <androidx.cardview.widget.CardView
                android:id="@+id/layout"
            <ScrollView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginHorizontal="@dimen/dp_12"
                android:layout_height="0dp"
                android:layout_marginTop="@dimen/dp_25"
                app:cardCornerRadius="@dimen/dp_8"
                app:cardElevation="@dimen/dp_2"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/infoLayout">
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:padding="16dp">
                    <LinearLayout
                        android:id="@+id/schedulingLayout"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:gravity="center_vertical"
                        android:onClick="@{viewModel.onClick}"
                        android:orientation="horizontal">
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:text="排班考勤"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold" />
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_arrow_r" />
                    </LinearLayout>
<!--                    show="@{viewModel.clockModel!=null}"-->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="12dp"
                        android:background="@drawable/shape_f8f9fa_bg_4"
                        android:gravity="center_vertical"
                        android:orientation="horizontal"
                        android:paddingHorizontal="@dimen/dp_9"
                        android:paddingVertical="@dimen/dp_7">
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="20dp"
                            android:layout_height="20dp"
                            android:src="@drawable/svg_banci"
                            app:tint="#1976D2" />
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_marginStart="8dp"
                            show="@{viewModel.clockModel!=null}"
                            android:text='@{viewModel.clockModel.xpShift.shiftName+""}'
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_12"
                            tools:text="早班" />
                        <View
                            android:layout_width="0dp"
                            android:layout_height="0dp"
                            android:layout_weight="1" />
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="@{viewModel.clockTime}"
                            show="@{viewModel.clockModel!=null}"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_12"
                            android:textStyle="bold"
                            tools:text="2026-09-21 09:00~17:00" />
                    </LinearLayout>
<!--                    show="@{viewModel.clockModel!=null}"-->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="12dp"
                        android:gravity="center_vertical"
                        android:orientation="horizontal">
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:text="打卡地点"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold" />
                        <View
                            android:layout_width="0dp"
                            android:layout_height="0dp"
                            android:layout_weight="1" />
<!--                        show="@{viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true}"-->
                        <androidx.appcompat.widget.AppCompatImageView
                            show="@{viewModel.clockModel!=null &amp;&amp; (viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true)}"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_location" />
<!--                        show="@{viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true}"-->
                        <androidx.appcompat.widget.AppCompatTextView
                            android:id="@+id/goCheck"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            show="@{viewModel.clockModel!=null &amp;&amp; (viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true)}"
                            android:layout_marginStart="4dp"
                            android:onClick="@{viewModel.onClick}"
                            android:text="去打卡"
                            android:textColor="@color/blue_5"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold" />
                    </LinearLayout>
<!--                    show="@{viewModel.clockModel!=null}"-->
                    <FrameLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="@dimen/dp_12"
                        android:background="@mipmap/img_map">
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_gravity="center_vertical"
                            android:layout_marginVertical="@dimen/dp_10"
                            android:layout_marginStart="@dimen/dp_12"
                            android:layout_marginEnd="@dimen/dp_60"
                            android:text="@{viewModel.clockModel.xpCheckLocation.locationAddress}"
                            show="@{viewModel.clockModel!=null}"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold"
                            tools:text="蜀山区华侨中心A座2301" />
                        <!-- 导航按钮 -->
                        <androidx.appcompat.widget.AppCompatImageView
                            android:id="@+id/nav"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_gravity="end|center_vertical"
                            android:layout_margin="@dimen/dp_12"
                            android:src="@drawable/svg_map_nav" />
                    </FrameLayout>
                </LinearLayout>
            </androidx.cardview.widget.CardView>
            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginHorizontal="@dimen/dp_12"
                android:layout_marginTop="@dimen/dp_12"
                android:layout_marginBottom="@dimen/dp_25"
                app:cardCornerRadius="@dimen/dp_8"
                app:cardElevation="@dimen/dp_2"
                app:layout_constraintTop_toBottomOf="@+id/layout">
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
                    <LinearLayout
                        android:id="@+id/setLayout"
                    <androidx.cardview.widget.CardView
                        android:id="@+id/layout"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/dp_56"
                        android:gravity="center_vertical"
                        android:onClick="@{viewModel.onClick}"
                        android:orientation="horizontal"
                        android:paddingHorizontal="@dimen/dp_16">
                        android:layout_height="wrap_content"
                        android:layout_marginHorizontal="@dimen/dp_12"
                        app:cardCornerRadius="@dimen/dp_8"
                        app:cardElevation="@dimen/dp_2">
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_mine_set" />
                            android:orientation="vertical"
                            android:padding="16dp">
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_marginStart="12dp"
                            android:layout_weight="1"
                            android:text="设置"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold" />
                            <LinearLayout
                                android:id="@+id/schedulingLayout"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:gravity="center_vertical"
                                android:onClick="@{viewModel.onClick}"
                                android:orientation="horizontal">
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_arrow_r" />
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_weight="1"
                                    android:text="排班考勤"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                    </LinearLayout>
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_arrow_r" />
                    <View
                            </LinearLayout>
                            <!--                    show="@{viewModel.clockModel!=null}"-->
                            <LinearLayout
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginTop="12dp"
                                android:background="@drawable/shape_f8f9fa_bg_4"
                                android:gravity="center_vertical"
                                android:orientation="horizontal"
                                android:paddingHorizontal="@dimen/dp_9"
                                android:paddingVertical="@dimen/dp_7">
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="20dp"
                                    android:layout_height="20dp"
                                    android:src="@drawable/svg_banci"
                                    app:tint="#1976D2" />
                                <androidx.appcompat.widget.AppCompatTextView
                                    show="@{viewModel.clockModel!=null}"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="8dp"
                                    android:text='@{viewModel.clockModel.xpShift.shiftName+""}'
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_12"
                                    tools:text="早班" />
                                <View
                                    android:layout_width="0dp"
                                    android:layout_height="0dp"
                                    android:layout_weight="1" />
                                <androidx.appcompat.widget.AppCompatTextView
                                    show="@{viewModel.clockModel!=null}"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:text="@{viewModel.clockTime}"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_12"
                                    android:textStyle="bold"
                                    tools:text="2026-09-21 09:00~17:00" />
                            </LinearLayout>
                            <!--                    show="@{viewModel.clockModel!=null}"-->
                            <LinearLayout
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginTop="12dp"
                                android:gravity="center_vertical"
                                android:orientation="horizontal">
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_weight="1"
                                    android:text="打卡地点"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                                <View
                                    android:layout_width="0dp"
                                    android:layout_height="0dp"
                                    android:layout_weight="1" />
                                <!--                        show="@{viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true}"-->
                                <androidx.appcompat.widget.AppCompatImageView
                                    show="@{viewModel.clockModel!=null &amp;&amp; (viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true)}"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_location" />
                                <!--                        show="@{viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true}"-->
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:id="@+id/goCheck"
                                    show="@{viewModel.clockModel!=null &amp;&amp; (viewModel.clockModel.clockIn != true || viewModel.clockModel.clockOut != true)}"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="4dp"
                                    android:onClick="@{viewModel.onClick}"
                                    android:text="去打卡"
                                    android:textColor="@color/blue_5"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                            </LinearLayout>
                            <!--                    show="@{viewModel.clockModel!=null}"-->
                            <FrameLayout
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginTop="@dimen/dp_12"
                                android:background="@mipmap/img_map">
                                <androidx.appcompat.widget.AppCompatTextView
                                    show="@{viewModel.clockModel!=null}"
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:layout_gravity="center_vertical"
                                    android:layout_marginVertical="@dimen/dp_10"
                                    android:layout_marginStart="@dimen/dp_12"
                                    android:layout_marginEnd="@dimen/dp_60"
                                    android:text="@{viewModel.clockModel.xpCheckLocation.locationAddress}"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold"
                                    tools:text="蜀山区华侨中心A座2301" />
                                <!-- 导航按钮 -->
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:id="@+id/nav"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:layout_gravity="end|center_vertical"
                                    android:layout_margin="@dimen/dp_12"
                                    android:src="@drawable/svg_map_nav" />
                            </FrameLayout>
                        </LinearLayout>
                    </androidx.cardview.widget.CardView>
                    <androidx.cardview.widget.CardView
                        android:id="@+id/cv_task"
                        show="@{viewModel.cruiseCheckInMissionInfo.effective}"
                        android:layout_width="match_parent"
                        android:layout_height="1px"
                        android:layout_marginHorizontal="@dimen/dp_16"
                        android:background="@color/gray_4" />
                        android:layout_height="wrap_content"
                        android:layout_marginHorizontal="@dimen/dp_12"
                        android:layout_marginTop="@dimen/dp_16"
                        android:visibility="gone"
                        app:cardCornerRadius="@dimen/dp_8"
                        app:cardElevation="@dimen/dp_2"
                        app:layout_constraintTop_toBottomOf="@+id/layout"
                        tools:visibility="visible">
                    <LinearLayout
                        android:id="@+id/tiaokuan"
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:orientation="vertical"
                            android:padding="16dp">
                            <LinearLayout
                                android:id="@+id/ll_cruise_check_in_mission"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:gravity="center_vertical"
                                android:onClick="@{viewModel.onClick}"
                                android:orientation="horizontal">
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_weight="1"
                                    android:text="巡游打卡任务"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_arrow_r" />
                            </LinearLayout>
                            <!--                    show="@{viewModel.clockModel!=null}"-->
                            <LinearLayout
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginTop="12dp"
                                android:background="@drawable/shape_f8f9fa_bg_4"
                                android:gravity="center_vertical"
                                android:orientation="horizontal"
                                android:paddingHorizontal="@dimen/dp_9"
                                android:paddingVertical="@dimen/dp_7">
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="20dp"
                                    android:layout_height="20dp"
                                    android:src="@mipmap/ic_clock_note" />
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="@dimen/dp_4"
                                    android:text="任务时间"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_12" />
                                <View
                                    android:layout_width="0dp"
                                    android:layout_height="0dp"
                                    android:layout_weight="1" />
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:id="@+id/tv_mission_time"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_12"
                                    tools:text="2026-09-21 09:00~17:00" />
                            </LinearLayout>
                            <!--                    show="@{viewModel.clockModel!=null}"-->
                            <LinearLayout
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginTop="12dp"
                                android:gravity="center_vertical"
                                android:orientation="horizontal">
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_weight="1"
                                    android:text="打卡情况"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                                <View
                                    android:layout_width="0dp"
                                    android:layout_height="0dp"
                                    android:layout_weight="1" />
                            </LinearLayout>
                            <!--                    show="@{viewModel.clockModel!=null}"-->
                            <LinearLayout
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginTop="@dimen/dp_12"
                                android:gravity="center"
                                android:orientation="horizontal">
                                <FrameLayout
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:layout_gravity="center">
                                    <com.xw.yunwei.ui.widget.CircularProgressView
                                        android:id="@+id/cpv_progress"
                                        android:layout_width="@dimen/dp_100"
                                        android:layout_height="@dimen/dp_100"
                                        app:backgroundProgressColor="@color/orange_5"
                                        app:maxProgress="100"
                                        app:progress="0"
                                        app:progressColor="@color/green_5"
                                        app:ringWidth="@dimen/dp_10" />
                                    <LinearLayout
                                        android:layout_width="wrap_content"
                                        android:layout_height="wrap_content"
                                        android:layout_gravity="center"
                                        android:gravity="center"
                                        android:orientation="vertical">
                                        <TextView
                                            android:id="@+id/tv_mission_point_count"
                                            android:layout_width="wrap_content"
                                            android:layout_height="wrap_content"
                                            android:text="0"
                                            android:textColor="@color/color_3d3d3d"
                                            android:textSize="@dimen/sp_20"
                                            android:textStyle="bold" />
                                        <TextView
                                            android:layout_width="wrap_content"
                                            android:layout_height="wrap_content"
                                            android:text="打卡点位"
                                            android:textColor="@color/color_3d3d3d"
                                            android:textSize="@dimen/sp_12"
                                            android:textStyle="bold" />
                                    </LinearLayout>
                                </FrameLayout>
                                <LinearLayout
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="@dimen/dp_24"
                                    android:gravity="center"
                                    android:orientation="vertical">
                                    <LinearLayout
                                        android:layout_width="@dimen/dp_124"
                                        android:layout_height="wrap_content"
                                        android:gravity="center"
                                        android:orientation="horizontal">
                                        <TextView
                                            android:layout_width="wrap_content"
                                            android:layout_height="wrap_content"
                                            android:text="已打卡"
                                            android:textColor="@color/gray_9"
                                            android:textSize="@dimen/sp_12"
                                            android:textStyle="bold" />
                                        <View
                                            android:layout_width="0dp"
                                            android:layout_height="@dimen/dp_1"
                                            android:layout_weight="1" />
                                        <TextView
                                            android:id="@+id/tv_had_report_num"
                                            android:layout_width="wrap_content"
                                            android:layout_height="wrap_content"
                                            android:textColor="@color/green_5"
                                            android:textSize="@dimen/sp_16"
                                            android:textStyle="bold"
                                            tools:text="16" />
                                    </LinearLayout>
                                    <ProgressBar
                                        style="?android:attr/progressBarStyleHorizontal"
                                        android:layout_width="@dimen/dp_124"
                                        android:layout_height="@dimen/dp_8"
                                        android:max="@{viewModel.cruiseCheckInMissionInfo.checkInPointCount}"
                                        android:maxHeight="@dimen/dp_8"
                                        android:minHeight="@dimen/dp_8"
                                        android:progress="@{viewModel.cruiseCheckInMissionInfo.checkInPointFinishCount}"
                                        android:progressDrawable="@drawable/layer_list_green_progress_bar" />
                                    <LinearLayout
                                        android:layout_width="@dimen/dp_124"
                                        android:layout_height="wrap_content"
                                        android:layout_marginTop="@dimen/dp_20"
                                        android:gravity="center"
                                        android:orientation="horizontal">
                                        <TextView
                                            android:layout_width="wrap_content"
                                            android:layout_height="wrap_content"
                                            android:text="未打卡"
                                            android:textColor="@color/gray_9"
                                            android:textSize="@dimen/sp_12"
                                            android:textStyle="bold" />
                                        <View
                                            android:layout_width="0dp"
                                            android:layout_height="@dimen/dp_1"
                                            android:layout_weight="1" />
                                        <TextView
                                            android:id="@+id/tv_not_report_num"
                                            android:layout_width="wrap_content"
                                            android:layout_height="wrap_content"
                                            android:textColor="@color/orange_5"
                                            android:textSize="@dimen/sp_16"
                                            android:textStyle="bold"
                                            tools:text="16" />
                                    </LinearLayout>
                                    <ProgressBar
                                        style="?android:attr/progressBarStyleHorizontal"
                                        android:layout_width="@dimen/dp_124"
                                        android:layout_height="@dimen/dp_8"
                                        android:max="@{viewModel.cruiseCheckInMissionInfo.checkInPointCount}"
                                        android:maxHeight="@dimen/dp_8"
                                        android:minHeight="@dimen/dp_8"
                                        android:progress="@{viewModel.cruiseCheckInMissionInfo.checkInPointFinishCount}"
                                        android:progressDrawable="@drawable/layer_list_orange_progress_bar" />
                                </LinearLayout>
                            </LinearLayout>
                        </LinearLayout>
                    </androidx.cardview.widget.CardView>
                    <androidx.cardview.widget.CardView
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/dp_56"
                        android:gravity="center_vertical"
                        android:orientation="horizontal"
                        android:paddingHorizontal="@dimen/dp_16">
                        android:layout_height="wrap_content"
                        android:layout_marginHorizontal="@dimen/dp_12"
                        android:layout_marginTop="@dimen/dp_16"
                        android:layout_marginBottom="@dimen/dp_25"
                        app:cardCornerRadius="@dimen/dp_8"
                        app:cardElevation="@dimen/dp_2"
                        app:layout_constraintTop_toBottomOf="@+id/cv_task">
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                        <LinearLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_mine_tiaokuan" />
                            android:orientation="vertical">
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_marginStart="12dp"
                            android:layout_weight="1"
                            android:text="服务条款"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold" />
                            <LinearLayout
                                android:id="@+id/setLayout"
                                android:layout_width="match_parent"
                                android:layout_height="@dimen/dp_56"
                                android:gravity="center_vertical"
                                android:onClick="@{viewModel.onClick}"
                                android:orientation="horizontal"
                                android:paddingHorizontal="@dimen/dp_16">
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_arrow_r" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_mine_set" />
                    </LinearLayout>
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="12dp"
                                    android:layout_weight="1"
                                    android:text="设置"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                    <View
                        android:layout_width="match_parent"
                        android:layout_height="1px"
                        android:layout_marginHorizontal="@dimen/dp_16"
                        android:background="@color/gray_4" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_arrow_r" />
                    <LinearLayout
                        android:id="@+id/contact"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/dp_56"
                        android:gravity="center_vertical"
                        android:orientation="horizontal"
                        android:paddingHorizontal="@dimen/dp_16">
                            </LinearLayout>
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_mine_kefu" />
                            <View
                                android:layout_width="match_parent"
                                android:layout_height="1px"
                                android:layout_marginHorizontal="@dimen/dp_16"
                                android:background="@color/gray_4" />
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_marginStart="12dp"
                            android:layout_weight="1"
                            android:text="联系客服"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold" />
                            <LinearLayout
                                android:id="@+id/tiaokuan"
                                android:layout_width="match_parent"
                                android:layout_height="@dimen/dp_56"
                                android:gravity="center_vertical"
                                android:orientation="horizontal"
                                android:paddingHorizontal="@dimen/dp_16">
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_arrow_r" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_mine_tiaokuan" />
                    </LinearLayout>
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="12dp"
                                    android:layout_weight="1"
                                    android:text="服务条款"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                    <View
                        android:layout_width="match_parent"
                        android:layout_height="1px"
                        android:layout_marginHorizontal="@dimen/dp_16"
                        android:background="@color/gray_4" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_arrow_r" />
                    <LinearLayout
                        android:id="@+id/uploadLayout"
                        android:layout_width="match_parent"
                        android:layout_height="@dimen/dp_56"
                        android:gravity="center_vertical"
                        android:onClick="@{viewModel.onClick}"
                        android:orientation="horizontal"
                        android:paddingHorizontal="@dimen/dp_16">
                            </LinearLayout>
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_mine_card" />
                            <View
                                android:layout_width="match_parent"
                                android:layout_height="1px"
                                android:layout_marginHorizontal="@dimen/dp_16"
                                android:background="@color/gray_4" />
                        <androidx.appcompat.widget.AppCompatTextView
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_marginStart="12dp"
                            android:layout_weight="1"
                            android:text="证件上传"
                            android:textColor="@color/gray_9"
                            android:textSize="@dimen/sp_14"
                            android:textStyle="bold" />
                            <LinearLayout
                                android:id="@+id/contact"
                                android:layout_width="match_parent"
                                android:layout_height="@dimen/dp_56"
                                android:gravity="center_vertical"
                                android:orientation="horizontal"
                                android:paddingHorizontal="@dimen/dp_16">
                        <androidx.appcompat.widget.AppCompatImageView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:src="@drawable/svg_arrow_r" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_mine_kefu" />
                    </LinearLayout>
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="12dp"
                                    android:layout_weight="1"
                                    android:text="联系客服"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                    <View
                        android:layout_width="match_parent"
                        android:layout_height="1px"
                        android:layout_marginHorizontal="@dimen/dp_16"
                        android:background="@color/gray_4" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_arrow_r" />
                            </LinearLayout>
                            <View
                                android:layout_width="match_parent"
                                android:layout_height="1px"
                                android:layout_marginHorizontal="@dimen/dp_16"
                                android:background="@color/gray_4" />
                            <LinearLayout
                                android:id="@+id/uploadLayout"
                                android:layout_width="match_parent"
                                android:layout_height="@dimen/dp_56"
                                android:gravity="center_vertical"
                                android:onClick="@{viewModel.onClick}"
                                android:orientation="horizontal"
                                android:paddingHorizontal="@dimen/dp_16">
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_mine_card" />
                                <androidx.appcompat.widget.AppCompatTextView
                                    android:layout_width="0dp"
                                    android:layout_height="wrap_content"
                                    android:layout_marginStart="12dp"
                                    android:layout_weight="1"
                                    android:text="证件上传"
                                    android:textColor="@color/gray_9"
                                    android:textSize="@dimen/sp_14"
                                    android:textStyle="bold" />
                                <androidx.appcompat.widget.AppCompatImageView
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:src="@drawable/svg_arrow_r" />
                            </LinearLayout>
                            <View
                                android:layout_width="match_parent"
                                android:layout_height="1px"
                                android:layout_marginHorizontal="@dimen/dp_16"
                                android:background="@color/gray_4" />
                        </LinearLayout>
                    </androidx.cardview.widget.CardView>
                </LinearLayout>
            </androidx.cardview.widget.CardView>
            </ScrollView>
        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.core.widget.NestedScrollView>
app/src/main/res/layout/item_check_in_mission_point.xml
New file
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginHorizontal="@dimen/dp_20"
    android:orientation="horizontal">
    <LinearLayout
        android:id="@+id/ll_no"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:gravity="center"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="@id/ll_content"
        app:layout_constraintEnd_toStartOf="@id/ll_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <!-- 序号 -->
        <TextView
            android:id="@+id/tv_index"
            android:layout_width="@dimen/dp_16"
            android:layout_height="@dimen/dp_16"
            android:background="@drawable/circle_gray_6"
            android:gravity="center"
            android:text="1"
            android:textColor="@color/white"
            android:textSize="@dimen/sp_10"
            android:textStyle="bold" />
        <View
            android:layout_width="@dimen/dp_1"
            android:layout_height="0dp"
            android:layout_marginVertical="@dimen/dp_6"
            android:layout_weight="1" />
    </LinearLayout>
    <LinearLayout
        android:id="@+id/ll_content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/dp_12"
        android:orientation="vertical"
        android:paddingBottom="@dimen/dp_12"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/ll_no"
        app:layout_constraintTop_toTopOf="parent">
        <!-- 顶部信息行 -->
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="打卡限时"
                android:textColor="@color/gray_9"
                android:textSize="@dimen/sp_12"
                android:textStyle="bold" />
            <TextView
                android:id="@+id/tv_time_range"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/tv_title"
                android:textColor="@color/gray_9"
                android:textSize="@dimen/sp_14"
                android:textStyle="bold"
                tools:text="08:00~09:00" />
            <TextView
                android:id="@+id/tv_status"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/tv_title"
                android:layout_alignParentEnd="true"
                android:paddingHorizontal="@dimen/dp_8"
                android:paddingVertical="@dimen/dp_1"
                android:textColor="@color/gray_9"
                android:textSize="@dimen/sp_12"
                tools:text="08:41:57 已打卡" />
        </RelativeLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/dp_8"
            android:background="@mipmap/img_map"
            android:minHeight="@dimen/dp_58"
            android:orientation="horizontal">
            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/address"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginStart="@dimen/dp_12"
                android:layout_weight="1"
                android:textColor="@color/gray_9"
                android:textSize="@dimen/sp_14"
                android:textStyle="bold"
                tools:text="蜀山区华侨中心A座2301" />
            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/nav"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="end|center_vertical"
                android:layout_margin="@dimen/dp_12"
                android:src="@drawable/svg_map_nav" />
        </LinearLayout>
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
app/src/main/res/mipmap-xhdpi/ic_apply_reassign_bg.png
app/src/main/res/mipmap-xhdpi/ic_clock_note.png
app/src/main/res/mipmap-xhdpi/icon_note.png
app/src/main/res/values/attrs.xml
New file
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircularProgressView">
        <!-- 环宽度 -->
        <attr name="ringWidth" format="dimension" />
        <!-- 背景环颜色 -->
        <attr name="backgroundProgressColor" format="color" />
        <!-- 进度环颜色 -->
        <attr name="progressColor" format="color" />
        <!-- 当前进度 -->
        <attr name="progress" format="float" />
        <!-- 最大进度 -->
        <attr name="maxProgress" format="float" />
    </declare-styleable>
</resources>
app/src/main/res/values/colors.xml
@@ -7,4 +7,5 @@
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="color_3d3d3d">#FF3D3D3D</color>
</resources>
common/build.gradle.kts
@@ -60,6 +60,7 @@
    api(libs.kotlinx.coroutines.core)
    api(libs.kotlinx.coroutines.android)
    api(libs.okhttp)
    api(libs.okhttp.logging.interceptor)
    api(libs.retrofit)
    api(libs.retrofit.converter.gson)
    api(libs.retrofit.converter.scalars)
gradle/libs.versions.toml
@@ -24,6 +24,7 @@
lifecycleVersion = "2.10.0"
mmkv = "2.3.0"
okhttp = "4.9.3"  # 升级有兼容性问题
okhttpLoggingInterceptor = "4.9.3"  # 升级有兼容性问题
pdfrendererview = "1.0.10"
photoview = "2.3.0"
pictureselector = "v3.11.2"
@@ -35,6 +36,7 @@
xxpermissions = "20.0"
flexbox = "3.0.0"
crashreport = "latest.release"
alicloud = "3.10.1"
[libraries]
@@ -56,6 +58,8 @@
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
# okhttp
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttpLoggingInterceptor" }
# retrofit
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
@@ -106,6 +110,7 @@
# 腾讯bugly
crashreport = { module = "com.tencent.bugly:crashreport", version.ref = "crashreport" }
nativecrashreport = { module = "com.tencent.bugly:nativecrashreport", version.ref = "crashreport" }
alicloud = { module = "com.aliyun.ams:alicloud-android-push", version.ref = "alicloud" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }