1、接入登录接口
2、登录UI优化修改
3、添加依赖
This commit is contained in:
shenzuqiang 2026-02-26 13:51:42 +08:00
parent 71e9917da3
commit 19dc61e9af
66 changed files with 3136 additions and 356 deletions

View File

@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-26T05:37:22.871237900Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=JRBI89BIE6AI5TG6" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

View File

@ -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) //解密
}

Binary file not shown.

BIN
app/libs/humesdk-1.0.0.aar Normal file

Binary file not shown.

View File

@ -27,6 +27,22 @@
<!--开关wifi状态解决国内机型移动网络权限问题需要-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<queries>
<package android:name="com.eg.android.AlipayGphone" /> <!-- 支付宝 -->
<package android:name="hk.alipay.wallet" /> <!-- AlipayHK -->
</queries>
<queries>
<package android:name="com.tencent.mm" />
</queries>
<queries>
<intent>
<action android:name="com.getui.sdk.action" />
</intent>
</queries>
<application
android:name=".BaseApplication"
android:allowBackup="true"
@ -48,7 +64,8 @@
<activity
android:name="com.img.rabbit.MainActivity"
android:theme="@style/SplashTheme"
android:exported="true">
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -56,6 +73,22 @@
</intent-filter>
</activity>
<activity
android:name=".WebViewActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity
android:name=".wxapi.WXEntryActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@ -65,6 +98,7 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepath_data" />
</provider>
</application>
</manifest>

View File

@ -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() {

View File

@ -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

View File

@ -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)
}
}

View File

@ -1,7 +0,0 @@
package com.img.rabbit.bean
data class UserInfo(
val id: Int,
val name: String,
val login: Boolean,
)

View File

@ -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<String, String>.toAlipayResult(): AlipayBean {
return AlipayBean(
resultStatus = this["resultStatus"],
result = this["result"],
memo = this["memo"]
)
}

View File

@ -1,4 +1,4 @@
package com.img.rabbit.bean
package com.img.rabbit.bean.local
data class ClothingBean(
//衣服索引(区分男女)

View File

@ -0,0 +1,3 @@
package com.img.rabbit.bean.local
data class ErrorBean(var code: String,var message: String)

View File

@ -1,4 +1,4 @@
package com.img.rabbit.bean
package com.img.rabbit.bean.local
data class FormatBean(
//格式id

View File

@ -1,4 +1,4 @@
package com.img.rabbit.bean
package com.img.rabbit.bean.local
data class HairstyleBean(
//发型索引(区分男女)

View File

@ -1,4 +1,4 @@
package com.img.rabbit.bean
package com.img.rabbit.bean.local
import android.graphics.Bitmap
import android.net.Uri

View File

@ -1,4 +1,4 @@
package com.img.rabbit.bean
package com.img.rabbit.bean.local
import kotlinx.serialization.Serializable

View File

@ -1,4 +1,4 @@
package com.img.rabbit.bean
package com.img.rabbit.bean.local
data class ResizeBean(
//尺寸id

View File

@ -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,
)

View File

@ -0,0 +1,6 @@
package com.img.rabbit.bean.local
data class WxBean(
val code: String,
val state: String
)

View File

@ -0,0 +1,8 @@
package com.img.rabbit.bean.response
import kotlinx.serialization.Serializable
@Serializable
data class AlipayParamEntity(
val param: String = ""
)

View File

@ -0,0 +1,8 @@
package com.img.rabbit.bean.response
import kotlinx.serialization.Serializable
@Serializable
data class CaptchaCodeEntity(
val timestamp: String = ""
)

View File

@ -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<String>? = emptyList()
@SerializedName("client.ad.switch") //广告总开关
var adSwitch: Boolean = false
@SerializedName("client.service.phone") //客服电话
var servicePhoneList: List<String> = emptyList()
@SerializedName("client.chatwarning") //聊天安全提示
var chatWarning: String? = null
@SerializedName("client.travel.ad") //聊天安全提示
val travelAd: List<String> = emptyList()
// 圈子-顶部banner占位图配置
@SerializedName("client.team.ad") //聊天安全提示
val teamAd: List<String> = emptyList()
}

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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
)

View File

@ -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
}

View File

@ -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 = ""
)

View File

@ -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 = ""
)

View File

@ -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
/**
* 底部画板(证件)选择器

View File

@ -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"
}

View File

@ -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 {
//背景颜色

View File

@ -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"
}

View File

@ -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())
}
}
@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)
}

View File

@ -196,7 +196,8 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
LoginScreen(
navController = navController,
generalViewModel = generalViewModel,
loginViewModel = loginViewModel
loginViewModel = loginViewModel,
isVisibilityBreak = true
)
}
}

View File

@ -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)
)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
) {

View File

@ -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),
)
)
}

View File

@ -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)
) {

View File

@ -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()
}
}

View File

@ -0,0 +1,9 @@
package com.img.rabbit.provider.api
class ResultVo<T>(val code: Int, val data: T) {
val status: Boolean
get() {
return code == 0
}
val message: String = ""
}

View File

@ -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<Preferences> 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<Boolean?> {
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<String?> {
return context.storeData.data.map {
preferences ->
preferences[GLOBAL_WX_AUTHORIZATION]
}
}
}

View File

@ -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<UserEntity>? {
/**
*[{"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<UserEntity>::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
}
}

View File

@ -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<TrustManager>(
object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?
) {
}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?
) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> =
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)
}
}

View File

@ -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)
}
}
}

View File

@ -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()) + "&timestamp=" + PreferenceUtil.serverTimeMillis() / 1000
} else {
("nonce=" + StringUtils.createUUID()) + "&timestamp=" + 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,"└─────────────────────────────────────────────────────────────────────────────")
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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();
}
}

View File

@ -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 ""
}
}

View File

@ -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(

View File

@ -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 <T : Parcelable> put(key: String, t: T?): Boolean {
if (t == null) {
return false
}
return mmkv?.encode(key, t)!!
}
fun put(key: String, sets: Set<String>?): 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<Class>("")
*/
inline fun <reified T : Parcelable> getParcelable(key: String): T? {
return mmkv?.decodeParcelable(key, T::class.java)
}
fun getStringSet(key: String): Set<String>? {
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()
}
}

View File

@ -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<String, String> {
val hashtable = Hashtable<String, String>()
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<String?>?, listOld: List<String?>?): 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<String, String> {
val resultMap = mutableMapOf<String, String>()
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
}
}

View File

@ -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)
}
}

View File

@ -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 = "接口请求失败"
}
}
}
}
}

View File

@ -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<Boolean>()
val agreementStatus: LiveData<Boolean> = _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)
}
}
}
}

View File

@ -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<WxBean?>()
val wxState: LiveData<WxBean?> = _wxState
fun updateWxState(newState: WxBean) {
_wxState.value = newState
}
// private val _authInfo = MutableLiveData<String>()
val authInfoForAlipay: MutableState<String> = 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<Boolean> = _policyAgreement
@ -44,7 +84,10 @@ class LoginViewModel : ViewModel() {
_policyAgreement.value = isAgreement
}
//用户配置
val _userConfig = MutableLiveData<UserConfigEntity?>()
val userConfig: UserConfigEntity? get() = _userConfig.value
private val _isLogin = mutableStateOf(false)
val isLogin: State<Boolean> = _isLogin
@ -53,6 +96,31 @@ class LoginViewModel : ViewModel() {
_isLogin.value = isLogin
}
val loginState = mutableStateOf<ResultVo<UserEntity>?>(null)
val errorState = mutableStateOf<ErrorBean?>(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_idpidtarget_id 等信息
*/
fun loginWithAliPay(context: Context,onAuthResult: (Map<String, String>) -> 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<String, String> {
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 // 加载完成
}
}
}

View File

@ -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<ResultListVo<MutableList<HospitalNameData>>>
@POST("/dictionary/getLogo")
suspend fun requestHospitalLogo(): Response<ResultListVo<HospitalLogoData>>
@POST("/nurse/login")
suspend fun requestLogin(@Body request: LoginRequest): Response<ResultListVo<LoginData>>
*/
/**
* 获取服务器时间
*/
@GET("/api/time")
suspend fun getServerTime(): ResultVo<Long>
/**
* 获取客户端配置
*/
@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<UserConfigEntity>
@GET("api/alipay/app_param")
suspend fun getAlipayAuthParam(): ResultVo<AlipayParamEntity>
/**
* 发送验证码
*/
@POST("api/user/code")
suspend fun sendCode(@Body requestBody: RequestBody): ResultVo<CaptchaCodeEntity>
/**
* 登录
*/
@POST("/api/user/login")
suspend fun login(@Body requestBody: RequestBody): ResultVo<UserEntity>
/**
* 退出登录
*/
@POST("/mapi/user/logout")
suspend fun logout(): ResultVo<Any>
}

View File

@ -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 -> {}
}
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M902.1,652.9l-251,-84.4s19.3,-28.9 39.9,-85.5c20.6,-56.6 23.5,-87.7 23.5,-87.7l-162.5,-1.3v-55.5l196.7,-1.4v-39.2L552.1,297.9v-89.3h-96.4v89.3L272.1,297.9v39.2l183.6,-1.3v59.5h-147.2v31.1h303.1s-3.3,25.2 -15,56.6c-11.6,31.4 -23.6,58.9 -23.6,58.9s-142.3,-49.8 -217.3,-49.8c-75,0 -166.2,30.1 -175,117.6 -8.8,87.4 42.5,134.7 114.7,152.1 72.3,17.5 139,-0.2 197,-28.6 58.1,-28.4 115.1,-92.9 115.1,-92.9l292.5,142c-11.9,69.3 -72.1,119.9 -142.4,119.8L266.4,902.1c-79.7,0.1 -144.4,-64.5 -144.5,-144.2L121.9,266.4c-0.1,-79.7 64.5,-144.4 144.2,-144.5h491.5c79.7,-0.1 144.4,64.5 144.5,144.2v386.8zM536.3,604s-91.3,115.3 -198.9,115.3c-107.6,0 -130.2,-54.8 -130.2,-94.2 0,-39.3 22.4,-82.1 113.9,-88.3 91.5,-6.2 215.2,67.2 215.2,67.2h-0z"
android:fillColor="#02A9F1"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M669.3,369.4c9.8,0 19.6,0 29.4,1.6C671,245.2 536.9,152 383.2,152 211.6,152 71,269.7 71,416.8c0,85 45.8,156.9 124.2,210.9l-31.1,93.2L273.6,667c39.2,8.2 70.3,16.3 109.5,16.3 9.8,0 19.6,0 31.1,-1.6 -6.5,-21.3 -9.8,-42.5 -9.8,-65.4 0.1,-135.7 116.2,-246.9 264.9,-246.9zM500.9,284.4c24.5,0 39.2,16.3 39.2,39.2 0,22.9 -16.3,39.2 -39.2,39.2 -24.5,0 -47.4,-16.4 -47.4,-39.2 0,-24.5 24.6,-39.2 47.4,-39.2zM284.6,357.5c-24.7,0 -47.8,-16.2 -47.8,-38.8 0,-24.3 24.7,-38.8 47.8,-38.8s39.5,16.2 39.5,38.8c0.1,22.7 -16.4,38.8 -39.5,38.8z"
android:fillColor="#24DB5A"/>
<path
android:pathData="M953.8,613c0,-125.9 -124.2,-227.2 -264.8,-227.2 -148.8,0 -266.5,103 -266.5,227.2 0,125.9 117.7,227.2 266.5,227.2 31.1,0 62.1,-8.2 93.2,-16.3l85,47.4 -22.9,-78.5c62.1,-47.4 109.5,-109.5 109.5,-179.8zM602.3,573.8c-14.7,0 -31.1,-14.7 -31.1,-31.1 0,-14.7 16.3,-31.1 31.1,-31.1 22.9,0 39.2,16.3 39.2,31.1 0,16.4 -14.7,31.1 -39.2,31.1zM780.3,566.2c-14.8,0 -31.3,-14.6 -31.3,-30.7 0,-14.6 16.5,-30.7 31.3,-30.7 23.1,0 39.5,16.2 39.5,30.7 0,16.2 -16.4,30.7 -39.5,30.7z"
android:fillColor="#24DB5A"/>
</vector>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:minHeight="46dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_web_break"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:src="@mipmap/ic_back"/>
<TextView
android:id="@+id/iv_web_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="服务协议"
android:textColor="@color/black"
android:textSize="16dp"
android:layout_gravity="center"/>
</FrameLayout>
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -25,6 +25,10 @@ androidx-test-ext-junit = "1.1.5"
androidx-test-espresso-core = "3.5.1"
navigationRuntimeKtx = "2.9.7"
# Network Version
retrofit = "3.0.0"
okhttp = "5.1.0"
# Third-party version
gson = "2.13.2"
gysdk = "3.2.3.0"
@ -37,6 +41,20 @@ faceDetection = "16.1.5"
foundation = "1.10.2"
androidGifDrawableEncoder = "1.2.30"
gifeEncoder = "0.10.1"
cropify = "0.5.2"
matisse = "2.3.0"
pictureselector = "v3.11.2"
compress = "v3.11.2"
wechatSdkAndroidWithoutMta = "6.8.0"
# Umeng version
umengUmsdkCommon = "9.8.9"
umengUmsdkAsms = "1.8.7.2"
umengUmsdkApm = "2.0.6"
umengUmsdkShareCore = "7.3.7"
tencentHelper = "3.0.6"
#oaid
android_cn_oaid = "4.2.12"
fastaes = "1.1.5"
[libraries]
@ -70,17 +88,41 @@ androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "a
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" }
androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" }
# Network dependencies
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version = "1.0.0" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
# Third-party dependencies
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
gysdk = { module = "com.getui:gysdk", version.ref = "gysdk" }
matisse = { module = "io.github.leavesczy:matisse", version.ref = "matisse" }
mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" }
pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref = "pictureselector" }
compress = { module = "io.github.lucksiege:compress", version.ref = "compress" }
segmentation-selfie = { module = "com.google.mlkit:segmentation-selfie", version.ref = "segmentationSelfie" }
face-detection = { module = "com.google.mlkit:face-detection", version.ref = "faceDetection" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawableEncoder" }
gif-encoder = { module = "com.squareup:gifencoder", version.ref = "gifeEncoder" }
cropify = { module = "com.github.moyuruaizawa:cropify", version.ref = "cropify" }
wechat-sdk = { module = "com.tencent.mm.opensdk:wechat-sdk-android-without-mta", version.ref = "wechatSdkAndroidWithoutMta" }
#Umeng Sdk
umeng-umsdk-common = { module = "com.umeng.umsdk:common", version.ref = "umengUmsdkCommon" }
umeng-umsdk-asms = { module = "com.umeng.umsdk:asms", version.ref = "umengUmsdkAsms" }
#noinspection Aligned16KB
umeng-umsdk-apm = { module = "com.umeng.umsdk:apm", version.ref = "umengUmsdkApm" }
umeng-umsdk-share-core = { module = "com.umeng.umsdk:share-core", version.ref = "umengUmsdkShareCore" }
umeng-umsdk-share-wx = { module = "com.umeng.umsdk:share-wx", version.ref = "umengUmsdkShareCore" }
tencent-helper = { module = "com.tencent.vasdolly:helper", version.ref = "tencentHelper" }
android_cn_oaid = { module = "com.github.gzu-liyujiang:Android_CN_OAID", version.ref = "android_cn_oaid" }
#Decrypt
fastaes = { module = "io.github.billywei01:fastaes", version.ref = "fastaes" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

View File

@ -7,6 +7,10 @@ pluginManagement {
includeGroupByRegex("androidx.*")
}
}
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
maven { url = uri("https://developer.huawei.com/repo/") }
mavenCentral()
gradlePluginPortal()
}
@ -17,9 +21,19 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
maven { url = uri("https://company/com/maven2") }
maven { url = uri("https://repo1.maven.org/maven2/") }
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
maven { url = uri("https://mvn.getui.com/nexus/content/repositories/releases") }
maven { url = uri("https://mvn.getui.com/nexus/content/repositories/releases") } //个推
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
maven { url = uri("https://developer.huawei.com/repo/") }//添加华为仓库 获取oaid
maven { url = uri("https://developer.hihonor.com/repo/") }
maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") } //巨量融合
maven { url = uri("https://repo.eclipse.org/content/repositories/paho-snapshots/") } //mqtt
mavenCentral()
google()
}
}