questionEditor.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. /**
  2. * 调查问卷题目编辑组件 Mixin
  3. * @fileoverview 提供题目编辑、选项管理等功能的混入
  4. */
  5. import {
  6. getQuestionList,
  7. addQuestion,
  8. updateQuestion
  9. } from '@/api/survey/question'
  10. import {
  11. getOptionList,
  12. addOption,
  13. updateOption
  14. } from '@/api/survey/option'
  15. import {
  16. QUESTION_TYPE_OPTIONS,
  17. QUESTION_REQUIRED_OPTIONS,
  18. QUESTION_TYPE,
  19. getQuestionTypeLabel,
  20. getQuestionTypeType,
  21. getQuestionTypeIcon,
  22. getQuestionRequiredLabel,
  23. getQuestionRequiredType,
  24. isQuestionTypeNeedOptions
  25. } from '@/constants/survey'
  26. /**
  27. * @typedef {Object} QuestionEditorData
  28. * @property {string|number} surveyId - 调查问卷ID
  29. * @property {Array<QuestionItem>} questionList - 题目列表
  30. * @property {boolean} loading - 加载状态
  31. * @property {number} total - 总数
  32. * @property {number} current - 当前页码
  33. * @property {number} size - 每页数量
  34. * @property {boolean} questionDialogVisible - 题目对话框显示状态
  35. * @property {string} questionDialogMode - 题目对话框模式 add|edit
  36. * @property {QuestionForm} questionForm - 题目表单
  37. * @property {Object} questionFormRules - 题目表单验证规则
  38. * @property {Array<{label: string, value: number}>} questionTypeOptions - 题目类型选项
  39. * @property {Array<{label: string, value: number}>} questionRequiredOptions - 是否必填选项
  40. * @property {Array<OptionItem>} currentQuestionOptions - 当前题目的选项列表
  41. * @property {boolean} optionDialogVisible - 选项对话框显示状态
  42. * @property {string} optionDialogMode - 选项对话框模式 add|edit
  43. * @property {OptionForm} optionForm - 选项表单
  44. * @property {Object} optionFormRules - 选项表单验证规则
  45. * @property {string|number} currentQuestionId - 当前编辑的题目ID
  46. */
  47. export default {
  48. name: 'QuestionEditorMixin',
  49. props: {
  50. /**
  51. * 调查问卷ID
  52. * @type {string|number}
  53. */
  54. surveyId: {
  55. type: [String, Number],
  56. required: true
  57. }
  58. },
  59. data() {
  60. return {
  61. /**
  62. * 题目列表
  63. * @type {Array<QuestionItem>}
  64. */
  65. questionList: [],
  66. /**
  67. * 加载状态
  68. * @type {boolean}
  69. */
  70. loading: false,
  71. /**
  72. * 总数
  73. * @type {number}
  74. */
  75. total: 0,
  76. /**
  77. * 当前页码
  78. * @type {number}
  79. */
  80. current: 1,
  81. /**
  82. * 每页数量
  83. * @type {number}
  84. */
  85. size: 100,
  86. /**
  87. * 题目对话框显示状态
  88. * @type {boolean}
  89. */
  90. questionDialogVisible: false,
  91. /**
  92. * 题目对话框模式
  93. * @type {string}
  94. */
  95. questionDialogMode: 'add',
  96. /**
  97. * 题目表单
  98. * @type {QuestionForm}
  99. */
  100. questionForm: {
  101. surveyId: '',
  102. questionNo: 1,
  103. title: '',
  104. questionType: QUESTION_TYPE.SINGLE_CHOICE,
  105. isRequired: 0
  106. },
  107. /**
  108. * 题目表单验证规则
  109. * @type {Object}
  110. */
  111. questionFormRules: {
  112. title: [
  113. { required: true, message: '请输入题目标题', trigger: 'blur' },
  114. { min: 1, max: 200, message: '题目标题长度在 1 到 200 个字符', trigger: 'blur' }
  115. ],
  116. questionType: [
  117. { required: true, message: '请选择题目类型', trigger: 'change' }
  118. ],
  119. questionNo: [
  120. { required: true, message: '请输入题目序号', trigger: 'blur' },
  121. { type: 'number', min: 1, message: '题目序号必须大于0', trigger: 'blur' }
  122. ]
  123. },
  124. /**
  125. * 题目类型选项
  126. * @type {Array<{label: string, value: number}>}
  127. */
  128. questionTypeOptions: QUESTION_TYPE_OPTIONS,
  129. /**
  130. * 是否必填选项
  131. * @type {Array<{label: string, value: number}>}
  132. */
  133. questionRequiredOptions: QUESTION_REQUIRED_OPTIONS,
  134. /**
  135. * 当前题目的选项列表
  136. * @type {Array<OptionItem>}
  137. */
  138. currentQuestionOptions: [],
  139. /**
  140. * 选项对话框显示状态
  141. * @type {boolean}
  142. */
  143. optionDialogVisible: false,
  144. /**
  145. * 选项对话框显示状态
  146. * @type {boolean}
  147. */
  148. optionAddDialogVisible: false,
  149. /**
  150. * 选项对话框模式
  151. * @type {string}
  152. */
  153. optionDialogMode: 'add',
  154. /**
  155. * 选项表单
  156. * @type {OptionForm}
  157. */
  158. optionForm: {
  159. questionId: '',
  160. optionNo: 1,
  161. optionText: ''
  162. },
  163. /**
  164. * 选项表单验证规则
  165. * @type {Object}
  166. */
  167. optionFormRules: {
  168. optionText: [
  169. { required: true, message: '请输入选项内容', trigger: 'blur' },
  170. { min: 1, max: 100, message: '选项内容长度在 1 到 100 个字符', trigger: 'blur' }
  171. ],
  172. optionNo: [
  173. { required: true, message: '请输入选项序号', trigger: 'blur' },
  174. { type: 'number', min: 1, message: '选项序号必须大于0', trigger: 'blur' }
  175. ]
  176. },
  177. /**
  178. * 当前编辑的题目ID
  179. * @type {string|number}
  180. */
  181. currentQuestionId: ''
  182. }
  183. },
  184. computed: {
  185. /**
  186. * 题目对话框标题
  187. * @returns {string} 对话框标题
  188. */
  189. questionDialogTitle() {
  190. return this.questionDialogMode === 'add' ? '新增题目' : '编辑题目'
  191. },
  192. /**
  193. * 选项对话框标题
  194. * @returns {string} 对话框标题
  195. */
  196. optionDialogTitle() {
  197. return this.optionDialogMode === 'add' ? '新增选项' : '编辑选项'
  198. },
  199. /**
  200. * 是否为新增题目模式
  201. * @returns {boolean} 是否为新增模式
  202. */
  203. isAddQuestionMode() {
  204. return this.questionDialogMode === 'add'
  205. },
  206. /**
  207. * 是否为编辑题目模式
  208. * @returns {boolean} 是否为编辑模式
  209. */
  210. isEditQuestionMode() {
  211. return this.questionDialogMode === 'edit'
  212. },
  213. /**
  214. * 是否为新增选项模式
  215. * @returns {boolean} 是否为新增模式
  216. */
  217. isAddOptionMode() {
  218. return this.optionDialogMode === 'add'
  219. },
  220. /**
  221. * 是否为编辑选项模式
  222. * @returns {boolean} 是否为编辑模式
  223. */
  224. isEditOptionMode() {
  225. return this.optionDialogMode === 'edit'
  226. }
  227. },
  228. mounted() {
  229. this.loadQuestionList()
  230. },
  231. methods: {
  232. /**
  233. * 加载题目列表
  234. * @returns {Promise<void>}
  235. */
  236. async loadQuestionList() {
  237. try {
  238. this.loading = true
  239. const params = {
  240. surveyId: this.surveyId,
  241. size: this.size,
  242. current: this.current
  243. }
  244. const responseOrigin = await getQuestionList(params)
  245. const response = responseOrigin.data
  246. if (response.code === 200 && response.success) {
  247. this.questionList = response.data.records || []
  248. this.total = response.data.total || 0
  249. // 为每个题目加载选项
  250. await this.loadAllQuestionOptions()
  251. } else {
  252. this.$message.error(response.msg || '获取题目列表失败')
  253. }
  254. } catch (error) {
  255. console.error('加载题目列表失败:', error)
  256. this.$message.error('加载题目列表失败')
  257. } finally {
  258. this.loading = false
  259. }
  260. },
  261. /**
  262. * 加载所有题目的选项
  263. * @returns {Promise<void>}
  264. */
  265. async loadAllQuestionOptions() {
  266. for (const question of this.questionList) {
  267. if (isQuestionTypeNeedOptions(question.questionType)) {
  268. question.options = await this.loadQuestionOptions(question.id)
  269. } else {
  270. question.options = []
  271. }
  272. }
  273. },
  274. /**
  275. * 加载指定题目的选项
  276. * @param {string|number} questionId - 题目ID
  277. * @returns {Promise<Array<OptionItem>>} 选项列表
  278. */
  279. async loadQuestionOptions(questionId) {
  280. try {
  281. const params = {
  282. questionId,
  283. size: 100,
  284. current: 1
  285. }
  286. const responseOrigin = await getOptionList(params)
  287. const response = responseOrigin.data
  288. if (response.code === 200 && response.success) {
  289. return response.data.records || []
  290. } else {
  291. console.error('获取选项列表失败:', response.msg)
  292. return []
  293. }
  294. } catch (error) {
  295. console.error('加载选项列表失败:', error)
  296. return []
  297. }
  298. },
  299. /**
  300. * 新增题目
  301. * @returns {void}
  302. */
  303. handleAddQuestion() {
  304. this.questionDialogMode = 'add'
  305. this.resetQuestionForm()
  306. this.questionForm.surveyId = this.surveyId
  307. this.questionForm.questionNo = this.getNextQuestionNo()
  308. this.questionDialogVisible = true
  309. },
  310. /**
  311. * 编辑题目
  312. * @param {QuestionItem} question - 题目数据
  313. * @returns {void}
  314. */
  315. handleEditQuestion(question) {
  316. this.questionDialogMode = 'edit'
  317. this.setQuestionForm(question)
  318. this.questionDialogVisible = true
  319. },
  320. /**
  321. * 管理题目选项
  322. * @param {QuestionItem} question - 题目数据
  323. * @returns {void}
  324. */
  325. async handleManageOptions(question) {
  326. if (!isQuestionTypeNeedOptions(question.questionType)) {
  327. this.$message.warning('文本类型题目不需要设置选项')
  328. return
  329. }
  330. this.currentQuestionId = question.id
  331. this.currentQuestionOptions = await this.loadQuestionOptions(question.id)
  332. this.optionDialogVisible = true
  333. },
  334. /**
  335. * 新增选项
  336. * @returns {void}
  337. */
  338. handleAddOption() {
  339. this.optionDialogMode = 'add'
  340. this.optionAddDialogVisible = true
  341. this.resetOptionForm()
  342. this.optionForm.questionId = this.currentQuestionId
  343. this.optionForm.optionNo = this.getNextOptionNo()
  344. this.optionDialogVisible = true
  345. },
  346. /**
  347. * 编辑选项
  348. * @param {OptionItem} option - 选项数据
  349. * @returns {void}
  350. */
  351. handleEditOption(option) {
  352. this.optionDialogMode = 'edit'
  353. this.setOptionForm(option)
  354. this.optionDialogVisible = true
  355. this.optionAddDialogVisible = true
  356. },
  357. /**
  358. * 提交题目表单
  359. * @returns {Promise<void>}
  360. */
  361. async handleSubmitQuestion() {
  362. try {
  363. const valid = await this.$refs.questionForm.validate()
  364. if (!valid) return
  365. this.loading = true
  366. let response
  367. if (this.isAddQuestionMode) {
  368. response = await addQuestion(this.questionForm)
  369. } else {
  370. response = await updateQuestion(this.questionForm)
  371. }
  372. response = response.data
  373. if (response.code === 200 && response.success) {
  374. this.$message.success(response.msg || '操作成功')
  375. this.questionDialogVisible = false
  376. await this.loadQuestionList()
  377. } else {
  378. this.$message.error(response.msg || '操作失败')
  379. }
  380. } catch (error) {
  381. console.error('提交题目表单失败:', error)
  382. this.$message.error('操作失败')
  383. } finally {
  384. this.loading = false
  385. }
  386. },
  387. /**
  388. * 提交选项表单
  389. * @returns {Promise<void>}
  390. */
  391. async handleSubmitOption() {
  392. try {
  393. const valid = await this.$refs.optionForm.validate()
  394. if (!valid) return
  395. this.loading = true
  396. let response
  397. if (this.isAddOptionMode) {
  398. response = await addOption(this.optionForm)
  399. } else {
  400. response = await updateOption(this.optionForm)
  401. }
  402. response = response.data
  403. if (response.code === 200 && response.success) {
  404. this.$message.success(response.msg || '操作成功')
  405. // this.optionDialogVisible = false
  406. this.optionAddDialogVisible = false
  407. this.currentQuestionOptions = await this.loadQuestionOptions(this.currentQuestionId)
  408. await this.loadQuestionList()
  409. } else {
  410. this.$message.error(response.msg || '操作失败')
  411. }
  412. } catch (error) {
  413. console.error('提交选项表单失败:', error)
  414. this.$message.error('操作失败')
  415. } finally {
  416. this.loading = false
  417. }
  418. },
  419. /**
  420. * 设置题目表单数据
  421. * @param {QuestionItem} question - 题目数据
  422. * @returns {void}
  423. */
  424. setQuestionForm(question) {
  425. this.questionForm = {
  426. id: question.id,
  427. surveyId: question.surveyId,
  428. questionNo: question.questionNo,
  429. title: question.title,
  430. questionType: question.questionType,
  431. isRequired: question.isRequired
  432. }
  433. },
  434. /**
  435. * 重置题目表单
  436. * @returns {void}
  437. */
  438. resetQuestionForm() {
  439. this.questionForm = {
  440. surveyId: this.surveyId,
  441. questionNo: 1,
  442. title: '',
  443. questionType: QUESTION_TYPE.SINGLE_CHOICE,
  444. isRequired: 0
  445. }
  446. this.$nextTick(() => {
  447. this.$refs.questionForm?.clearValidate()
  448. })
  449. },
  450. /**
  451. * 设置选项表单数据
  452. * @param {OptionItem} option - 选项数据
  453. * @returns {void}
  454. */
  455. setOptionForm(option) {
  456. this.optionForm = {
  457. id: option.id,
  458. questionId: option.questionId,
  459. optionNo: option.optionNo,
  460. optionText: option.optionText
  461. }
  462. },
  463. /**
  464. * 重置选项表单
  465. * @returns {void}
  466. */
  467. resetOptionForm() {
  468. this.optionForm = {
  469. questionId: this.currentQuestionId,
  470. optionNo: 1,
  471. optionText: ''
  472. }
  473. this.$nextTick(() => {
  474. this.$refs.optionForm?.clearValidate()
  475. })
  476. },
  477. /**
  478. * 关闭题目对话框
  479. * @returns {void}
  480. */
  481. handleCloseQuestionDialog() {
  482. this.questionDialogVisible = false
  483. this.resetQuestionForm()
  484. },
  485. /**
  486. * 关闭选项对话框
  487. * @returns {void}
  488. */
  489. handleCloseOptionDialog() {
  490. this.optionDialogVisible = false
  491. this.resetOptionForm()
  492. this.currentQuestionOptions = []
  493. this.optionAddDialogVisible = false
  494. },
  495. /**
  496. * 获取下一个题目序号
  497. * @returns {number} 下一个序号
  498. */
  499. getNextQuestionNo() {
  500. if (this.questionList.length === 0) return 1
  501. const maxNo = Math.max(...this.questionList.map(q => q.questionNo))
  502. return maxNo + 1
  503. },
  504. /**
  505. * 获取下一个选项序号
  506. * @returns {number} 下一个序号
  507. */
  508. getNextOptionNo() {
  509. if (this.currentQuestionOptions.length === 0) return 1
  510. const maxNo = Math.max(...this.currentQuestionOptions.map(o => o.optionNo))
  511. return maxNo + 1
  512. },
  513. /**
  514. * 获取题目类型标签
  515. * @param {number} questionType - 题目类型
  516. * @returns {string} 类型标签
  517. */
  518. getQuestionTypeLabel(questionType) {
  519. return getQuestionTypeLabel(questionType)
  520. },
  521. /**
  522. * 获取题目类型类型
  523. * @param {number} questionType - 题目类型
  524. * @returns {string} 类型类型
  525. */
  526. getQuestionTypeType(questionType) {
  527. return getQuestionTypeType(questionType)
  528. },
  529. /**
  530. * 获取题目类型图标
  531. * @param {number} questionType - 题目类型
  532. * @returns {string} 类型图标
  533. */
  534. getQuestionTypeIcon(questionType) {
  535. return getQuestionTypeIcon(questionType)
  536. },
  537. /**
  538. * 获取是否必填标签
  539. * @param {number} isRequired - 是否必填
  540. * @returns {string} 必填标签
  541. */
  542. getQuestionRequiredLabel(isRequired) {
  543. return getQuestionRequiredLabel(isRequired)
  544. },
  545. /**
  546. * 获取是否必填类型
  547. * @param {number} isRequired - 是否必填
  548. * @returns {string} 必填类型
  549. */
  550. getQuestionRequiredType(isRequired) {
  551. return getQuestionRequiredType(isRequired)
  552. },
  553. /**
  554. * 判断题目类型是否需要选项
  555. * @param {number} questionType - 题目类型
  556. * @returns {boolean} 是否需要选项
  557. */
  558. isQuestionTypeNeedOptions(questionType) {
  559. return isQuestionTypeNeedOptions(questionType)
  560. }
  561. }
  562. }