diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fed6354..0ceb916 100644 --- a/app/build.gradle.kts +++ b/app/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") /** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eab844f..34d063a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,16 +16,26 @@ + android:theme="@style/Theme.CSAMS.NoActionBar"> + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/APP.kt b/app/src/main/java/com/gyf/csams/APP.kt index ba69430..5c41d89 100644 --- a/app/src/main/java/com/gyf/csams/APP.kt +++ b/app/src/main/java/com/gyf/csams/APP.kt @@ -6,6 +6,8 @@ import com.orhanobut.logger.DiskLogAdapter import com.orhanobut.logger.Logger class APP : Application() { + + override fun onCreate() { super.onCreate() //初始化日志 diff --git a/app/src/main/java/com/gyf/csams/Api.kt b/app/src/main/java/com/gyf/csams/Api.kt index b9b80b8..3c62a5f 100644 --- a/app/src/main/java/com/gyf/csams/Api.kt +++ b/app/src/main/java/com/gyf/csams/Api.kt @@ -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}" diff --git a/app/src/main/java/com/gyf/csams/InitActivity.kt b/app/src/main/java/com/gyf/csams/InitActivity.kt new file mode 100644 index 0000000..e1899a5 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/InitActivity.kt @@ -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)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/InitViewModel.kt b/app/src/main/java/com/gyf/csams/InitViewModel.kt new file mode 100644 index 0000000..a0ac3f7 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/InitViewModel.kt @@ -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() + val isNetWorkWorking: LiveData = _isNetWorkWorking + + /** + * token + */ + private val _token = MutableLiveData() + val token: LiveData = _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( + action=action, + onSuccess = { + _token.postValue(it.body) + Logger.i("token校验结果:${it.body}") + }, + onFail = { TODO("token校验失败")}, + type = object : TypeToken>(){}.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) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/MainActivity.kt b/app/src/main/java/com/gyf/csams/MainActivity.kt deleted file mode 100644 index 455117b..0000000 --- a/app/src/main/java/com/gyf/csams/MainActivity.kt +++ /dev/null @@ -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") - } - } - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/account/model/RegisterViewModel.kt b/app/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt similarity index 53% rename from app/src/main/java/com/gyf/csams/account/model/RegisterViewModel.kt rename to app/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt index fe38811..c7a123c 100644 --- a/app/src/main/java/com/gyf/csams/account/model/RegisterViewModel.kt +++ b/app/src/main/java/com/gyf/csams/account/model/AccountViewModel.kt @@ -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() val isValidStudentId:LiveData = _isValidStudentId val studentIdDesc="学号" - val studentIdPlaceholder="学号纯数字" + val studentIdPlaceholder="请输入$studentIdDesc" val studentIdFormat="入学年份(四位)+班级代码(两位)+学生代码(两位)" //学号已存在 private val _isRepeat=MutableLiveData() + + val regBtnDesc="注册" + //已注册 - val registered="已注册" + val registered="已$regBtnDesc" //可注册 - val canRegister="可注册" + val canRegister="可$regBtnDesc" //提示信息 - val checkRegTip="检测学号是否已注册。。。" + val checkRegTip="检测学号是否已${regBtnDesc}。。。" val isRepeat:LiveData = _isRepeat private var checkJob: Job? = null //姓名 private val _name=MutableLiveData() val name:LiveData = _name val nameDesc="姓名" - val namePlaceholder=nameDesc + val namePlaceholder="请输入$nameDesc" private val _isValidName=MutableLiveData() val isValidName:LiveData = _isValidName val nameFormat="姓名不能为空" + //密码 + private val _password=MutableLiveData() + val password:LiveData = _password + val passwordDesc="密码" + val passwordPlaceholder="请输入$passwordDesc" + private val _isValidPwd=MutableLiveData() + val isValidPwd:LiveData = _isValidPwd + val passwordFormat="八位纯数字" + + //注册按钮 private val _isValidForm=MutableLiveData() - val regBtnDesc="注册" + val isValidForm:LiveData = _isValidForm //注册请求响应信息 private val _snackBarMsg=MutableLiveData() @@ -71,16 +115,28 @@ class RegisterViewModel:ViewModel() { private val _dialogMsg=MutableLiveData() val dialogMsg:LiveData = _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() + val finishLogin:LiveData = _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( @@ -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( - action = "注册", + action = regBtnDesc, onSuccess = { _dialogMsg.postValue(DialogMessage(message = it.message,userResDto = it.body)) }, onFail = { _snackBarMsg.postValue(it)}, type = object : TypeToken>() {}.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( + action = loginDesc, + onSuccess = { + _snackBarMsg.postValue(it.message) + + val context= getApplication().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>(){}.type + ),jsonBody = Gson().toJson(UserLoginVo(studentId = "${studentId.value}",password = "${password.value}",device = "${Build.MANUFACTURER} ${Build.MODEL}"))) + }else{ + Logger.wtf("表单校验失败,无法$loginDesc!!!") + } + } + + + } \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/account/ui/AccountActivity.kt b/app/src/main/java/com/gyf/csams/account/ui/AccountActivity.kt new file mode 100644 index 0000000..9804cd0 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/account/ui/AccountActivity.kt @@ -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)) + } + } + } +} + diff --git a/app/src/main/java/com/gyf/csams/ui/Base.kt b/app/src/main/java/com/gyf/csams/ui/Base.kt new file mode 100644 index 0000000..007e902 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/ui/Base.kt @@ -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") +} \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/ui/MainActivity.kt b/app/src/main/java/com/gyf/csams/ui/MainActivity.kt new file mode 100644 index 0000000..aed0362 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/ui/MainActivity.kt @@ -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") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/util/HttpUtil.kt b/app/src/main/java/com/gyf/csams/util/HttpUtil.kt index 10d3947..aa8b9a4 100644 --- a/app/src/main/java/com/gyf/csams/util/HttpUtil.kt +++ b/app/src/main/java/com/gyf/csams/util/HttpUtil.kt @@ -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)) diff --git a/app/src/main/java/com/gyf/csams/util/TokenUtil.kt b/app/src/main/java/com/gyf/csams/util/TokenUtil.kt new file mode 100644 index 0000000..5e6e8d6 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/util/TokenUtil.kt @@ -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 + + @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() + + /** + * 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 + 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() + } + + + } +} \ No newline at end of file diff --git a/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt b/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt index f0435f9..070132b 100644 --- a/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt +++ b/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt @@ -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)) + } + } diff --git a/build.gradle.kts b/build.gradle.kts index b7b47a5..29f0ac1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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")