18034727885
2026-04-15 a7f3a04529a938b0f646847ef6baa9d3491d2759
优化登录页协议展示和同意协议提示逻辑
5个文件已添加
9个文件已修改
737 ■■■■■ 已修改文件
app/src/main/AndroidManifest.xml 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/model/AgreeInfo.kt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/UserApi.kt 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/net/UserRepository.kt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt 327 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/site/SiteErrorUploadActivity.kt 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/xw/yunwei/ui/work/site/SiteNavActivity.kt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_login.xml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_site_error_upload.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_site_nav.xml 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/xw/common/CommonDialog.kt 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/res/drawable/shape_while_bg_5.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/res/layout/dialog_notice.xml 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml
@@ -1,22 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!--允许访问网络,必选权限-->
    <uses-permission android:name="android.permission.INTERNET" />
    <!--允许获取粗略位置,若用GPS实现定位小蓝点功能则必选-->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <!--允许获取精准位置-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <!--允许获取网络状态,用于网络定位,若无gps但仍需实现定位小蓝点功能则此权限必选-->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!--允许获取wifi网络信息,用于网络定位,若无gps但仍需实现定位小蓝点功能则此权限必选-->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <!--允许获取wifi状态改变,用于网络定位,若无gps但仍需实现定位小蓝点功能则此权限必选-->
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <!--允许写设备缓存,用于问题排查-->
    <!-- 允许访问网络,必选权限 -->
    <uses-permission android:name="android.permission.INTERNET" /> <!-- 允许获取粗略位置,若用GPS实现定位小蓝点功能则必选 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 允许获取精准位置 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 允许获取网络状态,用于网络定位,若无gps但仍需实现定位小蓝点功能则此权限必选 -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 允许获取wifi网络信息,用于网络定位,若无gps但仍需实现定位小蓝点功能则此权限必选 -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <!-- 允许获取wifi状态改变,用于网络定位,若无gps但仍需实现定位小蓝点功能则此权限必选 -->
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <!-- 允许写设备缓存,用于问题排查 -->
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-feature
@@ -24,48 +16,44 @@
        android:required="false" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
    <!--Android 13版本适配,细化存储权限-->
    <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" /> <!-- Android 13版本适配,细化存储权限 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <!--允许通知权限-->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <!-- 普通震动 -->
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- 允许通知权限 -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 普通震动 -->
    <uses-permission android:name="android.permission.VIBRATE" />
    <application
        android:name=".MyApp"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:extractNativeLibs="true"
        android:requestLegacyExternalStorage="true"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/img_logo"
        android:label="${APP_NAME}"
        android:networkSecurityConfig="@xml/network_security_config"
        android:requestLegacyExternalStorage="true"
        android:roundIcon="@mipmap/img_logo"
        android:supportsRtl="true"
        android:theme="@style/Theme.Yunwei">
        <activity
            android:name=".ui.work.site.SiteErrorUploadActivity"
            android:exported="false" />
        <activity
            android:name=".ui.user.login.LoginActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".ui.MainActivity"
            android:launchMode="singleTask" />
        <activity
            android:name=".ui.user.login.FindPswActivity"
            android:launchMode="singleTop" />
@@ -87,7 +75,6 @@
        <activity
            android:name=".ui.user.mine.protocol.ProtocolActivity"
            android:launchMode="singleTop" />
        <activity
            android:name=".ui.work.notice.NoticeActivity"
            android:launchMode="singleTop" />
@@ -139,42 +126,34 @@
        <activity
            android:name=".ui.work.site.SiteNavActivity"
            android:launchMode="singleTop" />
        <activity
            android:name="com.xw.yunwei.ui.task.normal.NormalTaskDetailActivity"
            android:name=".ui.task.normal.NormalTaskDetailActivity"
            android:launchMode="singleTop" />
        <activity
            android:name="com.xw.yunwei.ui.task.selectCar.SelectCarActivity"
            android:name=".ui.task.selectCar.SelectCarActivity"
            android:launchMode="singleTop" />
        <activity
            android:name="com.xw.yunwei.ui.task.create.CreateTaskActivity"
            android:name=".ui.task.create.CreateTaskActivity"
            android:launchMode="singleTop" />
        <activity
            android:name="com.xw.yunwei.ui.task.accident.detail.AccidentDetailActivity"
            android:name=".ui.task.accident.detail.AccidentDetailActivity"
            android:launchMode="singleTop" />
        <activity
            android:name="com.xw.yunwei.ui.task.accident.handle.AccidentProcessActivity"
            android:name=".ui.task.accident.handle.AccidentProcessActivity"
            android:launchMode="singleTop" />
        <activity
            android:name="com.xw.yunwei.ui.task.taskSub.TaskSubActivity"
            android:name=".ui.task.taskSub.TaskSubActivity"
            android:launchMode="singleTop" />
        <activity
            android:name="com.xw.yunwei.ui.task.taskDetail.TaskDetailActivity"
            android:launchMode="singleTop" />
        <!-- 配置高德key -->
            android:name=".ui.task.taskDetail.TaskDetailActivity"
            android:launchMode="singleTop" /> <!-- 配置高德key -->
        <meta-data
            android:name="com.amap.api.v2.apikey"
            android:value="dc26e21a7c2a494443eddbe52a331f19" />
        <!--            android:value="c237d9248cc928ec73f078507122ed7d" />-->
            android:value="dc26e21a7c2a494443eddbe52a331f19" /> <!-- android:value="c237d9248cc928ec73f078507122ed7d" /> -->
        <!-- 配置高德定位服务 -->
        <service android:name="com.amap.api.location.APSService" />
        <!--推送消息接收监听器-->
        <receiver android:name=".push.PushMsgReceiver"
        <service android:name="com.amap.api.location.APSService" /> <!-- 推送消息接收监听器 -->
        <receiver
            android:name=".push.PushMsgReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="com.alibaba.push2.action.NOTIFICATION_OPENED" />
@@ -187,7 +166,6 @@
            </intent-filter>
        </receiver>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.xw.yunwei.fileprovider"
@@ -198,7 +176,6 @@
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>
app/src/main/java/com/xw/yunwei/model/AgreeInfo.kt
New file
@@ -0,0 +1,10 @@
package com.xw.yunwei.model
//协议信息
data class AgreeInfo(
    val id: String,
    val content: String,
    val type: Int,
    val webType: Int = 1,
    val version: String
)
app/src/main/java/com/xw/yunwei/net/UserApi.kt
@@ -2,6 +2,7 @@
import com.xw.common.net.interceptor.HttpResponse
import com.xw.common.net.interceptor.PageResponse
import com.xw.yunwei.model.AgreeInfo
import com.xw.yunwei.model.AppPermissionModel
import com.xw.yunwei.model.AppVersionModel
import com.xw.yunwei.model.ClockInStatusModel
@@ -12,8 +13,6 @@
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
@@ -85,4 +84,7 @@
    @GET("operation/maintenance-app/api/v1/getAppPermission")
    fun getAppPermission(): Call<HttpResponse<List<AppPermissionModel>>>
    /*获取协议配置 */
    @GET("operation/maintenance-app/base/v1/loginAgreement/getLoginAgreementList")
    fun getAgreeConfig(): Call<HttpResponse<List<AgreeInfo>>>
}
app/src/main/java/com/xw/yunwei/net/UserRepository.kt
@@ -36,6 +36,9 @@
    suspend fun clockOut(map: Map<String, Any?>) =
        api.clockOut(getBody(map)).interceptAwait()
    /*获取协议配置 */
    suspend fun getAgreeConfig() = api.getAgreeConfig().interceptAwait()
    fun getUserInfo() = flow { emit(api.getUserInfo().interceptAwait()) }
app/src/main/java/com/xw/yunwei/ui/user/login/LoginActivity.kt
@@ -1,21 +1,15 @@
package com.xw.yunwei.ui.user.login
import android.annotation.SuppressLint
import android.graphics.Color
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Button
import androidx.core.os.bundleOf
import androidx.databinding.DataBindingUtil
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import androidx.lifecycle.Observer
import com.blankj.utilcode.util.SpanUtils
import com.google.android.material.tabs.TabLayout
import com.xw.common.CommonDialog
import com.xw.common.Constants
import com.xw.common.R
import com.xw.common.base.BaseActivity
@@ -25,7 +19,7 @@
import com.xw.yunwei.BR
import com.xw.yunwei.MyApp
import com.xw.yunwei.databinding.ActivityLoginBinding
import com.xw.yunwei.databinding.DialogProtocolBinding
import com.xw.yunwei.model.AgreeInfo
import com.xw.yunwei.ui.MainActivity
import com.xw.yunwei.ui.work.car.detail.StandardDescriptionActivity
@@ -46,67 +40,39 @@
            binding.tabLayout.addTab(tab1, true)
            binding.tabLayout.addTab(tab2, false)
            val protocol =
                SpanUtils().append("我已阅读并同意")
                    .setForegroundColor(getResColor(R.color.gray_9))
                    .append("《小鹏Robotaxi自动驾驶出行运维隐私政策》").setBold()
                    .setForegroundColor(getResColor(R.color.blue_5))
                    .setClickSpan(object : ClickableSpan() {
                        override fun onClick(widget: View) {
//                            showProtocolDialog(1)
                            startActivity(
                                StandardDescriptionActivity::class.java,
                                bundleOf("type" to 8, "webType" to 1)
                            )
                        }
                        override fun updateDrawState(ds: TextPaint) {}
                    })
                    .append("《第三方共享信息清单》").setBold()
                    .setForegroundColor(getResColor(R.color.blue_5))
                    .setClickSpan(object : ClickableSpan() {
                        override fun onClick(widget: View) {
//                            showProtocolDialog(2)
                            startActivity(
                                StandardDescriptionActivity::class.java,
                                bundleOf("type" to 9, "webType" to 1)
                            )
                        }
                        override fun updateDrawState(ds: TextPaint) {}
                    })
                    .append("《个人信息收集清单》").setBold()
                    .setForegroundColor(getResColor(R.color.blue_5))
                    .setClickSpan(object : ClickableSpan() {
                        override fun onClick(widget: View) {
                            startActivity(
                                StandardDescriptionActivity::class.java,
                                bundleOf("type" to 10, "webType" to 1)
                            )
                        }
                        override fun updateDrawState(ds: TextPaint) {}
                    })
                    .append("《权限调用》").setBold()
                    .setForegroundColor(getResColor(R.color.blue_5))
                    .setClickSpan(object : ClickableSpan() {
                        override fun onClick(widget: View) {
                            startActivity(
                                StandardDescriptionActivity::class.java,
                                bundleOf("type" to 11, "webType" to 1)
                            )
                        }
                        override fun updateDrawState(ds: TextPaint) {}
                    })
                    .create()
            binding.protocol.text = protocol
            binding.protocol.highlightColor = Color.TRANSPARENT
            binding.protocol.movementMethod = LinkMovementMethod.getInstance()
            viewModel.getAgreeConfig()
        }
    }
    //获取协议文案
    private fun getAgreeText(
        prefix: String,
        prefixTextColor: Int = R.color.gray_9,
        list: List<AgreeInfo>,
        agreeBold: Boolean = true,
        agreeTextColor: Int = R.color.blue_5
    ): CharSequence {
        val spanUtils = SpanUtils().append(prefix).setForegroundColor(getResColor(prefixTextColor))
        list.forEach {
            spanUtils.append(it.content)
            if (agreeBold) {
                spanUtils.setBold()
            }
            spanUtils.setForegroundColor(getResColor(agreeTextColor))
                .setClickSpan(object : ClickableSpan() {
                    override fun onClick(widget: View) {
                        startActivity(
                            StandardDescriptionActivity::class.java,
                            bundleOf("type" to it.type, "webType" to it.webType)
                        )
                    }
                    override fun updateDrawState(ds: TextPaint) {}
                })
        }
        return spanUtils.create()
    }
    override fun setListener() {
@@ -125,208 +91,43 @@
            override fun onTabUnselected(tab: TabLayout.Tab?) {}
            override fun onTabReselected(tab: TabLayout.Tab?) {}
        })
        binding.login.setOnClickListener {
            if (viewModel.protocolStatus.value != true) {
                CommonDialog.showNoticeDialog(
                    context = this,
                    title = "服务协议及隐私保护",
                    content = getAgreeText(
                        prefix = "为了更好地保障您的合法权益,请您阅读并同意以下协议",
                        prefixTextColor = R.color.gray_9,
                        list = viewModel.agreeList.value ?: listOf(),
                        agreeBold = false,
                        agreeTextColor = R.color.gray_9
                    ),
                    leftOperationText = "不同意",
                    rightOperationText = "同意",
                    rightOperationBlock = {
                        viewModel.protocolStatus.value = true
                        viewModel.login()
                        return@showNoticeDialog true
                    })
                return@setOnClickListener
            }
            viewModel.login()
        }
        viewModel.agreeList.observe(this, Observer<List<AgreeInfo>> {
            binding.protocol.text = getAgreeText(prefix = "我已阅读并同意", list = it)
            binding.protocol.highlightColor = Color.TRANSPARENT
            binding.protocol.movementMethod = LinkMovementMethod.getInstance()
        })
    }
    override fun setObservable() {
        super.setObservable()
        viewModel.protocolStatus.observe(this){
        viewModel.protocolStatus.observe(this) {
            MMKVManager.getInstance().saveBoolean(KeyPreferences.NEED_READ, !it)
        }
        viewModel.phone.observe(this){
        viewModel.phone.observe(this) {
            viewModel.canGetCode.value = it.length == 11
        }
    }
    // 标记两个协议是否都已读到底部
    private var userProtocolRead = false
    private var privacyProtocolRead = false
    private var currentTab = 1 // 当前选中标签
    fun showProtocolDialog(type: Int) {
        MaterialDialog(this).cornerRadius(15f).cancelable(true).show {
            customView(
                com.xw.yunwei.R.layout.dialog_protocol,
                noVerticalPadding = true,
                dialogWrapContent = true
            )
            DataBindingUtil.bind<DialogProtocolBinding>(getCustomView())?.let { bind ->
                // 初始禁用确认按钮
                bind.btnRight.isEnabled = false
                // bind.btnRight.alpha = 0.5f
                bind.btnRight.setTextColor(getResColor(R.color.gray_7))
                val tab1 = bind.protocolLayout.tabLayout.newTab().apply { text = "用户协议" }
                val tab2 = bind.protocolLayout.tabLayout.newTab().apply { text = "隐私条款" }
                bind.protocolLayout.tabLayout.addTab(tab1, type == 1)
                bind.protocolLayout.tabLayout.addTab(tab2, type == 2)
                bind.btnLeft.setOnClickListener { dismiss() }
                bind.btnRight.setOnClickListener {
                    viewModel.protocolStatus.value = true
                    dismiss()
                }
                initWeb(bind.protocolLayout.web,bind.btnRight)
                val getProtocol: (Int) -> Unit = { type ->
                    currentTab = type
                    viewModel.getProtocol(type) {
                        setContent(bind.protocolLayout.web, it, bind.btnRight)
                    }
                }
                getProtocol(type)
                bind.protocolLayout.tabLayout.addOnTabSelectedListener(object :
                    TabLayout.OnTabSelectedListener {
                    override fun onTabSelected(tab: TabLayout.Tab?) {
                        tab?.let {
                            when (it.text) {
                                "用户协议" -> getProtocol(1)
                                "隐私条款" -> getProtocol(2)
                                else -> {}
                            }
                        }
                    }
                    override fun onTabUnselected(tab: TabLayout.Tab?) {}
                    override fun onTabReselected(tab: TabLayout.Tab?) {}
                })
            }
        }
    }
    @SuppressLint("SetJavaScriptEnabled")
    private fun initWeb(webView: WebView, confirmBtn: Button) {
        //声明WebSettings子类
        val webSettings = webView.settings
        webSettings.domStorageEnabled = true //设置适应Html5
        //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
        webSettings.javaScriptEnabled = true //允许js调用
        webSettings.allowFileAccess = true //在File域下,能够执行任意的JavaScript代码,同源策略跨域访问能够对私有目录文件进行访问等
        webSettings.allowContentAccess = true //是否允许在WebView中访问内容URL(Content Url),默认允许
        //缩放操作
        webSettings.setSupportZoom(true) //支持缩放,默认为true。是下面那个的前提。
        webSettings.builtInZoomControls = true //设置内置的缩放控件。若为false,则该WebView不可缩放
        webSettings.displayZoomControls = false //隐藏原生的缩放控件
        //其他细节操作
        webSettings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //关闭webview中缓存
        webSettings.allowFileAccess = true //设置可以访问文件
        webSettings.javaScriptCanOpenWindowsAutomatically = true //支持通过JS打开新窗口
        webSettings.loadsImagesAutomatically = true //支持自动加载图片
        webSettings.defaultTextEncodingName = "utf-8" //设置编码格式
        // 关键:添加滚动监听
        webView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
            // 只有向下滑动时才检测
            if (scrollY > oldScrollY) {
                checkScrollBottom(webView, confirmBtn)
            }
        }
    }
    @SuppressLint("SetJavaScriptEnabled")
    private fun setContent(webView: WebView, content: String?, confirmBtn: Button) {
        content?.let {
            webView.loadDataWithBaseURL(null, it, "text/html", "utf-8", null)
//            confirmBtn.text = if(currentTab == 1) "请阅读协议" else "请阅读条款"
            // 禁用按钮直到滚动到底
            confirmBtn.isEnabled = false
//            confirmBtn.alpha = 0.5f
            confirmBtn.setTextColor(getResColor(R.color.gray_7))
            // 页面加载完成后延迟检查(确保内容渲染完成)
            webView.postDelayed({
                checkScrollBottom(webView, confirmBtn)
            }, 500)
        }
    }
    /**
     * 检测是否滚动到底部
     */
    private fun checkScrollBottom(webView: WebView, confirmBtn: Button) {
        webView.evaluateJavascript(
            """
            (function() {
                var scrollHeight = Math.max(
                    document.body.scrollHeight,
                    document.documentElement.scrollHeight,
                    document.body.offsetHeight,
                    document.documentElement.offsetHeight,
                    document.body.clientHeight,
                    document.documentElement.clientHeight
                );
                var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
                var clientHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || 0;
                // 计算距离底部距离,允许20px误差
                var distanceToBottom = scrollHeight - scrollTop - clientHeight;
                return distanceToBottom <= 20;
            })();
            """.trimIndent()
        ) { result ->
            val isBottom = result?.trim() == "true"
            if (isBottom) {
                // 标记当前协议已读
                when (currentTab) {
                    1 -> userProtocolRead = true
                    2 -> privacyProtocolRead = true
                }
                updateButtonState(confirmBtn)
            }
        }
    }
    /**
     * 更新按钮状态:两个协议都必须阅读完才能点击
     */
    private fun updateButtonState(confirmBtn: Button) {
        val allRead = userProtocolRead && privacyProtocolRead
        if (allRead && !confirmBtn.isEnabled) {
            confirmBtn.isEnabled = true
//            confirmBtn.alpha = 1.0f
            confirmBtn.setTextColor(getResColor(R.color.blue_5))
            confirmBtn.text = "同意并继续"
            // 可选:启用动画
            confirmBtn.animate()
                .scaleX(1.05f)
                .scaleY(1.05f)
                .setDuration(150)
                .withEndAction {
                    confirmBtn.animate()
                        .scaleX(1.0f)
                        .scaleY(1.0f)
                        .setDuration(150)
                        .start()
                }
                .start()
        } else if (!allRead) {
            confirmBtn.isEnabled = false
//            confirmBtn.alpha = 0.5f
            confirmBtn.setTextColor(getResColor(R.color.gray_7))
            // 提示还需阅读哪个协议
//            val hint = if(currentTab == 1){
//                when {
//                    !userProtocolRead -> "请阅读协议"
//                    !privacyProtocolRead -> "请阅读条款"
//                    else -> "请阅读协议"
//                }
//            }else{
//                when {
//                    !privacyProtocolRead -> "请阅读条款"
//                    !userProtocolRead -> "请阅读协议"
//                    else -> "请阅读协议"
//                }
//            }
//            confirmBtn.text = hint
        }
    }
}
app/src/main/java/com/xw/yunwei/ui/user/login/LoginVM.kt
@@ -10,13 +10,10 @@
import com.xw.common.utils.MMKVManager
import com.xw.common.utils.ToastUtilCommon
import com.xw.yunwei.R
import com.xw.yunwei.model.AgreeInfo
import com.xw.yunwei.net.UserRepository
import com.xw.yunwei.net.WorkRepository
import com.xw.yunwei.ui.MainActivity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.component.inject
import kotlin.getValue
class LoginVM(application: Application) : BaseViewModel(application) {
    val userRepository: UserRepository by inject()
@@ -35,6 +32,8 @@
    val protocolStatus = MutableLiveData(false)
    //协议列表
    val agreeList: MutableLiveData<List<AgreeInfo>> = MutableLiveData(mutableListOf<AgreeInfo>())
    val onClick = View.OnClickListener {
        when (it.id) {
            R.id.protocolStatus -> protocolStatus.value = !protocolStatus.value!!
@@ -48,7 +47,7 @@
        }
    }
    private fun login() {
    public fun login() {
        if (protocolStatus.value != true) {
            ToastUtilCommon.showWarn("请先阅读并同意用户协议和隐私政策")
            return
@@ -69,7 +68,11 @@
            return
        }
        launchRequest(tips = "登录中...") {
            val map = mapOf("phoneNumber" to phone.value, "verificationCode" to code.value, "deviceType" to 1)
            val map = mapOf(
                "phoneNumber" to phone.value,
                "verificationCode" to code.value,
                "deviceType" to 1
            )
            val data = userRepository.loginByCode(map)
            data?.let {
                Constants.token = it.token?.access_token ?: ""
@@ -92,13 +95,15 @@
        launchRequestFlow(
            tips = "登录中...",
            flowBlock = {
                val map = mapOf("phoneNumber" to phone.value, "password" to psw.value, "deviceType" to 1)
                val map =
                    mapOf("phoneNumber" to phone.value, "password" to psw.value, "deviceType" to 1)
                userRepository.loginByPassword(map)
            },
            onCollect = { data ->
                data?.let {
                    Constants.token = it.token?.access_token ?: ""
                    MMKVManager.getInstance().saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                    MMKVManager.getInstance()
                        .saveModel(KeyPreferences.USER, it.info?.xpOperationsUser)
                    startActivity(MainActivity::class.java)
                    finish()
                }
@@ -174,10 +179,11 @@
        }
    }
    fun getProtocol(type: Int, block: (String) -> Unit) {
        launchRequest(showLoading = false) {
            val data = userRepository.getProtocol(type)
            runOnMainThread { block(data?.content ?: "") }
    /*获取协议配置 */
    fun getAgreeConfig() {
        launchRequest {
            val data = userRepository.getAgreeConfig()
            runOnMainThread { agreeList.postValue(data ?: mutableListOf()) }
        }
    }
app/src/main/java/com/xw/yunwei/ui/work/site/SiteErrorUploadActivity.kt
New file
@@ -0,0 +1,26 @@
package com.xw.yunwei.ui.work.site
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.xw.yunwei.R
class SiteErrorUploadActivity : AppCompatActivity() {
    companion object{
        fun getParams(params:List<Pair<String,Any>>): Bundle{
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_site_error_upload)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
    }
}
app/src/main/java/com/xw/yunwei/ui/work/site/SiteNavActivity.kt
@@ -155,6 +155,11 @@
                adapter.selectItem = it
            }
        }
        binding.btnErrorUpload.setOnClickListener {
            viewModel.siteItem.value?.let {
            }
        }
    }
    private fun setBehavior() {
app/src/main/res/layout/activity_login.xml
@@ -154,7 +154,6 @@
            android:layout_marginHorizontal="@dimen/dp_32"
            android:layout_marginTop="@dimen/dp_24"
            android:backgroundTint="@color/blue_5"
            android:onClick="@{viewModel.onClick}"
            android:text="登录"
            android:textSize="@dimen/sp_16"
            android:textStyle="bold"
app/src/main/res/layout/activity_site_error_upload.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.site.SiteErrorUploadActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
app/src/main/res/layout/activity_site_nav.xml
@@ -21,24 +21,24 @@
            android:layout_height="match_parent"
            tools:background="@mipmap/img_test_map" />
<!--        <androidx.appcompat.widget.AppCompatTextView-->
<!--            showAnim='@{!"".equals(viewModel.selectSiteName)}'-->
<!--            android:layout_width="wrap_content"-->
<!--            android:layout_height="wrap_content"-->
<!--            android:layout_marginBottom="@dimen/dp_80"-->
<!--            android:background="@drawable/shape_while_bg_100"-->
<!--            android:elevation="@dimen/dp_2"-->
<!--            android:paddingHorizontal="@dimen/dp_20"-->
<!--            android:paddingVertical="@dimen/dp_6"-->
<!--            android:text="@{viewModel.selectSiteName}"-->
<!--            android:textColor="@color/gray_9"-->
<!--            android:textSize="@dimen/sp_12"-->
<!--            android:textStyle="bold"-->
<!--            app:layout_constraintBottom_toBottomOf="parent"-->
<!--            app:layout_constraintLeft_toLeftOf="parent"-->
<!--            app:layout_constraintRight_toRightOf="parent"-->
<!--            app:layout_constraintTop_toTopOf="parent"-->
<!--            tools:text="点位名称" />-->
        <!--        <androidx.appcompat.widget.AppCompatTextView-->
        <!--            showAnim='@{!"".equals(viewModel.selectSiteName)}'-->
        <!--            android:layout_width="wrap_content"-->
        <!--            android:layout_height="wrap_content"-->
        <!--            android:layout_marginBottom="@dimen/dp_80"-->
        <!--            android:background="@drawable/shape_while_bg_100"-->
        <!--            android:elevation="@dimen/dp_2"-->
        <!--            android:paddingHorizontal="@dimen/dp_20"-->
        <!--            android:paddingVertical="@dimen/dp_6"-->
        <!--            android:text="@{viewModel.selectSiteName}"-->
        <!--            android:textColor="@color/gray_9"-->
        <!--            android:textSize="@dimen/sp_12"-->
        <!--            android:textStyle="bold"-->
        <!--            app:layout_constraintBottom_toBottomOf="parent"-->
        <!--            app:layout_constraintLeft_toLeftOf="parent"-->
        <!--            app:layout_constraintRight_toRightOf="parent"-->
        <!--            app:layout_constraintTop_toTopOf="parent"-->
        <!--            tools:text="点位名称" />-->
        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/topLayout"
@@ -132,9 +132,9 @@
                app:behavior_fitToContents="false"
                app:behavior_halfExpandedRatio="0.58"
                app:behavior_peekHeight="@dimen/dp_92"
                tools:behavior_peekHeight="@dimen/dp_500"
                app:behavior_skipCollapsed="false"
                app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
                app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
                tools:behavior_peekHeight="@dimen/dp_500">
                <androidx.appcompat.widget.AppCompatImageView
                    android:id="@+id/location"
@@ -207,19 +207,19 @@
                            <androidx.recyclerview.widget.RecyclerView
                                android:id="@+id/siteRv"
                                showAnim="@{viewModel.siteItem==null}"
                                tools:visibility="gone"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:nestedScrollingEnabled="false" />
                                android:nestedScrollingEnabled="false"
                                tools:visibility="gone" />
                            <LinearLayout
                                android:id="@+id/siteDetail"
                                showAnim="@{viewModel.siteItem!=null}"
                                android:layout_width="match_parent"
                                tools:visibility="visible"
                                android:layout_height="wrap_content"
                                android:orientation="vertical"
                                android:visibility="gone">
                                android:visibility="gone"
                                tools:visibility="visible">
                                <androidx.constraintlayout.widget.ConstraintLayout
                                    android:layout_width="match_parent"
@@ -292,11 +292,11 @@
                                        android:id="@+id/arrow1"
                                        android:layout_width="wrap_content"
                                        android:layout_height="wrap_content"
                                        android:layout_marginEnd="@dimen/dp_20"
                                        app:layout_constraintTop_toBottomOf="@+id/address"
                                        android:src="@drawable/svg_arrow_r"
                                        android:layout_marginTop="@dimen/dp_8"
                                        app:layout_constraintRight_toRightOf="parent" />
                                        android:layout_marginEnd="@dimen/dp_20"
                                        android:src="@drawable/svg_arrow_r"
                                        app:layout_constraintRight_toRightOf="parent"
                                        app:layout_constraintTop_toBottomOf="@+id/address" />
                                    <LinearLayout
                                        android:id="@+id/nav"
@@ -363,6 +363,18 @@
                                    android:textColor="@color/gray_7"
                                    android:textSize="@dimen/sp_14" />
                                <com.google.android.material.button.MaterialButton
                                    android:id="@+id/btn_error_upload"
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:layout_marginHorizontal="@dimen/dp_20"
                                    android:layout_marginTop="@dimen/dp_50"
                                    android:backgroundTint="@color/blue_5"
                                    android:text="现场情况上报"
                                    android:textSize="@dimen/sp_16"
                                    android:textStyle="bold"
                                    app:cornerRadius="@dimen/dp_4" />
                            </LinearLayout>
                        </RelativeLayout>
common/src/main/java/com/xw/common/CommonDialog.kt
@@ -1,13 +1,15 @@
package com.xw.common
import android.content.Context
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.text.method.LinkMovementMethod
import android.util.TypedValue
import android.view.Gravity
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.databinding.DataBindingUtil
import com.afollestad.materialdialogs.LayoutMode
@@ -91,4 +93,88 @@
            }
    }
    /**
     * 通用提示弹窗
     * @param cancelable 是否能够点击物理返回按钮取消
     * @param cancelOnTouchOutside 是否能够点击空白区域取消
     * @param title 标题
     * @param titleTextSize 标题字号大小 单位SP
     * @param titleTextColor 标题字体颜色资源id
     * @param content 内容
     * @param contentTextSize 内容字号大小 单位SP
     * @param contentTextColor 内容字体颜色资源id
     * @param leftOperationText 左边操作按钮文案
     * @param leftOperationTextSize 左边操作按钮文案字体大小
     * @param leftOperationTextColor 左边操作按钮文案字体颜色
     * @param rightOperationText 右边操作按钮文案
     * @param rightOperationTextSize 右边操作按钮文案字体大小
     * @param rightOperationTextColor 右边操作按钮文案字体颜色
     * @param leftOperationBlock 回调事件  返回true dismiss
     * @param rightOperationBlock 回调事件  返回true dismiss
     */
    fun showNoticeDialog(
        context: Context,
        cancelable: Boolean = true,
        cancelOnTouchOutside: Boolean = true,
        title: CharSequence,
        titleTextSize: Float = 18f,
        titleTextColor: Int = R.color.gray_10,
        content: CharSequence,
        contentTextSize: Float = 14f,
        contentTextColor: Int = R.color.gray_9,
        leftOperationText: String? = "取消",
        leftOperationTextSize: Float = 16f,
        leftOperationTextColor: Int = R.color.gray_10,
        rightOperationText: String? = "确定",
        rightOperationTextSize: Float = 16f,
        rightOperationTextColor: Int = R.color.blue_5,
        leftOperationBlock: () -> Boolean = { true },
        rightOperationBlock: () -> Boolean = { true }
    ) {
        Handler(Looper.getMainLooper()).post {
            val dialog = MaterialDialog(context).apply {
                customView(
                    R.layout.dialog_notice, noVerticalPadding = true, dialogWrapContent = false
                )
                cancelable(cancelable)
                cancelOnTouchOutside(cancelOnTouchOutside)
                cornerRadius(5f)
                val tvTitle = findViewById<AppCompatTextView>(R.id.title)
                val tvContent = findViewById<AppCompatTextView>(R.id.content)
                val btnLeft = findViewById<Button>(R.id.btnLeft)
                val btnRight = findViewById<Button>(R.id.btnRight)
                tvTitle.text = title
                tvTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, titleTextSize)
                tvTitle.setTextColor(ContextCompat.getColor(context, titleTextColor))
                tvContent.text = content
                tvContent.highlightColor = Color.TRANSPARENT
                tvContent.movementMethod = LinkMovementMethod.getInstance()
                tvContent.setTextSize(TypedValue.COMPLEX_UNIT_SP, contentTextSize)
                tvContent.setTextColor(ContextCompat.getColor(context, contentTextColor))
                btnLeft.text = leftOperationText ?: ""
                btnLeft.setTextSize(TypedValue.COMPLEX_UNIT_SP, leftOperationTextSize)
                btnLeft.setTextColor(ContextCompat.getColor(context, leftOperationTextColor))
                btnRight.text = rightOperationText ?: ""
                btnRight.setTextSize(TypedValue.COMPLEX_UNIT_SP, rightOperationTextSize)
                btnRight.setTextColor(ContextCompat.getColor(context, rightOperationTextColor))
                btnLeft.isVisible = !leftOperationText.isNullOrEmpty()
                btnRight.isVisible = !rightOperationText.isNullOrEmpty()
                btnLeft.setOnClickListener {
                    if (leftOperationBlock()) {
                        dismiss()
                    }
                }
                btnRight.setOnClickListener {
                    if (rightOperationBlock()) {
                        dismiss()
                    }
                }
            }
            dialog.show()
            dialog.window?.setGravity(Gravity.CENTER)
            dialog.window?.setWindowAnimations(R.style.AnimLeft)
        }
    }
}
common/src/main/res/drawable/shape_while_bg_5.xml
New file
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/white" />
    <corners android:radius="@dimen/dp_5" />
</shape>
common/src/main/res/layout/dialog_notice.xml
New file
@@ -0,0 +1,77 @@
<?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:background="@drawable/shape_while_bg_5">
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dp_32"
        android:layout_marginHorizontal="@dimen/dp_20"
        android:textColor="@color/gray_10"
        android:textSize="@dimen/sp_18"
        android:gravity="center"
        android:textStyle="bold"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="文本内容" />
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dp_16"
        android:layout_marginHorizontal="@dimen/dp_20"
        android:textColor="@color/gray_10"
        android:textSize="@dimen/sp_14"
        android:gravity="center"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/title"
        tools:text="文本内容" />
    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:layout_marginTop="@dimen/dp_30"
        android:background="@color/gray_4"
        app:layout_constraintTop_toBottomOf="@+id/content" />
        <Button
            android:id="@+id/btnLeft"
            android:layout_width="0dp"
            android:layout_height="@dimen/dp_60"
            android:layout_marginTop="@dimen/dp_30"
            android:background="@android:color/transparent"
            android:text="取消"
            android:textColor="@color/gray_10"
            android:textSize="@dimen/sp_16"
            android:textStyle="bold"
            app:layout_constraintHorizontal_weight="1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/content"
            app:layout_constraintRight_toLeftOf="@+id/btnRight" />
        <Button
            android:id="@+id/btnRight"
            android:layout_width="0dp"
            android:layout_height="@dimen/dp_60"
            android:background="@android:color/transparent"
            android:layout_marginTop="@dimen/dp_30"
            android:text="确定"
            android:textColor="@color/blue_5"
            android:textSize="@dimen/sp_16"
            android:textStyle="bold"
            app:layout_constraintHorizontal_weight="1"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/btnLeft"
            app:layout_constraintTop_toBottomOf="@id/content" />
</androidx.constraintlayout.widget.ConstraintLayout>