CreateQuestionnaire.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <template>
  2. <div ref="courseDom" class="main_container">
  3. <TopBackNav>
  4. <template #topRight>
  5. <span class="paper_score">阅卷总分:<span class="score">{{questionTotalScore}}分</span></span>
  6. <el-button class="button_refresh" @click="PreviewQuestionnaire">预览问卷</el-button>
  7. <el-button class="button_background" @click="SaveQuestionnaire">保存问卷</el-button>
  8. </template>
  9. </TopBackNav>
  10. <div class="page_jg_20" style="flex-shrink: 0;"></div>
  11. <div class="questionnaire_config" >
  12. <div class="left_panel">
  13. <div class="panel_section page_item">
  14. <h3 class="section_title">统计题</h3>
  15. <!-- 题型循环 -->
  16. <div class="type_list">
  17. <template v-for="typeItm in statisticQuestionType" :key="typeItm.value">
  18. <el-button class="button_refresh"
  19. @click="CheckedQuestionType(typeItm.value)" >
  20. <img :src="typeItm.icon" alt="">
  21. {{ typeItm.label }}
  22. </el-button>
  23. </template>
  24. </div>
  25. </div>
  26. <div class="panel_section page_item">
  27. <h3 class="section_title">评分题</h3>
  28. <!-- 题型循环 -->
  29. <div class="type_list">
  30. <template v-for="typeItm in scoredQuestionType" :key="typeItm.value">
  31. <el-button class="button_refresh"
  32. @click="CheckedQuestionType(typeItm.value)">
  33. <img :src="typeItm.icon" alt="">
  34. {{ typeItm.label }}
  35. </el-button>
  36. </template>
  37. </div>
  38. </div>
  39. </div>
  40. <!-- 中间面板:编辑区域 -->
  41. <div class="center_panel">
  42. <div class="page_item questionnaire_base">
  43. <QuestionnaireBaseForm
  44. v-model:formRef="questionnaireBaseFormRef"
  45. :defaultFormData="{}"
  46. @formChage="SyncQuestionnaireFormData"
  47. ></QuestionnaireBaseForm>
  48. </div>
  49. <!-- 已添加题目展示区 -->
  50. <ul class="question_list_area">
  51. <li v-for="(question,index) in addedQuestionList" :key="question.id">
  52. <div class="question_title"
  53. :class="{required:question.extraConfig.includes('required')}">
  54. {{index + 1}}.{{ question.questionTitleName }}
  55. <span class="question_type">[{{ qustionTypeToName(question.questionType) }}]</span>
  56. </div>
  57. <QuestionTypeShow
  58. :isHorizontal="question.extraConfig.includes('isHorizontal')"
  59. :questionType="Number(question.questionType)"
  60. :questionSubOptions="question.options"
  61. :evaluationType="Number(question.evaluationType)"
  62. :questionScore="Number(question.questionScore)"
  63. ></QuestionTypeShow>
  64. </li>
  65. </ul>
  66. <!-- 题目添加区域 -->
  67. <div class=" add_question_area">
  68. <div v-if="!selectedQuestionType" class="add_question">
  69. <div class="add_action">+ 点击左侧添加题型</div>
  70. </div>
  71. <div v-else class="question_itm_edit">
  72. <QuestionConfig
  73. :displayMode="selectedQuestionGetScore ? 'scored' : 'statistic'"
  74. :questionType="selectedQuestionType"
  75. @confirm="AddQuesiton"
  76. ></QuestionConfig>
  77. </div>
  78. </div>
  79. </div>
  80. <!-- 右侧面板:目录 -->
  81. <div class="right_panel page_item">
  82. <h3 class="section_title right_title">问卷目录 (拖动排序)</h3>
  83. <div v-if="!(addedQuestionList?.length >0)" class="empty_state">暂无问卷题目</div>
  84. <ul v-else class="question_list_brief">
  85. <li v-for="(question,index) in addedQuestionList" :key="question.id">
  86. <img :src="moveUpDownIcon" alt=""
  87. :draggable="true"
  88. @dragstart="(e)=>HandleDragStart(e)"
  89. @dragend="(e)=>HandleDragEnd(e,question,index)"
  90. >
  91. {{ index + 1 }}.{{ question.questionTitleName }}
  92. </li>
  93. </ul>
  94. </div>
  95. </div>
  96. </div>
  97. </template>
  98. <script lang="ts" setup>
  99. import {computed, onMounted, ref} from 'vue'
  100. import { useRouter } from 'vue-router';
  101. import TopBackNav from '@/components/TopBackNav.vue';
  102. import {statisticQuestionType,scoredQuestionType,qustionTypeToName} from '../../../components/questionConfig/questionTypeList'
  103. import QuestionConfig from '@/components/questionConfig/QuestionConfig.vue';
  104. import QuestionnaireBaseForm from '@/components/questionConfig/QuestionnaireBaseForm.vue';
  105. import { ElMessage } from 'element-plus';
  106. import { unioformDateTransform } from '@/utils/dateTransform';
  107. import { useHandleMoveUpAndDown } from '@/baseComponents/useHandleMoveUpAndDown';
  108. import QuestionTypeShow from '@/components/QuestionTypeShow.vue';
  109. import moveUpDownIcon from '@/assets/icon/move_up_down.png'
  110. import {addQuestionnaireApi} from '@/api/surveyManagement'
  111. // 题目选项信息 参数
  112. interface Option {
  113. label: String
  114. score?: String
  115. value?: Number
  116. }
  117. // 题目信息 参数
  118. interface QuestionItm {
  119. id:Number
  120. questionTitleName: String //题目名称
  121. questionScore?: String | Number //题目分值
  122. questionType: String | String //题目类型
  123. extraConfig: String[] // 是否必答 required, 是否横排 isHorizontal
  124. options: Option[], //单选/多选 的 选项配置
  125. evaluationType: Number|String // 评分题 的 分值展示形式
  126. }
  127. interface QuestionnaireInfo {
  128. assessmentTitle:String,
  129. assessmentIllustrate:String
  130. }
  131. const router = useRouter()
  132. // 试卷头表单
  133. const questionnaireBaseFormRef = ref()
  134. // 已存储的试卷整体信息
  135. const questionnaireMsg = ref()
  136. // 试卷头基本信息
  137. const questionnaireInfo = ref<QuestionnaireInfo>({
  138. assessmentTitle:'',
  139. assessmentIllustrate:''
  140. })
  141. // 已经添加的题型
  142. const addedQuestionList = ref<QuestionItm[]>([
  143. {
  144. id:0,
  145. questionTitleName: '测试', //题目名称
  146. questionScore: 20, //题目分值
  147. questionType: '2', //题目类型
  148. extraConfig:[], // 是否必答 required, 是否横排 isHorizontal
  149. options: [] ,
  150. evaluationType:''
  151. }
  152. ])
  153. // 当前选中的题型
  154. const selectedQuestionType = ref('')
  155. // 选中题型 是否得分 // 得分题 true 非得分题 false
  156. const selectedQuestionGetScore = ref(false)
  157. // 试卷总分
  158. const questionTotalScore = computed(()=>{
  159. return addedQuestionList.value?.reduce((a,b)=>{
  160. let aScore = Number(a.questionScore || 0)
  161. let bScore = Number(b.questionScore || 0)
  162. return {
  163. questionScore:aScore + bScore
  164. }
  165. },{questionScore:0}).questionScore
  166. })
  167. // 选中题型时的处理
  168. const CheckedQuestionType = (type:any) => {
  169. selectedQuestionType.value = type
  170. switch(type){
  171. case '0':
  172. case '1':
  173. case '2':
  174. selectedQuestionGetScore.value = false
  175. break
  176. case '3':
  177. case '4':
  178. selectedQuestionGetScore.value = true
  179. break
  180. }
  181. }
  182. // 同步问卷头表单数据
  183. const SyncQuestionnaireFormData = (params)=>{
  184. questionnaireInfo.value = {...params}
  185. }
  186. // 处理试题排序
  187. const { HandleDragStart, HandleDragEnd } = useHandleMoveUpAndDown(addedQuestionList,42)
  188. const addedIndex = ref(1)
  189. // 添加试题信息
  190. const AddQuesiton = (params)=>{
  191. const {formRef,formData} = params
  192. console.log('获取到试题信息了',formData)
  193. // 不能用最后一个id来添加
  194. let id = addedIndex.value + 1
  195. addedIndex.value += 1
  196. let options = formData.options.map(item=>{
  197. return {
  198. label:item.content,
  199. value:item.id,
  200. score:item.score
  201. }
  202. })
  203. addedQuestionList.value.push({id ,...formData,options})
  204. formRef.resetFields()
  205. selectedQuestionType.value = ''
  206. }
  207. // 保存试卷
  208. const SaveQuestionnaire = async ()=>{
  209. // 试卷总表单
  210. let formRef = questionnaireBaseFormRef.value
  211. if (!formRef) return
  212. await formRef.validate(async(valid) => {
  213. if (valid) {
  214. if(addedQuestionList.value.length > 0){
  215. // addQuestionnaireApi()
  216. /**
  217. * 模拟数据
  218. */
  219. // 模拟保存数据 和页面跳转
  220. let questionnaireList = JSON.parse(localStorage.getItem('questionnaireList') || '[]')
  221. questionnaireMsg.value = {
  222. ...(questionnaireInfo.value),
  223. questionData:addedQuestionList.value,
  224. id:Number(questionnaireList[questionnaireList.length - 1]?.id || 0) + 1,
  225. createTime:unioformDateTransform(new Date()),
  226. createBy:'管理员',
  227. assessmentScore:questionTotalScore.value,
  228. useStatus:1
  229. }
  230. questionnaireList.push(questionnaireMsg.value)
  231. localStorage.setItem('questionnaireList',JSON.stringify(questionnaireList || []))
  232. }else{
  233. ElMessage.warning('请添加问卷题目')
  234. }
  235. }
  236. }).catch(err=>{
  237. console.log('报错信息',err)
  238. })
  239. }
  240. // 预览试卷
  241. const PreviewQuestionnaire =()=>{
  242. sessionStorage.setItem('questionnaire',JSON.stringify(questionnaireMsg.value || '{}'))
  243. router.push(`/main/previewQuestionnaire`)
  244. }
  245. </script>
  246. <style lang="scss" scoped>
  247. :deep(.top_header){
  248. .top_right{
  249. display: flex;
  250. align-items: center;
  251. .el-button{
  252. margin-left:0px !important;
  253. &.button_refresh{
  254. margin-right: 10px;
  255. }
  256. }
  257. .paper_score{
  258. font-weight: 600;
  259. margin-right: 20px;
  260. .score{
  261. color:#2E64FA;
  262. }
  263. }
  264. }
  265. }
  266. .questionnaire_config {
  267. display: flex;
  268. justify-content: space-between;
  269. align-items: flex-start;
  270. gap: 20px;
  271. max-height: calc(100vh - 116px - 60px);
  272. height: calc(100vh - 116px - 60px);
  273. overflow-y:auto ;
  274. /* --- 左侧面板 --- */
  275. .left_panel {
  276. width: 200px;
  277. display: flex;
  278. flex-direction: column;
  279. gap: 20px;
  280. .panel_section {
  281. padding: 16px 20px;
  282. .type_list {
  283. display: flex;
  284. flex-direction: column;
  285. gap: 16px;
  286. .el-button {
  287. display: flex;
  288. justify-content: flex-start;
  289. align-items: center;
  290. padding:12px;
  291. margin: 0;
  292. font-size: 14px;
  293. color:#666666;
  294. img{
  295. vertical-align: middle;
  296. width: 14px;
  297. height: 14px;
  298. border-radius: 50%;
  299. margin-right: 8px;
  300. position: relative;
  301. }
  302. &:hover{
  303. color:#2E64FA;
  304. border:solid #2E64FA 1px;
  305. }
  306. }
  307. }
  308. }
  309. }
  310. /* --- 中间面板 --- */
  311. .center_panel {
  312. max-height:100%;
  313. height:100%;
  314. width: calc(100vw - 440px - 80px);
  315. display: flex;
  316. flex-direction: column;
  317. border-radius: 10px;
  318. background-color: white;
  319. overflow-y:auto ;
  320. .questionnaire_base{
  321. border-radius:10px 10px 0 0;
  322. padding-bottom: 0;
  323. }
  324. // 已添加试题区
  325. .question_list_area{
  326. display: flex;
  327. flex-direction: column;
  328. gap:20px;
  329. padding:20px;
  330. font-size: 14px;
  331. color:#000;
  332. li{
  333. .question_title{
  334. margin-bottom: 20px;
  335. .question_type{
  336. color:#2E64FA;
  337. margin-left: 8px;
  338. }
  339. &.required{
  340. position: relative;
  341. padding-left: 5px;
  342. ::before{
  343. content:'*';
  344. position: absolute;
  345. left:0;
  346. color:#F56C6C;
  347. }
  348. }
  349. }
  350. }
  351. }
  352. // 添加试题区域
  353. .add_question_area{
  354. .add_question {
  355. // flex-grow: 0;
  356. margin:20px;
  357. border: 1px dashed #C0C4CC;
  358. border-radius: 4px;
  359. display: flex;
  360. justify-content: center;
  361. align-items: center;
  362. height: 240px;
  363. .add_action {
  364. color: #c0c4cc;
  365. font-size: 16px;
  366. cursor: pointer;
  367. }
  368. }
  369. .question_itm_edit {
  370. // flex-grow: 0;
  371. background-color: #F8FAFE;
  372. }
  373. }
  374. }
  375. /* --- 右侧面板 --- */
  376. .right_panel {
  377. max-height:100%;
  378. overflow-y:auto ;
  379. align-self: flex-start;
  380. width: 240px;
  381. min-height: calc(100vh - 156px - 40px);
  382. display: flex;
  383. flex-direction: column;
  384. .empty_state {
  385. flex: 1;
  386. display: flex;
  387. justify-content: center;
  388. align-items: center;
  389. font-size: 16px;
  390. color: #C0C4CC;
  391. }
  392. .question_list_brief{
  393. display: flex;
  394. flex-direction: column;
  395. font-size: 14px;
  396. li{
  397. display: flex;
  398. align-items: center;
  399. gap:8px;
  400. padding:10px 0;
  401. img{
  402. width:20px;
  403. height:20px;
  404. }
  405. }
  406. }
  407. }
  408. .section_title {
  409. font-weight: 600;
  410. font-size: 16px;
  411. margin-bottom: 10px;
  412. }
  413. }
  414. </style>