diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6894e73..da5402c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,7 +26,7 @@ android { val serverAddress=rootProject.extra["SERVER_ADDRESS"] debug { manifestPlaceholders.apply { - this["APP_NAME"] = appName; + this["APP_NAME"] = appName } buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"") buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"") @@ -38,7 +38,7 @@ android { "proguard-rules.pro" ) manifestPlaceholders.apply { - this["APP_NAME"] = appName; + this["APP_NAME"] = appName } buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"") buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"") @@ -122,6 +122,8 @@ dependencies { * https://kotlinlang.org/docs/serialization.html */ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") + + implementation("org.jetbrains.kotlin:kotlin-reflect:${rootProject.extra["kotlin_version"]}") /** * https://developer.android.com/jetpack/androidx/releases/navigation */ diff --git a/app/src/androidTest/java/com/gyf/csams/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/gyf/csams/ExampleInstrumentedTest.kt index 6833343..a2231e6 100644 --- a/app/src/androidTest/java/com/gyf/csams/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/gyf/csams/ExampleInstrumentedTest.kt @@ -18,5 +18,7 @@ class ExampleInstrumentedTest { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.gyf.csams", appContext.packageName) + + } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c4db7e..b7cb9b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/association/model/ExamViewModel.kt b/app/src/main/java/com/gyf/csams/association/model/ExamViewModel.kt new file mode 100644 index 0000000..65c43b4 --- /dev/null +++ b/app/src/main/java/com/gyf/csams/association/model/ExamViewModel.kt @@ -0,0 +1,227 @@ +package com.gyf.csams.association.model + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.gyf.csams.uikit.ScrollList +import com.gyf.csams.uikit.StringForm +import kotlin.random.Random + +/** + * 题型 + * + */ +enum class ExamType(val type:String){ + //选择题 + cq("选择题"), + //开放题 + oq("开放题") +} + +sealed class Exam{ + abstract val examType:ExamType + abstract val question:StringForm +} + + +/** + * 开放题 + * + * @property examType 题型描述 + * @property question 问题 + */ +data class OpenQuestionsVo( + override val examType: ExamType = ExamType.oq, override val question: StringForm + +) : Exam() + +/** + * 选择题 + * + * @property examType 题型描述 + * @property answers 答案 + * @property rightAnswer 正确答案 + * @property question 问题 + */ +data class ChoiceQuestionVo(override val examType: ExamType = ExamType.cq, + val answers:List, + val rightAnswer:Int, + override val question: StringForm +):Exam() + + + + +/** + * 题库状态管理 + * + */ + +/** + * 问题长度 + */ +const val QUESTION_TEXT_LENGTH=30 + +/** + * 选择题选项数 + */ +const val ANSWER_SIZE=4 + +/** + * 答案长度 + * + */ +const val ANSWER_TEXT_LENGTH=15 + + +class ExamViewModel:ScrollList() { + val questionIsNull: String = "问题不能为空" + val deleteLeastOne: String="至少保留一道题目" + val updateExam="更新题库" + val back="返回" + val menuName="入团题库" + val deleteTip="确定删除此题目?" + val addTip="确定添加此题目?" + val actionLabel="确定" + + + override val initSize = 10 + + + private val _newExam:MutableLiveData = MutableLiveData(createExam(ExamType.cq)) + val newExam:LiveData = _newExam + + init { + load() + } + + /** + * 切换题型 + * + */ + fun switchType(exam: Exam){ + if(exam is ChoiceQuestionVo) _newExam.value=createExam(ExamType.oq) else _newExam.value=createExam(ExamType.cq) + } + + + /** + * 创建题目 + * + * @param type + * @return + */ + private fun createExam(type: ExamType): Exam { + val question=StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH) + return when(type){ + ExamType.cq-> ChoiceQuestionVo( + answers = ('A'..'D').map { StringForm(formDesc = "选项",textLength = ANSWER_TEXT_LENGTH,value = "选项$it") }, + rightAnswer = 0, + question = question + ) + ExamType.oq-> OpenQuestionsVo(question = question) + } + } + + + /** + * 更新题目 + * + */ + fun update(oldExam: Exam,newExam:Exam){ + if(oldExam==_newExam.value){ + _newExam.value=newExam + }else{ + _data.value?.apply { + this[indexOf(oldExam)]=newExam + val list = mutableListOf() + list.addAll(this) + _data.value?.clear() + _data.value=list + } + } + } + + /** + * TODO 更新题库 + * + * @param callback + */ + fun updateExam(callback: (message: String) -> Unit){ + callback("功能尚未实现,敬请期待") + + } + + /** + * 加载题目 + * + */ + override fun load() { + _data.value?.apply { + repeat(initSize) { + if (Random.nextBoolean()){ + add(OpenQuestionsVo(question = StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH,value = "这是一道开放题:$size"))) + } else{ + add( + ChoiceQuestionVo( + question = StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH,value = "这是一道选择题:$size"), + answers = ('A'..'D').map { StringForm(formDesc = "选项",textLength = ANSWER_TEXT_LENGTH,value = "选项$it") }, + rightAnswer = Random.nextInt(ANSWER_SIZE) + ) + ) + } + + } + } + } + + /** + *TODO 加载更多题目 + * + * @param callback + */ + override fun loadMore(callback: (message: String) -> Unit) { +// _data.value?.apply { +// val list= mutableListOf() +// list.addAll(this) +// list.apply { +// repeat(10) { +// if (Random.nextBoolean()) add(OpenQuestionsVo(question = "这是一道开放题:$size")) else add( +// ChoiceQuestionVo( +// question = "这是一道选择题:$size,请从选项中选出正确答案", +// answers = listOf("选项A", "选项B", "选项C", "选项D"), +// rightAnswer = 3 +// ) +// ) +// } +// } +// _data.postValue(list) +// callback("成功加载更多题目") +// } + +// callback("功能尚未实现,敬请期待") + } + + fun addQuestion() { + _data.value?.apply { + _newExam.value?.let{ + val list= mutableListOf() + list.addAll(this) + list.add(it) + _data.postValue(list) + } + _newExam.value=createExam(ExamType.cq) + } + } + + /** + * TODO 删除题目 + * + */ + fun deleteQuestion(exam: Exam) { + _data.value?.apply { + val list = mutableListOf() + remove(exam) + list.addAll(this) + _data.postValue(list) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt b/app/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt index 5c3b9d3..6f825ad 100644 --- a/app/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt +++ b/app/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt @@ -108,7 +108,7 @@ private fun Search(modifier:Modifier=Modifier, model: MemberViewModel= viewModel Spacer(modifier = Modifier.weight((spaceWeight))) BaseTextField(modifier = Modifier.weight(textFieldWeight),form = model.name,singeLine = true) Spacer(modifier = Modifier.weight(spaceWeight)) - OutlinedButton(onClick = { model.search { scaffoldModel.update(it) } },modifier = Modifier.weight(buttonWeight)) { + OutlinedButton(onClick = { model.search { scaffoldModel.update(message=it) } },modifier = Modifier.weight(buttonWeight)) { Text(text = model.search) } Spacer(modifier = Modifier.weight(spaceWeight)) @@ -148,7 +148,7 @@ private fun MemberList(modifier: Modifier=Modifier, model: MemberViewModel=viewM item { Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) { IconButton(onClick = { model.loadMore{ - scaffoldModel.update(it) + scaffoldModel.update(message=it) } }) { Icon(painter = painterResource(id = R.drawable.ic_arrow_down), contentDescription = null) } @@ -302,7 +302,7 @@ private fun HistoryActivityList(modifier: Modifier,model:HistoryActViewModel= vi } } if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==model.initSize/2-1){ - model.loadMore { scaffoldModel.update(it) } + model.loadMore { scaffoldModel.update(message=it) } } } diff --git a/app/src/main/java/com/gyf/csams/association/ui/ExamActivity.kt b/app/src/main/java/com/gyf/csams/association/ui/ExamActivity.kt new file mode 100644 index 0000000..a16279d --- /dev/null +++ b/app/src/main/java/com/gyf/csams/association/ui/ExamActivity.kt @@ -0,0 +1,306 @@ +package com.gyf.csams.association.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.* +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.gyf.csams.R +import com.gyf.csams.association.model.* +import com.gyf.csams.uikit.* + +/** + * 题库管理 + * + */ +class ExamActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Body { scaffoldState -> + MainFrame(background = { Background(image = BackgroundImage.exam) }) { + Spacer(modifier = Modifier.weight(0.1F)) + Title(modifier = Modifier.weight(0.1F)) + Exam(modifier = Modifier.weight(0.8F)) + ShowSnackbar(scaffoldState = scaffoldState) + } + } + } + } +} + +/** + * 底部按钮 + * + */ +@Composable +private fun BottomButton( + modifier: Modifier = Modifier, + model: ExamViewModel = viewModel(), + scaffoldModel: ScaffoldModel = viewModel() +) { + val context = LocalContext.current as ExamActivity + Row(modifier = modifier, horizontalArrangement = Arrangement.Center) { + OutlinedButton(onClick = { + model.updateExam { scaffoldModel.update(message=it) } + }, modifier = Modifier.background(color = MaterialTheme.colors.primary)) { + Text(text = model.updateExam) + } + Spacer(modifier = Modifier.width(10.dp)) + OutlinedButton(onClick = { + context.onBackPressed() + }, modifier = Modifier.background(color = MaterialTheme.colors.secondary)) { + Text(text = model.back) + } + } +} + +/** + * 标题 + * + * @param modifier + * @param model + */ +@Composable +private fun Title(modifier: Modifier = Modifier, model: ExamViewModel = viewModel()) { + Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) { + Text(text = model.menuName, style = MaterialTheme.typography.h4) + } +} + +@Composable +private fun ExamChild(it:Exam,examHeight: Dp,isAdd: Boolean=false){ + val questionWeight=0.3F + when (it) { + is OpenQuestionsVo -> ExamOQ( + openQuestionsVo = it, + modifier = Modifier.height(examHeight*questionWeight), + isAdd = isAdd + ) + is ChoiceQuestionVo -> ExamCQ( + choiceQuestionVo = it, + modifier = Modifier.height(examHeight), + questionWeight = questionWeight, + isAdd = isAdd + ) + } +} + +/** + * 题目列表 + * + * @param modifier + * @param model + */ +@Composable +private fun Exam( + modifier: Modifier = Modifier, + model: ExamViewModel = viewModel(), + scaffoldModel: ScaffoldModel = viewModel(), + examHeight: Dp = 350.dp +) { + val listState = rememberLazyListState() + val data by model.data.observeAsState() + val newExam by model.newExam.observeAsState() + LazyColumn(state = listState, modifier = modifier) { + data?.forEach { + item { + ExamChild(it = it, examHeight = examHeight) + Spacer(modifier = Modifier.height(20.dp)) + } + } + newExam?.let { + item { + Column { + OutlinedButton(onClick = { model.switchType(it) }) { + Text(text = "切换到${if (newExam is ChoiceQuestionVo) "开放题" else "选择题"}") + } + ExamChild(it = it, examHeight = examHeight,isAdd = true) + } + } + } + + item { + Column { + Divider(color = MaterialTheme.colors.background) + Spacer(modifier = Modifier.height(30.dp)) + BottomButton(modifier = Modifier.fillMaxWidth()) + } + } + + + } + if (listState.layoutInfo.totalItemsCount - listState.firstVisibleItemIndex == model.initSize / 2 - 1) { + model.loadMore { scaffoldModel.update(message=it) } + } + + +} + +/** + * 问题 + * + * @param modifier + * @param exam + */ +@Composable +private fun Question(modifier: Modifier = Modifier, exam: Exam) { + BaseTextField( + form = exam.question, + modifier = modifier + .fillMaxSize() + .background(color = MaterialTheme.colors.background) + ) + +} + +/** + * 操作按钮 + * + */ +@Composable +private fun ActionButton(modifier: Modifier = Modifier, isAdd: Boolean, + model: ExamViewModel= viewModel(), + scaffoldModel: ScaffoldModel= viewModel(), + exam: Exam) { + val list by model.data.observeAsState() + val newExam by model.newExam.observeAsState() + Box( + contentAlignment = Alignment.Center, + modifier = modifier + ) { + IconButton(onClick = { + if(isAdd){ + if((newExam?.question?.formValue?.value ?: "").isNotEmpty()){ + scaffoldModel.update(message=model.addTip,actionLabel = model.actionLabel){ + model.addQuestion() + } + }else{ + scaffoldModel.update(message = model.questionIsNull) + } + + }else{ + if(list?.size==1){ + scaffoldModel.update(model.deleteLeastOne) + }else{ + scaffoldModel.update(message=model.deleteTip,actionLabel = model.actionLabel){ + model.deleteQuestion(exam = exam) + } + } + } + }) { + Icon( + painter = painterResource(id = if (isAdd) R.drawable.ic_add_select else R.drawable.ic_sami_select), + contentDescription = null + ) + } + } +} + + +/** + * 开放题 + * + * @param openQuestionsVo + */ +@Composable +private fun ExamOQ( + modifier: Modifier = Modifier, + openQuestionsVo: OpenQuestionsVo, + isAdd: Boolean = false +) { + Row(modifier = modifier) { + Question( + exam = openQuestionsVo, modifier = Modifier + .weight(0.8F) + .fillMaxHeight() + ) + ActionButton( + modifier = Modifier + .weight(0.2F) + .fillMaxHeight(), + isAdd = isAdd, + exam = openQuestionsVo + ) + } +} + +/** + * 选择题 + * + * @param choiceQuestionVo + */ +@Composable +private fun ExamCQ( + modifier: Modifier = Modifier, + choiceQuestionVo: ChoiceQuestionVo, + isAdd: Boolean = false, + model: ExamViewModel = viewModel(), + questionWeight:Float +) { + Row(modifier = modifier) { + Column( + modifier = Modifier + .weight(0.8F) + .fillMaxHeight() + ) { + Question( + exam = choiceQuestionVo, + modifier = Modifier + .fillMaxWidth() + .weight(questionWeight) + ) + Card( + backgroundColor = MaterialTheme.colors.secondaryVariant, + modifier = Modifier + .fillMaxWidth() + .weight(1 - questionWeight) + ) { + + choiceQuestionVo.answers.apply { + Column { + forEach { + Row(modifier = Modifier + .fillMaxWidth() + .weight(1F / ANSWER_SIZE)) { + val answerIndex: Int = indexOf(it) + val click = { + model.update( + oldExam = choiceQuestionVo, + newExam = choiceQuestionVo.copy(rightAnswer = answerIndex) + ) + } + val isRightAnswer = + choiceQuestionVo.rightAnswer == answerIndex + RadioButton(selected = isRightAnswer, onClick = click) + BaseTextField(form = it) + } + } + } + } + } + } + + ActionButton( + modifier = Modifier + .weight(0.2F) + .fillMaxHeight(), + isAdd = isAdd, + exam = choiceQuestionVo + ) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gyf/csams/association/ui/ReNameActivity.kt b/app/src/main/java/com/gyf/csams/association/ui/ReNameActivity.kt index 5b858b4..b204ddd 100644 --- a/app/src/main/java/com/gyf/csams/association/ui/ReNameActivity.kt +++ b/app/src/main/java/com/gyf/csams/association/ui/ReNameActivity.kt @@ -101,7 +101,7 @@ private fun BottomButton(modifier: Modifier=Modifier,model:RenameViewModel= view val context= LocalContext.current as ReNameActivity Spacer(modifier = Modifier.weight(weight)) Row(modifier=Modifier.weight(0.5F)) { - OutlinedButton(onClick = { model.post{scaffoldModel.update(it)} }) { + OutlinedButton(onClick = { model.post{scaffoldModel.update(message=it)} }) { Text(text = model.postDesc) } Spacer(modifier = Modifier.width(10.dp)) diff --git a/app/src/main/java/com/gyf/csams/association/ui/RegAssociationActivity.kt b/app/src/main/java/com/gyf/csams/association/ui/RegAssociationActivity.kt index 9656279..cfd59f7 100644 --- a/app/src/main/java/com/gyf/csams/association/ui/RegAssociationActivity.kt +++ b/app/src/main/java/com/gyf/csams/association/ui/RegAssociationActivity.kt @@ -179,7 +179,7 @@ private fun BottomButton(modifier: Modifier=Modifier,scaffoldModel: ScaffoldMode val context= LocalContext.current as RegAssociationActivity Row(modifier = modifier,horizontalArrangement = Arrangement.Center) { OutlinedButton(onClick = { - model.register { scaffoldModel.update(it) } + model.register { scaffoldModel.update(message=it) } },modifier = Modifier.background(color = MaterialTheme.colors.primary)) { Text(text = model.register) } diff --git a/app/src/main/java/com/gyf/csams/main/ui/MainActivity.kt b/app/src/main/java/com/gyf/csams/main/ui/MainActivity.kt index 132e19a..5581691 100644 --- a/app/src/main/java/com/gyf/csams/main/ui/MainActivity.kt +++ b/app/src/main/java/com/gyf/csams/main/ui/MainActivity.kt @@ -206,7 +206,7 @@ private fun AssociationListBody(model: ListViewModel = viewModel(),scaffoldModel } if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==model.associationListSize/2-1){ - model.loadMore { scaffoldModel.update(it) } + model.loadMore { scaffoldModel.update(message=it) } } } @@ -243,7 +243,7 @@ private fun AssociationSearch(model: ListViewModel = viewModel(),scaffoldModel: modifier = Modifier.height(10.dp) ) Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { - OutlinedButton(onClick = { model.search { scaffoldModel.update(it) } }, modifier = Modifier.width(100.dp)) { + OutlinedButton(onClick = { model.search { scaffoldModel.update(message=it) } }, modifier = Modifier.width(100.dp)) { Text(text = model.searchDesc) } } @@ -269,7 +269,7 @@ private fun Notification(mainViewModel: MainViewModel= viewModel(),scaffoldModel .padding(10.dp) ) { IconButton(onClick = { - mainViewModel.openNotification { scaffoldModel.update(it) } + mainViewModel.openNotification { scaffoldModel.update(message=it) } }) { Icon( painter = painterResource(id = R.drawable.ic_notification), @@ -312,7 +312,7 @@ private fun MessageBoard(model: MarqueeViewModel = viewModel(),mainViewModel:Mai ) { - IconButton(onClick = { mainViewModel.sendMessage { scaffoldModel.update(it) } }) { + IconButton(onClick = { mainViewModel.sendMessage { scaffoldModel.update(message=it) } }) { Icon( painter = painterResource(id = R.drawable.ic_comments), contentDescription = null, diff --git a/app/src/main/java/com/gyf/csams/uikit/BaseView.kt b/app/src/main/java/com/gyf/csams/uikit/BaseView.kt index ce579d8..2482c64 100644 --- a/app/src/main/java/com/gyf/csams/uikit/BaseView.kt +++ b/app/src/main/java/com/gyf/csams/uikit/BaseView.kt @@ -45,7 +45,7 @@ import kotlinx.coroutines.launch * @param text */ @Composable -fun AnimationText(text:String){ +fun AnimationText(text: String) { val infiniteTransition = rememberInfiniteTransition() val color by infiniteTransition.animateColor( initialValue = MaterialTheme.colors.primary, @@ -67,14 +67,18 @@ fun AnimationText(text:String){ * 主菜单 * */ -enum class MainMenu(@DrawableRes val selectedIcon:Int, - @DrawableRes val unSelectedIcon:Int){ +enum class MainMenu( + @DrawableRes val selectedIcon: Int, + @DrawableRes val unSelectedIcon: Int +) { //主页 - Main(R.drawable.ic_home_fill,R.drawable.ic_home), + Main(R.drawable.ic_home_fill, R.drawable.ic_home), + //社团列表 - List(R.drawable.ic_all_fill,R.drawable.ic_all), + List(R.drawable.ic_all_fill, R.drawable.ic_all), + //个人中心 - Center(R.drawable.ic_account_fill,R.drawable.ic_account) + Center(R.drawable.ic_account_fill, R.drawable.ic_account) } /** @@ -86,9 +90,10 @@ enum class MainMenu(@DrawableRes val selectedIcon:Int, * @param onClick */ @Composable -fun MenuIconButton(_menu: MainMenu,menu: MainMenu,modifier: Modifier,onClick: () -> Unit){ - Row(modifier = modifier - ,horizontalArrangement = Arrangement.Center) { +fun MenuIconButton(_menu: MainMenu, menu: MainMenu, modifier: Modifier, onClick: () -> Unit) { + Row( + modifier = modifier, horizontalArrangement = Arrangement.Center + ) { IconButton(onClick = onClick) { Icon( painter = painterResource(id = if (_menu == menu) menu.selectedIcon else menu.unSelectedIcon), @@ -106,19 +111,19 @@ fun MenuIconButton(_menu: MainMenu,menu: MainMenu,modifier: Modifier,onClick: () * @param modifier */ @Composable -fun MainBottomAppBar(menu:MainMenu, nav: NavHostController, modifier: Modifier=Modifier){ - BottomAppBar(backgroundColor = MaterialTheme.colors.background,modifier=modifier) { +fun MainBottomAppBar(menu: MainMenu, nav: NavHostController, modifier: Modifier = Modifier) { + BottomAppBar(backgroundColor = MaterialTheme.colors.background, modifier = modifier) { //图标宽度平等分 - val weight=1/(MainMenu.values().size*1.0f) + val weight = 1 / (MainMenu.values().size * 1.0f) Row(Modifier.fillMaxWidth()) { - MenuIconButton(_menu = menu,menu = MainMenu.Main,Modifier.weight(weight), - onClick = { nav.navigate(MainMenu.Main.name)}) - MenuIconButton(_menu = menu,menu = MainMenu.List,Modifier.weight(weight), - onClick = { nav.navigate(MainMenu.List.name)}) - MenuIconButton(_menu = menu,menu = MainMenu.Center,Modifier.weight(weight), - onClick = { nav.navigate(MainMenu.Center.name)}) + MenuIconButton(_menu = menu, menu = MainMenu.Main, Modifier.weight(weight), + onClick = { nav.navigate(MainMenu.Main.name) }) + MenuIconButton(_menu = menu, menu = MainMenu.List, Modifier.weight(weight), + onClick = { nav.navigate(MainMenu.List.name) }) + MenuIconButton(_menu = menu, menu = MainMenu.Center, Modifier.weight(weight), + onClick = { nav.navigate(MainMenu.Center.name) }) } } @@ -128,7 +133,7 @@ fun MainBottomAppBar(menu:MainMenu, nav: NavHostController, modifier: Modifier=M * 社团菜单 * */ -enum class AssociationMenu(val menuName:String){ +enum class AssociationMenu(val menuName: String) { member("社团成员"), main("社团主页"), list("活动列表") @@ -139,34 +144,52 @@ enum class AssociationMenu(val menuName:String){ * */ @Composable -fun AssociationAppBar(menu: AssociationMenu,nav: NavHostController,back:()->Unit,dropMenu:()->Unit){ - TopAppBar(backgroundColor = MaterialTheme.colors.secondary) { - Row(modifier = Modifier.fillMaxWidth(),verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = back,modifier = Modifier.weight(0.1F)) { - Icon(painter = painterResource(id = R.drawable.ic_arrow_left), contentDescription = null) - } - Row(modifier = Modifier - .weight(0.8F) - .fillMaxHeight(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - val menus=AssociationMenu.values() - menus.forEach { - Row(modifier = Modifier - .weight(1F / menus.size) - .clickable(onClick = { nav.navigate(it.name) }), - horizontalArrangement = Arrangement.Center) { - Text(text = it.menuName,color = if(menu==it) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground) - } - } - } - IconButton(onClick = dropMenu,modifier = Modifier.weight(0.1F)) { - Icon(painter = painterResource(id = R.drawable.ic_configuration), contentDescription = null) - } - } - } +fun AssociationAppBar( + menu: AssociationMenu, + nav: NavHostController, + back: () -> Unit, + dropMenu: () -> Unit +) { + TopAppBar(backgroundColor = MaterialTheme.colors.secondary) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = back, modifier = Modifier.weight(0.1F)) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_left), + contentDescription = null + ) + } + Row( + modifier = Modifier + .weight(0.8F) + .fillMaxHeight(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + val menus = AssociationMenu.values() + menus.forEach { + Row( + modifier = Modifier + .weight(1F / menus.size) + .clickable(onClick = { nav.navigate(it.name) }), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = it.menuName, + color = if (menu == it) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground + ) + } + } + } + IconButton(onClick = dropMenu, modifier = Modifier.weight(0.1F)) { + Icon( + painter = painterResource(id = R.drawable.ic_configuration), + contentDescription = null + ) + } + } + } } + /** * 跑马灯 * @@ -174,11 +197,14 @@ fun AssociationAppBar(menu: AssociationMenu,nav: NavHostController,back:()->Unit * @param content */ @Composable -fun Marquee(model: MarqueeViewModel = viewModel(), content: @Composable BoxScope.(model: MarqueeViewModel, - value: State -) -> Unit) { +fun Marquee( + model: MarqueeViewModel = viewModel(), content: @Composable BoxScope.( + model: MarqueeViewModel, + value: State + ) -> Unit +) { - val delayMillis=2000 + val delayMillis = 2000 Column { BoxWithConstraints { val transition = rememberInfiniteTransition() @@ -232,7 +258,12 @@ fun MarqueeText(model: MarqueeViewModel = viewModel(), offset: State) { * @param body 内容 */ @Composable -fun MainFrame( background:@Composable ()->Unit,mainMenu: MainMenu,nav: NavHostController,body:@Composable ColumnScope.()->Unit){ +fun MainFrame( + background: @Composable () -> Unit, + mainMenu: MainMenu, + nav: NavHostController, + body: @Composable ColumnScope.() -> Unit +) { Box(modifier = Modifier.fillMaxSize()) { background() Column { @@ -252,7 +283,7 @@ fun MainFrame( background:@Composable ()->Unit,mainMenu: MainMenu,nav: NavHostCo * @param body */ @Composable -fun MainFrame(background:@Composable ()->Unit,body:@Composable ColumnScope.()->Unit){ +fun MainFrame(background: @Composable () -> Unit, body: @Composable ColumnScope.() -> Unit) { Box(modifier = Modifier.fillMaxSize()) { background() Column(content = body) @@ -284,19 +315,23 @@ fun Carousel( * @param singeLine */ @Composable -fun BaseTextField(modifier:Modifier=Modifier,form:T, singeLine:Boolean=false){ +fun BaseTextField( + modifier: Modifier = Modifier, + form: T, + singeLine: Boolean = false +) { val name: String by form.formValue.observeAsState("") val focusManager = LocalFocusManager.current OutlinedTextField( modifier = modifier, value = name, - onValueChange = {form.onChange(it)}, - label={ Text(text = form.formDesc)}, - placeholder = { Text(text = form.formPlaceholder)}, + onValueChange = { form.onChange(it) }, + label = { Text(text = form.formDesc) }, + placeholder = { Text(text = form.formPlaceholder) }, singleLine = singeLine, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - trailingIcon={ Text(text = "${name.length}/${form.textLength}") }) + trailingIcon = { Text(text = "${name.length}/${form.textLength}") }) } /** @@ -306,17 +341,34 @@ fun BaseTextField(modifier:Modifier=Modifier,form:T, singeLine:Bo * @param scaffoldState */ @Composable -fun ShowSnackbar(model:ScaffoldModel= viewModel(),scaffoldState: ScaffoldState){ - val message:String? by model.message.observeAsState() - message?.let { - Logger.i("message=$it") - LaunchedEffect(scaffoldState){ - launch { - scaffoldState.snackbarHostState.showSnackbar( - message = it - ) - model.update() +fun ShowSnackbar(model: ScaffoldModel = viewModel(), scaffoldState: ScaffoldState) { + val snackBar: SnackBar? by model.data.observeAsState() + snackBar?.apply { + if (message != null) { + LaunchedEffect(scaffoldState) { + launch { + if (actionLabel != null) { + val result=scaffoldState.snackbarHostState.showSnackbar( + message = message, actionLabel = actionLabel, + duration = duration + + ) + when(result){ + SnackbarResult.ActionPerformed->{ + Logger.i("点击操作按钮") + callback() + } + SnackbarResult.Dismissed->{ + Logger.d("窗口消失") + } + } + } else { + scaffoldState.snackbarHostState.showSnackbar(message = message) + } + model.update() + } } + } } } @@ -327,19 +379,28 @@ fun ShowSnackbar(model:ScaffoldModel= viewModel(),scaffoldState: ScaffoldState){ * * @property id 资源id */ -enum class BackgroundImage(@DrawableRes val id:Int){ +enum class BackgroundImage(@DrawableRes val id: Int) { //主页 main(R.drawable.mb_bg_fb_08), + //社团列表 list(R.drawable.mb_bg_fb_07), + //个人中心 center(R.drawable.mb_bg_fb_28), + //注册社团 reg_association(R.drawable.mb_bg_fb_06), + //社团主界面 association_main(R.drawable.mb_bg_fb_25_180), + //社团重命名 - rename(R.drawable.mb_bg_fb_27) + rename(R.drawable.mb_bg_fb_27), + + //社团题库管理 + exam(R.drawable.mb_bg_fb_02) + } /** @@ -349,12 +410,16 @@ enum class BackgroundImage(@DrawableRes val id:Int){ * @param alpha */ @Composable -fun Background(image: BackgroundImage, alpha:Float= DefaultAlpha){ - val app= LocalContext.current.applicationContext as APP +fun Background(image: BackgroundImage, alpha: Float = DefaultAlpha) { + val app = LocalContext.current.applicationContext as APP BoxWithConstraints { Image( - bitmap = app.getImage(image = image,reqHeight = maxHeight.value.toInt()/2,reqWidth = maxWidth.value.toInt()/2), + bitmap = app.getImage( + image = image, + reqHeight = maxHeight.value.toInt() / 2, + reqWidth = maxWidth.value.toInt() / 2 + ), contentDescription = null, contentScale = ContentScale.FillHeight, alpha = alpha, @@ -369,12 +434,12 @@ fun Background(image: BackgroundImage, alpha:Float= DefaultAlpha){ * @param content */ @Composable -fun Body(content:@Composable (scaffoldState:ScaffoldState)->Unit){ +fun Body(content: @Composable (scaffoldState: ScaffoldState) -> Unit) { Surface(color = MaterialTheme.colors.background) { val scaffoldState = rememberScaffoldState() Scaffold(scaffoldState = scaffoldState) { - content(scaffoldState=scaffoldState) + content(scaffoldState = scaffoldState) } } } @@ -385,13 +450,13 @@ fun Body(content:@Composable (scaffoldState:ScaffoldState)->Unit){ * @param content */ @Composable -fun Body( content:@Composable (nav:NavHostController, scaffoldState:ScaffoldState)->Unit){ +fun Body(content: @Composable (nav: NavHostController, scaffoldState: ScaffoldState) -> Unit) { Surface(color = MaterialTheme.colors.background) { val navController = rememberNavController() val scaffoldState = rememberScaffoldState() Scaffold(scaffoldState = scaffoldState) { - content(nav=navController,scaffoldState=scaffoldState) + content(nav = navController, scaffoldState = scaffoldState) } } } @@ -401,7 +466,7 @@ fun Body( content:@Composable (nav:NavHostController, scaffoldState:ScaffoldStat * */ @Composable -fun Poster(modifier: Modifier=Modifier,@DrawableRes id: Int){ +fun Poster(modifier: Modifier = Modifier, @DrawableRes id: Int) { Card( modifier = modifier, backgroundColor = Color.Transparent @@ -410,12 +475,12 @@ fun Poster(modifier: Modifier=Modifier,@DrawableRes id: Int){ Image( painter = painterResource(id = R.drawable.hot_activity_background), contentDescription = null, - modifier=Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize() ) Image( painter = painterResource(id = id), contentDescription = null, - modifier=Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize() ) } } @@ -426,7 +491,7 @@ fun Poster(modifier: Modifier=Modifier,@DrawableRes id: Int){ * */ @Composable -fun DescCard(modifier: Modifier){ +fun DescCard(modifier: Modifier) { Card( modifier = modifier, backgroundColor = Color.Transparent @@ -459,13 +524,13 @@ fun DescCard(modifier: Modifier){ @Preview @Composable -fun AnimationTextPreview(){ +fun AnimationTextPreview() { AnimationText(text = "6666") } //@Preview @Composable -fun MyBottomAppBarPreview(){ +fun MyBottomAppBarPreview() { CSAMSTheme { } diff --git a/app/src/main/java/com/gyf/csams/uikit/ViewModel.kt b/app/src/main/java/com/gyf/csams/uikit/ViewModel.kt index 10c9c31..cff2820 100644 --- a/app/src/main/java/com/gyf/csams/uikit/ViewModel.kt +++ b/app/src/main/java/com/gyf/csams/uikit/ViewModel.kt @@ -1,5 +1,6 @@ package com.gyf.csams.uikit +import androidx.compose.material.SnackbarDuration import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -29,6 +30,11 @@ abstract class FormName(val formDesc:String){ */ open class StringForm(formDesc: String, val textLength: Int) : FormName(formDesc = formDesc), FormLength { + + constructor(formDesc: String,textLength: Int,value:String) : this(formDesc = formDesc,textLength = textLength) { + _formValue.value=value + } + override val nameLengthError="${formDesc}不能超过最大长度$textLength" override fun onChange(value: String) { @@ -41,16 +47,23 @@ open class StringForm(formDesc: String, val textLength: Int) : FormName( } } + +data class SnackBar(val message:String?, val actionLabel:String?=null,val duration: SnackbarDuration=SnackbarDuration.Short, val callback: () -> Unit?) + /** * snackbar * */ class ScaffoldModel:ViewModel(){ - private val _message=MutableLiveData() - val message:LiveData = _message - - fun update(message:String?=null){ - _message.value=message + private val _data=MutableLiveData() + val data:LiveData = _data + + fun update(message:String?=null,actionLabel: String? = null,callback: () -> Unit? = {}){ + if(message==null){ + _data.value=null + }else { + _data.value = SnackBar(message = message, actionLabel = actionLabel,callback = callback) + } } } diff --git a/app/src/main/java/com/gyf/csams/util/GsonUtil.kt b/app/src/main/java/com/gyf/csams/util/GsonUtil.kt new file mode 100644 index 0000000..45f0d0d --- /dev/null +++ b/app/src/main/java/com/gyf/csams/util/GsonUtil.kt @@ -0,0 +1,111 @@ +package com.gyf.csams.util + +import com.google.gson.* +import com.google.gson.reflect.TypeToken +import com.gyf.csams.association.model.* +import com.gyf.csams.uikit.StringForm +import java.lang.reflect.Type + +class OpenQuestionsVoSerializer: JsonSerializer { + override fun serialize( + vo: OpenQuestionsVo?, + p1: Type?, + p2: JsonSerializationContext? + ): JsonElement { + val gson= Gson() + val c = JsonObject() + c.add("examType", gson.toJsonTree(vo?.examType?.name)) + c.add("question",gson.toJsonTree(vo?.question?.formValue?.value)) + return c + } +} + +class OpenQuestionsVoDeserializer: JsonDeserializer { + override fun deserialize( + json: JsonElement?, + p1: Type?, + p2: JsonDeserializationContext? + ): OpenQuestionsVo { + val root=json?.asJsonObject + val value=root?.get("question")?.asString + val question= StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH) + if (value != null) { + question.onChange(value) + return OpenQuestionsVo(question = question) + }else{ + throw NullPointerException("问题无法解析!!!!") + } + } +} + +class ChoiceQuestionVoSerializer: JsonSerializer { + override fun serialize( + vo: ChoiceQuestionVo?, + p1: Type?, + p2: JsonSerializationContext? + ): JsonElement { + val gson= Gson() + val c = JsonObject() + c.add("examType", gson.toJsonTree(vo?.examType?.name)) + c.add("question",gson.toJsonTree(vo?.question?.formValue?.value)) + c.add("answers",gson.toJsonTree(vo?.answers)) + c.add("rightAnswer",gson.toJsonTree(vo?.rightAnswer)) + return c + } +} + +class ChoiceQuestionVoDeserializer: JsonDeserializer { + override fun deserialize( + json: JsonElement?, + p1: Type?, + p2: JsonDeserializationContext? + ): ChoiceQuestionVo { + val root=json?.asJsonObject + val value=root?.get("question")?.asString + val question=StringForm(formDesc = "问题",textLength = QUESTION_TEXT_LENGTH) + if (value != null) { + question.onChange(value) + val gson=Gson() + val answers:List = gson.fromJson(root.get("answers"),object : TypeToken>() {}.type) + if(answers.size!= ANSWER_SIZE){ + throw IllegalArgumentException("选项数量!=$QUESTION_TEXT_LENGTH") + } + val rightAnswer=root.get("rightAnswer").asInt + return ChoiceQuestionVo( + question = question, + answers = answers.map { StringForm(formDesc = "选项",textLength = ANSWER_TEXT_LENGTH,value=it) }, + rightAnswer = rightAnswer + ) + }else{ + throw NullPointerException("问题无法解析!!!!") + } + } +} + +/** + * 题目反序列化 + * + */ +class ExamDeserializer:JsonDeserializer{ + companion object{ + val gson: Gson =GsonBuilder() + .registerTypeAdapter(OpenQuestionsVo::class.java,OpenQuestionsVoDeserializer()) + .registerTypeAdapter(ChoiceQuestionVo::class.java,ChoiceQuestionVoDeserializer()) + .create() + } + + override fun deserialize(json: JsonElement?, p1: Type?, p2: JsonDeserializationContext?): Exam { + val root=json?.asJsonObject + + val type=root?.get("examType")?.asString + + return when (gson.fromJson(type, ExamType::class.java)) { + ExamType.cq -> gson.fromJson(json, + object : TypeToken() {}.type) + ExamType.oq -> gson.fromJson(json, + object : TypeToken() {}.type) + null->throw NullPointerException("无法识别题目类型!") + } + + } +} \ 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 aa8b9a4..dacfd92 100644 --- a/app/src/main/java/com/gyf/csams/util/HttpUtil.kt +++ b/app/src/main/java/com/gyf/csams/util/HttpUtil.kt @@ -1,6 +1,8 @@ package com.gyf.csams.util import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.gyf.csams.association.model.Exam import com.orhanobut.logger.Logger import okhttp3.* import okhttp3.MediaType.Companion.toMediaType @@ -93,10 +95,18 @@ object HttpClient{ 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{ + + companion object{ + val gson: Gson =GsonBuilder() + .registerTypeAdapter(Exam::class.java,ExamDeserializer()) + .create() + } + override fun onFailure(call: Call, e: IOException) { onFail("${action}失败,请联系管理员") Logger.e(e,"${action}请求失败,发生IO异常") @@ -107,7 +117,7 @@ class SimpleCallback(private val action:String, val body=response.body if (body!=null&&body.contentType()?.subtype == "json") { val jsonRes=body.string() - val res:ApiResponse = Gson().fromJson(jsonRes, type) + val res:ApiResponse = gson.fromJson(jsonRes, type) Logger.i("${action}请求响应成功:") Logger.json(jsonRes) onSuccess(res) diff --git a/app/src/main/res/drawable/ic_add_select.xml b/app/src/main/res/drawable/ic_add_select.xml new file mode 100644 index 0000000..94dd401 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_select.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sami_select.xml b/app/src/main/res/drawable/ic_sami_select.xml new file mode 100644 index 0000000..4e24c08 --- /dev/null +++ b/app/src/main/res/drawable/ic_sami_select.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt b/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt index 070132b..df62de0 100644 --- a/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt +++ b/app/src/test/java/com/gyf/csams/ExampleUnitTest.kt @@ -5,8 +5,6 @@ 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 /** @@ -36,7 +34,14 @@ class ExampleUnitTest { @Test fun testYear(){ - println( LocalDateTime.now().get(ChronoField.YEAR)) + repeat(10,{ + println(it) + }) + } + + @Test + fun testCharRange(){ + println(('A'..'D').map { "选项$it" }) } } diff --git a/build.gradle.kts b/build.gradle.kts index ea6d7c1..b5bab5d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,13 +8,14 @@ buildscript { val APP_NAME by extra("大学生社团管理系统") val SERVER_ADDRESS by extra("http://192.168.50.107:8080") val room_version by extra("2.2.6") + val kotlin_version by extra("1.4.32") repositories { maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/public") } dependencies { classpath("com.android.tools.build:gradle:7.0.0-alpha15") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle.kts files