diff --git a/.gitignore b/.gitignore index d4c3a57..584e7a0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ .cxx local.properties /.idea/ +*.log diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 924c9f1..fed6354 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("kotlin-android") + id("org.jetbrains.kotlin.plugin.serialization") version "1.4.32" } android { @@ -21,9 +22,11 @@ android { buildTypes { val appName="${rootProject.extra["APP_NAME"]}" + val serverAddress=rootProject.extra["SERVER_ADDRESS"] debug { manifestPlaceholders(mapOf("APP_NAME" to appName)) buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"") + buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"") } release { isMinifyEnabled = false @@ -33,6 +36,7 @@ android { ) manifestPlaceholders(mapOf("APP_NAME" to appName)) buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"") + buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"") } } compileOptions { @@ -49,6 +53,10 @@ android { composeOptions { kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String } + packagingOptions { + resources.excludes.add("META-INF/AL2.0") + resources.excludes.add("META-INF/LGPL2.1") + } } dependencies { @@ -90,13 +98,25 @@ dependencies { * 访问基于 Activity 构建的可组合 API。 * https://developer.android.com/jetpack/androidx/releases/activity */ - implementation("androidx.activity:activity-compose:1.3.0-alpha06") + implementation("androidx.activity:activity-compose:1.3.0-alpha07") /** * Simple, pretty and powerful logger for android * https://github.com/orhanobut/logger */ implementation("com.orhanobut:logger:2.2.0") - + /** + * + * https://github.com/square/okhttp + */ + implementation("com.squareup.okhttp3:okhttp:4.9.1") + /** + * https://github.com/google/gson + */ + implementation("com.google.code.gson:gson:2.8.6") + /** + * https://kotlinlang.org/docs/serialization.html + */ + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") //测试 testImplementation("junit:junit:4.13.2") /** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index de45ccc..eab844f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + Unit){ - Column(modifier = modifier.width(IntrinsicSize.Min),content = content) -} - /** * 学号 * @@ -100,7 +105,8 @@ fun BaseColumn(modifier: Modifier = Modifier,content:@Composable ColumnScope.() */ @Composable fun StudentId(registerViewModel: RegisterViewModel=viewModel()){ - BaseColumn { + Column { + val studentId: String by registerViewModel.studentId.observeAsState("") val isValidStudentId : Boolean by registerViewModel.isValidStudentId.observeAsState(false) val focusManager = LocalFocusManager.current @@ -114,15 +120,118 @@ fun StudentId(registerViewModel: RegisterViewModel=viewModel()){ singleLine = true, isError = !isValidStudentId ) - if (!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 + 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) + }) + }) } + /** * 姓名 * @@ -130,7 +239,7 @@ fun StudentId(registerViewModel: RegisterViewModel=viewModel()){ */ @Composable fun Name(name:String,registerViewModel: RegisterViewModel=viewModel()){ - BaseColumn { + Column { val isValidName:Boolean by registerViewModel.isValidName.observeAsState(false) val focusManager = LocalFocusManager.current @@ -151,12 +260,118 @@ fun Name(name:String,registerViewModel: RegisterViewModel=viewModel()){ } +@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 { - Register() + + 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/RegisterViewModel.kt index 20e90ab..fe38811 100644 --- a/app/src/main/java/com/gyf/csams/account/model/RegisterViewModel.kt +++ b/app/src/main/java/com/gyf/csams/account/model/RegisterViewModel.kt @@ -3,12 +3,36 @@ package com.gyf.csams.account.model 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.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.orhanobut.logger.Logger +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable +data class UserResDto(val password:String) + +data class UserVo(val studentId:String,val name:String) + +data class DialogMessage(val message:String,val userResDto: UserResDto?) /** * 注册表单 */ class RegisterViewModel:ViewModel() { + + //欢迎信息 + val welcomeStart="同学您好\n" + val welcomeEnd="欢迎使用" + //学号 private val _studentId=MutableLiveData() val studentId:LiveData = _studentId @@ -17,6 +41,17 @@ class RegisterViewModel:ViewModel() { val studentIdDesc="学号" val studentIdPlaceholder="学号纯数字" val studentIdFormat="入学年份(四位)+班级代码(两位)+学生代码(两位)" + + //学号已存在 + private val _isRepeat=MutableLiveData() + //已注册 + val registered="已注册" + //可注册 + val canRegister="可注册" + //提示信息 + val checkRegTip="检测学号是否已注册。。。" + val isRepeat:LiveData = _isRepeat + private var checkJob: Job? = null //姓名 private val _name=MutableLiveData() val name:LiveData = _name @@ -27,7 +62,25 @@ class RegisterViewModel:ViewModel() { val nameFormat="姓名不能为空" //注册按钮 private val _isValidForm=MutableLiveData() + val regBtnDesc="注册" val isValidForm:LiveData = _isValidForm + //注册请求响应信息 + private val _snackBarMsg=MutableLiveData() + val snackBarMsg:LiveData = _snackBarMsg + + private val _dialogMsg=MutableLiveData() + val dialogMsg:LiveData = _dialogMsg + + //返回登陆 + val backDesc="返回登陆" + //确定按钮 + val confirmDesc="确定" + //显示密码提示 + val title="提示信息" + val passwordTip="密码会在点击注册以后,在后台自动生成,请留意系统提示。" + val passwordDialogStart="注册成功,后台为您自动生成的密码是" + val passwordDialogEnd="\n密码有且只有这里显示一次,请在记住密码后点击确定或${backDesc}。" + /** * 更新学号 @@ -36,7 +89,10 @@ class RegisterViewModel:ViewModel() { */ fun onStudentIdChange(studentId:String){ _studentId.value=studentId - _isValidForm.value = checkStudentId() && _isValidForm.value ?: false + + viewModelScope.launch { + checkRepeat() + } } @@ -49,6 +105,36 @@ class RegisterViewModel:ViewModel() { return _isValidStudentId.value==true } + /** + * 检查学号是否已注册 + * + */ + suspend fun checkRepeat(){ + if (checkStudentId()) { + if (checkJob?.isActive == true) { + checkJob?.join() + }else { + _isRepeat.postValue(null) + checkJob = viewModelScope.launch { + val url = Api.buildUrl(RegisterApi.checkId) + Logger.i("检测$studentIdDesc,请求接口$url") + HttpClient.get( + url, SimpleCallback( + action = "${studentIdDesc}重复检测", + onSuccess = { _isRepeat.postValue(it.body) + _isValidForm.postValue( _isValidName.value==true && it.body==false) + }, + onFail = { _snackBarMsg.postValue(it) }, + type = object : TypeToken>() {}.type) + , mapOf("studentId" to "${_studentId.value}")) + } + } + }else{ + _isValidForm.postValue(false) + } + } + + /** * 更新姓名 * @@ -56,19 +142,63 @@ class RegisterViewModel:ViewModel() { */ fun onNameChange(name:String){ _name.value=name - _isValidForm.value = checkName() && _isValidForm.value ?: false + checkForm() } + /** + * 检测姓名 + * + * @return + */ private fun checkName():Boolean{ _isValidName.value= _name.value?.isNotEmpty() return _isValidName.value==true } + + private fun checkForm(): Boolean { + if(checkJob?.isActive==true){ + _isValidForm.value = false + }else{ + _isValidForm.value = checkName() && checkStudentId() && isRepeat.value==false + } + return _isValidForm.value == true + } + + /** + * 注册 + * + */ fun register(){ - if(_isValidForm.value==true){ - Logger.i("开始注册") + if(checkForm()){ + val url= Api.buildUrl(RegisterApi.register) + Logger.i("开始注册,请求接口:$url") + HttpClient.post(url,SimpleCallback( + action = "注册", + 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("表单校验失败,无法注册!!!") } } + + /** + * 重置信息 + * + */ + fun resetRegisterResMsg(){ + _snackBarMsg.value="" + } + + fun resetDialogMsg(){ + _dialogMsg.value=null + } + + private fun resetForm(){ + _studentId.value="" + _name.value="" + } } \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/ui/theme/Theme.kt b/app/src/main/java/com/gyf/csams/ui/theme/Theme.kt index 12d3828..1ea718e 100644 --- a/app/src/main/java/com/gyf/csams/ui/theme/Theme.kt +++ b/app/src/main/java/com/gyf/csams/ui/theme/Theme.kt @@ -15,7 +15,7 @@ private val DarkColorPalette = darkColors( private val LightColorPalette = lightColors( primary = Purple500, primaryVariant = Purple700, - secondary = Teal200 + secondary = Teal200, /* Other default colors to override background = Color.White, diff --git a/app/src/main/java/com/gyf/csams/util/HttpUtil.kt b/app/src/main/java/com/gyf/csams/util/HttpUtil.kt new file mode 100644 index 0000000..10d3947 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/util/HttpUtil.kt @@ -0,0 +1,122 @@ +package com.gyf.csams.util + +import com.google.gson.Gson +import com.orhanobut.logger.Logger +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import java.lang.reflect.Type + +object HttpClient{ + private val httpClient:OkHttpClient=OkHttpClient() + + private val JSON_CONTENT_TYPE="application/json; charset=UTF-8".toMediaType() + + + private fun buildQueryParams(params:Map?):String{ + return if(params?.isNotEmpty() == true) { + val urlPath = StringBuilder("?") + for(i in params){ + urlPath.append(i.key).append("=").append(i.value) + } + + urlPath.toString() + }else{ + "" + } + } + + private fun buildFormBody(params: Map?):FormBody{ + val builder=FormBody.Builder() + if(params?.isNotEmpty()==true){ + for(item in params){ + builder.add(item.key,item.value) + } + } + return builder.build() + } + + /** + * HTTP GET + * + * @param url + * @param callback + * @param params + */ + fun get(url:String, callback: Callback, params: Map?=null){ + val request = Request.Builder() + .url(url.plus(buildQueryParams(params = params))) + .build() + val call=httpClient.newCall(request) + call.enqueue(callback) + } + + /** + * HTTP POST + * 发送表单 + * + * @param url + * @param callback + * @param params + */ + fun post(url:String, callback: Callback, params: Map?=null){ + val request = Request.Builder() + .url(url) + .post(body = buildFormBody(params)) + .build() + val call=httpClient.newCall(request) + call.enqueue(callback) + } + + /** + * HTTP POST + * 发送JSON + * + * @param url + * @param callback + * @param jsonBody + */ + fun post(url:String, callback: Callback, jsonBody:String){ + val request = Request.Builder() + .url(url) + .post(body = jsonBody.toRequestBody(contentType = JSON_CONTENT_TYPE)) + .build() + val call=httpClient.newCall(request) + call.enqueue(callback) + } + + +} + + +data class ApiResponse(val code:Int,val message:String,val body:T?=null) + +class SimpleCallback(private val action:String, + private val onSuccess:(res:ApiResponse) -> Unit, + private val onFail:(error:String) -> Unit, + private val type: Type):Callback{ + override fun onFailure(call: Call, e: IOException) { + onFail("${action}失败,请联系管理员") + Logger.e(e,"${action}请求失败,发生IO异常") + } + + override fun onResponse(call: Call, response: Response) { + if (response.code == 200) { + val body=response.body + if (body!=null&&body.contentType()?.subtype == "json") { + val jsonRes=body.string() + val res:ApiResponse = Gson().fromJson(jsonRes, type) + Logger.i("${action}请求响应成功:") + Logger.json(jsonRes) + onSuccess(res) + } else { + onFail("${action}失败,请联系管理员") + Logger.e("无法解析${action}请求响应数据:,响应码:${response.code},${response.body}") + } + }else{ + onFail("${action}失败,请联系管理员") + Logger.e("${action}失败,请求响应码:${response.code}") + } + } +} \ 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 bf77638..f0435f9 100644 --- a/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt +++ b/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt @@ -1,8 +1,12 @@ package com.gyf.csams +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.gyf.csams.util.ApiResponse import org.junit.Assert.assertEquals import org.junit.Test + /** * Example local unit test, which will execute on the development machine (host). * @@ -14,6 +18,19 @@ class ExampleUnitTest { assertEquals(4, 2 + 2) } + data class Fuck(val name:String) + + + + @Test + fun testGson(){ +// val e="{\"code\":200,\"message\":\"学号可注册\",\"body\":false}" + val c=ApiResponse(code = 200,message = "aaa",body= null) + val d=Gson().toJson(c) + println(d) + val e=Gson().fromJson>(d,object : TypeToken>() {}.type) + println(e.body) + } } diff --git a/build.gradle.kts b/build.gradle.kts index a08e40b..b7b47a5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ buildscript { val lifecycle_version by extra("2.3.1") //APP应用名字 val APP_NAME by extra("大学生社团管理系统") + val SERVER_ADDRESS by extra("http://192.168.50.107:8080") repositories { maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/public")