QuestionConfig.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. <!-- 题目配置 -->
  2. <template>
  3. <div class="test_question_config">
  4. <Form
  5. :v-model:formRef="formRefDom"
  6. :form-item-list="finalFormItemList"
  7. :default-form-data="questionFormData"
  8. @form-item-change="HandleFormItemChange"
  9. labelWidth="92px"
  10. >
  11. <template #questionType>
  12. <div class="question_type">
  13. <!-- 题目类型下拉 -->
  14. <el-select
  15. v-model="questionFormData.questionType"
  16. class="type_select"
  17. @change="(val)=>HandleFormItemChange({key:'question_type',val,formData:questionFormData})"
  18. placeholder="请选择">
  19. <el-option
  20. v-for="selectItem in questionTypeOptions"
  21. :key="selectItem.value"
  22. :label="selectItem.label"
  23. :value="selectItem.value"
  24. />
  25. </el-select>
  26. <!-- 题目额外 展示配置 -->
  27. <el-checkbox-group
  28. class="type_cofig_check"
  29. v-model="questionFormData.extraConfig"
  30. >
  31. <el-checkbox
  32. v-for="checkItem in questionConfig"
  33. :key="checkItem.value"
  34. :label="checkItem.label"
  35. :value="checkItem.value"
  36. />
  37. </el-checkbox-group>
  38. </div>
  39. </template>
  40. <template #evaluationType="{formData}">
  41. <div class="score_reviewType">
  42. <el-radio-group v-model="questionFormData.evaluationType">
  43. <el-radio v-for="radioItem in scoreReviewTypeList" :key="radioItem.value" :value="radioItem.value">{{ radioItem.label }}</el-radio>
  44. </el-radio-group>
  45. <ul class="num_score" v-if="Number(formData.evaluationType) === 0 && Number(formData.questionScore)">
  46. <li v-for="value in Number(formData.questionScore)">
  47. {{ value }}分
  48. </li>
  49. </ul>
  50. <div class="star_score" v-else-if="Number(formData.evaluationType) === 1 && Number(formData.questionScore)">
  51. 非常不满意
  52. <el-rate size="large"
  53. :model-value="0"
  54. :disabled="true"
  55. disabled-void-color="#c6d1de"
  56. disabled-void-icon="Star"
  57. :max="Number(formData.questionScore)"
  58. clearable />
  59. 非常满意
  60. </div>
  61. <div class="null_score" v-else>
  62. 未查询到结果,请至题目分值处设置分值
  63. </div>
  64. </div>
  65. </template>
  66. <template #options="{ formData }">
  67. <div class="options_list_container">
  68. <div v-for="(option,index) in formData.options" :key="option.id" class="option_item">
  69. <div class="input_option">
  70. <el-input
  71. v-model="option.content"
  72. placeholder="请输入选项内容"
  73. />
  74. <!-- 统计题 -->
  75. <template v-if="displayMode === 'statistic'">
  76. <el-icon v-if="Number(questionFormData.questionType) === 0 && Number(index) > 1 ||
  77. Number(questionFormData.questionType) === 1 && Number(index) > 2 "
  78. class="delete_icon"
  79. @click="removeOption(option.id)"
  80. >
  81. <Close/>
  82. </el-icon>
  83. <div class="blank_icon" v-else></div>
  84. </template>
  85. </div>
  86. <!-- 得分题 -->
  87. <div v-if="displayMode === 'scored'" class="score_option">
  88. <span class="row_label">选项分值:</span>
  89. <el-input
  90. type="number"
  91. v-model="option.score"
  92. placeholder="请输入分值"
  93. />
  94. <el-icon v-if="Number(questionFormData.questionType) === 3 && Number(index) > 1"
  95. class="delete_icon"
  96. @click="removeOption(option.id)"
  97. >
  98. <Close/>
  99. </el-icon>
  100. <div class="blank_icon" v-else></div>
  101. </div>
  102. </div>
  103. </div>
  104. <div class="add_option_area">
  105. <el-button type="primary" link @click="addOption">
  106. <el-icon><CirclePlus /></el-icon>
  107. 添加选项
  108. </el-button>
  109. </div>
  110. </template>
  111. <template #submit="{formRef,formData}">
  112. <div class="action_bar">
  113. <el-button class="button_background" @click="HandleAddConfirm({formRef,formData})">确定</el-button>
  114. <el-button class="button_refresh" @click="HandleCancel({formRef,formData})">取消</el-button>
  115. </div>
  116. </template>
  117. </Form>
  118. </div>
  119. </template>
  120. <script setup lang="ts">
  121. import { ref, computed, watch } from 'vue'
  122. import { Close, CirclePlus } from '@element-plus/icons-vue'
  123. import Form from '@/baseComponents/Form.vue'
  124. import { questionFormItemList } from './form'
  125. import {
  126. statisticQuestionType, scoredQuestionType
  127. } from './questionTypeList'
  128. import { ElMessage } from 'element-plus'
  129. type DisplayMode = 'scored' | 'statistic'
  130. interface Option {
  131. content: String
  132. score?: String
  133. id?: Number
  134. }
  135. interface FormData {
  136. questionTitleName: String //题目名称
  137. questionScore?: String //题目分值
  138. questionType: String //题目类型
  139. extraConfig: String[] // 是否必答 required, 是否横排 isHorizontal
  140. options: Option[], //单选/多选 的 选项配置
  141. evaluationType?:Number, // 评价题 评分的展示形式
  142. }
  143. const props = defineProps({
  144. displayMode: { // 展示形式 带分值 scored 不带分值 statistic
  145. type: String as () => DisplayMode,
  146. default: 'statistic',
  147. required:true,
  148. },
  149. defaultFormData:{
  150. type:Object as ()=> FormData,
  151. }
  152. })
  153. const emit = defineEmits(['confirm', 'cancel', 'formChange'])
  154. const formRefDom = ref()
  155. // 试题题目信息
  156. const questionFormData = ref<FormData>({
  157. questionTitleName: '',
  158. questionScore: '',
  159. questionType: '',
  160. extraConfig: [], //默认勾选必答题
  161. options: [],
  162. evaluationType:0 //默认展示 数字式
  163. })
  164. /*
  165. 监听 props.displayMode 展示 不同的 题型配置 和 试卷配置
  166. */
  167. // 题型选项配置 监听 动态切换
  168. const questionTypeOptions = computed(() => {
  169. if (props.displayMode === 'scored') {
  170. return [...scoredQuestionType.value]
  171. } else {
  172. return [... statisticQuestionType.value]
  173. }
  174. })
  175. // 表单试题 展示项配置 监听 表单元素 动态显隐
  176. const finalFormItemList = computed(() => {
  177. return questionFormItemList.value.map(item => {
  178. const newItem = { ...item }
  179. // 题目分值仅在 scored 模式显示
  180. if (item.key === 'questionScore') {
  181. newItem.hidden = props.displayMode !== 'scored'
  182. }else if(item.key == 'options'){
  183. // 评价题 和 问答题 不展示 题目子选项配置
  184. newItem.hidden = [2,5].includes(Number(questionFormData.value.questionType))
  185. }else if(item.key == 'evaluationType'){
  186. // 评价题 展示评分的格式
  187. newItem.hidden = Number(questionFormData.value.questionType) !== 5
  188. }
  189. return newItem
  190. })
  191. })
  192. // 试题额外配置项 // 评价题 和 问答题 展示不同的配置项
  193. const questionConfig = computed(()=>{
  194. // 评价题 和 问答题
  195. if([2,5].includes(Number(questionFormData.value.questionType)) ){
  196. return [
  197. { label: '必答题', value: 'required' },
  198. ]
  199. }else{
  200. return [
  201. { label: '必答题', value: 'required' },
  202. { label: '选项横排', value: 'isHorizontal' }
  203. ]
  204. }
  205. })
  206. // 评价题 评分形式 列表
  207. const scoreReviewTypeList = ref([
  208. {label:'数字式',value:0},
  209. {label:'星星式', value:1},
  210. ])
  211. // 表单内部某些变化 触发 的操作
  212. const HandleFormItemChange = (params: any) => {
  213. const { key, val, formData: data } = params
  214. // console.log('表单项目变化了--',formData)
  215. emit('formChange', params)
  216. }
  217. // 添加题目选项配置
  218. const addOption = () => {
  219. questionFormData.value.options.push({ content: '', score: '',id:(Number(questionFormData.value.options[questionFormData.value.options.length - 1]?.id || 0) + 1) })
  220. }
  221. // 删除题目选项配置
  222. const removeOption = (id: number) => {
  223. questionFormData.value.options = questionFormData.value.options.filter((opt)=>{
  224. return opt.id !== id
  225. })
  226. }
  227. // 题目信息校验 并传递信息给父级
  228. const HandleAddConfirm = async (params:any)=>{
  229. const {formRef,formData} = params
  230. if (!formRef) return
  231. await formRef.validate(async(valid) => {
  232. if (valid) {
  233. // 要判断添加的选项是否有没有填写完的
  234. console.log('传递的表单数据---',formData)
  235. if(formData?.options?.length > 0){
  236. // 有选项
  237. let isOptNameFinished = true
  238. let isScoreFinished = true
  239. formData?.options.map(opt=>{
  240. if(!opt.content){
  241. isOptNameFinished = false
  242. }
  243. if(props.displayMode === 'scored' && !(opt.score || opt.score === 0)){
  244. isScoreFinished = false
  245. }
  246. })
  247. if(!isOptNameFinished){
  248. ElMessage.warning('请确保选项内容不为空')
  249. return
  250. }
  251. if(!isScoreFinished){
  252. ElMessage.warning('请确保选项分值不为空')
  253. return
  254. }
  255. }
  256. emit('confirm', {formData:formData,formRef})
  257. }
  258. }).catch(err=>{
  259. console.log('报错信息',err)
  260. })
  261. }
  262. const HandleCancel = async (params:any)=>{
  263. const {formRef} = params
  264. formRef.resetFields()
  265. emit('cancel')
  266. }
  267. // 同步监听 1
  268. watch(()=>props.defaultFormData,(newVal)=>{
  269. // 赋值除了类型之外的其他值
  270. console.log('监听到赋值了',newVal)
  271. if (newVal) {
  272. // 避免首次赋值选项置空的情况 _ 待完善 _ 可能发生
  273. questionFormData.value = newVal
  274. }
  275. },{deep:true, immediate:true})
  276. </script>
  277. <style lang="scss" scoped>
  278. .test_question_config {
  279. padding: 20px;
  280. background-color: #F8FAFE;
  281. }
  282. .question_type{
  283. display: flex;
  284. gap:32px;
  285. align-items: center;
  286. width: 100%;
  287. .type_select{
  288. width: 200px;
  289. }
  290. }
  291. .score_reviewType{
  292. display: flex;
  293. flex-direction: column;
  294. .num_score,
  295. .star_score,
  296. .null_score
  297. {
  298. display: flex;
  299. align-items: center;
  300. gap:16px;
  301. color: #CCCCCC;
  302. line-height: 1;
  303. }
  304. .num_score{
  305. flex-wrap: wrap;
  306. li{
  307. display: flex;
  308. justify-content: center;
  309. align-items: center;
  310. border-radius: 4px ;
  311. border: 1px solid #CCCCCC;
  312. width: 44px;
  313. height: 32px;
  314. font-size: 14px;
  315. background-color: white;
  316. }
  317. }
  318. .star_score{
  319. word-break: keep-all;
  320. .el-rate{
  321. display: flex;
  322. flex-wrap: wrap;
  323. gap:10px 20px;
  324. height: auto;
  325. }
  326. }
  327. }
  328. // 选项列表样式
  329. .options_list_container {
  330. width:100% ;
  331. display: flex;
  332. flex-direction: column;
  333. gap: 16px;
  334. margin-bottom: 16px;
  335. .option_item {
  336. display: flex;
  337. justify-content: space-between;
  338. align-items: center;
  339. gap: 16px;
  340. .input_option{
  341. width:60% ;
  342. display: flex;
  343. align-items: center;
  344. padding-left: 92px;
  345. .blank_icon{
  346. margin-left: 20px;
  347. width: 20px !important;
  348. height: 20px;
  349. }
  350. }
  351. .score_option{
  352. width: 35%;
  353. display: flex;
  354. align-items: center;
  355. .row_label {
  356. color: #999999;
  357. font-size: 14px;
  358. flex-shrink: 0;
  359. }
  360. .blank_icon{
  361. margin-left: calc(20px + 6px);
  362. width: 20px !important;
  363. height: 20px;
  364. }
  365. }
  366. .input_option,
  367. .score_option{
  368. .delete_icon {
  369. font-size: 20px;
  370. color: #c0c4cc;
  371. cursor: pointer;
  372. margin-left: 20px;
  373. width: 20px !important;
  374. height: 20px;
  375. &:hover {
  376. color: #f56c6c;
  377. }
  378. }
  379. }
  380. }
  381. }
  382. // 添加选项按钮
  383. .add_option_area {
  384. margin-left: 92px;
  385. }
  386. // 底部操作栏
  387. .action_bar {
  388. margin-left: 75px;
  389. display: flex;
  390. gap:20px;
  391. .el-button {
  392. margin:0;
  393. padding:10px 36px;
  394. height: 40px;
  395. }
  396. }
  397. // 表单项样式
  398. :deep(.el-form) {
  399. display: flex;
  400. flex-wrap: wrap;
  401. justify-content: space-between;
  402. gap: 16px 0;
  403. .el-form-item {
  404. margin: 0px;
  405. &.custom_itm {
  406. width: 100%;
  407. .el-form-item__content{
  408. display: flex;
  409. flex-direction: column;
  410. align-items: flex-start;
  411. }
  412. }
  413. &.percent_70 {
  414. width: 60%;
  415. }
  416. &.percent_30 {
  417. width: 35%;
  418. }
  419. }
  420. }
  421. </style>