parent
e8dfc71a90
commit
54ecee40c0
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
) |
||||
|
||||
} |
||||
} |
@ -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("无法识别题目类型!") |
||||
} |
||||
|
||||
} |
||||
} |
@ -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> |
Loading…
Reference in new issue