注册登录对接服务端

master
pan 4 years ago
parent 0e2e960c31
commit 5f5f8bb45c
  1. 14
      app/build.gradle.kts
  2. 16
      app/src/main/AndroidManifest.xml
  3. 2
      app/src/main/java/com/gyf/csams/APP.kt
  4. 7
      app/src/main/java/com/gyf/csams/Api.kt
  5. 56
      app/src/main/java/com/gyf/csams/InitActivity.kt
  6. 67
      app/src/main/java/com/gyf/csams/InitViewModel.kt
  7. 377
      app/src/main/java/com/gyf/csams/MainActivity.kt
  8. 164
      app/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt
  9. 391
      app/src/main/java/com/gyf/csams/account/ui/AccountActivity.kt
  10. 41
      app/src/main/java/com/gyf/csams/ui/Base.kt
  11. 42
      app/src/main/java/com/gyf/csams/ui/MainActivity.kt
  12. 1
      app/src/main/java/com/gyf/csams/util/HttpUtil.kt
  13. 102
      app/src/main/java/com/gyf/csams/util/TokenUtil.kt
  14. 7
      app/src/test/java/com/gyf/csams/ExampleUnitTest.kt
  15. 1
      build.gradle.kts

@ -2,6 +2,7 @@ plugins {
id("com.android.application")
id("kotlin-android")
id("org.jetbrains.kotlin.plugin.serialization") version "1.4.32"
id("kotlin-kapt")
}
android {
@ -117,6 +118,19 @@ dependencies {
* https://kotlinlang.org/docs/serialization.html
*/
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
/**
* https://developer.android.com/jetpack/androidx/releases/navigation
*/
implementation("androidx.navigation:navigation-compose:1.0.0-alpha10")
/**
* https://developer.android.com/jetpack/androidx/releases/room
*/
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
kapt("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
// optional - Test helpers
testImplementation("androidx.room:room-testing:${rootProject.extra["room_version"]}")
//测试
testImplementation("junit:junit:4.13.2")
/**

@ -16,16 +16,26 @@
<activity
android:name=".MainActivity"
android:name=".InitActivity"
android:exported="true"
android:theme="@style/Theme.CSAMS.NoActionBar"
android:windowSoftInputMode="stateVisible|adjustResize" >
android:theme="@style/Theme.CSAMS.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".account.ui.AccountActivity"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="true">
</activity>
<activity
android:name=".ui.MainActivity"
android:exported="true">
</activity>
</application>
</manifest>

@ -6,6 +6,8 @@ import com.orhanobut.logger.DiskLogAdapter
import com.orhanobut.logger.Logger
class APP : Application() {
override fun onCreate() {
super.onCreate()
//初始化日志

@ -5,9 +5,12 @@ interface UrlPath{
fun build():String
}
enum class RegisterApi(val path: String):UrlPath{
enum class AccountApi(val path: String):UrlPath{
register("/register"),
checkId("/register/checkId");
checkId("/register/checkId"),
login("/login"),
loginToken("/login/token");
override fun build(): String {
return "/api/account${this.path}"

@ -0,0 +1,56 @@
package com.gyf.csams
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import com.gyf.csams.account.ui.AccountActivity
import com.gyf.csams.ui.AnimationText
import com.gyf.csams.ui.MainActivity
import com.orhanobut.logger.Logger
class InitActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 检查网络
setContent {
val initViewModel:InitViewModel= viewModel()
initViewModel.checkServer()
val isNetWorkWorking:Boolean? by initViewModel.isNetWorkWorking.observeAsState(null)
when(isNetWorkWorking){
null-> AnimationText(text = "检查服务器网络状态中!!!")
true-> {
Init()
finish()
}
false->{
TODO("无法连接到服务器,请检查本地网络或联系管理员")
}
}
}
}
}
@Composable
private fun Init(initViewModel:InitViewModel= viewModel()){
Logger.i("初始化。。。。")
val context= LocalContext.current
//后台检查token
initViewModel.hasOnlyUserToken(context)
//监听token校验状态
val isValid: Boolean? by initViewModel.token.observeAsState(null)
when (isValid) {
false -> context.startActivity(Intent(context, AccountActivity::class.java))
true -> context.startActivity(Intent(context,MainActivity::class.java))
}
}

@ -0,0 +1,67 @@
package com.gyf.csams
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.gyf.csams.util.*
import com.orhanobut.logger.Logger
import kotlinx.coroutines.launch
data class TokenVo(val token:String,val studentId:String)
class InitViewModel: ViewModel() {
/**
* 服务器网络状态是否正常true=正常false=不正常
*/
private val _isNetWorkWorking = MutableLiveData<Boolean>()
val isNetWorkWorking: LiveData<Boolean> = _isNetWorkWorking
/**
* token
*/
private val _token = MutableLiveData<Boolean>()
val token: LiveData<Boolean> = _token
fun checkServer(){
Logger.i("测试连接到服务端")
_isNetWorkWorking.postValue(true)
}
/**
* 查询本地是否有且只有一个用户token如果有则自动登录
*/
fun hasOnlyUserToken(context: Context){
viewModelScope.launch{
val db=AppDatabase.getInstance(context)
val tokenList=db?.tokenDao()?.queryAll()
if (tokenList != null && tokenList.size == 1) {
val currentToken: Token = tokenList[0]
val url=Api.buildUrl(AccountApi.loginToken)
val action="校验token"
Logger.i("${action}api=$url")
HttpClient.post(url,SimpleCallback<Boolean>(
action=action,
onSuccess = {
_token.postValue(it.body)
Logger.i("token校验结果:${it.body}")
},
onFail = { TODO("token校验失败")},
type = object : TypeToken<ApiResponse<Boolean>>(){}.type
),jsonBody = Gson().toJson(TokenVo(token=currentToken.token,studentId = currentToken.studentId)))
}else if(tokenList != null && tokenList.size > 1){
//TODO 实现切换历史登录帐号
Logger.i("token数量大于一,需要手动登录")
_token.postValue(false)
}else{
Logger.i("本地没有任何token,跳转到登录界面")
_token.postValue(false)
}
}
}
}

@ -1,377 +0,0 @@
package com.gyf.csams
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.gyf.csams.account.model.DialogMessage
import com.gyf.csams.account.model.RegisterViewModel
import com.gyf.csams.ui.theme.CSAMSTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CSAMSTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Register()
RegisterDialog()
}
}
}
}
}
/**
* 注册表单
*
*/
@Composable
fun Register(registerViewModel: RegisterViewModel=viewModel()){
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState=scaffoldState) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
val name: String by registerViewModel.name.observeAsState("")
Text(buildAnnotatedString {
withStyle(
style = MaterialTheme.typography.subtitle1.toSpanStyle()
.copy(color = MaterialTheme.colors.primary)
) {
append(name)
}
withStyle(style = MaterialTheme.typography.subtitle1.toSpanStyle()) {
append(registerViewModel.welcomeStart)
}
withStyle(style = MaterialTheme.typography.subtitle2.toSpanStyle()) {
append(registerViewModel.welcomeEnd)
}
withStyle(
style = MaterialTheme.typography.subtitle2.toSpanStyle()
.copy(color = MaterialTheme.colors.secondary)
) {
append(BuildConfig.APP_NAME)
}
})
StudentId()
Spacer(modifier = Modifier.height(10.dp))
Name(name)
Spacer(modifier = Modifier.height(10.dp))
val isValidForm: Boolean by registerViewModel.isValidForm.observeAsState(false)
if(isValidForm) {
Password()
}
Spacer(modifier = Modifier.height(10.dp))
RegisterButton(isValidForm,scaffoldState = scaffoldState)
}
}
}
}
/**
* 学号
*
* @param registerViewModel
*/
@Composable
fun StudentId(registerViewModel: RegisterViewModel=viewModel()){
Column {
val studentId: String by registerViewModel.studentId.observeAsState("")
val isValidStudentId : Boolean by registerViewModel.isValidStudentId.observeAsState(false)
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = studentId,
onValueChange = { registerViewModel.onStudentIdChange(it) },
label = { Text(text = registerViewModel.studentIdDesc) },
placeholder = { Text(text = registerViewModel.studentIdPlaceholder) },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Number,imeAction = ImeAction.Done),
singleLine = true,
isError = !isValidStudentId
)
if (isValidStudentId) {
val isRepeat:Boolean? by registerViewModel.isRepeat.observeAsState(null)
when(isRepeat){
null-> AnimationText(text = registerViewModel.checkRegTip)
true->
Text(buildAnnotatedString {
append(registerViewModel.studentIdDesc)
withStyle(style = MaterialTheme.typography.body1.toSpanStyle().copy(
color=MaterialTheme.colors.error)){
append(studentId)
}
append(registerViewModel.registered)
})
false->
Text(buildAnnotatedString {
append(registerViewModel.studentIdDesc)
withStyle(style = MaterialTheme.typography.body1.toSpanStyle().copy(
color=MaterialTheme.colors.primary)){
append(studentId)
}
append(registerViewModel.canRegister)
})
}
}else{
Text(
text = registerViewModel.studentIdFormat,
color = MaterialTheme.colors.error,
style = MaterialTheme.typography.body1
)
}
}
}
/**
* 淡入淡出并且颜色变化文本
*
* @param text
*/
@Composable
fun AnimationText(text:String){
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Text(
text = text,
color = color,
style = MaterialTheme.typography.body1
)
}
/**
* 注册弹窗
*
* @param registerViewModel
*/
@Composable
fun RegisterDialog(registerViewModel: RegisterViewModel=viewModel()){
val dialogMsg:DialogMessage? by registerViewModel.dialogMsg.observeAsState(null)
val message=dialogMsg?.userResDto?.password
if(message?.isNotEmpty() == true){
PasswordDialog(message = message)
}
}
/**
* 密码弹窗
*
* @param registerViewModel
* @param message
*/
@Composable
fun PasswordDialog(registerViewModel: RegisterViewModel=viewModel(),message:String){
val button:@Composable () -> Unit = {
Row(horizontalArrangement=Arrangement.Center,modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)) {
OutlinedButton(onClick = { registerViewModel.resetDialogMsg() },
modifier = Modifier.padding(end = 10.dp)) {
Text(text = registerViewModel.confirmDesc)
}
OutlinedButton(onClick = { TODO() },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onBackground)) {
Text(text = registerViewModel.backDesc)
}
}
}
AlertDialog(onDismissRequest = { registerViewModel.resetDialogMsg() },
buttons = button,
title = { Text(text = registerViewModel.title) },
text = {
Text(buildAnnotatedString {
append(registerViewModel.passwordDialogStart)
withStyle(style = MaterialTheme.typography.body1.toSpanStyle()
.copy(color = MaterialTheme.colors.secondary)){
append(message)
}
append(registerViewModel.passwordDialogEnd)
})
})
}
/**
* 姓名
*
* @param registerViewModel
*/
@Composable
fun Name(name:String,registerViewModel: RegisterViewModel=viewModel()){
Column {
val isValidName:Boolean by registerViewModel.isValidName.observeAsState(false)
val focusManager = LocalFocusManager.current
OutlinedTextField(value = name,
onValueChange = {registerViewModel.onNameChange(it)},
label={ Text(text = registerViewModel.nameDesc)},
placeholder = { Text(text = registerViewModel.namePlaceholder)},
singleLine = true,
isError = !isValidName,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done))
if (!isValidName){
Text(text = registerViewModel.nameFormat,
color=MaterialTheme.colors.error)
}
}
}
@Composable
fun Password(registerViewModel: RegisterViewModel=viewModel()) {
Text(text = registerViewModel.passwordTip
,color=MaterialTheme.colors.primary,
modifier = Modifier.fillMaxWidth())
}
/**
* 注册按钮
*
* @param registerViewModel
*/
@Composable
fun RegisterButton(isValidForm:Boolean,scaffoldState:ScaffoldState,registerViewModel: RegisterViewModel=viewModel()){
OutlinedButton(onClick = { registerViewModel.register()},
enabled = isValidForm,
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp)) {
Text(text = registerViewModel.regBtnDesc)
}
OutlinedButton(onClick = { TODO()},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onBackground)) {
Text(text = registerViewModel.backDesc)
}
val snackBarMsg:String by registerViewModel.snackBarMsg.observeAsState("")
if(snackBarMsg!=""){
LaunchedEffect(scaffoldState) {
scaffoldState.snackbarHostState.showSnackbar(
message = snackBarMsg
)
registerViewModel.resetRegisterResMsg()
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
CSAMSTheme {
Row (
horizontalArrangement=Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
val model=RegisterViewModel()
StudentId(model)
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}
@Preview
@Composable
fun AnimationTextPreview(){
AnimationText(text = "6666")
}
@Preview
@Composable
fun PasswordDial(){
CSAMSTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
// val model=RegisterViewModel()
// PasswordDialog(registerViewModel=model,message = "99999")
val openDialog = remember { mutableStateOf(true) }
AlertDialog(
onDismissRequest = {
// Dismiss the dialog when the user clicks outside the dialog or on the back
// button. If you want to disable that functionality, simply use an empty
// onCloseRequest.
openDialog.value = false
},
title = {
Text(text = "Title")
},
text = {
Text(
"This area typically contains the supportive text " +
"which presents the details regarding the Dialog's purpose."
)
},
confirmButton = {
TextButton(
onClick = {
openDialog.value = false
}
) {
Text("Confirm")
}
},
dismissButton = {
TextButton(
onClick = {
openDialog.value = false
}
) {
Text("Dismiss")
}
}
)
}
}
}

@ -1,33 +1,64 @@
package com.gyf.csams.account.model
import android.app.Application
import android.content.Intent
import android.os.Build
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.gyf.csams.AccountApi
import com.gyf.csams.Api
import com.gyf.csams.RegisterApi
import com.gyf.csams.util.ApiResponse
import com.gyf.csams.util.HttpClient
import com.gyf.csams.util.SimpleCallback
import com.gyf.csams.InitActivity
import com.gyf.csams.account.ui.AccountRoute
import com.gyf.csams.util.*
import com.orhanobut.logger.Logger
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
/**
* 响应自动生成密码
*
* @property password
*/
@Serializable
data class UserResDto(val password:String)
/**
* 构造登录注册信息实体表单
*
* @property studentId 学号
* @property name 姓名
*/
data class UserVo(val studentId:String,val name:String)
/**
* 用户登陆表单
*
* @property studentId 学号
* @property password 密码
* @property device 设备型号
*/
data class UserLoginVo(val studentId: String,val password: String,val device: String)
/**
* 密码弹窗信息
*
* @property message
* @property userResDto
*/
data class DialogMessage(val message:String,val userResDto: UserResDto?)
typealias Token= TokenResDto
/**
* 注册表单
*/
class RegisterViewModel:ViewModel() {
class AccountViewModel(application: Application) : AndroidViewModel(application) {
//欢迎信息
val welcomeStart="同学您好\n"
@ -39,30 +70,43 @@ class RegisterViewModel:ViewModel() {
private val _isValidStudentId=MutableLiveData<Boolean>()
val isValidStudentId:LiveData<Boolean> = _isValidStudentId
val studentIdDesc="学号"
val studentIdPlaceholder="学号纯数字"
val studentIdPlaceholder="请输入$studentIdDesc"
val studentIdFormat="入学年份(四位)+班级代码(两位)+学生代码(两位)"
//学号已存在
private val _isRepeat=MutableLiveData<Boolean?>()
val regBtnDesc="注册"
//已注册
val registered="已注册"
val registered="$regBtnDesc"
//可注册
val canRegister="注册"
val canRegister="$regBtnDesc"
//提示信息
val checkRegTip="检测学号是否已注册。。。"
val checkRegTip="检测学号是否已${regBtnDesc}。。。"
val isRepeat:LiveData<Boolean?> = _isRepeat
private var checkJob: Job? = null
//姓名
private val _name=MutableLiveData<String>()
val name:LiveData<String> = _name
val nameDesc="姓名"
val namePlaceholder=nameDesc
val namePlaceholder="请输入$nameDesc"
private val _isValidName=MutableLiveData<Boolean>()
val isValidName:LiveData<Boolean> = _isValidName
val nameFormat="姓名不能为空"
//密码
private val _password=MutableLiveData<String>()
val password:LiveData<String> = _password
val passwordDesc="密码"
val passwordPlaceholder="请输入$passwordDesc"
private val _isValidPwd=MutableLiveData<Boolean>()
val isValidPwd:LiveData<Boolean> = _isValidPwd
val passwordFormat="八位纯数字"
//注册按钮
private val _isValidForm=MutableLiveData<Boolean>()
val regBtnDesc="注册"
val isValidForm:LiveData<Boolean> = _isValidForm
//注册请求响应信息
private val _snackBarMsg=MutableLiveData<String>()
@ -71,16 +115,28 @@ class RegisterViewModel:ViewModel() {
private val _dialogMsg=MutableLiveData<DialogMessage>()
val dialogMsg:LiveData<DialogMessage> = _dialogMsg
val loginDesc="登陆"
//返回登陆
val backDesc="返回登陆"
val backLogin="返回$loginDesc"
//确定按钮
val confirmDesc="确定"
//显示密码提示
val title="提示信息"
val passwordTip="密码会在点击注册以后,在后台自动生成,请留意系统提示。"
val passwordDialogStart="注册成功,后台为您自动生成的密码是"
val passwordDialogEnd="\n密码有且只有这里显示一次,请在记住密码后点击确定或${backDesc}"
val passwordTip="密码会在点击${regBtnDesc}以后,在后台自动生成,请留意系统提示。"
val passwordDialogStart="${regBtnDesc}成功,后台为您自动生成的密码是"
val passwordDialogEnd="\n密码有且只有这里显示一次,请在记住密码后点击确定或${backLogin}"
//转到注册
var goRegister="转到$regBtnDesc"
/**
* 完成登录状态
*/
private val _finishLogin=MutableLiveData<Boolean>()
val finishLogin:LiveData<Boolean> = _finishLogin
lateinit var route:AccountRoute
/**
* 更新学号
@ -101,6 +157,7 @@ class RegisterViewModel:ViewModel() {
*
*/
private fun checkStudentId(): Boolean {
_isValidStudentId.value= _studentId.value?.matches(Regex("\\d{8}"))
return _isValidStudentId.value==true
}
@ -109,14 +166,14 @@ class RegisterViewModel:ViewModel() {
* 检查学号是否已注册
*
*/
suspend fun checkRepeat(){
private suspend fun checkRepeat(){
if (checkStudentId()) {
if (checkJob?.isActive == true) {
checkJob?.join()
}else {
_isRepeat.postValue(null)
checkJob = viewModelScope.launch {
val url = Api.buildUrl(RegisterApi.checkId)
val url = Api.buildUrl(AccountApi.checkId)
Logger.i("检测$studentIdDesc,请求接口$url")
HttpClient.get(
url, SimpleCallback<Boolean>(
@ -155,12 +212,31 @@ class RegisterViewModel:ViewModel() {
return _isValidName.value==true
}
/**
* 更新密码
*
* @param password 密码
*/
fun onPasswordChange(password: String){
_password.value=password
checkForm()
}
/**
* 检测密码
*
* @return
*/
private fun checkPassword():Boolean{
_isValidPwd.value= _password.value?.matches(Regex("\\d{8}"))
return _isValidPwd.value==true
}
private fun checkForm(): Boolean {
if(checkJob?.isActive==true){
_isValidForm.value = false
}else{
_isValidForm.value = checkName() && checkStudentId() && isRepeat.value==false
_isValidForm.value = checkStudentId() && (if (route==AccountRoute.register) checkName()&&isRepeat.value==false else checkPassword())
}
return _isValidForm.value == true
}
@ -171,20 +247,24 @@ class RegisterViewModel:ViewModel() {
*/
fun register(){
if(checkForm()){
val url= Api.buildUrl(RegisterApi.register)
Logger.i("开始注册,请求接口:$url")
val url= Api.buildUrl(AccountApi.register)
Logger.i("开始$regBtnDesc,请求接口:$url")
HttpClient.post(url,SimpleCallback<UserResDto>(
action = "注册",
action = regBtnDesc,
onSuccess = { _dialogMsg.postValue(DialogMessage(message = it.message,userResDto = it.body)) },
onFail = { _snackBarMsg.postValue(it)},
type = object : TypeToken<ApiResponse<UserResDto>>() {}.type),
jsonBody = Gson().toJson(UserVo(studentId = "${studentId.value}",name = "${name.value}")))
resetForm()
}else{
Logger.wtf("表单校验失败,无法注册!!!")
Logger.wtf("表单校验失败,无法$regBtnDesc!!!")
}
}
/**
*
*/
/**
* 重置信息
*
@ -201,4 +281,40 @@ class RegisterViewModel:ViewModel() {
_studentId.value=""
_name.value=""
}
/**
* 登录
*
*/
fun login(){
if(checkForm()){
val url = Api.buildUrl(AccountApi.login)
Logger.i("开始$loginDesc,请求接口:$url")
HttpClient.post(url,SimpleCallback<Token>(
action = loginDesc,
onSuccess = {
_snackBarMsg.postValue(it.message)
val context= getApplication<Application>().applicationContext
val token = it.body?.token
if(token!=null){
val db= AppDatabase.getInstance(context)
viewModelScope.launch {
db?.tokenDao()?.save(token = token)
}.invokeOnCompletion {
context.startActivity(Intent(context, InitActivity::class.java))
_finishLogin.postValue(true)
}
}
},
onFail = {_snackBarMsg.postValue(it)},
type = object : TypeToken<ApiResponse<Token>>(){}.type
),jsonBody = Gson().toJson(UserLoginVo(studentId = "${studentId.value}",password = "${password.value}",device = "${Build.MANUFACTURER} ${Build.MODEL}")))
}else{
Logger.wtf("表单校验失败,无法$loginDesc!!!")
}
}
}

@ -0,0 +1,391 @@
package com.gyf.csams.account.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigate
import androidx.navigation.compose.rememberNavController
import com.gyf.csams.BuildConfig
import com.gyf.csams.account.model.AccountViewModel
import com.gyf.csams.account.model.DialogMessage
import com.gyf.csams.ui.AnimationText
import com.gyf.csams.ui.theme.CSAMSTheme
enum class AccountRoute{
login,
register
}
class AccountActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CSAMSTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState=scaffoldState) {
NavHost(navController, startDestination = AccountRoute.login.name) {
composable(AccountRoute.login.name) {
Account(scaffoldState=scaffoldState,route = AccountRoute.login) { isValidForm: Boolean, accountViewModel: AccountViewModel ->
Spacer(modifier = Modifier.height(10.dp))
OutlinedButton(onClick = {accountViewModel.login()},
enabled = isValidForm,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)) {
Text(text = accountViewModel.loginDesc)
}
val finishLogin:Boolean? by accountViewModel.finishLogin.observeAsState()
if(finishLogin==true){
finish()
}
OutlinedButton(onClick = { navController.navigate(AccountRoute.register.name)},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onBackground)) {
Text(text = accountViewModel.goRegister)
}
}
}
composable(AccountRoute.register.name) {
Account(scaffoldState=scaffoldState,route = AccountRoute.register) { isValidForm: Boolean, accountViewModel: AccountViewModel ->
Spacer(modifier = Modifier.height(10.dp))
OutlinedButton(onClick = { accountViewModel.register()},
enabled = isValidForm,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)) {
Text(text = accountViewModel.regBtnDesc)
}
OutlinedButton(onClick = { navController.navigate(AccountRoute.login.name)},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onBackground)) {
Text(text = accountViewModel.backLogin)
}
}
}
}
}
}
}
}
}
}
/**
* 帐号表单
*
* @param accountViewModel
* @param Action 表单操作区域
*/
@Composable
private fun Account(accountViewModel: AccountViewModel = viewModel(),
scaffoldState: ScaffoldState,
route: AccountRoute,
Action: @Composable (isValidForm:Boolean,accountViewModel: AccountViewModel) -> Unit){
accountViewModel.route=route
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
val name: String by accountViewModel.name.observeAsState("")
Text(buildAnnotatedString {
withStyle(
style = MaterialTheme.typography.subtitle1.toSpanStyle()
.copy(color = MaterialTheme.colors.primary)
) {
append(name)
}
withStyle(style = MaterialTheme.typography.subtitle1.toSpanStyle()) {
append(accountViewModel.welcomeStart)
}
withStyle(style = MaterialTheme.typography.subtitle2.toSpanStyle()) {
append(accountViewModel.welcomeEnd)
}
withStyle(
style = MaterialTheme.typography.subtitle2.toSpanStyle()
.copy(color = MaterialTheme.colors.secondary)
) {
append(BuildConfig.APP_NAME)
}
})
StudentId(checkRepeat=route==AccountRoute.register)
Spacer(modifier = Modifier.height(10.dp))
if (route==AccountRoute.register) Name(name=name) else Password()
Spacer(modifier = Modifier.height(10.dp))
PasswordTip()
val isValidForm: Boolean by accountViewModel.isValidForm.observeAsState(false)
Action(isValidForm=isValidForm,accountViewModel=accountViewModel)
val snackBarMsg:String by accountViewModel.snackBarMsg.observeAsState("")
if(snackBarMsg!=""){
LaunchedEffect(scaffoldState) {
scaffoldState.snackbarHostState.showSnackbar(
message = snackBarMsg
)
accountViewModel.resetRegisterResMsg()
}
}
RegisterDialog()
}
}
}
/**
* 学号
*
* @param accountViewModel
*/
@Composable
private fun StudentId(accountViewModel: AccountViewModel = viewModel(),checkRepeat:Boolean){
Column {
val studentId: String by accountViewModel.studentId.observeAsState("")
val isValidStudentId : Boolean by accountViewModel.isValidStudentId.observeAsState(false)
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = studentId,
onValueChange = { accountViewModel.onStudentIdChange(it) },
label = { Text(text = accountViewModel.studentIdDesc) },
placeholder = { Text(text = accountViewModel.studentIdPlaceholder) },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Number,imeAction = ImeAction.Done),
singleLine = true,
isError = !isValidStudentId
)
if (isValidStudentId) {
if(checkRepeat) {
val isRepeat: Boolean? by accountViewModel.isRepeat.observeAsState(null)
when (isRepeat) {
null -> AnimationText(text = accountViewModel.checkRegTip)
true ->
Text(buildAnnotatedString {
append(accountViewModel.studentIdDesc)
withStyle(
style = MaterialTheme.typography.body1.toSpanStyle().copy(
color = MaterialTheme.colors.error
)
) {
append(studentId)
}
append(accountViewModel.registered)
})
false ->
Text(buildAnnotatedString {
append(accountViewModel.studentIdDesc)
withStyle(
style = MaterialTheme.typography.body1.toSpanStyle().copy(
color = MaterialTheme.colors.primary
)
) {
append(studentId)
}
append(accountViewModel.canRegister)
})
}
}
}else{
Text(
text = accountViewModel.studentIdFormat,
color = MaterialTheme.colors.error,
style = MaterialTheme.typography.body1
)
}
}
}
/**
* 注册弹窗
*
* @param accountViewModel
*/
@Composable
private fun RegisterDialog(accountViewModel: AccountViewModel = viewModel()){
val dialogMsg: DialogMessage? by accountViewModel.dialogMsg.observeAsState(null)
val message=dialogMsg?.userResDto?.password
if(message?.isNotEmpty() == true){
PasswordDialog(message = message)
}
}
/**
* 密码弹窗
*
* @param accountViewModel
* @param message
*/
@Composable
private fun PasswordDialog(accountViewModel: AccountViewModel = viewModel(), message:String){
val context= LocalContext.current
val button:@Composable () -> Unit = {
Row(horizontalArrangement= Arrangement.Center,modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)) {
OutlinedButton(onClick = { accountViewModel.resetDialogMsg() },
modifier = Modifier.padding(end = 10.dp)) {
Text(text = accountViewModel.confirmDesc)
}
OutlinedButton(onClick = {
context.startActivity(Intent(context,AccountActivity::class.java))
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onBackground)) {
Text(text = accountViewModel.backLogin)
}
}
}
AlertDialog(onDismissRequest = { accountViewModel.resetDialogMsg() },
buttons = button,
title = { Text(text = accountViewModel.title) },
text = {
Text(buildAnnotatedString {
append(accountViewModel.passwordDialogStart)
withStyle(style = MaterialTheme.typography.body1.toSpanStyle()
.copy(color = MaterialTheme.colors.secondary)){
append(message)
}
append(accountViewModel.passwordDialogEnd)
})
})
}
/**
* 姓名文本框
*
* @param name 姓名
* @param accountViewModel
*/
@Composable
private fun Name(name:String, accountViewModel: AccountViewModel = viewModel()){
Column {
val isValidName:Boolean by accountViewModel.isValidName.observeAsState(false)
val focusManager = LocalFocusManager.current
OutlinedTextField(value = name,
onValueChange = {accountViewModel.onNameChange(it)},
label={ Text(text = accountViewModel.nameDesc)},
placeholder = { Text(text = accountViewModel.namePlaceholder)},
singleLine = true,
isError = !isValidName,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done))
if (!isValidName){
Text(text = accountViewModel.nameFormat,
color=MaterialTheme.colors.error)
}
}
}
/**
* 密码框
*
* @param accountViewModel
*/
@Composable
private fun Password(accountViewModel: AccountViewModel= viewModel()){
Column {
val isValidPwd:Boolean by accountViewModel.isValidPwd.observeAsState(false)
val focusManager = LocalFocusManager.current
val password:String by accountViewModel.password.observeAsState("")
OutlinedTextField(value = password,
visualTransformation=PasswordVisualTransformation(),
onValueChange = {accountViewModel.onPasswordChange(it)},
label={ Text(text = accountViewModel.passwordDesc)},
placeholder = { Text(text = accountViewModel.passwordPlaceholder)},
singleLine = true,
isError = !isValidPwd,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done,keyboardType = KeyboardType.Number))
if(!isValidPwd){
Text(text = accountViewModel.passwordFormat,
color=MaterialTheme.colors.error)
}
}
}
/**
* 提示自动生成密码
*
* @param accountViewModel
*/
@Composable
private fun PasswordTip(accountViewModel: AccountViewModel = viewModel()) {
if(accountViewModel.isValidForm.value==true) {
Text(
text = accountViewModel.passwordTip, color = MaterialTheme.colors.primary,
modifier = Modifier.fillMaxWidth()
)
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
CSAMSTheme {
Row (
horizontalArrangement=Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
val model:AccountViewModel= viewModel()
StudentId(model,false)
Spacer(modifier = Modifier.height(10.dp))
}
}
}
}

@ -0,0 +1,41 @@
package com.gyf.csams.ui
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
/**
* 淡入淡出并且颜色变化文本
*
* @param text
*/
@Composable
fun AnimationText(text:String){
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Text(
text = text,
color = color,
style = MaterialTheme.typography.body1
)
}
@Preview
@Composable
fun AnimationTextPreview(){
AnimationText(text = "6666")
}

@ -0,0 +1,42 @@
package com.gyf.csams.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.gyf.csams.ui.theme.CSAMSTheme
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CSAMSTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Row(horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()){
AnimationText(text = "主界面设计中。。。。")
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
CSAMSTheme {
// A surface container using the 'background' color from the theme
AnimationText(text = "sdfsdf")
}
}

@ -78,6 +78,7 @@ object HttpClient{
* @param jsonBody
*/
fun post(url:String, callback: Callback, jsonBody:String){
Logger.json(jsonBody)
val request = Request.Builder()
.url(url)
.post(body = jsonBody.toRequestBody(contentType = JSON_CONTENT_TYPE))

@ -0,0 +1,102 @@
package com.gyf.csams.util
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.room.*
import kotlinx.serialization.Serializable
/**
* 登陆令牌
*/
@Entity
@Serializable
data class Token(@PrimaryKey val studentId:String,@ColumnInfo val token:String,@ColumnInfo val createTime:Long)
/**
* 令牌传输
*
* @property isValid
* @property token
*/
@Serializable
data class TokenResDto(val isValid:Boolean,val token: Token?)
@Dao
interface TokenDao {
@Query("select * from token")
suspend fun queryAll(): List<Token>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(token: Token)
@Delete
suspend fun delete(user: Token)
}
class TokenManager private constructor(private var token: Token?) {
companion object {
@Volatile
private var instance: TokenManager? = null
fun getInstance(token: Token?=null) =
instance ?: synchronized(this) {
instance ?: TokenManager(token).also { instance = it }
}
}
}
@Database(entities = [Token::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun tokenDao(): TokenDao
private val mIsDatabaseCreated = MutableLiveData<Boolean>()
/**
* Check whether the database already exists and expose it via [.getDatabaseCreated]
*/
private fun updateDatabaseCreated(context: Context) {
if (context.getDatabasePath(DATABASE_NAME).exists()) {
setDatabaseCreated()
}
}
private fun setDatabaseCreated() {
mIsDatabaseCreated.postValue(true)
}
val databaseCreated: LiveData<Boolean>
get() = mIsDatabaseCreated
companion object {
private var sInstance: AppDatabase? = null
const val DATABASE_NAME = "basic-sample-db"
fun getInstance(context: Context): AppDatabase? {
if (sInstance == null) {
synchronized(AppDatabase::class.java) {
if (sInstance == null) {
sInstance =
buildDatabase(context.applicationContext)
sInstance!!.updateDatabaseCreated(context.applicationContext)
}
}
}
return sInstance
}
/**
* Build the database. [Builder.build] only sets up the database configuration and
* creates a new instance of the database.
* The SQLite database is only created when it's accessed for the first time.
*/
private fun buildDatabase(appContext: Context): AppDatabase {
return Room.databaseBuilder(appContext, AppDatabase::class.java, DATABASE_NAME)
.build()
}
}
}

@ -5,6 +5,8 @@ import com.google.gson.reflect.TypeToken
import com.gyf.csams.util.ApiResponse
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.LocalDateTime
import java.time.temporal.ChronoField
/**
@ -32,5 +34,10 @@ class ExampleUnitTest {
println(e.body)
}
@Test
fun testYear(){
println( LocalDateTime.now().get(ChronoField.YEAR))
}
}

@ -7,6 +7,7 @@ buildscript {
//APP应用名字
val APP_NAME by extra("大学生社团管理系统")
val SERVER_ADDRESS by extra("http://192.168.50.107:8080")
val room_version by extra("2.2.6")
repositories {
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/public")

Loading…
Cancel
Save