mx-question-content.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <template>
  2. <view ref="questionEl" class="mx-question-content flex flex-col gap-40 text-content">
  3. <view v-html="titleWithSeq"/>
  4. <view class="mx-question-content-opt" :class="{'pl-30': deep>0}">
  5. <template v-if="question.options?.length">
  6. <slot name="options" v-bind="{options: optionsWithCode, deep, disabled}">
  7. <component :is="optGroupComponent" :modelValue="modelValue" :disabled="disabled" placement="column"
  8. :class="groupClass" @change="emits('update:modelValue', $event)">
  9. <component :is="optComponent" v-for="op in optionsWithCode" :name="op.code"
  10. :class="calcOptionClass(op)">
  11. <view v-html="op.option" :class="textClass"/>
  12. </component>
  13. </component>
  14. </slot>
  15. </template>
  16. <template v-if="question.subQuestions?.length">
  17. <mx-question-content v-for="sub in question.subQuestions" :question="sub" :deep="deep+1"
  18. :disabled="disabled">
  19. <template v-for="(fn, name) in $slots" #[name]="scope">
  20. <slot :name="name" v-bind="scope"/>
  21. </template>
  22. </mx-question-content>
  23. </template>
  24. </view>
  25. <mx-question-subjective v-if="useSubjectiveStyle" :modelValue="modelValue" :attachments="attachments"
  26. :disabled="disabled" @update:modelValue="emits('update:modelValue', $event)"
  27. @update:attachments="emits('update:attachments', $event)"/>
  28. </view>
  29. </template>
  30. <script setup>
  31. import {computed, onMounted, ref, watch} from 'vue'
  32. import {createPropDefine} from "@/utils";
  33. import _ from 'lodash';
  34. import UvCheckboxGroup from "@/uni_modules/uv-checkbox/components/uv-checkbox-group/uv-checkbox-group.vue";
  35. import UvRadioGroup from "@/uni_modules/uv-radio/components/uv-radio-group/uv-radio-group.vue";
  36. import MxQuestionPlainOptionGroup from "@/components/mx-question-content/components/mx-question-plain-option-group.vue";
  37. import UvRadio from "@/uni_modules/uv-radio/components/uv-radio/uv-radio.vue";
  38. import UvCheckbox from "@/uni_modules/uv-checkbox/components/uv-checkbox/uv-checkbox.vue";
  39. import MxQuestionPlainOption from "@/components/mx-question-content/components/mx-question-plain-option.vue";
  40. import mxConst from "@/common/mxConst";
  41. import MxQuestionSubjective from "@/components/mx-question-content/components/mx-question-subjective.vue";
  42. import {useMathJaxService} from "@/hooks/useMathJaxService";
  43. import {useInjectQuestionOptionFormatter} from "@/components/mx-question-content/useQuestionOptionInjection";
  44. import {useInjectMathJaxSwitch} from "@/components/mx-question-content/useMathJaxSwitchInjection";
  45. import {func} from "@/uni_modules/uv-ui-tools/libs/function/test";
  46. /*NOTE:递归调用目前没有做事件穿透,所以子题还不支持作答
  47. NOTE: mx-question-content 支持MathJax
  48. * */
  49. const props = defineProps({
  50. question: createPropDefine({}, Object),
  51. deep: createPropDefine(0, Number),
  52. disabled: createPropDefine(false, Boolean),
  53. modelValue: createPropDefine('', [String, Number, Array]),
  54. attachments: createPropDefine([], [String, Array]),
  55. sysAnswer: createPropDefine('', [String, Array]),
  56. readonly: createPropDefine(false, Boolean)
  57. })
  58. const emits = defineEmits(['update:modelValue', 'update:attachments'])
  59. const questionEl = ref(null)
  60. const {updateMathJax} = useMathJaxService()
  61. const {formatter, optionClass} = useInjectQuestionOptionFormatter()
  62. const {disabled: disabledMathJax} = useInjectMathJaxSwitch()
  63. const titleWithSeq = computed(() => {
  64. const {title, seq, type, questionId} = props.question
  65. const arr = []
  66. if (seq) arr.push(`(${seq})`)
  67. if (type) arr.push(`[${type}]`)
  68. if (questionId) arr.push(`[${questionId}]`)
  69. arr.push(title)
  70. return arr.join('')
  71. })
  72. const optionsWithCode = computed(() => {
  73. const {options} = props.question
  74. return formatter(options)
  75. })
  76. const useMultipleStyle = computed(() => {
  77. const {deep, readonly, question: {typeId}} = props
  78. return deep == 0 && !readonly && mxConst.question.isCheckbox(typeId)
  79. })
  80. const useRadioStyle = computed(() => {
  81. // 子题都算主观题, 学生手写作答
  82. const {deep, readonly, question: {typeId}} = props
  83. return deep == 0 && !readonly && mxConst.question.isRadio(typeId)
  84. })
  85. const useSubjectiveStyle = computed(() => {
  86. // 一级题的主观题渲染输入框
  87. const {deep, readonly, question: {typeId}} = props
  88. return deep == 0 && !readonly && !mxConst.question.isObjective(typeId)
  89. })
  90. const optGroupComponent = computed(() => {
  91. if (useRadioStyle.value) return UvRadioGroup
  92. if (useMultipleStyle.value) return UvCheckboxGroup
  93. return MxQuestionPlainOptionGroup // 为了对齐UvCheckboxGroup/UvRadioGroup
  94. })
  95. const optComponent = computed(() => {
  96. if (useRadioStyle.value) return UvRadio
  97. if (useMultipleStyle.value) return UvCheckbox
  98. return MxQuestionPlainOption // 为了对齐UvCheckbox/UvRadio
  99. })
  100. const groupClass = computed(() => {
  101. return ['gap-20']
  102. })
  103. const textClass = computed(() => {
  104. return []
  105. })
  106. onMounted(() => updateMathJaxIfNeeded())
  107. watch(() => props.question, () => updateMathJaxIfNeeded())
  108. const updateMathJaxIfNeeded = () => {
  109. if (disabledMathJax) return
  110. updateMathJax(questionEl.value?.$el)
  111. }
  112. const calcOptionClass = function (op) {
  113. const defaultClass = 'px-15 py-20 border border-solid rounded'
  114. const injectClass = func(optionClass) ? optionClass(props.question) : optionClass
  115. const classes = [injectClass === undefined ? defaultClass : injectClass]
  116. if (props.sysAnswer) {
  117. // 如果有传入标准答案,则突出显示正确错误
  118. if (op.code == props.modelValue || props.modelValue.includes(op.code)) {
  119. // classes.push('border-primary')
  120. if (op.code != props.sysAnswer && !props.sysAnswer.includes(op.code)) {
  121. // 答错了
  122. classes.push('bg-[#f9e6e6] text-[#571515] border-[#e6c3c3]')
  123. }
  124. }
  125. if (op.code == props.sysAnswer || props.sysAnswer.includes(op.code)) {
  126. // 正确答案
  127. classes.push('bg-[#e0f9e6] text-[#155724] border-[#c3e6cb]')
  128. }
  129. } else {
  130. classes.push('border-border')
  131. }
  132. return classes
  133. }
  134. const tryRandomAnswer = () => {
  135. if (useRadioStyle.value) {
  136. // 随机选1个
  137. const rd = _.sample(optionsWithCode.value)
  138. emits('update:modelValue', rd.code)
  139. } else if (useMultipleStyle.value) {
  140. // 随机选N个
  141. const num = _.random(1, optionsWithCode.value.length + 1)
  142. const rd = _.sampleSize(optionsWithCode.value, num)
  143. // 与原选项保持顺序并提取code
  144. const codes = optionsWithCode.value.filter(i => rd.includes(i)).map(i => i.code)
  145. emits('update:modelValue', codes)
  146. }
  147. }
  148. defineExpose({tryRandomAnswer})
  149. </script>
  150. <style scoped lang="scss">
  151. .mx-question-content + .mx-question-content {
  152. margin-top: 40rpx;
  153. }
  154. </style>