注册界面

master
pan 4 years ago
parent a467f95730
commit 0e2e960c31
  1. 1
      .gitignore
  2. 24
      app/build.gradle.kts
  3. 1
      app/src/main/AndroidManifest.xml
  4. 25
      app/src/main/java/com/gyf/csams/Api.kt
  5. 277
      app/src/main/java/com/gyf/csams/MainActivity.kt
  6. 138
      app/src/main/java/com/gyf/csams/account/model/RegisterViewModel.kt
  7. 2
      app/src/main/java/com/gyf/csams/ui/theme/Theme.kt
  8. 122
      app/src/main/java/com/gyf/csams/util/HttpUtil.kt
  9. 17
      app/src/test/java/com/gyf/csams/ExampleUnitTest.kt
  10. 1
      build.gradle.kts

1
.gitignore vendored

@ -14,3 +14,4 @@
.cxx
local.properties
/.idea/
*.log

@ -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")
/**

@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gyf.csams">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"

@ -0,0 +1,25 @@
package com.gyf.csams
interface UrlPath{
fun build():String
}
enum class RegisterApi(val path: String):UrlPath{
register("/register"),
checkId("/register/checkId");
override fun build(): String {
return "/api/account${this.path}"
}
}
class Api {
companion object{
fun buildUrl(urlPath: UrlPath):String{
return "${BuildConfig.SERVER_ADDRESS}${urlPath.build()}"
}
}
}

@ -3,25 +3,26 @@ 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.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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
@ -36,6 +37,7 @@ class MainActivity : ComponentActivity() {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Register()
RegisterDialog()
}
}
}
@ -50,47 +52,50 @@ class MainActivity : ComponentActivity() {
*/
@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.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)){
withStyle(
style = MaterialTheme.typography.subtitle1.toSpanStyle()
.copy(color = MaterialTheme.colors.primary)
) {
append(name)
}
withStyle(style = MaterialTheme.typography.subtitle1.toSpanStyle()) {
append("同学您好\n")
append(registerViewModel.welcomeStart)
}
withStyle(style = MaterialTheme.typography.subtitle2.toSpanStyle()) {
append("欢迎使用")
append(registerViewModel.welcomeEnd)
}
withStyle(style = MaterialTheme.typography.subtitle2.toSpanStyle()
.copy(color=MaterialTheme.colors.secondary)){
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 modifier
* @param content
*/
@Composable
fun BaseColumn(modifier: Modifier = Modifier,content:@Composable ColumnScope.() -> 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")
}
}
)
}
}
}

@ -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<String>()
val studentId:LiveData<String> = _studentId
@ -17,6 +41,17 @@ class RegisterViewModel:ViewModel() {
val studentIdDesc="学号"
val studentIdPlaceholder="学号纯数字"
val studentIdFormat="入学年份(四位)+班级代码(两位)+学生代码(两位)"
//学号已存在
private val _isRepeat=MutableLiveData<Boolean?>()
//已注册
val registered="已注册"
//可注册
val canRegister="可注册"
//提示信息
val checkRegTip="检测学号是否已注册。。。"
val isRepeat:LiveData<Boolean?> = _isRepeat
private var checkJob: Job? = null
//姓名
private val _name=MutableLiveData<String>()
val name:LiveData<String> = _name
@ -27,7 +62,25 @@ class RegisterViewModel:ViewModel() {
val nameFormat="姓名不能为空"
//注册按钮
private val _isValidForm=MutableLiveData<Boolean>()
val regBtnDesc="注册"
val isValidForm:LiveData<Boolean> = _isValidForm
//注册请求响应信息
private val _snackBarMsg=MutableLiveData<String>()
val snackBarMsg:LiveData<String> = _snackBarMsg
private val _dialogMsg=MutableLiveData<DialogMessage>()
val dialogMsg:LiveData<DialogMessage> = _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<Boolean>(
action = "${studentIdDesc}重复检测",
onSuccess = { _isRepeat.postValue(it.body)
_isValidForm.postValue( _isValidName.value==true && it.body==false)
},
onFail = { _snackBarMsg.postValue(it) },
type = object : TypeToken<ApiResponse<Boolean>>() {}.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<UserResDto>(
action = "注册",
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("表单校验失败,无法注册!!!")
}
}
/**
* 重置信息
*
*/
fun resetRegisterResMsg(){
_snackBarMsg.value=""
}
fun resetDialogMsg(){
_dialogMsg.value=null
}
private fun resetForm(){
_studentId.value=""
_name.value=""
}
}

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

@ -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,String>?):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<String, String>?):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<String, String>?=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<String, String>?=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<T>(val code:Int,val message:String,val body:T?=null)
class SimpleCallback<T>(private val action:String,
private val onSuccess:(res:ApiResponse<T>) -> 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<T> = 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}")
}
}
}

@ -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<ApiResponse<Boolean>>(d,object : TypeToken<ApiResponse<Boolean>>() {}.type)
println(e.body)
}
}

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

Loading…
Cancel
Save