From 19dc61e9af0e0adcd3dcae0f178e8b3d3e4a8ae3 Mon Sep 17 00:00:00 2001 From: shenzuqiang Date: Thu, 26 Feb 2026 13:51:42 +0800 Subject: [PATCH] =?UTF-8?q?Dev=EF=BC=9A=201=E3=80=81=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=8E=A5=E5=8F=A3=202=E3=80=81=E7=99=BB?= =?UTF-8?q?=E5=BD=95UI=E4=BC=98=E5=8C=96=E4=BF=AE=E6=94=B9=203=E3=80=81?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/deploymentTargetSelector.xml | 8 + app/build.gradle.kts | 36 +- app/libs/channelsdk-0.2.2.aar | Bin 0 -> 8265 bytes app/libs/humesdk-1.0.0.aar | Bin 0 -> 7527 bytes app/src/main/AndroidManifest.xml | 36 +- .../java/com/img/rabbit/BaseApplication.kt | 19 + .../main/java/com/img/rabbit/MainActivity.kt | 309 +++++++- .../java/com/img/rabbit/WebViewActivity.kt | 38 + .../main/java/com/img/rabbit/bean/UserInfo.kt | 7 - .../com/img/rabbit/bean/local/AlipayBean.kt | 14 + .../rabbit/bean/{ => local}/ClothingBean.kt | 2 +- .../com/img/rabbit/bean/local/ErrorBean.kt | 3 + .../img/rabbit/bean/{ => local}/FormatBean.kt | 2 +- .../rabbit/bean/{ => local}/HairstyleBean.kt | 2 +- .../rabbit/bean/{ => local}/LongImageBean.kt | 2 +- .../rabbit/bean/{ => local}/OnekeyPreLogin.kt | 2 +- .../img/rabbit/bean/{ => local}/ResizeBean.kt | 2 +- .../com/img/rabbit/bean/local/UserInfo.kt | 9 + .../java/com/img/rabbit/bean/local/WxBean.kt | 6 + .../rabbit/bean/response/AlipayParamEntity.kt | 8 + .../rabbit/bean/response/CaptchaCodeEntity.kt | 8 + .../img/rabbit/bean/response/ConfigEntity.kt | 47 ++ .../rabbit/bean/response/PopupConfigEntity.kt | 28 + .../bean/response/PopupTimeConfigEntity.kt | 17 + .../rabbit/bean/response/UserConfigEntity.kt | 14 + .../img/rabbit/bean/response/UserEntity.kt | 14 + .../img/rabbit/bean/response/VersionEntity.kt | 15 + .../img/rabbit/bean/response/WxShareEntity.kt | 11 + .../com/img/rabbit/components/DrawingBoard.kt | 8 +- .../main/java/com/img/rabbit/config/Common.kt | 6 - .../java/com/img/rabbit/config/CommonData.kt | 8 +- .../java/com/img/rabbit/config/Constants.kt | 19 + .../java/com/img/rabbit/pages/LoginPage.kt | 729 +++++++++++++----- .../java/com/img/rabbit/pages/MainPage.kt | 3 +- .../com/img/rabbit/pages/screen/MineScreen.kt | 114 ++- .../rabbit/pages/screen/make/CutoutScreen.kt | 4 +- .../pages/screen/make/LongImageScreen.kt | 31 +- .../pages/screen/mine/setting/AboutScreen.kt | 9 +- .../mine/setting/AccountManagerScreen.kt | 9 +- .../com/img/rabbit/pages/toolbar/TitleBar.kt | 26 +- .../com/img/rabbit/provider/api/ApiManager.kt | 110 +++ .../com/img/rabbit/provider/api/ResultVo.kt | 9 + .../provider/storage/GlobalStateManager.kt | 49 ++ .../rabbit/provider/storage/PreferenceUtil.kt | 93 +++ .../provider/utils/CustomX509TrustManager.kt | 53 ++ .../provider/utils/HeaderInterceptor.kt | 46 ++ .../provider/utils/RequestInterceptor.kt | 177 +++++ .../provider/utils/ResponseInterceptor.kt | 86 +++ .../img/rabbit/utils/AESpkcs7paddingUtil.kt | 30 + .../java/com/img/rabbit/utils/Bitmap2SVG.java | 6 +- .../java/com/img/rabbit/utils/ChannelUtils.kt | 56 ++ .../java/com/img/rabbit/utils/ImageUtils.kt | 3 +- .../java/com/img/rabbit/utils/MMKVUtils.kt | 99 +++ .../java/com/img/rabbit/utils/StringUtils.kt | 494 ++++++++++++ .../java/com/img/rabbit/utils/UrlLinkUtils.kt | 19 +- .../com/img/rabbit/viewmodel/BaseViewModel.kt | 26 + .../img/rabbit/viewmodel/GeneralViewModel.kt | 28 + .../img/rabbit/viewmodel/LoginViewModel.kt | 272 ++++++- .../rabbit/viewmodel/interface/ServiceVo.kt | 63 ++ .../com/img/rabbit/wxapi/WXEntryActivity.kt | 34 + app/src/main/res/drawable/ic_alipay_icon.xml | 9 + app/src/main/res/drawable/ic_wx_icon.xml | 12 + app/src/main/res/layout/layout_web.xml | 35 + app/src/main/res/mipmap-xxhdpi/ic_copy.webp | Bin 0 -> 1380 bytes gradle/libs.versions.toml | 42 + settings.gradle.kts | 16 +- 66 files changed, 3136 insertions(+), 356 deletions(-) create mode 100644 app/libs/channelsdk-0.2.2.aar create mode 100644 app/libs/humesdk-1.0.0.aar create mode 100644 app/src/main/java/com/img/rabbit/WebViewActivity.kt delete mode 100644 app/src/main/java/com/img/rabbit/bean/UserInfo.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt rename app/src/main/java/com/img/rabbit/bean/{ => local}/ClothingBean.kt (86%) create mode 100644 app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt rename app/src/main/java/com/img/rabbit/bean/{ => local}/FormatBean.kt (70%) rename app/src/main/java/com/img/rabbit/bean/{ => local}/HairstyleBean.kt (86%) rename app/src/main/java/com/img/rabbit/bean/{ => local}/LongImageBean.kt (89%) rename app/src/main/java/com/img/rabbit/bean/{ => local}/OnekeyPreLogin.kt (94%) rename app/src/main/java/com/img/rabbit/bean/{ => local}/ResizeBean.kt (81%) create mode 100644 app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/local/WxBean.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt create mode 100644 app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt delete mode 100644 app/src/main/java/com/img/rabbit/config/Common.kt create mode 100644 app/src/main/java/com/img/rabbit/config/Constants.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt create mode 100644 app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt create mode 100644 app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt create mode 100644 app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt create mode 100644 app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt create mode 100644 app/src/main/java/com/img/rabbit/utils/StringUtils.kt create mode 100644 app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt create mode 100644 app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt create mode 100644 app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt create mode 100644 app/src/main/res/drawable/ic_alipay_icon.xml create mode 100644 app/src/main/res/drawable/ic_wx_icon.xml create mode 100644 app/src/main/res/layout/layout_web.xml create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_copy.webp diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..3d207df 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 946741c..07a0971 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { manifestPlaceholders.putAll(mapOf("UMENG_CHANNEL" to name)) } manifestPlaceholders.putAll(mapOf( - "GETUI_APPID" to "40qbPjPkYs7TnVAYCX0Ig6", + "GETUI_APPID" to (project.findProperty("GETUI_APPID") as? String ?: ""), "GT_INSTALL_CHANNEL" to "general", )) } @@ -145,11 +145,33 @@ dependencies { implementation(libs.face.detection) implementation(libs.android.gif.drawable) implementation(libs.gif.encoder) - implementation("com.caverock:androidsvg:1.4") - implementation("io.github.lucksiege:pictureselector:v3.11.2") - // 压缩库 (可选,建议长图拼接前先压缩防止OOM) - implementation("io.github.lucksiege:compress:v3.11.2") - implementation("io.github.leavesczy:matisse:2.3.0") - implementation("com.github.moyuruaizawa:cropify:0.5.2") + //implementation("com.caverock:androidsvg:1.4") + implementation(libs.pictureselector) + implementation(libs.compress) + implementation(libs.matisse) + implementation(libs.cropify) + //noinspection GradleDynamicVersion + api("com.alipay.sdk:alipaysdk-android:+@aar") + implementation(libs.wechat.sdk) //微信 + //Retrofit 依赖 + implementation(libs.retrofit) + implementation(libs.retrofit.kotlin.serialization) + implementation (libs.retrofit.converter.gson) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + + //友盟 + implementation (libs.umeng.umsdk.common)// 必选 + implementation (libs.umeng.umsdk.asms)// 必选 + implementation (libs.umeng.umsdk.apm) // U-APM包依赖(必选) + implementation (libs.umeng.umsdk.share.core)//分享核心库,必选 + implementation (libs.umeng.umsdk.share.wx) //微信完整版 + //分包 + implementation (libs.tencent.helper) //腾讯分包 + implementation (files("libs/channelsdk-0.2.2.aar")) //快手分包 + implementation (files("libs/humesdk-1.0.0.aar")) //巨量分包 + + implementation(libs.android.cn.oaid) //获取手机设备id + implementation(libs.fastaes) //解密 } \ No newline at end of file diff --git a/app/libs/channelsdk-0.2.2.aar b/app/libs/channelsdk-0.2.2.aar new file mode 100644 index 0000000000000000000000000000000000000000..93ce2470bdf2a57591d90a22bd806d6656a3f6e4 GIT binary patch literal 8265 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 literal 0 HcmV?d00001 diff --git a/app/libs/humesdk-1.0.0.aar b/app/libs/humesdk-1.0.0.aar new file mode 100644 index 0000000000000000000000000000000000000000..2db40915dcf76bffe307b34f148f8aabfac24262 GIT binary patch literal 7527 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 C*F?Dh literal 0 HcmV?d00001 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1611682..3dd512f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,22 @@ + + + + + + + + + + + + + + + + + android:exported="true" + android:launchMode="singleTask"> @@ -56,6 +73,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/BaseApplication.kt b/app/src/main/java/com/img/rabbit/BaseApplication.kt index af420a6..4b03f44 100644 --- a/app/src/main/java/com/img/rabbit/BaseApplication.kt +++ b/app/src/main/java/com/img/rabbit/BaseApplication.kt @@ -4,7 +4,11 @@ import android.app.Application import android.util.Log import com.img.rabbit.utils.NetworkMonitor import com.g.gysdk.GYManager +import com.img.rabbit.config.Constants import com.tencent.mmkv.MMKV +import com.umeng.analytics.MobclickAgent +import com.umeng.commonsdk.UMConfigure +import com.umeng.socialize.PlatformConfig class BaseApplication : Application() { @@ -18,6 +22,21 @@ class BaseApplication : Application() { initMMKV() // 初始化个推SDK initGeTuiOneKeyLogin() + // 初始化友盟 + initUM() + } + + /** + * 初始化友盟 + */ + private fun initUM() { + UMConfigure.setLogEnabled(true) + + PlatformConfig.setFileProvider("${BuildConfig.APPLICATION_ID}.fileprovider") + PlatformConfig.setWeixin(Constants.WechatAppId, Constants.WechatAppSecret) + MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO) + + UMConfigure.setProcessEvent(true) } private fun initGeTuiOneKeyLogin() { diff --git a/app/src/main/java/com/img/rabbit/MainActivity.kt b/app/src/main/java/com/img/rabbit/MainActivity.kt index 7d3086e..1423157 100644 --- a/app/src/main/java/com/img/rabbit/MainActivity.kt +++ b/app/src/main/java/com/img/rabbit/MainActivity.kt @@ -1,5 +1,6 @@ package com.img.rabbit +import android.app.Activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -7,11 +8,21 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -21,23 +32,43 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.viewmodel.compose.viewModel +import com.img.rabbit.config.Constants +import com.img.rabbit.config.Constants.agreementUrl +import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.pages.LoginScreen +import com.img.rabbit.pages.LoginScreenType import com.img.rabbit.pages.MainScreen +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.ChannelUtils +import com.img.rabbit.utils.UrlLinkUtils.openAgreement import com.img.rabbit.viewmodel.GeneralViewModel import com.img.rabbit.viewmodel.LoginViewModel import com.img.rabbit.viewmodel.SplashViewModel +import com.umeng.analytics.MobclickAgent +import com.umeng.commonsdk.UMConfigure import kotlinx.coroutines.delay +import kotlin.system.exitProcess class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { // 必须在 super.onCreate 之前调用 val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) + initUM() // 启用Edge-to-Edge模式(沉浸模式) enableEdgeToEdge() @@ -46,6 +77,15 @@ class MainActivity : ComponentActivity() { val splashViewModel: SplashViewModel = viewModel() val generalViewModel: GeneralViewModel = viewModel() val loginViewModel: LoginViewModel = viewModel() + val context = LocalContext.current + var showSplash by remember { mutableStateOf(false) } + + //获取服务器时间 + generalViewModel.getServerTime() + // 获取用户配置 + loginViewModel.requestUserConfig() + //初始化微信登录 + loginViewModel.initWXApi(this) // 设置启动页显示条件 splashScreen.setKeepOnScreenCondition { @@ -53,26 +93,69 @@ class MainActivity : ComponentActivity() { } AppTheme { - SplashScreenContent { - val token = generalViewModel.kv.decodeString("token") - // 未登录,显示登录页 - if (token?.isNotEmpty() == false && !loginViewModel.isLogin.value) { - // 显示登录页 - LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) - } else { - //已登录,显示主页面 - MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) + SplashScreenContent{ + //未同意提示政策弹窗 + if (generalViewModel.agreementStatus.value == false){ + //同意继续 + PrivacyPolicyScreen( + viewModel = loginViewModel, + ) { isAllowPrivacyPolicy -> + if (isAllowPrivacyPolicy) { + generalViewModel.setIsAgreement(true) + showSplash = true + } else { + // 不同意隐私协议政策,直接退出应用 + (context as MainActivity).finish() + // 强制退出应用进程 + exitProcess(0) + } + } + }else{ + showSplash = true + } + + if(showSplash){ + val token = PreferenceUtil.getAccessToken() + // 未登录,显示登录页 + if (token.isNullOrEmpty() && !loginViewModel.isLogin.value) { + // 同意隐私协议政策,检验是否有一键登录权限 + loginViewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> + if (isAllowShowOneKeyScreen) { + loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY + } else { + // 检验是否有一键登录权限失败,显示验证码登录 + loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA + } + } + + // 显示登录页 + LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel, isVisibilityBreak = false) + } else { + //已登录,显示主页面 + MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel) + } } } } // 模拟加载过程,2秒后关闭启动页 LaunchedEffect(Unit) { - delay(100L) + delay(500L) splashViewModel.setLoading(false) } } } + + + + /** + * 初始化友盟 + */ + private fun initUM() { + UMConfigure.preInit(applicationContext, Constants.UmengAppkey, ChannelUtils.getChannel(applicationContext)) + UMConfigure.init(this, Constants.UmengAppkey, ChannelUtils.getChannel(applicationContext), UMConfigure.DEVICE_TYPE_PHONE, "") + MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO) + } } @@ -128,6 +211,212 @@ fun SplashScreenContent( } +@Composable +private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (Boolean) -> Unit) { + val context = LocalContext.current + Box( + modifier = Modifier.fillMaxSize() + ){ + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xCC000000)) + ){ + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 38.dp, vertical = 213.dp) + .align(Alignment.Center) + .background(Color.White, shape = RoundedCornerShape(26.dp)) + ){ + Image( + painter = painterResource(id = R.mipmap.ic_privacy_policy_top_mask), + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + alignment = Alignment.TopCenter + ) + + Column( + modifier = Modifier + .padding(top = 36.dp) + .align(Alignment.TopCenter) + ) { + Text( + text = "用户协议与隐私政策", + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterHorizontally), + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = Color(0xFF1A1A1A) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(24.dp) + ) + + val agreement = "请您务必审慎阅读、充分理解《服务协议》与《隐私政策》各条款,包括但不限于:为了更好的向您提供服务,我们需要访问您的相册、相机等。您可以阅读《隐私政策》了解详细信息。如果您同意,请点击下面同意按钮开始接受我们的服务。" + val annotatedText = buildAnnotatedString { + append(agreement) + val startIndexForService = agreement.indexOf("《服务协议》") + val startIndexPrivacy1 = agreement.indexOf("《隐私政策》") + val startIndexPrivacy2 = agreement.lastIndexOf("《隐私政策》") + + val serviceLength = "《服务协议》".length + val privacyLength = "《服务协议》".length + + // 高亮显示 "《服务协议》" + addStyle( + style = SpanStyle( + color = Color(0xFF0066CC), + textDecoration = TextDecoration.None + ), + start = startIndexForService, // "《服务协议》" 开始下标13 + end = startIndexForService + serviceLength // "《服务协议》" 结束下标19 + ) + addStringAnnotation( + tag = "AGREEMENT", + annotation = "service_agreement", + start = startIndexForService, + end = startIndexForService + serviceLength + ) + + // 高亮显示 "《隐私政策》" + addStyle( + style = SpanStyle( + color = Color(0xFF0066CC), + textDecoration = TextDecoration.None + ), + start = startIndexPrivacy1, // "《隐私政策》" 开始下标20 + end = startIndexPrivacy1 + privacyLength // "《隐私政策》" 结束下标26 + ) + addStringAnnotation( + tag = "PRIVACY", + annotation = "privacy_policy", + start = startIndexPrivacy1, + end = startIndexPrivacy1 + privacyLength + ) + + // 高亮显示 "《隐私政策》" + addStyle( + style = SpanStyle( + color = Color(0xFF0066CC), + textDecoration = TextDecoration.None + ), + start = startIndexPrivacy2, // "《隐私政策》" 开始下标 + end = startIndexPrivacy2 + privacyLength // "《隐私政策》" 结束下标 + ) + addStringAnnotation( + tag = "PRIVACY", + annotation = "privacy_policy", + start = startIndexPrivacy2, + end = startIndexPrivacy2 + privacyLength + ) + } + + ClickableText( + text = annotatedText, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + .padding(horizontal = 12.dp), + style = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = Color(0xFF1A1A1A) + ), + onClick = { offset -> + val agreementAnnotation = annotatedText.getStringAnnotations("AGREEMENT", offset, offset).firstOrNull() + val privacyAnnotation = annotatedText.getStringAnnotations("PRIVACY", offset, offset).firstOrNull() + + when { + agreementAnnotation != null -> { + openAgreement(context, "服务协议", agreementUrl, false) + } + privacyAnnotation != null -> { + openAgreement(context, "隐私政策", privacyUrl, false) + } + } + } + ) + + } + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .align(Alignment.BottomCenter) + ) { + //同意按钮,用户协议与隐私政策 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onAgreementChange(true) + viewModel.setIsPolicyAgreement(true) + } + ) { + Text( + "同意", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + //不同按钮,意用户协议与隐私政策 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 33.dp, end = 33.dp) + .background( + Color(0x00000000), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onAgreementChange(false) + viewModel.setIsPolicyAgreement(false) + } + ) { + Text( + "不同意", + color = Color(0xFFAAAAAA), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(vertical = 12.dp) + .align(Alignment.Center) + ) + } + } + } + } + } +} + @Preview(showBackground = true) @Composable diff --git a/app/src/main/java/com/img/rabbit/WebViewActivity.kt b/app/src/main/java/com/img/rabbit/WebViewActivity.kt new file mode 100644 index 0000000..c1c0a00 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/WebViewActivity.kt @@ -0,0 +1,38 @@ +package com.img.rabbit + +import android.annotation.SuppressLint +import android.os.Bundle +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.ImageView +import android.widget.TextView +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity + +class WebViewActivity : AppCompatActivity() { + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + + // 启用Edge-to-Edge模式(沉浸模式) + enableEdgeToEdge() + + setContentView(R.layout.layout_web) + + val webView: WebView = findViewById(R.id.webView) + val ivWebBreak: ImageView = findViewById(R.id.iv_web_break) + val ivWebTitle: TextView = findViewById(R.id.iv_web_title) + + val title = intent.getStringExtra("title") ?: "" + val url = intent.getStringExtra("url") ?: "" + + ivWebBreak.setOnClickListener { finish() } + ivWebTitle.text = title + + webView.settings.javaScriptEnabled = true + webView.webViewClient = WebViewClient() + webView.loadUrl(url) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/UserInfo.kt b/app/src/main/java/com/img/rabbit/bean/UserInfo.kt deleted file mode 100644 index fe4922f..0000000 --- a/app/src/main/java/com/img/rabbit/bean/UserInfo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.img.rabbit.bean - -data class UserInfo( - val id: Int, - val name: String, - val login: Boolean, -) diff --git a/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt b/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt new file mode 100644 index 0000000..28a12f7 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt @@ -0,0 +1,14 @@ +package com.img.rabbit.bean.local + +data class AlipayBean( + val resultStatus: String?, // 状态码:9000-成功,6001-取消,4000-失败 + val result: String?, // 包含 auth_code 等关键信息 + val memo: String? // 提示语 +) +fun Map.toAlipayResult(): AlipayBean { + return AlipayBean( + resultStatus = this["resultStatus"], + result = this["result"], + memo = this["memo"] + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt similarity index 86% rename from app/src/main/java/com/img/rabbit/bean/ClothingBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt index bd26ebd..aec2e3d 100644 --- a/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class ClothingBean( //衣服索引(区分男女) diff --git a/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt new file mode 100644 index 0000000..d935ab3 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt @@ -0,0 +1,3 @@ +package com.img.rabbit.bean.local + +data class ErrorBean(var code: String,var message: String) diff --git a/app/src/main/java/com/img/rabbit/bean/FormatBean.kt b/app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt similarity index 70% rename from app/src/main/java/com/img/rabbit/bean/FormatBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt index 8d5c80c..35a3281 100644 --- a/app/src/main/java/com/img/rabbit/bean/FormatBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class FormatBean( //格式id diff --git a/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt b/app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt similarity index 86% rename from app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt index 3079eeb..b74a162 100644 --- a/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class HairstyleBean( //发型索引(区分男女) diff --git a/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt b/app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt similarity index 89% rename from app/src/main/java/com/img/rabbit/bean/LongImageBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt index d83ab6a..5e6ae9d 100644 --- a/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local import android.graphics.Bitmap import android.net.Uri diff --git a/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt b/app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt similarity index 94% rename from app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt rename to app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt index 4708b1a..fcf1323 100644 --- a/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt similarity index 81% rename from app/src/main/java/com/img/rabbit/bean/ResizeBean.kt rename to app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt index e8259ce..e0d8a5b 100644 --- a/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt +++ b/app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt @@ -1,4 +1,4 @@ -package com.img.rabbit.bean +package com.img.rabbit.bean.local data class ResizeBean( //尺寸id diff --git a/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt b/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt new file mode 100644 index 0000000..b74e058 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt @@ -0,0 +1,9 @@ +package com.img.rabbit.bean.local + +data class UserInfo( + val user_id: Int, + val name: String, + val avater: String, + val token: String, + val login: Boolean, +) diff --git a/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt b/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt new file mode 100644 index 0000000..efad729 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt @@ -0,0 +1,6 @@ +package com.img.rabbit.bean.local + +data class WxBean( + val code: String, + val state: String +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt new file mode 100644 index 0000000..57b712d --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt @@ -0,0 +1,8 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class AlipayParamEntity( + val param: String = "" +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt new file mode 100644 index 0000000..d832972 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt @@ -0,0 +1,8 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class CaptchaCodeEntity( + val timestamp: String = "" +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt new file mode 100644 index 0000000..2a0587a --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt @@ -0,0 +1,47 @@ +package com.img.rabbit.bean.response + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +class ConfigEntity { + @SerializedName("client.popup.display") //显示开关控制 + var popupConfig: PopupConfigEntity? = null + + @SerializedName("client.time.setting") //弹窗时间控制 + var popupTimeConfig: PopupTimeConfigEntity? = null + + @SerializedName("client.weixin.open.appid") //微信appid + var wxAppId: String = "" + + @SerializedName("client.version.upgrade") //版本更新 + var versionEntity: VersionEntity? = null + + @SerializedName("client.weixin.share") //微信分享 + var wxShareEntity: WxShareEntity? = null + + @SerializedName("client.guide.enable") + var guideEnable: Boolean? = true //是否开启引导页 + + @SerializedName("client.pay.agreement") //是否显示支付协议 + var payAgreementEnable: Boolean? = true + + @SerializedName("client.login.type") //登录方式 + var loginType: List? = emptyList() + + @SerializedName("client.ad.switch") //广告总开关 + var adSwitch: Boolean = false + + @SerializedName("client.service.phone") //客服电话 + var servicePhoneList: List = emptyList() + + @SerializedName("client.chatwarning") //聊天安全提示 + var chatWarning: String? = null + + @SerializedName("client.travel.ad") //聊天安全提示 + val travelAd: List = emptyList() + + // 圈子-顶部banner占位图配置 + @SerializedName("client.team.ad") //聊天安全提示 + val teamAd: List = emptyList() +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt new file mode 100644 index 0000000..ac66fdc --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt @@ -0,0 +1,28 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + + +@Serializable +data class PopupConfigEntity( + val android_alipay: Boolean = true, + val android_wxpay: Boolean = true, + val guest_payment: Boolean = false, + val honor_tip_double_install: Boolean = false, + val huawei_map: Boolean = false, + val nonvip_search_results: Boolean = true, + val search_phone: Boolean = true, + val sos_entry: Boolean = true, + val timelimit_discount: Boolean = true, + val vip_back_popup: Boolean = true, + val vivo_login_before_payment: Boolean = false, + val chat_non_vip_Banned: Boolean = false, + val hot_group: Boolean = false, + val group_list: Boolean = false, + val search_location: Boolean = true, + val service_display: Boolean = true, + val home_tip_double_install: Boolean = false, + // Vip优惠全屏页面,默认不显示:从进入程序开始计算到弹出框的时间(当pay_pop=true时生效,需要PopupTimeConfigEntity下的pay_pop_time进行倒计时) + val pay_pop: Boolean = false, + +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt new file mode 100644 index 0000000..512386d --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt @@ -0,0 +1,17 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class PopupTimeConfigEntity( + val buy_vip_tip: Long = 45L, + val search_phone: Long = 7L, + val signup_tip: Long = 45L, + val min_discount: Long = 28L, + val min_member_tip: Long = 10000L, + val order_item_top: Int = 3, + /// 进入APP后是否弹出开通会员界面弹出,默认20s后显示 + val pay_pop_time: Long = 20L, + //// 默认支付类型,1支付宝,2微信 + val pay_pop_type: Int = 1, +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt new file mode 100644 index 0000000..c6a0017 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt @@ -0,0 +1,14 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + + +@Serializable +data class UserConfigEntity( + var token: String = "", + var temp: Boolean = false, + var name: String = "", + var user_id: String = "", + var nowtime: String = "", + var config: ConfigEntity? = null +) diff --git a/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt new file mode 100644 index 0000000..db9d590 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt @@ -0,0 +1,14 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +class UserEntity { + val user_id: String = "" + val name: String = "" + val avater: String = "" + val token: String = "" + + //是否登录,有本地登录后写入(非接口字段) + var isLogin: Boolean = false +} diff --git a/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt new file mode 100644 index 0000000..6f73feb --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt @@ -0,0 +1,15 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + + +@Serializable +class VersionEntity( + var description: String = "", + var force: Boolean = false, + var last_version_force: String = "", + var title: String = "", + var url: String = "", + var app_size: String = "", + var version: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt new file mode 100644 index 0000000..5c3db64 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt @@ -0,0 +1,11 @@ +package com.img.rabbit.bean.response + +import kotlinx.serialization.Serializable + +@Serializable +data class WxShareEntity( + val content: String = "", + val image: String = "", + val link: String = "", + val title: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt index 015c715..040430b 100644 --- a/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt +++ b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt @@ -43,10 +43,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.img.rabbit.R -import com.img.rabbit.bean.ClothingBean -import com.img.rabbit.bean.FormatBean -import com.img.rabbit.bean.HairstyleBean -import com.img.rabbit.bean.ResizeBean +import com.img.rabbit.bean.local.ClothingBean +import com.img.rabbit.bean.local.FormatBean +import com.img.rabbit.bean.local.HairstyleBean +import com.img.rabbit.bean.local.ResizeBean /** * 底部画板(证件)选择器 diff --git a/app/src/main/java/com/img/rabbit/config/Common.kt b/app/src/main/java/com/img/rabbit/config/Common.kt deleted file mode 100644 index 3dc9b14..0000000 --- a/app/src/main/java/com/img/rabbit/config/Common.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.img.rabbit.config - -object Common { - const val privacyUrl = "https://www.baidu.com" - const val agreementUrl = "https://www.baidu.com" -} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/config/CommonData.kt b/app/src/main/java/com/img/rabbit/config/CommonData.kt index 98a974c..4ff9b79 100644 --- a/app/src/main/java/com/img/rabbit/config/CommonData.kt +++ b/app/src/main/java/com/img/rabbit/config/CommonData.kt @@ -2,10 +2,10 @@ package com.img.rabbit.config import androidx.compose.ui.graphics.Color import com.img.rabbit.R -import com.img.rabbit.bean.ClothingBean -import com.img.rabbit.bean.FormatBean -import com.img.rabbit.bean.HairstyleBean -import com.img.rabbit.bean.ResizeBean +import com.img.rabbit.bean.local.ClothingBean +import com.img.rabbit.bean.local.FormatBean +import com.img.rabbit.bean.local.HairstyleBean +import com.img.rabbit.bean.local.ResizeBean object CommonData { //背景颜色 diff --git a/app/src/main/java/com/img/rabbit/config/Constants.kt b/app/src/main/java/com/img/rabbit/config/Constants.kt new file mode 100644 index 0000000..cb33f07 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/config/Constants.kt @@ -0,0 +1,19 @@ +package com.img.rabbit.config + +object Constants { + const val RELEASE_BASE_URL = "https://jitutu.batiao8.com" //release + const val DEBUG_BASE_URL = "https://jitutu.batiao8.com" + const val LOG_REQUEST = "RabbitRequest" + const val agreementUrl = "https://jitutu.batiao8.com/static/policy-jietutu/user.html"//用户协议 + const val privacyUrl = "https://jitutu.batiao8.com/static/policy-jietutu/privacy-ios.html"//隐私政策 + + //const val getuiAppId = "40qbPjPkYs7TnVAYCX0Ig6"//个推appid (gradle.properties) + const val WechatAppId = "wx7d1a7d1507482cef"// 微信APPID + const val WechatAppSecret = ""//微信secret + const val UmengAppkey = ""//TODO 友盟appKey + + //解密 + const val AESDecrypt = "e4rOtnF8tJjtHO7ecZeJHN1rapED5ImB" + //加密字符 + const val Signature = "xn08hYoizXhZ1zHP8DVqfCm2yHxPmhil" +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/pages/LoginPage.kt b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt index 877d825..c111bb2 100644 --- a/app/src/main/java/com/img/rabbit/pages/LoginPage.kt +++ b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt @@ -10,7 +10,6 @@ import android.view.LayoutInflater import android.widget.CheckBox import android.widget.TextView import android.widget.Toast -import androidx.compose.animation.core.Animatable import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -28,7 +27,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -38,11 +36,13 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -64,6 +64,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController import com.img.rabbit.R import com.img.rabbit.utils.AgreementTextHelper import com.img.rabbit.viewmodel.GeneralViewModel @@ -72,19 +73,25 @@ import com.g.gysdk.EloginActivityParam import com.g.gysdk.GYManager import com.g.gysdk.GYResponse import com.g.gysdk.GyCallBack -import com.img.rabbit.config.Common.agreementUrl -import com.img.rabbit.config.Common.privacyUrl +import com.img.rabbit.bean.local.toAlipayResult +import com.img.rabbit.config.Constants.agreementUrl +import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.pages.toolbar.TitleBar +import com.img.rabbit.provider.storage.GlobalStateManager +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.StringUtils import com.img.rabbit.utils.UrlLinkUtils.openAgreement import kotlinx.coroutines.delay +import org.json.JSONObject +@SuppressLint("UnrememberedMutableState") @Composable -fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel) { +fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel, isVisibilityBreak: Boolean) { val context = LocalContext.current - val scale = remember { Animatable(0f) } val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true) var showNetworkDisconnected by remember { mutableStateOf(false) } + // 网络状态监听 LaunchedEffect(networkStatus) { if (!networkStatus) { @@ -94,8 +101,53 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene Log.w("NetworkStatus","网络已连接") } } - Scaffold{ + // 登录成功后,保存 token + LaunchedEffect(loginViewModel.loginState.value) { + if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) { + //登录成功 + PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token) + loginViewModel.setLogin(true) + + Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show() + }else if(loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token == null){ + //登录失败 + loginViewModel.setLogin(false) + Log.w("LoginScreen","登录失败,无有效的Token") + Toast.makeText(context, "登录失败,请重新登录", Toast.LENGTH_SHORT).show() + } + } + + var globalWxAuthorization by mutableStateOf(GlobalStateManager(context).globalWxAuthorizationFlow().collectAsState(initial = "")) + LaunchedEffect(globalWxAuthorization.value) { + globalWxAuthorization.value?.let { loginViewModel.requestWxLogin(it) } + } + + LaunchedEffect(loginViewModel.authInfoForAlipay.value) { + if(loginViewModel.authInfoForAlipay.value.isEmpty()) return@LaunchedEffect + loginViewModel.loginWithAliPay(context){rawResult -> + Log.i("loginWithAliPay", "支付宝登录结果:$rawResult") + val alipayResult = rawResult.toAlipayResult() + // 处理支付宝登录结果 + when (alipayResult.resultStatus) { + "9000" -> { + // 登录成功,result 字段中包含 auth_code + val authCode = StringUtils.parseAlipayResult(alipayResult.result ?: "")["auth_code"] ?: "" + loginViewModel.requestAlipayLogin(authCode) + } + + "6001" -> { + "用户取消登录" + } + + else -> { + alipayResult.memo ?: "登录失败" + } + } + } + } + + Scaffold{ Box( modifier = Modifier.fillMaxSize() ) { @@ -109,78 +161,46 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene ) // 顶部栏 - TitleBar(navController = navController, paddingValues = it, title = "", showSave = false) + TitleBar(navController = navController, paddingValues = it, title = "", showSave = false, showBreak = isVisibilityBreak) Column( modifier = Modifier .fillMaxSize() ) { - when (loginViewModel.loginScreenType.value) { - LoginScreenType.LOGIN_ONE_KEY -> { - Box( - modifier = Modifier.fillMaxSize() - ) { - // 检验是否有一键登录权限成功,显示一键登录页 + Box(modifier = Modifier.fillMaxSize()){ + when(loginViewModel.loginScreenType.value){ + LoginScreenType.LOGIN_ONE_KEY -> { + //一键登录 OneKeyLoginScreen(context, loginViewModel, generalViewModel) - - // 其他登录方式 - Column ( - modifier = Modifier - .fillMaxSize() - .padding(top = 27.dp), - verticalArrangement = Arrangement.Bottom - ){ - OtherLoginBar(context = context, viewModel = loginViewModel) - } } - } - LoginScreenType.LOGIN_CAPTCHA -> { - Box( - modifier = Modifier.fillMaxSize() - ) { - // 显示验证码登录页 + LoginScreenType.LOGIN_WX -> { + //微信登录 + Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){ + WxLoginScreen(context, loginViewModel) + } + + } + LoginScreenType.LOGIN_ALIPAY -> { + //支付宝登录 + Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){ + AliPayLoginScreen(context, loginViewModel) + } + + } + else -> { + //默认验证码登录 CaptchaLoginScreen(context, loginViewModel, generalViewModel) - - - // 其他登录方式 - Column ( - modifier = Modifier - .fillMaxSize() - .padding(top = 27.dp), - verticalArrangement = Arrangement.Bottom - ){ - OtherLoginBar(context = context, viewModel = loginViewModel) - } } } - else -> { - // 显示隐私协议政策(同意后才能继续登录) - PrivacyPolicyScreen(viewModel = loginViewModel) { //isAllowPrivacyPolicy -> - loginViewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> - if (isAllowShowOneKeyScreen) { - loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY - } else { - // 检验是否有一键登录权限失败,显示验证码登录 - loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA - } - } - /* - if (isAllowPrivacyPolicy) { - // 同意隐私协议政策,检验是否有一键登录权限 - viewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen -> - if (isAllowShowOneKeyScreen) { - viewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY - } else { - // 检验是否有一键登录权限失败,显示验证码登录 - viewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA - } - } - } else { - // 不同意隐私协议政策,直接退出应用 - (context as MainActivity).onBackPressed() - } - */ - } + + // 其他登录方式Bar + Column ( + modifier = Modifier + .fillMaxSize() + .padding(top = 27.dp), + verticalArrangement = Arrangement.Bottom + ){ + OtherLoginBar(context = context, viewModel = loginViewModel) } } } @@ -191,139 +211,21 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene } if(showNetworkDisconnected){ if(!networkStatus){ - NetworkDisconnectedPage(onNetworkStatus = { - if(it){ + NetworkDisconnectedPage(onNetworkStatus = {isNetworkAvailable-> + if(isNetworkAvailable){ Toast.makeText(context, "网络已连接", Toast.LENGTH_SHORT).show() }else{ Toast.makeText(context, "网络已断开", Toast.LENGTH_SHORT).show() } - generalViewModel.setNetworkStatus(it) + generalViewModel.setNetworkStatus(isNetworkAvailable) }) } } - } } } - -@Composable -private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (Boolean) -> Unit) { - val context = LocalContext.current - Box( - modifier = Modifier.fillMaxSize() - ){ - // 其他登录方式 - Column ( - modifier = Modifier - .fillMaxSize() - .padding(top = 27.dp), - verticalArrangement = Arrangement.Bottom - ){ - OtherLoginBar(context = context, viewModel = viewModel) - } - Box( - modifier = Modifier - .fillMaxSize() - .background(Color(0xCC000000)) - ){ - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 38.dp, vertical = 213.dp) - .align(Alignment.Center) - .background(Color.White, shape = RoundedCornerShape(26.dp)) - ){ - Image( - painter = painterResource(id = R.mipmap.ic_privacy_policy_top_mask), - contentDescription = null, - modifier = Modifier - .fillMaxSize(), - alignment = Alignment.TopCenter - ) - Text( - text = "用户协议与隐私政策", - modifier = Modifier - .padding(top = 54.dp) - .align(Alignment.TopCenter), - fontWeight = FontWeight.Normal, - fontSize = 18.sp, - color = Color(0xFF1A1A1A) - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .align(Alignment.BottomCenter) - ) { - //同意按钮,用户协议与隐私政策 - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(start = 33.dp, end = 33.dp) - .background( - Color(0xFF252525), - shape = RoundedCornerShape(359.dp), - ) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - onAgreementChange(true) - viewModel.setIsPolicyAgreement(true) - } - ) { - Text( - "同意", - color = Color(0xFFC2FF43), - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight() - .padding(vertical = 12.dp) - .align(Alignment.Center) - ) - } - //不同按钮,意用户协议与隐私政策 - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(start = 33.dp, end = 33.dp) - .background( - Color(0x00000000), - shape = RoundedCornerShape(359.dp), - ) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - onAgreementChange(false) - viewModel.setIsPolicyAgreement(false) - } - ) { - Text( - "不同意", - color = Color(0xFFAAAAAA), - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight() - .padding(vertical = 12.dp) - .align(Alignment.Center) - ) - } - } - } - } - } -} - /** * 验证码登录 */ @@ -542,8 +444,8 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene showToast = true ) ) { - //TODO 请求验证码(请完善requestCaptcha函数) - viewModel.requestCaptcha() + // 请求验证码(请完善requestCaptcha函数) + viewModel.requestCaptcha(viewModel.userName.value) // 开始倒计时(倒计时应该在requestCaptcha完成后开始) isCaptchaCountdown = true @@ -597,12 +499,11 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene showToast = true ) ) { - //TODO 验证码登录请求 - Toast.makeText(context, "登录成功!", Toast.LENGTH_SHORT).show() - //TODO 登录成功后,保存 token - generalViewModel.kv.encode("token", "123232123231231") - // 登录成功后,设置登录状态为 true - viewModel.setLogin(true) + // 验证通过(通过验证码验证),请求登录 + viewModel.requestLoginForCaptcha( + viewModel.userName.value, + viewModel.captcha.value + ) } } ) { @@ -685,11 +586,11 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene when (annotation.tag) { "USER_AGREEMENT" -> { // 打开用户协议 - openAgreement(context, agreementUrl) + openAgreement(context = context, title = "用户协议", url = agreementUrl) } "PRIVACY_POLICY" -> { // 打开隐私政策 - openAgreement(context, privacyUrl) + openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } } @@ -783,9 +684,9 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener // 设置可点击的协议文本 AgreementTextHelper.setupAgreementTextView(agreementText, targets, agreementTextView, isUnderlineText = false) { agreementType -> when (agreementType) { - "serviceAgreement" -> openAgreement(context, privacyUrl) - "userAgreement" -> openAgreement(context, agreementUrl) - "privacyAgreement" -> openAgreement(context, privacyUrl) + "serviceAgreement" -> openAgreement(context = context, title = privacyName, url = privacyUrl) + "userAgreement" -> openAgreement(context = context, title = "用户协议", url = agreementUrl) + "privacyAgreement" -> openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } @@ -808,6 +709,357 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener } } +@Composable +private fun WxLoginScreen( + context: Context, + viewModel: LoginViewModel, +) { + Column { + Image( + painter = painterResource(id = R.mipmap.ic_launcher_logo), + contentDescription = null, + modifier = Modifier + .size(86.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = "截图兔", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + ) + + Text( + text = "为了更好地为您提供服务,请先完成微信授权", + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = Color(0xFFAAAAAA), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 46.dp) + .align(Alignment.CenterHorizontally) + ) + + // 登录按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 8.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 启动微信验证,请求登录 + if (viewModel.isPolicyAgreement.value) { + //TODO 打开微信登录 + viewModel.loginWithWechat(context) + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + } + ) { + Row( + modifier = Modifier + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.drawable.ic_wx_icon), + contentDescription = null, + modifier = Modifier + .size(32.dp) + .padding(start = 12.dp) + .align(Alignment.CenterVertically) + ) + + Text( + "微信授权登录", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 12.dp) + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 14.dp) + ) { + Checkbox( + checked = viewModel.isPolicyAgreement.value, + onCheckedChange = { isChecked -> + viewModel.setIsPolicyAgreement(isChecked) + }, + modifier = Modifier + .size(16.dp) + .scale(0.35f) + .padding(start = 6.dp) + .background( + if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color.Transparent, + shape = RoundedCornerShape(36.dp) + ) + .border( + width = 1.dp, + color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color(0xFFCCCCCC), + shape = RoundedCornerShape(36.dp) + ) + .align(Alignment.CenterVertically), + colors = androidx.compose.material3.CheckboxDefaults.colors( + checkedColor = Color.Transparent, // 隐藏默认背景 + uncheckedColor = Color.Transparent, // 隐藏默认背景 + checkmarkColor = Color.White + ) + + ) + val annotatedText = buildAnnotatedString { + append("我已阅读并同意") + + // 用户协议部分 + pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《用户协议》") + } + pop() + + append("和") + + // 隐私政策部分 + pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《隐私政策》") + } + pop() + } + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations(offset, offset) + .firstOrNull()?.let { annotation -> + when (annotation.tag) { + "USER_AGREEMENT" -> { + // 打开用户协议 + openAgreement(context = context, title = "用户协议", url = agreementUrl) + } + "PRIVACY_POLICY" -> { + // 打开隐私政策 + openAgreement(context = context, title = "隐私政策", url = privacyUrl) + } + } + } + }, + style = androidx.compose.ui.text.TextStyle( + fontSize = 12.sp, + color = Color.Gray + ), + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically) + ) + } + } +} + +@Composable +private fun AliPayLoginScreen( + context: Context, + viewModel: LoginViewModel, +) { + Column{ + Image( + painter = painterResource(id = R.mipmap.ic_launcher_logo), + contentDescription = null, + modifier = Modifier + .size(86.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = "截图兔", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = Color(0xFF1A1A1A), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .align(Alignment.CenterHorizontally) + ) + + Text( + text = "为了更好地为您提供服务,请先完成支付宝授权", + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = Color(0xFFAAAAAA), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 46.dp) + .align(Alignment.CenterHorizontally) + ) + // 登录按钮 + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 8.dp) + .background( + Color(0xFF252525), + shape = RoundedCornerShape(359.dp), + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 启动支付宝验证,请求登录 + if (viewModel.isPolicyAgreement.value) { + // 打开支付宝登录 + viewModel.requestAliPayAuthParam() + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + } + ) { + Row( + modifier = Modifier + .align(Alignment.Center) + ) { + Image( + painter = painterResource(id = R.drawable.ic_alipay_icon), + contentDescription = null, + modifier = Modifier + .size(32.dp) + .padding(start = 12.dp) + .align(Alignment.CenterVertically) + ) + + Text( + "支付宝授权登录", + color = Color(0xFFC2FF43), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(vertical = 12.dp) + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 30.dp, end = 30.dp, top = 14.dp) + ) { + Checkbox( + checked = viewModel.isPolicyAgreement.value, + onCheckedChange = { isChecked -> + viewModel.setIsPolicyAgreement(isChecked) + }, + modifier = Modifier + .size(16.dp) + .scale(0.35f) + .padding(start = 6.dp) + .background( + if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color.Transparent, + shape = RoundedCornerShape(36.dp) + ) + .border( + width = 1.dp, + color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) + else Color(0xFFCCCCCC), + shape = RoundedCornerShape(36.dp) + ) + .align(Alignment.CenterVertically), + colors = androidx.compose.material3.CheckboxDefaults.colors( + checkedColor = Color.Transparent, // 隐藏默认背景 + uncheckedColor = Color.Transparent, // 隐藏默认背景 + checkmarkColor = Color.White + ) + + ) + val annotatedText = buildAnnotatedString { + append("我已阅读并同意") + + // 用户协议部分 + pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《用户协议》") + } + pop() + + append("和") + + // 隐私政策部分 + pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") + withStyle(style = SpanStyle( + color = Color(0xFF767676), + fontWeight = FontWeight.Bold, + //textDecoration = TextDecoration.Underline + )) { + append("《隐私政策》") + } + pop() + } + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations(offset, offset) + .firstOrNull()?.let { annotation -> + when (annotation.tag) { + "USER_AGREEMENT" -> { + // 打开用户协议 + openAgreement(context = context, title = "用户协议", url = agreementUrl) + } + "PRIVACY_POLICY" -> { + // 打开隐私政策 + openAgreement(context = context, title = "隐私政策", url = privacyUrl) + } + } + } + }, + style = androidx.compose.ui.text.TextStyle( + fontSize = 12.sp, + color = Color.Gray + ), + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically) + ) + } + } +} + /** * 一键登录页 * 至少包含号码栏(NumberTextview)、品牌露出(SloganTextview)、登录按钮(LoginButton)、隐私确认(PrivacyCheckbox)、隐私标题(PrivacyTextview) @@ -993,7 +1245,7 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel) { @Composable private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) { - + val scope = rememberCoroutineScope() Column( modifier = Modifier .fillMaxWidth() @@ -1057,8 +1309,20 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) { indication = null, interactionSource = remember { MutableInteractionSource() } ) { - //TODO 打开微信登录 - Toast.makeText(context, "打开微信登录", Toast.LENGTH_SHORT).show() + /* + if (viewModel.isPolicyAgreement.value) { + //TODO 打开微信登录 + viewModel.loginWithWechat(context) + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + */ + // 微信登录 + viewModel.loginScreenType.value = LoginScreenType.LOGIN_WX } ) Image( @@ -1072,8 +1336,20 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) { indication = null, interactionSource = remember { MutableInteractionSource() } ) { - //TODO 打开支付宝登录 - Toast.makeText(context, "打开支付宝登录", Toast.LENGTH_SHORT).show() + /* + if (viewModel.isPolicyAgreement.value) { + // 打开支付宝登录 + viewModel.requestAliPayAuthParam() + } else { + Toast.makeText( + context, + "请先同意用户协议和隐私政策", + Toast.LENGTH_SHORT + ).show() + } + */ + // 支付宝登录 + viewModel.loginScreenType.value = LoginScreenType.LOGIN_ALIPAY } ) Image( @@ -1152,9 +1428,14 @@ private fun oneKeyLogin( override fun onSuccess(response: GYResponse?) { //TODO 登录成功,需要与后端交互 Log.i("OneKeyLogin", "onSuccess:$response") - //TODO 登录成功后,保存 token - generalViewModel.kv.encode("token", "123232123231231") - viewModel.setLogin(true) + try { + val jsonObject = JSONObject(response?.msg?:"{}") + val data = jsonObject.getJSONObject("data") + val token = data.getString("token") + viewModel.requestOneKeyLogin(response?.gyuid?:"", token) + } catch (e: Exception) { + e.printStackTrace() + } } override fun onFailed(p0: GYResponse?) { @@ -1169,10 +1450,32 @@ enum class LoginScreenType { LOGIN_NORMAL, LOGIN_ONE_KEY, LOGIN_CAPTCHA, + LOGIN_WX, + LOGIN_ALIPAY, } -@Preview +@Preview(showBackground = true) @Composable private fun PreviewOneKeyLoginScreen() { OneKeyLoginScreen(LocalContext.current, viewModel(), viewModel()) -} \ No newline at end of file +} + + +@Preview(showBackground = true) +@Composable +private fun PreviewWxLoginScreen() { + WxLoginScreen(LocalContext.current, viewModel()) +} + + +@Preview(showBackground = true) +@Composable +private fun PreviewAliPayLoginScreen() { + AliPayLoginScreen(LocalContext.current, viewModel()) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewLoginScreen() { + LoginScreen(navController = rememberNavController(), generalViewModel = viewModel(), loginViewModel = viewModel(), isVisibilityBreak = false) +} diff --git a/app/src/main/java/com/img/rabbit/pages/MainPage.kt b/app/src/main/java/com/img/rabbit/pages/MainPage.kt index f8dca7d..37ed9d3 100644 --- a/app/src/main/java/com/img/rabbit/pages/MainPage.kt +++ b/app/src/main/java/com/img/rabbit/pages/MainPage.kt @@ -196,7 +196,8 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode LoginScreen( navController = navController, generalViewModel = generalViewModel, - loginViewModel = loginViewModel + loginViewModel = loginViewModel, + isVisibilityBreak = true ) } } diff --git a/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt index 51c4310..70efe43 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -37,10 +38,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import coil3.compose.AsyncImage import com.img.rabbit.R +import com.img.rabbit.provider.storage.PreferenceUtil import com.img.rabbit.viewmodel.GeneralViewModel @Composable @@ -50,6 +54,7 @@ fun MineScreen( ) { val context = LocalContext.current val vipMember by remember { mutableStateOf(false) } + val userInfo by remember { mutableStateOf(PreferenceUtil.loginUserInfo()) } // 监听返回事件 val currentBackStackEntry = navController.currentBackStackEntry @@ -62,6 +67,7 @@ fun MineScreen( } } + Box( modifier = Modifier.fillMaxSize().background(Color(0xFFF9F9F9)) ){ @@ -80,21 +86,41 @@ fun MineScreen( Row( modifier = Modifier.fillMaxWidth() ) { - Image( - painter = painterResource(id = R.mipmap.ic_user_avatar_default), - contentDescription = null, - contentScale = ContentScale.FillWidth, - modifier = Modifier - .size(64.dp) - .clip(RoundedCornerShape(90.dp)) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - // 处理点击事件 - Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show() - } - ) + if(userInfo == null){ + Image( + painter = painterResource(id = R.mipmap.ic_user_avatar_default), + contentDescription = "用户头像", + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(90.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 处理点击事件 + Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show() + } + ) + }else{ + AsyncImage( + model = userInfo?.avater, + contentDescription = "用户头像", + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(90.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + // 处理点击事件 + Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show() + }, + fallback = painterResource(id = R.mipmap.ic_user_avatar_default), + error = painterResource(id = R.mipmap.ic_user_avatar_default) + ) + } Column( modifier = Modifier .padding(start = 16.dp) @@ -103,28 +129,62 @@ fun MineScreen( indication = null, interactionSource = remember { MutableInteractionSource() } ) { - // 隐藏TabBar - generalViewModel.setNavigationBarVisible(false) - // 跳转登录页面 - navController.navigate("login") + if(userInfo == null){ + // 隐藏TabBar + generalViewModel.setNavigationBarVisible(false) + // 跳转登录页面 + navController.navigate("login") + } else { + //TODO 已登录,跳转个人信息页面 + //navController.navigate("userInfo") + } } ) { Text( - text = "登录/注册", + text = if(userInfo == null){ "登录/注册" }else{ userInfo?.name?:"" }, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1A1A1A), modifier = Modifier.wrapContentSize() ) - Text( - text = "登录体验更多功能哦~", - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = Color(0xFF767676), + Row( modifier = Modifier - .wrapContentSize() .padding(top = 10.dp) - ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ){ + // 点击复制ID + if (userInfo != null) { + val clipboardManager = android.content.Context.CLIPBOARD_SERVICE + val clipboard = context.getSystemService(clipboardManager) as android.content.ClipboardManager + val clip = android.content.ClipData.newPlainText("User ID", userInfo?.user_id) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT).show() + } + } + ){ + Text( + text = if(userInfo == null){ "登录体验更多功能哦~" }else{ "ID:${userInfo?.user_id?:""}" }, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF767676), + modifier = Modifier + .wrapContentSize() + ) + Box( + modifier = Modifier + .size(4.dp) + ) + Image( + painter = painterResource(id = R.mipmap.ic_copy), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(12.dp) + .align(Alignment.CenterVertically) + ) + } } } diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt index d2564f0..b26d1ae 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt @@ -64,8 +64,8 @@ import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import coil3.compose.AsyncImage import com.img.rabbit.R -import com.img.rabbit.bean.ClothingBean -import com.img.rabbit.bean.HairstyleBean +import com.img.rabbit.bean.local.ClothingBean +import com.img.rabbit.bean.local.HairstyleBean import com.img.rabbit.components.AppearanceType import com.img.rabbit.components.DrawingBoardPicker import com.img.rabbit.config.CommonData.clothingForFemales diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt index 4f3ca39..03e517b 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt @@ -1,75 +1,46 @@ package com.img.rabbit.pages.screen.make -import android.annotation.SuppressLint import android.graphics.Bitmap import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavController -import com.img.rabbit.bean.LongImageBean +import com.img.rabbit.bean.local.LongImageBean import com.img.rabbit.pages.toolbar.TitleBar import com.img.rabbit.utils.ExportFormat import com.img.rabbit.utils.ImageUtils.getBitmapFromUri diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt index 5546738..fa11b97 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,8 +33,8 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.img.rabbit.R -import com.img.rabbit.config.Common.agreementUrl -import com.img.rabbit.config.Common.privacyUrl +import com.img.rabbit.config.Constants.agreementUrl +import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.pages.toolbar.TitleBar import com.img.rabbit.utils.UrlLinkUtils.openAgreement @@ -101,7 +100,7 @@ fun AboutScreen(navController: NavHostController) { interactionSource = remember { MutableInteractionSource() } ) { // 跳转用户协议页面 - openAgreement(context, agreementUrl) + openAgreement(context = context, title = "用户协议", url = agreementUrl) }, verticalAlignment = Alignment.CenterVertically ) { @@ -144,7 +143,7 @@ fun AboutScreen(navController: NavHostController) { interactionSource = remember { MutableInteractionSource() } ) { // 跳转隐私政策页面 - openAgreement(context, privacyUrl) + openAgreement(context = context, title = "隐私政策", url = privacyUrl) }, verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt index 63db93b..03fab5c 100644 --- a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt +++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,7 +39,7 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.img.rabbit.R -import com.img.rabbit.bean.UserInfo +import com.img.rabbit.bean.local.UserInfo import com.img.rabbit.pages.toolbar.TitleBar @Composable @@ -48,9 +47,9 @@ fun AccountManagerScreen(navController: NavHostController) { val userList by remember { mutableStateOf( listOf( - UserInfo(1, "张三", true), - UserInfo(2, "李四", false), - UserInfo(3, "王五", false), + UserInfo(1, "张三", "https://cdn.batiao8.com/jietutu/logo.png","",true), + UserInfo(2, "李四", "https://cdn.batiao8.com/jietutu/logo.png","",false), + UserInfo(3, "王五", "https://cdn.batiao8.com/jietutu/logo.png","",false), ) ) } diff --git a/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt index d3ff909..bdcbc18 100644 --- a/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt +++ b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt @@ -31,7 +31,7 @@ import androidx.navigation.compose.rememberNavController import com.img.rabbit.R @Composable -fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, onSubmit: (() -> Unit)? = null) { +fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, showBreak: Boolean = true, onSubmit: (() -> Unit)? = null) { Box( modifier = Modifier.fillMaxWidth().padding(paddingValues) ){ @@ -40,17 +40,19 @@ fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: .fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - // 返回按钮 - Icon( - painter = painterResource(id = R.mipmap.ic_back), - contentDescription = "返回", - modifier = Modifier - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { navController?.popBackStack() } - .padding(end = 26.dp) - ) + if(showBreak){ + // 返回按钮 + Icon( + painter = painterResource(id = R.mipmap.ic_back), + contentDescription = "返回", + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { navController?.popBackStack() } + .padding(end = 26.dp) + ) + } Column( modifier = Modifier.fillMaxWidth().weight(1f) ) { diff --git a/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt b/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt new file mode 100644 index 0000000..186c392 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt @@ -0,0 +1,110 @@ +@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + +package com.img.rabbit.provider.api + +import com.img.rabbit.BuildConfig +import com.img.rabbit.config.Constants +import com.img.rabbit.provider.utils.HeaderInterceptor +import com.img.rabbit.provider.utils.RequestInterceptor +import com.img.rabbit.provider.utils.ResponseInterceptor +import com.img.rabbit.provider.utils.getUnsafeOkHttpClient +import com.img.rabbit.viewmodel.`interface`.ServiceVo +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +object ApiManager { + private lateinit var retrofit: Retrofit + private lateinit var unsafeRetrofit: Retrofit + lateinit var serviceVo: ServiceVo + + init { + initialize() + } + + // 初始化方法,抽离出来以便重新初始化 + private fun initialize() { + // 获取基础URL并确保其包含http/https协议 + val baseUrl = if (BuildConfig.DEBUG) { + Constants.DEBUG_BASE_URL + } else { + Constants.RELEASE_BASE_URL + } + + if (!::retrofit.isInitialized) { + val client = OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .addInterceptor(RequestInterceptor()) + .addInterceptor(HeaderInterceptor()) + .build() + + retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + if (!::unsafeRetrofit.isInitialized) { + val client = getUnsafeOkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .writeTimeout(20, TimeUnit.SECONDS) + .addInterceptor(HeaderInterceptor()) + .addInterceptor(RequestInterceptor()) + .addInterceptor(ResponseInterceptor()) + .build() + + unsafeRetrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + if (!::serviceVo.isInitialized) { + serviceVo = unsafeRetrofit.create(ServiceVo::class.java) + } + } + + // 添加重新初始化方法,用于在baseUrl改变时调用 + fun reinitialize() { + // 重置所有实例,使其在下一次访问时重新初始化 + if (::retrofit.isInitialized) { + synchronized(this) { + // 标记为未初始化 + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (this as Object).getClass().getDeclaredField("retrofit").apply { + isAccessible = true + set(this@ApiManager, null) + } + } + } + + if (::unsafeRetrofit.isInitialized) { + synchronized(this) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (this as Object).getClass().getDeclaredField("unsafeRetrofit").apply { + isAccessible = true + set(this@ApiManager, null) + } + } + } + + if (::serviceVo.isInitialized) { + synchronized(this) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (this as Object).getClass().getDeclaredField("serviceVo").apply { + isAccessible = true + set(this@ApiManager, null) + } + } + } + + // 重新初始化 + initialize() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt b/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt new file mode 100644 index 0000000..8cf0621 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt @@ -0,0 +1,9 @@ +package com.img.rabbit.provider.api + +class ResultVo(val code: Int, val data: T) { + val status: Boolean + get() { + return code == 0 + } + val message: String = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt b/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt new file mode 100644 index 0000000..31550eb --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt @@ -0,0 +1,49 @@ +package com.img.rabbit.provider.storage + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.storeData: DataStore by preferencesDataStore(name = "global_state") + +class GlobalStateManager( + private val context: Context +) { + companion object { + private val GLOBAL_LOADING = booleanPreferencesKey("global_loading") + private val GLOBAL_WX_AUTHORIZATION = stringPreferencesKey("global_wx_authorization") + } + + suspend fun storeGlobalLoading(value: Boolean) { + context.storeData.edit { preferences -> + preferences[GLOBAL_LOADING] = value + } + } + fun isGlobalLoadingFlow(): Flow { + return context.storeData.data.map { + preferences -> + preferences[GLOBAL_LOADING] + } + } + + + + suspend fun storeGlobalWxAuthorization(value: String) { + context.storeData.edit { preferences -> + preferences[GLOBAL_WX_AUTHORIZATION] = value + } + } + fun globalWxAuthorizationFlow(): Flow { + return context.storeData.data.map { + preferences -> + preferences[GLOBAL_WX_AUTHORIZATION] + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt b/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt new file mode 100644 index 0000000..6defd3f --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt @@ -0,0 +1,93 @@ +package com.img.rabbit.provider.storage + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.img.rabbit.bean.response.UserEntity +import com.tencent.mmkv.MMKV + +/** + * SharedPreferences工具类,用于简化数据持久化操作 + */ +object PreferenceUtil { + private const val KEY_ACCESS_TOKEN = "access_token" + private const val KEY_X_TOKEN = "x_token" + + private const val KEY_USER_INFO = "user_info" + + + + // Gson实例 + private val gson: Gson = GsonBuilder().create() + + val kv by lazy { MMKV.defaultMMKV() } + + + //服务器时间和本地时间的偏移量 + private var timeDiff = 0L + + + /** + * 保存AccessToken + */ + fun saveAccessToken(token: String?) { + kv.encode(KEY_ACCESS_TOKEN, token) + } + + /** + * 获取保存的AccessToken + */ + fun getAccessToken(): String? { + return kv.decodeString(KEY_ACCESS_TOKEN, null) + } + + fun saveXToken(token: String?) { + kv.encode(KEY_X_TOKEN, token) + } + + fun getXToken(): String? { + return kv.decodeString(KEY_X_TOKEN, null) + } + + + fun getUserInfos(): MutableList? { + /** + *[{"user_id":"25","name":"手机用户0253","avater":"https://cdn.batiao8.com/jietutu/logo.png","token":"45731e27-d101-4ec3-975c-e665cf86a579"},{"user_id":"25","name":"手机用户0253","avater":"https://cdn.batiao8.com/jietutu/logo.png","token":"45731e27-d101-4ec3-975c-e665cf86a579"}] + */ + return gson.fromJson(kv.decodeString(KEY_USER_INFO, "[]"), Array::class.java)?.toMutableList() + } + + fun loginUserInfo(): UserEntity?{ + return getUserInfos()?.find { it.isLogin } + } + + fun saveUserInfo(userEntity: UserEntity) { + val userInfos = getUserInfos()?: mutableListOf() + val isContain = userInfos.find { it.user_id == userEntity.user_id } != null + if(!isContain){ + userInfos.add(userEntity) + } + + kv.encode(KEY_USER_INFO, gson.toJson(userInfos)) + } + + fun removeUserInfo(userEntity: UserEntity) { + val userInfos = getUserInfos()?: mutableListOf() + userInfos.removeIf { it.user_id == userEntity.user_id } + kv.encode(KEY_USER_INFO, gson.toJson(userInfos)) + } + + /** + * 清除所有数据 + */ + fun clearAll() { + kv.clearAll() + } + + //真实的服务器时间 + fun serverTimeMillis(): Long { + return System.currentTimeMillis() + timeDiff * 1000 + } + fun setTimeDiff(timeDiff: Long) { + this.timeDiff = timeDiff + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt b/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt new file mode 100644 index 0000000..e9b6496 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt @@ -0,0 +1,53 @@ +package com.img.rabbit.provider.utils + +import android.annotation.SuppressLint +import okhttp3.OkHttpClient +import java.security.SecureRandom +import java.security.cert.X509Certificate +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +@SuppressLint("CustomX509TrustManager") +fun getUnsafeOkHttpClient(): OkHttpClient { + try { + // 创建一个信任所有证书的 TrustManager + val trustAllCerts = arrayOf( + + object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String? + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String? + ) { + } + + override fun getAcceptedIssuers(): Array = + arrayOf() + } + ) + + // 初始化 SSLContext + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustAllCerts, SecureRandom()) + + // 创建一个允许所有主机名验证的 HostnameVerifier + val allHostsValid = HostnameVerifier { _, _ -> true } + + // 创建 OkHttpClient 并配置 SSL 和主机名验证 + return OkHttpClient.Builder() + .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) + .hostnameVerifier(allHostsValid) + .build() + } catch (e: Exception) { + throw RuntimeException(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt new file mode 100644 index 0000000..953283e --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt @@ -0,0 +1,46 @@ +package com.img.rabbit.provider.utils + +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.BuildConfig +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +class HeaderInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val original = chain.request() + try { + val mBuilder = original.newBuilder() + val accessToken = PreferenceUtil.getXToken()?:"" + mBuilder.header("x-token", accessToken) + mBuilder.header("x-platform", "android") + mBuilder.header("x-mobile-brand", android.os.Build.BRAND) + mBuilder.header("x-mobile-model", android.os.Build.MODEL) + mBuilder.header("x-package", BuildConfig.APPLICATION_ID) + + val request = mBuilder + .method(original.method, original.body) + .build() + + val response = chain.proceed(request) + /* + // 获取响应头 + val headers = response.headers + // 处理响应头 + headers.forEach { header -> + //Log.d(LOG_REQUEST, "ResponseHeader:${header.first} ${header.second}") + if (header.first == "Access-Token") { + authorization = header.second + } + } + */ + + return response + } catch (e: Exception) { + e.printStackTrace() + return chain.proceed(original) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt new file mode 100644 index 0000000..980a03e --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt @@ -0,0 +1,177 @@ +package com.img.rabbit.provider.utils + +import android.text.TextUtils +import android.util.Log +import com.img.rabbit.BuildConfig +import com.img.rabbit.config.Constants +import com.img.rabbit.config.Constants.LOG_REQUEST +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.StringUtils +import okhttp3.Interceptor +import okhttp3.Response +import okio.Buffer +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat +import java.util.Arrays +import java.util.Date +import java.util.Locale + +/** + * 加密数据 + */ +class RequestInterceptor : Interceptor { + private val TAG = "Encryption" + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) + + override fun intercept(chain: Interceptor.Chain): Response { + val startTime = System.currentTimeMillis() + val request = chain.request() + + // 记录请求信息 + logRequest(request) + + var modifiedRequest = request + val method = request.method.lowercase(Locale.getDefault()).trim { it <= ' ' } + val url = request.url + val apiPath = String.format("%s", url) + + val baseUrl = if (BuildConfig.DEBUG) { + Constants.DEBUG_BASE_URL + } else { + Constants.RELEASE_BASE_URL + } + + // 如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(baseUrl)) { + val response = chain.proceed(modifiedRequest) + return response + } + + /* + L.d("TAG-->>url=$apiPath") + //如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(Constants.BaseUrl)) { + L.d("TAG-->>content-type=${request.headers["Content-Type"]}") + val requestBody = request.body + L.d("TAG-->>${requestBody.toString()}") + return chain.proceed(request) + } + */ + + var queryString = url.encodedQuery + queryString = if (!TextUtils.isEmpty(queryString)) { + (queryString + "&nonce=" + StringUtils.createUUID()) + "×tamp=" + PreferenceUtil.serverTimeMillis() / 1000 + } else { + ("nonce=" + StringUtils.createUUID()) + "×tamp=" + PreferenceUtil.serverTimeMillis() / 1000 + } + val sortQueryString = Arrays.stream(queryString.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .sorted { obj: String, anotherString: String? -> obj.compareTo(anotherString!!) } + .reduce { x: String, y: String -> "$x&$y" } + .get() + //如果请求方式是get或者delete 则没有body + var paramsStr = "" + var bytes = ByteArray(0) + var signature: String? = "" + if (method == "put" || method == "post") { + val requestBody = request.body + Log.w(TAG, "TAG-->>${requestBody.toString()}") + val buffer = Buffer() + requestBody!!.writeTo(buffer) + bytes = StringUtils.addByte(bytes, buffer.readByteArray()) + Log.w(TAG, "签名后bodyByte的长度为" + bytes.size) + paramsStr = String(bytes, StandardCharsets.UTF_8) + // paramsStr = buffer.readUtf8(); + Log.w(TAG, "签名后body的长度为" + paramsStr.length) + } + signature = if (bytes.isNotEmpty()) { + Log.w(TAG, "当前的数组长度为----" + bytes.size) + StringUtils.getMD5Byte( + StringUtils.addByte( + StringUtils.addByte("$sortQueryString&".toByteArray(), bytes), + ("&" + StringUtils.getMD5String(Constants.Signature)).toByteArray() + ) + ) + } else { + StringUtils.getMD5String(sortQueryString + "&" + StringUtils.getMD5String(Constants.Signature)) + } + val newUrl = apiPath.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + "?" + queryString + "&signature=" + signature + Log.w(TAG, "签名后的路径--->$newUrl") + /* + request = request.newBuilder().url(newUrl).build() + return chain.proceed(request) + */ + + modifiedRequest = request.newBuilder().url(newUrl).build() + val response = chain.proceed(modifiedRequest) + logResponse(response, startTime) + return response + } + + //---------------------------------> 以下为格式化请求日志打印 <--------------------------------- + private fun logRequest(request: okhttp3.Request) { + val timestamp = dateFormat.format(Date()) + val method = request.method + val url = request.url.toString() + + Log.i(LOG_REQUEST,"┌─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ 📤 请求部分 [$timestamp]") + Log.i(LOG_REQUEST,"├─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ -->方法: $method") + Log.i(LOG_REQUEST,"│ -->URL: $url") + + // 记录请求体(如果有) + val requestBody = request.body + if (requestBody != null) { + try { + val buffer = Buffer() + requestBody.writeTo(buffer) + val bodyString = buffer.readUtf8() + if (bodyString.isNotEmpty()) { + Log.i(LOG_REQUEST,"│ -->请求参数:") + Log.i(LOG_REQUEST,"│ --> $bodyString") + } + } catch (e: IOException) { + Log.e(LOG_REQUEST,"│ -->读取请求体失败: ${e.message}") + } + }else{ + Log.i(LOG_REQUEST,"│ -->无请求参数") + } + Log.i(LOG_REQUEST,"└─────────────────────────────────────────────────────────────────────────────") + } + + private fun logResponse(response: Response, startTime: Long) { + val timestamp = dateFormat.format(Date()) + val duration = System.currentTimeMillis() - startTime + val code = response.code + val message = response.message + val url = response.request.url.toString() + val apiPath = url.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + + Log.i(LOG_REQUEST,"┌─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ 📥 响应部分 [$timestamp]") + Log.i(LOG_REQUEST,"├─────────────────────────────────────────────────────────────────────────────") + Log.i(LOG_REQUEST,"│ URL: $url") + Log.i(LOG_REQUEST,"│ API: $apiPath") + Log.i(LOG_REQUEST,"│ 状态码: $code $message") + Log.i(LOG_REQUEST,"│ 耗时: ${duration}ms") + + // 记录响应体 + try { + val responseBody = response.peekBody(1024 * 1024L) // 最多读取1MB + val bodyString = responseBody.string() + if (bodyString.isNotEmpty()) { + Log.i(LOG_REQUEST,"│ 响应内容: (前${minOf(bodyString.length, 2000)}字符):") + val preview = if (bodyString.length > 2000) { + bodyString.take(2000) + "..." + } else { + bodyString + } + Log.i(LOG_REQUEST,"│ $preview") + } + } catch (e: IOException) { + Log.e(LOG_REQUEST,"│ 读取响应体失败: ${e.message}") + } + Log.i(LOG_REQUEST,"└─────────────────────────────────────────────────────────────────────────────") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt new file mode 100644 index 0000000..5f656c5 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt @@ -0,0 +1,86 @@ +package com.img.rabbit.provider.utils + +import android.content.Intent +import android.text.TextUtils +import android.util.Log +import com.img.rabbit.BuildConfig +import com.img.rabbit.config.Constants +import com.img.rabbit.utils.AESpkcs7paddingUtil +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import org.json.JSONObject +import java.nio.charset.Charset +import java.nio.charset.UnsupportedCharsetException + + +/** + * 解密数据 + */ +class ResponseInterceptor : Interceptor { + private val TAG = "Decode" + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url + val apiPath = String.format("%s", url) + val baseUrl = if (BuildConfig.DEBUG) { + Constants.DEBUG_BASE_URL + } else { + Constants.RELEASE_BASE_URL + } + //如果请求的不是服务端的接口则不用加密 + if (!apiPath.startsWith(baseUrl)) { + return chain.proceed(request) + } + val response = chain.proceed(request) + var charset = Charset.forName("UTF-8") + val contentType = response.header("Content-Type") + if (contentType != null && contentType.contains("application/json")) { + val responseBody = response.body + val source = responseBody.source() + source.request(Long.MAX_VALUE) + val buffer = source.buffer + val mediaType = responseBody.contentType() + if (mediaType != null) { + try { + charset = mediaType.charset(Charset.forName("UTF-8")) + } catch (e: UnsupportedCharsetException) { + e.printStackTrace() + } + } + val respBody = buffer.clone().readString(charset!!) + Log.w(TAG, "response=${respBody}") + if (TextUtils.isEmpty(respBody)) { + val responseCode = if (response.code == 400) 200 else response.code //兼容某些接口400的情况 + return response.newBuilder().code(responseCode).body(responseBody).build() + } + + val isEncrypt = JSONObject(respBody).optBoolean("encrypt") + Log.w(TAG, "是否需要解密:$isEncrypt") + if (!isEncrypt) { //如果不需要加密 直接返回 + Log.w(TAG, "response=${response.body}") + val responseCode = if (response.code == 400) 200 else response.code //兼容某些接口400的情况 + return response.newBuilder().code(responseCode).body(responseBody).build() + } + val decrybody = JSONObject(respBody).optString("data") + Log.w(TAG, "Json解析后的字符串为---->$decrybody") + var decryString: String? + try { + decryString = AESpkcs7paddingUtil.decryptNormal(decrybody, Constants.AESDecrypt) + Log.e("ResponseInterceptor", "解密后返回的字符串为---->$decryString") + + //这里可以通过code处理token过去过期的情况 + //val decCode = JSONObject(decryString).optInt("code") + + //返回新创建的response + val responseCode = if (response.code == 400 && decrybody != null) 200 else response.code //兼容某些接口400的情况 + return response.newBuilder().code(responseCode).body(decryString.toResponseBody("text/plain".toMediaType())).build() + } catch (e: Exception) { + e.printStackTrace() + } + } + return response + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt b/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt new file mode 100644 index 0000000..e4578fe --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt @@ -0,0 +1,30 @@ +package com.img.rabbit.utils + + +import android.util.Base64 +import io.github.fastaes.FastAES + +object AESpkcs7paddingUtil { + + /** + * 编码格式 + */ + const val ENCODING = "utf-8" + + /** + * AES解密 + * + * @param encryptStr 加密后的密文 + * @param key 密钥 + * @return 源字符串 + * @throws Exception + */ + @Throws(Exception::class) + fun decryptNormal(encryptStr: String?, key: String): String { + val sourceBytes = Base64.decode(encryptStr, Base64.NO_WRAP) + val keyBytes = key.toByteArray(charset(ENCODING)) + val plain: ByteArray = FastAES.decrypt(sourceBytes, keyBytes, key.substring(0, 16).toByteArray(charset(ENCODING))) + return String(plain) + } +} + diff --git a/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java index c339e10..08c19cd 100644 --- a/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java +++ b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java @@ -39,8 +39,8 @@ public class Bitmap2SVG return true; } - private boolean trc_del; - private PrintWriter pw; + private final boolean trc_del; + private final PrintWriter pw; private int w, h; private Bitmap2SVG( PrintWriter pw, boolean trc_delete ) @@ -116,11 +116,13 @@ public class Bitmap2SVG byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { + assert os != null; os.write(buffer, 0, len); } // 此时文件已带上正确的 MIME 类型存入相册 Toast.makeText(context, "SVG已保存", Toast.LENGTH_SHORT).show(); } catch (IOException e) { + //noinspection CallToPrintStackTrace e.printStackTrace(); } } diff --git a/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt b/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt new file mode 100644 index 0000000..547fd5c --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt @@ -0,0 +1,56 @@ +package com.img.rabbit.utils + +import android.content.Context +import android.content.pm.PackageManager +import android.text.TextUtils +import com.bytedance.hume.readapk.HumeSDK +import com.img.rabbit.BuildConfig +import com.kwai.monitor.payload.TurboHelper +import com.tencent.vasdolly.helper.ChannelReaderUtil + + +object ChannelUtils { + + fun getChannel(context: Context): String { + if (BuildConfig.DEBUG) { + MMKVUtils.put("app_channel", "test") + } else { + if (TextUtils.isEmpty(MMKVUtils.getString("app_channel"))) { + MMKVUtils.put("app_channel", getChannelBy(context)) + } + } + return MMKVUtils.getString("app_channel") ?: "" + } + + private fun getChannelBy(context: Context): String? { + val kuaishou = TurboHelper.getChannel(context) + if (!TextUtils.isEmpty(kuaishou)) { + return kuaishou + } + val tengxun = ChannelReaderUtil.getChannel(context) + if (!TextUtils.isEmpty(tengxun)) { + return tengxun + } + val juliang = HumeSDK.getChannel(context) + if (!TextUtils.isEmpty(juliang)) { + return juliang + } + return channel(context) + } + + private fun channel(context: Context): String { + try { + val pm = context.packageManager + val appInfo = pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA) + val channel = appInfo.metaData.getString("UMENG_CHANNEL") + if (!TextUtils.isEmpty(channel)) { + return channel!! + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + +} + diff --git a/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt index 6b9c403..c9dfb6c 100644 --- a/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt @@ -20,10 +20,9 @@ import kotlin.apply import android.graphics.* import androidx.core.graphics.createBitmap -import com.img.rabbit.bean.LongImageBean +import com.img.rabbit.bean.local.LongImageBean import java.io.ByteArrayOutputStream import java.io.OutputStream -import androidx.core.graphics.withClip object ImageUtils { fun decodeSampledBitmapFromResource( diff --git a/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt b/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt new file mode 100644 index 0000000..29214b1 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt @@ -0,0 +1,99 @@ +package com.img.rabbit.utils + +import android.os.Parcelable +import com.tencent.mmkv.MMKV +import java.util.Collections + +object MMKVUtils { + + var mmkv: MMKV? = null + + init { + mmkv = MMKV.defaultMMKV() + } + + fun put(key: String, value: Any?): Boolean { + return when (value) { + is String -> mmkv?.encode(key, value)!! + is Float -> mmkv?.encode(key, value)!! + is Boolean -> mmkv?.encode(key, value)!! + is Int -> mmkv?.encode(key, value)!! + is Long -> mmkv?.encode(key, value)!! + is Double -> mmkv?.encode(key, value)!! + is ByteArray -> mmkv?.encode(key, value)!! + else -> false + } + } + + /** + * 这里使用安卓自带的Parcelable序列化,它比java支持的Serializer序列化性能好些 + */ + fun put(key: String, t: T?): Boolean { + if (t == null) { + return false + } + return mmkv?.encode(key, t)!! + } + + fun put(key: String, sets: Set?): Boolean { + if (sets == null) { + return false + } + return mmkv?.encode(key, sets)!! + } + + fun getInt(key: String): Int? { + return mmkv?.decodeInt(key, 0) + } + + fun getDouble(key: String): Double? { + return mmkv?.decodeDouble(key, 0.00) + } + + fun getLong(key: String): Long? { + return mmkv?.decodeLong(key, 0L) + } + + fun getBoolean(key: String, defaultValue: Boolean = false): Boolean { + return mmkv?.decodeBool(key, defaultValue) ?: defaultValue + } + + fun getFloat(key: String): Float? { + return mmkv?.decodeFloat(key, 0F) + } + + fun getByteArray(key: String): ByteArray? { + return mmkv?.decodeBytes(key) + } + + fun getString(key: String): String? { + return mmkv?.decodeString(key, "") + } + + /** + * SpUtils.getParcelable("") + */ + inline fun getParcelable(key: String): T? { + return mmkv?.decodeParcelable(key, T::class.java) + } + + fun getStringSet(key: String): Set? { + return mmkv?.decodeStringSet(key, Collections.emptySet()) + } + + /** + * 是否已经存在 + */ + fun contains(key: String): Boolean? { + return mmkv?.containsKey(key) + } + + fun removeKey(key: String) { + mmkv?.removeValueForKey(key) + } + + fun clearAll() { + mmkv?.clearAll() + } +} + diff --git a/app/src/main/java/com/img/rabbit/utils/StringUtils.kt b/app/src/main/java/com/img/rabbit/utils/StringUtils.kt new file mode 100644 index 0000000..f70907a --- /dev/null +++ b/app/src/main/java/com/img/rabbit/utils/StringUtils.kt @@ -0,0 +1,494 @@ +package com.img.rabbit.utils + +import android.text.TextUtils +import java.net.URLDecoder +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.Hashtable +import java.util.Locale +import java.util.UUID +import java.util.concurrent.ThreadLocalRandom +import java.util.regex.Matcher +import java.util.regex.Pattern + + +object StringUtils { + /** + * 功能:身份证的有效验证 + * + * @param IDStr 身份证号 + * @return 有效:返回"" 无效:返回String信息 + * @throws ParseException + */ + @Throws(ParseException::class) + fun IDCardValidate(IDStr: String): Boolean { + var errorInfo = "" // 记录错误信息 + val ValCodeArr = arrayOf( + "1", "0", "x", "9", "8", "7", "6", "5", "4", + "3", "2" + ) + val Wi = arrayOf( + "7", "9", "10", "5", "8", "4", "2", "1", "6", "3", "7", + "9", "10", "5", "8", "4", "2" + ) + var Ai = "" + // ================号码的长度 15位或18位 ================ + if (IDStr.length != 15 && IDStr.length != 18) { + errorInfo = "身份证号码长度应该为15位或18位。" + return false + } + // =======================(end)======================== + + // ================ 数字 除最后以为都为数字================ + if (IDStr.length == 18) { + Ai = IDStr.substring(0, 17) + } else if (IDStr.length == 15) { + Ai = IDStr.substring(0, 6) + "19" + IDStr.substring(6, 15) + } + if (isNumeric(Ai) == false) { + errorInfo = "身份证15位号码都应为数字 ; 18位号码除最后一位外,都应为数字。" + return false + } + // =======================(end)======================== + + // ================ 出生年月是否有效 ================ + val strYear = Ai.substring(6, 10) // 年份 + val strMonth = Ai.substring(10, 12) // 月份 + val strDay = Ai.substring(12, 14) // 月份 + if (isDataFormat("$strYear-$strMonth-$strDay") == false) { + errorInfo = "身份证生日无效。" + return false + } + val gc = GregorianCalendar() + val s = SimpleDateFormat("yyyy-MM-dd") + try { + if (gc[Calendar.YEAR] - strYear.toInt() > 150 + || gc.time.time - s.parse( + "$strYear-$strMonth-$strDay" + ).time < 0 + ) { + errorInfo = "身份证生日不在有效范围。" + return false + } + } catch (e: NumberFormatException) { + // TODO Auto-generated catch block + e.printStackTrace() + } catch (e: ParseException) { + // TODO Auto-generated catch block + e.printStackTrace() + } + if (strMonth.toInt() > 12 || strMonth.toInt() == 0) { + errorInfo = "身份证月份无效" + return false + } + if (strDay.toInt() > 31 || strDay.toInt() == 0) { + errorInfo = "身份证日期无效" + return false + } + // =====================(end)===================== + + // ================ 地区码时候有效================ + val h = GetAreaCode() + if (h[Ai.substring(0, 2)] == null) { + errorInfo = "身份证地区编码错误。" + return false + } + // ============================================== + + // ================ 判断最后一位的值================ + var TotalmulAiWi = 0 + for (i in 0..16) { + TotalmulAiWi = (TotalmulAiWi + + Ai[i].toString().toInt() * Wi[i].toInt()) + } + val modValue = TotalmulAiWi % 11 + val strVerifyCode = ValCodeArr[modValue] + Ai = Ai + strVerifyCode + if (IDStr.length == 18) { + if (Ai == IDStr == false) { + errorInfo = "身份证无效,不是合法的身份证号码" + return false + } + } else { + return true + } + // =====================(end)===================== + return true + } + + /** + * 功能:判断字符串是否为数字 + * + * @param str + * @return + */ + private fun isNumeric(str: String): Boolean { + val pattern = Pattern.compile("[0-9]*") + val isNum = pattern.matcher(str) + return if (isNum.matches()) { + true + } else { + false + } + } + + /** + * 功能:设置地区编码 + * + * @return Hashtable 对象 + */ + private fun GetAreaCode(): Hashtable { + val hashtable = Hashtable() + hashtable["11"] = "北京" + hashtable["12"] = "天津" + hashtable["13"] = "河北" + hashtable["14"] = "山西" + hashtable["15"] = "内蒙古" + hashtable["21"] = "辽宁" + hashtable["22"] = "吉林" + hashtable["23"] = "黑龙江" + hashtable["31"] = "上海" + hashtable["32"] = "江苏" + hashtable["33"] = "浙江" + hashtable["34"] = "安徽" + hashtable["35"] = "福建" + hashtable["36"] = "江西" + hashtable["37"] = "山东" + hashtable["41"] = "河南" + hashtable["42"] = "湖北" + hashtable["43"] = "湖南" + hashtable["44"] = "广东" + hashtable["45"] = "广西" + hashtable["46"] = "海南" + hashtable["50"] = "重庆" + hashtable["51"] = "四川" + hashtable["52"] = "贵州" + hashtable["53"] = "云南" + hashtable["54"] = "西藏" + hashtable["61"] = "陕西" + hashtable["62"] = "甘肃" + hashtable["63"] = "青海" + hashtable["64"] = "宁夏" + hashtable["65"] = "新疆" + hashtable["71"] = "台湾" + hashtable["81"] = "香港" + hashtable["82"] = "澳门" + hashtable["91"] = "国外" + return hashtable + } + + /** + * 验证日期字符串是否是YYYY-MM-DD格式 + * + * @param str + * @return + */ + private fun isDataFormat(str: String): Boolean { + var flag = false + // String + // regxStr="[1-9][0-9]{3}-[0-1][0-2]-((0[1-9])|([12][0-9])|(3[01]))"; + val regxStr = + "^((\\d{2}(([02468][048])|([13579][26]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])))))|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))(\\s(((0?[0-9])|([1-2][0-3]))\\:([0-5]?[0-9])((\\s)|(\\:([0-5]?[0-9])))))?$" + val pattern1 = Pattern.compile(regxStr) + val isNo = pattern1.matcher(str) + if (isNo.matches()) { + flag = true + } + return flag + } + //2.判断字符串是否是邮箱: + /** + * 描述:是否是邮箱. + * + * @param str 指定的字符串 + * @return 是否是邮箱:是为true,否则false + */ + fun isEmail(str: String): Boolean { + var isEmail = false + val expr = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$" + if (str.matches(expr.toRegex())) { + isEmail = true + } + return isEmail + } + //3.判断字符串是否是银行卡 + /** + * 判断是否是银行卡号 + * + * @param cardId + * @return + */ + fun checkBankCard(cardId: String): Boolean { + val bit = getBankCardCheckCode( + cardId + .substring(0, cardId.length - 1) + ) + return if (bit == 'N') { + false + } else cardId[cardId.length - 1] == bit + } + + private fun getBankCardCheckCode(nonCheckCodeCardId: String?): Char { + if (nonCheckCodeCardId == null || nonCheckCodeCardId.trim { it <= ' ' }.length == 0 || !nonCheckCodeCardId.matches("\\d+".toRegex())) { + // 如果传的不是数据返回N + return 'N' + } + val chs = nonCheckCodeCardId.trim { it <= ' ' }.toCharArray() + var luhmSum = 0 + var i = chs.size - 1 + var j = 0 + while (i >= 0) { + var k = chs[i].code - '0'.code + if (j % 2 == 0) { + k *= 2 + k = k / 10 + k % 10 + } + luhmSum += k + i-- + j++ + } + return if (luhmSum % 10 == 0) '0' else (10 - luhmSum % 10 + '0'.code).toChar() + } + //4、判断字符串是否是手机号 + /** + * 判断是否是手机号 + * + * @param phone + * @return + */ + fun checkPhone(phone: String?): Boolean { + val pattern = Pattern + .compile("^(13[0-9]|15[0-3]|15[5-9]|18[0-9]|14[57]|17[0678])\\d{8}$") + val matcher = pattern.matcher(phone) + return if (matcher.matches()) { + true + } else false + } + //5.判断字符串是否是中文或者包含中文 + /** + * 描述:判断一个字符串是否为null或空值. + * + * @param str 指定的字符串 + * @return true or false + */ + fun isEmpty(str: String?): Boolean { + return str == null || str.trim { it <= ' ' }.length == 0 + } + + /** + * 描述:是否是中文. + * + * @param str 指定的字符串 + * @return 是否是中文:是为true,否则false + */ + fun isChinese(str: String): Boolean { + var isChinese = true + val chinese = "[\u0391-\uFFE5]" + if (!isEmpty(str)) { + //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 + for (i in 0 until str.length) { + //获取一个字符 + val temp = str.substring(i, i + 1) + //判断是否为中文字符 + if (temp.matches(chinese.toRegex())) { + } else { + isChinese = false + } + } + } + return isChinese + } + + /** + * 描述:是否包含中文. + * + * @param str 指定的字符串 + * @return 是否包含中文:是为true,否则false + */ + fun isContainChinese(str: String): Boolean { + var isChinese = false + val chinese = "[\u0391-\uFFE5]" + if (!isEmpty(str)) { + //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 + for (i in 0 until str.length) { + //获取一个字符 + val temp = str.substring(i, i + 1) + //判断是否为中文字符 + if (temp.matches(chinese.toRegex())) { + isChinese = true + } else { + } + } + } + return isChinese + } + + /** + * 比较两个String的list 是否改变过 + * + * @param listNew + * @param listOld + * @return + */ + fun compareList(listNew: List?, listOld: List?): Boolean { + return if (listOld == null && listNew == null) { + false + } else if (listOld != null && listNew != null) { + if (listNew.size != listOld.size) { + true + } else !(listOld.containsAll(listNew) && listNew.containsAll(listOld)) + } else { + true + } + } + + /** + * 比较两个String 是否改变过 + * + * @param newStr + * @param oldStr + * @return + */ + fun compareString(newStr: String?, oldStr: String?): Boolean { + return if (newStr == null && oldStr == null) { + false + } else if (newStr != null && oldStr != null) { + newStr != oldStr + } else { + true + } + } + + /** + * 比较签名 是否改变过 + * + * @param newStr + * @param oldStr + * @return + */ + fun compareSign(newStr: String, oldStr: String): Boolean { + return if (TextUtils.isEmpty(oldStr)) { //当原始值没有的时候 无需验证 因为上报时需验证是否已签过字 而且当原始值有的时候 依据现有业务新值不可能清空 故无需再判断其他情况 + true + } else newStr != oldStr + } + + /** + * 随机生成一个UUID + */ + fun createUUID(): String { + return UUID.randomUUID().toString() + } + + fun createUUIDFromLong(): String { + return UUID(ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong()).toString() + } + + /** + * MD5加密 + */ + fun getMD5String(data: String): String? { + try { + val md = MessageDigest.getInstance("MD5") + md.update(data.toByteArray()) + val result = md.digest() + val stringBuffer = StringBuffer() + for (i in result.indices) { + val hex = Integer.toHexString(0xff and result[i].toInt()) + if (hex.length == 1) stringBuffer.append('0') + stringBuffer.append(hex) + } + return stringBuffer.toString() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return null + } + + /** + * 对数组进行MD5加密 + * + * @param bytes + * @return + */ + fun getMD5Byte(bytes: ByteArray?): String? { + try { + val md = MessageDigest.getInstance("MD5") + md.update(bytes) + val result = md.digest() + val stringBuffer = StringBuffer() + for (i in result.indices) { + val hex = Integer.toHexString(0xff and result[i].toInt()) + if (hex.length == 1) stringBuffer.append('0') + stringBuffer.append(hex) + } + return stringBuffer.toString() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return null + } + + //两个数组进行相加 + fun addByte(array1: ByteArray, array2: ByteArray): ByteArray { + val combined = ByteArray(array1.size + array2.size) + System.arraycopy(array1, 0, combined, 0, array1.size) + System.arraycopy(array2, 0, combined, array1.size, array2.size) + return combined + } + + //格式化时间yyyy-mm-dd + fun getSimpleYYYYMMDD(str: String): String { + if (TextUtils.isEmpty(str)) return "" + + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA) + val date = simpleDateFormat.parse(str) + return simpleDateFormat.format(date!!) + + } + + + /** + * 将字符串中的unicode字符转换为中文字符 + */ + fun convertUnicodeToCh(str: String): String { + var newStr = str + val pattern: Pattern = Pattern.compile("(\\\\u(\\w{4}))") + val matcher: Matcher = pattern.matcher(newStr) + + // 迭代,将str中的所有unicode转换为正常字符 + while (matcher.find()) { + val unicodeFull = matcher.group(1) // 匹配出的每个字的unicode,比如\u83b7 + val unicodeNum = matcher.group(2) // 匹配出每个字的数字,比如\u83b7,会匹配出u83b7 + + // 将匹配出的数字按照16进制转换为10进制,转换为char类型,就是对应的正常字符了 + @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + val singleChar = unicodeNum.toInt(16).toChar() + + // 替换原始字符串中的unicode码 + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + newStr = newStr.replace(unicodeFull, singleChar.toString() + "") + } + return newStr + } + + // 解析函数 + fun parseAlipayResult(rawResult: String): Map { + val resultMap = mutableMapOf() + val pairs = rawResult.split("&") + + for (pair in pairs) { + val keyValue = pair.split("=") + if (keyValue.size == 2) { + val key = URLDecoder.decode(keyValue[0], "UTF-8") + val value = URLDecoder.decode(keyValue[1], "UTF-8") + resultMap[key] = value + } + } + return resultMap + } +} + diff --git a/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt index ce376fa..ed09ad8 100644 --- a/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt @@ -3,13 +3,22 @@ package com.img.rabbit.utils import android.content.Context import android.content.Intent import androidx.core.net.toUri +import com.img.rabbit.WebViewActivity object UrlLinkUtils { - fun openAgreement(context: Context, url: String) { - // 打开服务协议 - Intent(Intent.ACTION_VIEW, url.toUri()).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }.let { intent -> + fun openAgreement(context: Context, title: String, url: String, isExternal:Boolean = false) { + if(isExternal){ + // 打开服务协议 + Intent(Intent.ACTION_VIEW, url.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }.let { intent -> + context.startActivity(intent) + } + }else{ + val intent = Intent(context, WebViewActivity::class.java).apply { + putExtra("url", url) + putExtra("title", title) + } context.startActivity(intent) } } diff --git a/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..10c68e5 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt @@ -0,0 +1,26 @@ +package com.img.rabbit.viewmodel +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +open class BaseViewModel : ViewModel() { + var isShowMsg = mutableStateOf(false) + var msgContent = mutableStateOf("") + val isLoading = mutableStateOf(false) + + fun mLaunch(block: suspend () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + try { + block() + } catch (e: Exception) { + withContext(Dispatchers.Main){ + isShowMsg.value = true + msgContent.value = "接口请求失败" + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt index 2bd5445..f462ce6 100644 --- a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt +++ b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt @@ -11,7 +11,12 @@ import android.os.Build import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.img.rabbit.provider.api.ApiManager +import com.img.rabbit.provider.storage.PreferenceUtil import com.tencent.mmkv.MMKV +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch @SuppressLint("ObsoleteSdkInt") class GeneralViewModel(application: Application) : AndroidViewModel(application) { @@ -38,6 +43,12 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application) } } + private val _agreementStatus = MutableLiveData() + val agreementStatus: LiveData = _agreementStatus + private fun getIsAgreement(): Boolean{ + return kv.getBoolean("isAgreement", false) + } + init { // 注册网络监听 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -51,6 +62,8 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application) // 初始化状态 _networkStatus.value = isNetworkAvailable() + // 初始化隐私政策状态 + _agreementStatus.value = getIsAgreement() } private fun isNetworkAvailable(): Boolean { @@ -69,8 +82,23 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application) _isNavigationBarVisible.value = visible } + fun setIsAgreement(agreement: Boolean){ + kv.putBoolean("isAgreement", agreement) + _agreementStatus.value = agreement + } + override fun onCleared() { super.onCleared() connectivityManager.unregisterNetworkCallback(networkCallback) } + + @OptIn(DelicateCoroutinesApi::class) + fun getServerTime() { + GlobalScope.launch { + val response = ApiManager.serviceVo.getServerTime() + if (response.status) { + PreferenceUtil.setTimeDiff(response.data - System.currentTimeMillis() / 1000) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt index c42d8e9..2c3f7bf 100644 --- a/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt @@ -1,29 +1,69 @@ package com.img.rabbit.viewmodel import android.app.Activity +import android.content.Context +import android.os.Build import android.util.Log +import android.widget.Toast +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel import androidx.compose.runtime.State +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.alipay.sdk.app.AuthTask import com.img.rabbit.pages.LoginScreenType import com.g.gysdk.GYManager import com.g.gysdk.GYResponse import com.g.gysdk.GyCallBack import com.g.gysdk.GyConfig -import com.img.rabbit.bean.OnekeyPreLogin +import com.github.gzuliyujiang.oaid.DeviceIdentifier +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.img.rabbit.bean.local.ErrorBean +import com.img.rabbit.bean.local.OnekeyPreLogin +import com.img.rabbit.bean.local.WxBean +import com.img.rabbit.bean.response.UserEntity +import com.img.rabbit.bean.response.UserConfigEntity +import com.img.rabbit.config.Constants +import com.img.rabbit.provider.api.ApiManager +import com.img.rabbit.provider.api.ResultVo +import com.img.rabbit.provider.storage.PreferenceUtil +import com.img.rabbit.utils.MMKVUtils +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.internal.platform.PlatformRegistry.applicationContext -class LoginViewModel : ViewModel() { +class LoginViewModel : BaseViewModel() { private val TAG = "LoginViewModel" private val ONEKEY_TAG = "OneKeyLoginViewModel" + + private lateinit var api: IWXAPI + + private val _wxState = MutableLiveData() + val wxState: LiveData = _wxState + + fun updateWxState(newState: WxBean) { + _wxState.value = newState + } + +// private val _authInfo = MutableLiveData() + val authInfoForAlipay: MutableState = mutableStateOf("") + val loginScreenType = mutableStateOf(LoginScreenType.LOGIN_NORMAL) // 登录用户名 val userName = mutableStateOf("") // 登录验证码 val captcha = mutableStateOf("") + // 登录验证码发送时间戳 + val captchaTimestamp = mutableStateOf("") // 是否同意政策协议 - private val _policyAgreement = mutableStateOf(false) + private val _policyAgreement = mutableStateOf(true) val isPolicyAgreement: State = _policyAgreement @@ -44,7 +84,10 @@ class LoginViewModel : ViewModel() { _policyAgreement.value = isAgreement } + //用户配置 + val _userConfig = MutableLiveData() + val userConfig: UserConfigEntity? get() = _userConfig.value private val _isLogin = mutableStateOf(false) val isLogin: State = _isLogin @@ -53,6 +96,31 @@ class LoginViewModel : ViewModel() { _isLogin.value = isLogin } + + + val loginState = mutableStateOf?>(null) + val errorState = mutableStateOf(null) + + fun requestUserConfig(){ + mLaunch { + val oaid = MMKVUtils.getString("oaid") ?: "" + val response = ApiManager.serviceVo.getUserConfig(oaid, Build.VERSION.SDK_INT, "", DeviceIdentifier.getAndroidID(applicationContext), MMKVUtils.getString("gt_cid") ?: "") + if (response.status) { + PreferenceUtil.saveXToken(response.data.token) + PreferenceUtil.setTimeDiff(response.data.nowtime.toLong() - System.currentTimeMillis() / 1000) + _userConfig.postValue(response.data) + + val resultJson = Gson().toJson(response.data) + MMKVUtils.put("userConfig", resultJson) + + Log.w("LoginViewModel", "获取配置成功: $resultJson") + }else{ + Log.w("LoginViewModel", "获取配置失败: code=${response.code}, message=${response.message}") + } + isLoading.value = false // 加载完成 + } + } + fun oneKeyLoginForGeTuiSdk(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) { // 初始化 SDK GYManager.getInstance().init(GyConfig.with(activity.applicationContext).callBack(object : GyCallBack { @@ -141,11 +209,203 @@ class LoginViewModel : ViewModel() { } } + fun requestOneKeyLogin(gyuid: String, token: String) { + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonOneKey = JsonObject() +// jsonOneKey.addProperty("gyuid", gyuid) +// jsonOneKey.addProperty("token", token) +// val jsonObject = JsonObject() +// jsonObject.addProperty("login_type", "onekey") +// jsonObject.add("onekey", jsonOneKey) +// jsonObject.addProperty("bind", false) + + jsonOneKey.addProperty("gyuid", gyuid) + jsonOneKey.addProperty("token", token) + val jsonObject = JsonObject() + jsonObject.addProperty("type", "onekey") + jsonObject.addProperty("bind", "") + jsonObject.add("data", jsonOneKey) + + requestLogin(jsonObject) + } + /** * 请求验证码 */ - fun requestCaptcha() { - // TODO: 发送请求获取验证码 + fun requestCaptcha(phone: String) { + // 发送请求获取验证码 + isLoading.value = true // 开始加载 + + mLaunch { + // 调用 API 获取数据 + val jsonObject = JsonObject() + jsonObject.addProperty("phone", phone) + val response = ApiManager.serviceVo.sendCode(jsonObject.toString().toRequestBody()) + if (response.status) { + Log.w("LoginViewModel", "请求验证码成功: ${response.data.timestamp}") + captchaTimestamp.value = response.data.timestamp + }else{ + errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "获取验证码失败" }) + } + isLoading.value = false // 加载完成 + } + } + + /** + * 请求登录(验证码) + */ + fun requestLoginForCaptcha(phone: String, code: String) { + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonPhone = JsonObject() + jsonPhone.addProperty("timestamp", captchaTimestamp.value) + jsonPhone.addProperty("phone", phone) + jsonPhone.addProperty("code", code) + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "phone") + jsonObject.add("phone", jsonPhone) + + requestLogin(jsonObject) + } + + + fun initWXApi(context: Context) { + api = WXAPIFactory.createWXAPI(context, Constants.WechatAppId) + } + + fun loginWithWechat(context: Context) { + if (isPolicyAgreement.value) { + doWxAuth(context) + }else{ + Toast.makeText(context, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT).show() + } + } + + /** + * 拿着微信授权码完成登录(在WXEntryActivity中调用) + * @param wechatCode 微信授权码 + */ + fun requestWxLogin(wechatCode: String) { + if(wechatCode.isEmpty()){ + return + } + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonWx = JsonObject() + jsonWx.addProperty("code", wechatCode) + jsonWx.addProperty("code_type", "") + + val jsonObject = JsonObject() + jsonObject.addProperty("login_type", "weixin") + jsonObject.add("weixin", jsonWx) + + requestLogin(jsonObject) + } + + //获取微信授权 + private fun doWxAuth(context: Context) { + if (!api.isWXAppInstalled) { + Toast.makeText(context, "您没有安装微信客户端,请先下载安装", Toast.LENGTH_SHORT).show() + return + } + val req = SendAuth.Req() + req.scope = "snsapi_userinfo" // 只能填 snsapi_userinfo + req.state = context.packageName + Math.random() * 1000 + "_phone" + api.sendReq(req) + } + + + /** + * 支付宝登录 + * authInfo: 该参数需由后端生成并加签,包含 app_id、pid、target_id 等信息 + */ + fun loginWithAliPay(context: Context,onAuthResult: (Map) -> Unit) { + if(authInfoForAlipay.value.isEmpty()){ + Toast.makeText(context, "请先获取支付宝登录授权码", Toast.LENGTH_SHORT).show() + return + } + // 发送请求获取支付宝登录授权码 + mLaunch { + onAuthResult(doAlipayLogin(context as Activity, authInfoForAlipay.value)) + } + } + + /** + * 请求支付宝登录参数 + */ + fun requestAliPayAuthParam() { + isLoading.value = true // 开始加载 + + mLaunch { + val response = ApiManager.serviceVo.getAlipayAuthParam() + val data = response.data + val param = data.param + + authInfoForAlipay.value = param + isLoading.value = false // 加载完成 + } + } + + /** + * 封装支付宝登录逻辑 + * @param activity 当前 Activity 上下文 + * @param authInfo 后端生成的授权字符串 + * @return 支付宝返回的原始 Map 结果 + */ + private suspend fun doAlipayLogin(activity: Activity, authInfo: String): Map { + return withContext(Dispatchers.IO) { + // 初始化 AuthTask + val authTask = AuthTask(activity) + + // 调用 authV2。第二个参数为 true 表示如果未安装支付宝则展示 Loading 界面, 该方法会阻塞当前线程直到用户操作结束 + val result = authTask.authV2(authInfo, true) + Log.i(TAG, "doAlipayLogin: $result") + + result ?: emptyMap() + } + } + + /** + * 拿着微信授权码完成登录(在WXEntryActivity中调用) + * @param authCode 微信授权码 + */ + fun requestAlipayLogin(authCode: String) { + if(authCode.isEmpty()){ + return + } + isLoading.value = true // 开始加载 + + // 调用 API 获取数据 + val jsonWx = JsonObject() + jsonWx.addProperty("auth_code", authCode)//code + + val jsonObject = JsonObject() + jsonObject.addProperty("type", "alipay") + jsonObject.addProperty("bind", "") + jsonObject.add("data", jsonWx) + + requestLogin(jsonObject) + } + + private fun requestLogin(jsonObject: JsonObject){ + mLaunch { + val response = ApiManager.serviceVo.login(jsonObject.toString().toRequestBody()) + if (response.status) { + loginState.value = response + val userEntity = response.data + userEntity.isLogin = true + //记录登录数据 + PreferenceUtil.saveUserInfo(userEntity) + }else{ + errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "登录失败" }) + } + isLoading.value = false // 加载完成 + } } } \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt b/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt new file mode 100644 index 0000000..62341b6 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt @@ -0,0 +1,63 @@ +package com.img.rabbit.viewmodel.`interface` + +import com.img.rabbit.bean.response.AlipayParamEntity +import com.img.rabbit.bean.response.CaptchaCodeEntity +import com.img.rabbit.bean.response.UserEntity +import com.img.rabbit.bean.response.UserConfigEntity +import com.img.rabbit.provider.api.ResultVo +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface ServiceVo { + /* + @POST("/dictionary/getTsSyspara") + suspend fun requestHospitalName(): Response>> + @POST("/dictionary/getLogo") + suspend fun requestHospitalLogo(): Response> + @POST("/nurse/login") + suspend fun requestLogin(@Body request: LoginRequest): Response> + */ + + /** + * 获取服务器时间 + */ + @GET("/api/time") + suspend fun getServerTime(): ResultVo + + /** + * 获取客户端配置 + */ + @GET("/api/user/config") + suspend fun getUserConfig( + @Query("oaid") oaid: String, + @Query("os_version") osVersion: Int, + @Query("ua") ua: String, + @Query("imei") imei: String, + @Query("cid") cid: String, + ): ResultVo + + @GET("api/alipay/app_param") + suspend fun getAlipayAuthParam(): ResultVo + + /** + * 发送验证码 + */ + @POST("api/user/code") + suspend fun sendCode(@Body requestBody: RequestBody): ResultVo + + /** + * 登录 + */ + @POST("/api/user/login") + suspend fun login(@Body requestBody: RequestBody): ResultVo + + + /** + * 退出登录 + */ + @POST("/mapi/user/logout") + suspend fun logout(): ResultVo +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt b/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt new file mode 100644 index 0000000..2e19ac4 --- /dev/null +++ b/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt @@ -0,0 +1,34 @@ +@file:OptIn(DelicateCoroutinesApi::class) + +package com.img.rabbit.wxapi + +import com.img.rabbit.provider.storage.GlobalStateManager +import com.tencent.mm.opensdk.constants.ConstantsAPI +import com.tencent.mm.opensdk.modelbase.BaseResp +import com.tencent.mm.opensdk.modelmsg.SendAuth +import com.umeng.socialize.weixin.view.WXCallbackActivity +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + + +class WXEntryActivity : WXCallbackActivity() { + override fun onResp(resp: BaseResp?) { + if (resp?.type == ConstantsAPI.COMMAND_SENDAUTH) { + when (resp.errCode) { + BaseResp.ErrCode.ERR_OK -> { + val authResp = resp as SendAuth.Resp + //val wxState = WxBean(code = authResp.code,state = authResp.state) + + GlobalScope.launch { + GlobalStateManager(this@WXEntryActivity).storeGlobalWxAuthorization(authResp.code) + + finish() + } + } + + else -> {} + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_alipay_icon.xml b/app/src/main/res/drawable/ic_alipay_icon.xml new file mode 100644 index 0000000..5e96051 --- /dev/null +++ b/app/src/main/res/drawable/ic_alipay_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wx_icon.xml b/app/src/main/res/drawable/ic_wx_icon.xml new file mode 100644 index 0000000..3c32362 --- /dev/null +++ b/app/src/main/res/drawable/ic_wx_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/layout_web.xml b/app/src/main/res/layout/layout_web.xml new file mode 100644 index 0000000..82b3f17 --- /dev/null +++ b/app/src/main/res/layout/layout_web.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-xxhdpi/ic_copy.webp b/app/src/main/res/mipmap-xxhdpi/ic_copy.webp new file mode 100644 index 0000000000000000000000000000000000000000..6b804b10ba252a14c143489b3fe2a498f649a509 GIT binary patch literal 1380 zcmWIYbaRVgWnc(*bqWXzu!!JdU|=u+VofmebaoCn!3g9sFrEOBEI_H;ypp0IcPE92 z$S4K&2Mmlr2nLDC#RU)+RuZV{+ck*V%eq)q;1}dfNi9wWD)|7!Dn&rcfo>83vPIG% zYz+`Q3BtAkvCB&eN`PttK=2?(*InG6iuTNxN6ZXv`}7BDdI zw=ghlJC6`Ugh@eSQ6ktl79h!#md3#FX*mM}ZzKbQ@C612u4u3vkig+K1)!7+&|UdJ zn9h*HkjkLI;K`83kj$V5WElY?)sVpq7{flf%nS_rnhsn7_aAFa$_<}c{Qv(GS#6UA zLj4LV>F;h7$aqd#9CH5d!umZM7_~nN@?4yyd3>UZrPtRxvcHaVOK%X8KPa$vX3?E( zy$bBkmEXVBGX}gbu&|ix_wD!IyLrzmY_{Fan|nU}u)&uKn;bLg`RCKSj~@E{cUy!` zjh+1Fn`gfN{`$9W?e*8pRd(}3uTEZ4D0BVp@BjbX5+^MA|36=X?f?JyNt0|sm+mgz zo;mB?|Np`JRT%q!^**k+Ya3cF-S&|6Ro<&rToZPw*ejHJE?c!q+#^=#Zfxk>S-<>0 zty=Z!3gi4=DwYZlSc6mtO0|7L+m7GhDEuzm(9N6mV0}f?3HbAuYce5{XDz=_ho+@JN_9L-1Z1uV%x^| zzfr|=5;k<{-NyCp?wD zb^GEwHyyF=qe&ajHcPWsyxH0@y;`(FQm>Eg@2=DT3KGvwYHF`CoGX7T&PC^#*`g%x z9mQud->-F(j6bsQO4pPY{sI;8ek)dCCDo(rBPMXWJTw(jQccxz+dOf@N0$uQ3;ON_ z;>)Xe+0wV{SX09^BPTO6bCr%J&l5imf1Yr;*gsWb{P9;~UVbRuYa5n0O=-iQm7T@U zpYQQ?xfXT!{qc`~UIbJ_!1V_qpTZbtolSDvzwvsaxaIncXOiMqo#y!O&|c*7?#v4N z%VL7D7uc3K1y%Kz&hLoaBYN6Zzw@_(Nsmr6VP7q5^B{-JfzwT3<2?NUYC+yyeu z4wc)QUI>2k+@&}1%c+a(Ca;XX=}z4^vpz=g*~G=N87IRG7A&-`-;`X{uAcrcCCcx( z+`eGf3FecX7k@r3zdwTeNw=Q+*7HxV{NlcH{qRva^ZyN9Nj0im{z1JsdkpIo!ZzCo_MU!!82MtSi;>xBrRuc(~dJn+xBG_^Gy^xEp|n) z^O@Mq%XU^rlU~U$(77toR=njw!j_63tP#G){dc^sWtlR&**7-&{terz%OCazDLm%R scJ>kcKJ$6u`X>>Fp}s19=Sm+xe^gSxP<={r)XAgH