自定义Gson序列化反序列化题目实体类逻辑

SnackBar增加操作属性和回调配置
增加社团题库管理
master
pan 4 years ago
parent e8dfc71a90
commit 54ecee40c0
  1. 6
      app/build.gradle.kts
  2. 2
      app/src/androidTest/java/com/gyf/csams/ExampleInstrumentedTest.kt
  3. 5
      app/src/main/AndroidManifest.xml
  4. 227
      app/src/main/java/com/gyf/csams/association/model/ExamViewModel.kt
  5. 6
      app/src/main/java/com/gyf/csams/association/ui/AssociationActivity.kt
  6. 306
      app/src/main/java/com/gyf/csams/association/ui/ExamActivity.kt
  7. 2
      app/src/main/java/com/gyf/csams/association/ui/ReNameActivity.kt
  8. 2
      app/src/main/java/com/gyf/csams/association/ui/RegAssociationActivity.kt
  9. 8
      app/src/main/java/com/gyf/csams/main/ui/MainActivity.kt
  10. 109
      app/src/main/java/com/gyf/csams/uikit/BaseView.kt
  11. 21
      app/src/main/java/com/gyf/csams/uikit/ViewModel.kt
  12. 111
      app/src/main/java/com/gyf/csams/util/GsonUtil.kt
  13. 12
      app/src/main/java/com/gyf/csams/util/HttpUtil.kt
  14. 9
      app/src/main/res/drawable/ic_add_select.xml
  15. 9
      app/src/main/res/drawable/ic_sami_select.xml
  16. 11
      app/src/test/java/com/gyf/csams/ExampleUnitTest.kt
  17. 3
      build.gradle.kts

@ -26,7 +26,7 @@ android {
val serverAddress=rootProject.extra["SERVER_ADDRESS"] val serverAddress=rootProject.extra["SERVER_ADDRESS"]
debug { debug {
manifestPlaceholders.apply { manifestPlaceholders.apply {
this["APP_NAME"] = appName; this["APP_NAME"] = appName
} }
buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"") buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"")
buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"") buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"")
@ -38,7 +38,7 @@ android {
"proguard-rules.pro" "proguard-rules.pro"
) )
manifestPlaceholders.apply { manifestPlaceholders.apply {
this["APP_NAME"] = appName; this["APP_NAME"] = appName
} }
buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"") buildConfigField(type="String",name="APP_NAME",value = "\"$appName\"")
buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"") buildConfigField(type="String",name="SERVER_ADDRESS",value = "\"$serverAddress\"")
@ -122,6 +122,8 @@ dependencies {
* https://kotlinlang.org/docs/serialization.html * https://kotlinlang.org/docs/serialization.html
*/ */
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") 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 * https://developer.android.com/jetpack/androidx/releases/navigation
*/ */

@ -18,5 +18,7 @@ class ExampleInstrumentedTest {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.gyf.csams", appContext.packageName) assertEquals("com.gyf.csams", appContext.packageName)
} }
} }

@ -57,6 +57,11 @@
<activity android:name=".association.ui.ReNameActivity" <activity android:name=".association.ui.ReNameActivity"
android:exported="true"> android:exported="true">
</activity> </activity>
<!--题库界面-->
<activity android:name=".association.ui.ExamActivity"
android:exported="true">
</activity>
</application> </application>
</manifest> </manifest>

@ -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<StringForm>,
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<Exam>() {
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<Exam> = MutableLiveData(createExam(ExamType.cq))
val newExam:LiveData<Exam> = _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<Exam>()
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<Exam>()
// 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<Exam>()
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<Exam>()
remove(exam)
list.addAll(this)
_data.postValue(list)
}
}
}

@ -108,7 +108,7 @@ private fun Search(modifier:Modifier=Modifier, model: MemberViewModel= viewModel
Spacer(modifier = Modifier.weight((spaceWeight))) Spacer(modifier = Modifier.weight((spaceWeight)))
BaseTextField(modifier = Modifier.weight(textFieldWeight),form = model.name,singeLine = true) BaseTextField(modifier = Modifier.weight(textFieldWeight),form = model.name,singeLine = true)
Spacer(modifier = Modifier.weight(spaceWeight)) 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) Text(text = model.search)
} }
Spacer(modifier = Modifier.weight(spaceWeight)) Spacer(modifier = Modifier.weight(spaceWeight))
@ -148,7 +148,7 @@ private fun MemberList(modifier: Modifier=Modifier, model: MemberViewModel=viewM
item { item {
Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth()) {
IconButton(onClick = { model.loadMore{ IconButton(onClick = { model.loadMore{
scaffoldModel.update(it) scaffoldModel.update(message=it)
} }) { } }) {
Icon(painter = painterResource(id = R.drawable.ic_arrow_down), contentDescription = null) 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){ if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==model.initSize/2-1){
model.loadMore { scaffoldModel.update(it) } model.loadMore { scaffoldModel.update(message=it) }
} }
} }

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

@ -101,7 +101,7 @@ private fun BottomButton(modifier: Modifier=Modifier,model:RenameViewModel= view
val context= LocalContext.current as ReNameActivity val context= LocalContext.current as ReNameActivity
Spacer(modifier = Modifier.weight(weight)) Spacer(modifier = Modifier.weight(weight))
Row(modifier=Modifier.weight(0.5F)) { 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) Text(text = model.postDesc)
} }
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))

@ -179,7 +179,7 @@ private fun BottomButton(modifier: Modifier=Modifier,scaffoldModel: ScaffoldMode
val context= LocalContext.current as RegAssociationActivity val context= LocalContext.current as RegAssociationActivity
Row(modifier = modifier,horizontalArrangement = Arrangement.Center) { Row(modifier = modifier,horizontalArrangement = Arrangement.Center) {
OutlinedButton(onClick = { OutlinedButton(onClick = {
model.register { scaffoldModel.update(it) } model.register { scaffoldModel.update(message=it) }
},modifier = Modifier.background(color = MaterialTheme.colors.primary)) { },modifier = Modifier.background(color = MaterialTheme.colors.primary)) {
Text(text = model.register) Text(text = model.register)
} }

@ -206,7 +206,7 @@ private fun AssociationListBody(model: ListViewModel = viewModel(),scaffoldModel
} }
if(listState.layoutInfo.totalItemsCount-listState.firstVisibleItemIndex==model.associationListSize/2-1){ 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) modifier = Modifier.height(10.dp)
) )
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { 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) Text(text = model.searchDesc)
} }
} }
@ -269,7 +269,7 @@ private fun Notification(mainViewModel: MainViewModel= viewModel(),scaffoldModel
.padding(10.dp) .padding(10.dp)
) { ) {
IconButton(onClick = { IconButton(onClick = {
mainViewModel.openNotification { scaffoldModel.update(it) } mainViewModel.openNotification { scaffoldModel.update(message=it) }
}) { }) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_notification), 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( Icon(
painter = painterResource(id = R.drawable.ic_comments), painter = painterResource(id = R.drawable.ic_comments),
contentDescription = null, contentDescription = null,

@ -67,12 +67,16 @@ fun AnimationText(text:String){
* 主菜单 * 主菜单
* *
*/ */
enum class MainMenu(@DrawableRes val selectedIcon:Int, enum class MainMenu(
@DrawableRes val unSelectedIcon:Int){ @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)
} }
@ -87,8 +91,9 @@ enum class MainMenu(@DrawableRes val selectedIcon:Int,
*/ */
@Composable @Composable
fun MenuIconButton(_menu: MainMenu, menu: MainMenu, modifier: Modifier, onClick: () -> Unit) { fun MenuIconButton(_menu: MainMenu, menu: MainMenu, modifier: Modifier, onClick: () -> Unit) {
Row(modifier = modifier Row(
,horizontalArrangement = Arrangement.Center) { modifier = modifier, horizontalArrangement = Arrangement.Center
) {
IconButton(onClick = onClick) { IconButton(onClick = onClick) {
Icon( Icon(
painter = painterResource(id = if (_menu == menu) menu.selectedIcon else menu.unSelectedIcon), painter = painterResource(id = if (_menu == menu) menu.selectedIcon else menu.unSelectedIcon),
@ -139,13 +144,22 @@ enum class AssociationMenu(val menuName:String){
* *
*/ */
@Composable @Composable
fun AssociationAppBar(menu: AssociationMenu,nav: NavHostController,back:()->Unit,dropMenu:()->Unit){ fun AssociationAppBar(
menu: AssociationMenu,
nav: NavHostController,
back: () -> Unit,
dropMenu: () -> Unit
) {
TopAppBar(backgroundColor = MaterialTheme.colors.secondary) { TopAppBar(backgroundColor = MaterialTheme.colors.secondary) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = back, modifier = Modifier.weight(0.1F)) { IconButton(onClick = back, modifier = Modifier.weight(0.1F)) {
Icon(painter = painterResource(id = R.drawable.ic_arrow_left), contentDescription = null) Icon(
painter = painterResource(id = R.drawable.ic_arrow_left),
contentDescription = null
)
} }
Row(modifier = Modifier Row(
modifier = Modifier
.weight(0.8F) .weight(0.8F)
.fillMaxHeight(), .fillMaxHeight(),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
@ -153,20 +167,29 @@ fun AssociationAppBar(menu: AssociationMenu,nav: NavHostController,back:()->Unit
) { ) {
val menus = AssociationMenu.values() val menus = AssociationMenu.values()
menus.forEach { menus.forEach {
Row(modifier = Modifier Row(
modifier = Modifier
.weight(1F / menus.size) .weight(1F / menus.size)
.clickable(onClick = { nav.navigate(it.name) }), .clickable(onClick = { nav.navigate(it.name) }),
horizontalArrangement = Arrangement.Center) { horizontalArrangement = Arrangement.Center
Text(text = it.menuName,color = if(menu==it) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground) ) {
Text(
text = it.menuName,
color = if (menu == it) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground
)
} }
} }
} }
IconButton(onClick = dropMenu, modifier = Modifier.weight(0.1F)) { IconButton(onClick = dropMenu, modifier = Modifier.weight(0.1F)) {
Icon(painter = painterResource(id = R.drawable.ic_configuration), contentDescription = null) Icon(
painter = painterResource(id = R.drawable.ic_configuration),
contentDescription = null
)
} }
} }
} }
} }
/** /**
* 跑马灯 * 跑马灯
* *
@ -174,9 +197,12 @@ fun AssociationAppBar(menu: AssociationMenu,nav: NavHostController,back:()->Unit
* @param content * @param content
*/ */
@Composable @Composable
fun Marquee(model: MarqueeViewModel = viewModel(), content: @Composable BoxScope.(model: MarqueeViewModel, fun Marquee(
model: MarqueeViewModel = viewModel(), content: @Composable BoxScope.(
model: MarqueeViewModel,
value: State<Float> value: State<Float>
) -> Unit) { ) -> Unit
) {
val delayMillis = 2000 val delayMillis = 2000
Column { Column {
@ -232,7 +258,12 @@ fun MarqueeText(model: MarqueeViewModel = viewModel(), offset: State<Float>) {
* @param body 内容 * @param body 内容
*/ */
@Composable @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()) { Box(modifier = Modifier.fillMaxSize()) {
background() background()
Column { Column {
@ -284,7 +315,11 @@ fun Carousel(
* @param singeLine * @param singeLine
*/ */
@Composable @Composable
fun <T:StringForm> BaseTextField(modifier:Modifier=Modifier,form:T, singeLine:Boolean=false){ fun <T : StringForm> BaseTextField(
modifier: Modifier = Modifier,
form: T,
singeLine: Boolean = false
) {
val name: String by form.formValue.observeAsState("") val name: String by form.formValue.observeAsState("")
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
OutlinedTextField( OutlinedTextField(
@ -307,17 +342,34 @@ fun <T:StringForm> BaseTextField(modifier:Modifier=Modifier,form:T, singeLine:Bo
*/ */
@Composable @Composable
fun ShowSnackbar(model: ScaffoldModel = viewModel(), scaffoldState: ScaffoldState) { fun ShowSnackbar(model: ScaffoldModel = viewModel(), scaffoldState: ScaffoldState) {
val message:String? by model.message.observeAsState() val snackBar: SnackBar? by model.data.observeAsState()
message?.let { snackBar?.apply {
Logger.i("message=$it") if (message != null) {
LaunchedEffect(scaffoldState) { LaunchedEffect(scaffoldState) {
launch { launch {
scaffoldState.snackbarHostState.showSnackbar( if (actionLabel != null) {
message = it 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() model.update()
} }
} }
}
} }
} }
@ -330,16 +382,25 @@ fun ShowSnackbar(model:ScaffoldModel= viewModel(),scaffoldState: ScaffoldState){
enum class BackgroundImage(@DrawableRes val id: Int) { enum class BackgroundImage(@DrawableRes val id: Int) {
//主页 //主页
main(R.drawable.mb_bg_fb_08), main(R.drawable.mb_bg_fb_08),
//社团列表 //社团列表
list(R.drawable.mb_bg_fb_07), list(R.drawable.mb_bg_fb_07),
//个人中心 //个人中心
center(R.drawable.mb_bg_fb_28), center(R.drawable.mb_bg_fb_28),
//注册社团 //注册社团
reg_association(R.drawable.mb_bg_fb_06), reg_association(R.drawable.mb_bg_fb_06),
//社团主界面 //社团主界面
association_main(R.drawable.mb_bg_fb_25_180), 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)
} }
/** /**
@ -354,7 +415,11 @@ fun Background(image: BackgroundImage, alpha:Float= DefaultAlpha){
BoxWithConstraints { BoxWithConstraints {
Image( 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, contentDescription = null,
contentScale = ContentScale.FillHeight, contentScale = ContentScale.FillHeight,
alpha = alpha, alpha = alpha,

@ -1,5 +1,6 @@
package com.gyf.csams.uikit package com.gyf.csams.uikit
import androidx.compose.material.SnackbarDuration
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -29,6 +30,11 @@ abstract class FormName<T>(val formDesc:String){
*/ */
open class StringForm(formDesc: String, val textLength: Int) : FormName<String>(formDesc = formDesc), open class StringForm(formDesc: String, val textLength: Int) : FormName<String>(formDesc = formDesc),
FormLength { FormLength {
constructor(formDesc: String,textLength: Int,value:String) : this(formDesc = formDesc,textLength = textLength) {
_formValue.value=value
}
override val nameLengthError="${formDesc}不能超过最大长度$textLength" override val nameLengthError="${formDesc}不能超过最大长度$textLength"
override fun onChange(value: String) { override fun onChange(value: String) {
@ -41,16 +47,23 @@ open class StringForm(formDesc: String, val textLength: Int) : FormName<String>(
} }
} }
data class SnackBar(val message:String?, val actionLabel:String?=null,val duration: SnackbarDuration=SnackbarDuration.Short, val callback: () -> Unit?)
/** /**
* snackbar * snackbar
* *
*/ */
class ScaffoldModel:ViewModel(){ class ScaffoldModel:ViewModel(){
private val _message=MutableLiveData<String>() private val _data=MutableLiveData<SnackBar>()
val message:LiveData<String> = _message val data:LiveData<SnackBar> = _data
fun update(message:String?=null){ fun update(message:String?=null,actionLabel: String? = null,callback: () -> Unit? = {}){
_message.value=message if(message==null){
_data.value=null
}else {
_data.value = SnackBar(message = message, actionLabel = actionLabel,callback = callback)
}
} }
} }

@ -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<OpenQuestionsVo> {
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<OpenQuestionsVo> {
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<ChoiceQuestionVo> {
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<ChoiceQuestionVo> {
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<String> = gson.fromJson(root.get("answers"),object : TypeToken<List<String>>() {}.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<Exam>{
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<ChoiceQuestionVo>() {}.type)
ExamType.oq -> gson.fromJson(json,
object : TypeToken<OpenQuestionsVo>() {}.type)
null->throw NullPointerException("无法识别题目类型!")
}
}
}

@ -1,6 +1,8 @@
package com.gyf.csams.util package com.gyf.csams.util
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.gyf.csams.association.model.Exam
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -93,10 +95,18 @@ object HttpClient{
data class ApiResponse<T>(val code:Int,val message:String,val body:T?=null) data class ApiResponse<T>(val code:Int,val message:String,val body:T?=null)
class SimpleCallback<T>(private val action:String, class SimpleCallback<T>(private val action:String,
private val onSuccess:(res:ApiResponse<T>) -> Unit, private val onSuccess:(res:ApiResponse<T>) -> Unit,
private val onFail:(error:String) -> Unit, private val onFail:(error:String) -> Unit,
private val type: Type):Callback{ private val type: Type):Callback{
companion object{
val gson: Gson =GsonBuilder()
.registerTypeAdapter(Exam::class.java,ExamDeserializer())
.create()
}
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
onFail("${action}失败,请联系管理员") onFail("${action}失败,请联系管理员")
Logger.e(e,"${action}请求失败,发生IO异常") Logger.e(e,"${action}请求失败,发生IO异常")
@ -107,7 +117,7 @@ class SimpleCallback<T>(private val action:String,
val body=response.body val body=response.body
if (body!=null&&body.contentType()?.subtype == "json") { if (body!=null&&body.contentType()?.subtype == "json") {
val jsonRes=body.string() val jsonRes=body.string()
val res:ApiResponse<T> = Gson().fromJson(jsonRes, type) val res:ApiResponse<T> = gson.fromJson(jsonRes, type)
Logger.i("${action}请求响应成功:") Logger.i("${action}请求响应成功:")
Logger.json(jsonRes) Logger.json(jsonRes)
onSuccess(res) onSuccess(res)

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M544,213.33v266.67H810.67v64H544V810.67h-64V544H213.33v-64h266.67V213.33z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M810.67,480v64H213.33v-64z"/>
</vector>

@ -5,8 +5,6 @@ import com.google.gson.reflect.TypeToken
import com.gyf.csams.util.ApiResponse import com.gyf.csams.util.ApiResponse
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.time.LocalDateTime
import java.time.temporal.ChronoField
/** /**
@ -36,7 +34,14 @@ class ExampleUnitTest {
@Test @Test
fun testYear(){ fun testYear(){
println( LocalDateTime.now().get(ChronoField.YEAR)) repeat(10,{
println(it)
})
}
@Test
fun testCharRange(){
println(('A'..'D').map { "选项$it" })
} }
} }

@ -8,13 +8,14 @@ buildscript {
val APP_NAME by extra("大学生社团管理系统") val APP_NAME by extra("大学生社团管理系统")
val SERVER_ADDRESS by extra("http://192.168.50.107:8080") val SERVER_ADDRESS by extra("http://192.168.50.107:8080")
val room_version by extra("2.2.6") val room_version by extra("2.2.6")
val kotlin_version by extra("1.4.32")
repositories { repositories {
maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/public") maven("https://maven.aliyun.com/repository/public")
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:7.0.0-alpha15") 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle.kts files // in the individual module build.gradle.kts files

Loading…
Cancel
Save