index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. <template>
  2. <div class="live-question-bank-container">
  3. <div class="question-bank-container">
  4. <div class="question-bank-header">
  5. <el-button
  6. type="primary"
  7. icon="el-icon-plus"
  8. v-hasPermi="['live:liveQuestionBank:add']"
  9. @click="handleAdd"
  10. >添加试题</el-button>
  11. <div class="actions">
  12. <el-input
  13. v-model="queryParams.title"
  14. @input="handleSearch"
  15. placeholder="请输入搜索内容"
  16. style="width: 300px;"
  17. />
  18. </div>
  19. </div>
  20. <el-table :data="questionBankList" style="width: 100%; margin-top: 20px;" v-loading="loading">
  21. <el-table-column prop="title" label="题干内容" width="500"></el-table-column>
  22. <el-table-column label="题型" width="100">
  23. <template slot-scope="scope">
  24. {{ formatType(scope.row) }}
  25. </template>
  26. </el-table-column>
  27. <el-table-column prop="createBy" label="创建人"></el-table-column>
  28. <el-table-column prop="updateTime" label="最后更新时间"></el-table-column>
  29. <el-table-column label="操作" width="150" fixed="right">
  30. <template slot-scope="scope">
  31. <el-button
  32. type="text"
  33. size="small"
  34. style="color: #00CC66;"
  35. @click="handleEdit(scope.row)"
  36. >编辑</el-button>
  37. <el-button
  38. type="text"
  39. size="small"
  40. style="color: #F56C6C;"
  41. @click="handleDelete(scope.row)"
  42. >删除</el-button>
  43. </template>
  44. </el-table-column>
  45. </el-table>
  46. <pagination
  47. v-show="total>0"
  48. :total="total"
  49. :page.sync="queryParams.pageNum"
  50. :limit.sync="queryParams.pageSize"
  51. @pagination="getQuestionBankList"
  52. />
  53. <!-- 添加试题弹窗 -->
  54. <el-dialog
  55. :title="dialogTitle"
  56. :visible.sync="dialogVisible"
  57. :close-on-click-modal="false"
  58. :close-on-press-escape="false"
  59. >
  60. <el-form :model="form" ref="form" :rules="dynamicRules" label-width="80px">
  61. <el-form-item label="题型" prop="type">
  62. <el-select v-model="form.type" placeholder="请选择题型" style="width: 100%">
  63. <el-option
  64. v-for="item in questionTypes"
  65. :key="item.value"
  66. :label="item.label"
  67. :value="item.value"
  68. ></el-option>
  69. </el-select>
  70. </el-form-item>
  71. <el-form-item label="题干" prop="title">
  72. <el-input
  73. type="textarea"
  74. v-model="form.title"
  75. placeholder="请输入题干"
  76. :autosize="{ minRows: 4 }"
  77. ></el-input>
  78. </el-form-item>
  79. <!-- 动态渲染选项 -->
  80. <el-form-item
  81. v-for="(option, index) in form.options"
  82. :key="index"
  83. :label="`选项${option.label}`"
  84. :prop="`options.${index}.content`"
  85. >
  86. <el-input
  87. type="textarea"
  88. v-model="option.content"
  89. placeholder="请输入选项内容"
  90. :autosize="{ minRows: 4 }"
  91. ></el-input>
  92. <div class="option-helper">
  93. <el-checkbox
  94. v-model="option.isCorrect"
  95. @change="() => handleCorrectChange(option)"
  96. >设为正确答案</el-checkbox>
  97. <el-link type="primary" style="margin-left: auto;" @click="removeOption(index)">删除</el-link>
  98. </div>
  99. </el-form-item>
  100. <!-- 添加选项按钮 -->
  101. <el-form-item>
  102. <el-button type="text" @click="addOption" icon="el-icon-plus">添加选项</el-button>
  103. </el-form-item>
  104. <el-form-item label="答案" prop="analysis">
  105. <el-input
  106. type="text"
  107. v-model="form.analysis"
  108. style="width: 150px"
  109. disabled
  110. ></el-input>
  111. </el-form-item>
  112. </el-form>
  113. <div slot="footer" class="dialog-footer">
  114. <el-button @click="dialogVisible = false">取 消</el-button>
  115. <el-button type="primary" @click="submitForm">保 存</el-button>
  116. <el-button
  117. v-if="!isEdit"
  118. type="success"
  119. @click="submitAndContinue"
  120. >保存并继续添加</el-button>
  121. </div>
  122. </el-dialog>
  123. </div>
  124. </div>
  125. </template>
  126. <script>
  127. import { listLiveQuestionBank, addLiveQuestionBank, updateLiveQuestionBank, deleteLiveQuestionBank } from '@/api/live/liveQuestionBank'
  128. export default {
  129. name: 'LiveQuestionBank',
  130. data() {
  131. return {
  132. total: 0,
  133. loading: true,
  134. queryParams: {
  135. title: null,
  136. pageNum: 1,
  137. pageSize: 10
  138. },
  139. questionBankList: [],
  140. dialogVisible: false,
  141. dialogTitle: '添加试题',
  142. isEdit: false,
  143. form: {
  144. id: null,
  145. title: '',
  146. type: 1,
  147. options: [
  148. { label: 'A', content: '', isCorrect: false },
  149. { label: 'B', content: '', isCorrect: false },
  150. { label: 'C', content: '', isCorrect: false },
  151. { label: 'D', content: '', isCorrect: false }
  152. ],
  153. analysis: ''
  154. },
  155. questionTypes: [
  156. { label: '单选题', value: 1 },
  157. { label: '多选题', value: 2 }
  158. ]
  159. }
  160. },
  161. computed: {
  162. dynamicRules() {
  163. const rules = {
  164. title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
  165. type: [{ required: true, message: '请选择题型', trigger: 'change' }],
  166. analysis: [{ required: true, message: '请设置正确答案', trigger: 'change' }]
  167. }
  168. // 动态添加选项验证规则
  169. this.form.options.forEach((_, index) => {
  170. rules[`options.${index}.content`] = [{ required: true, message: '请输入选项内容', trigger: 'blur' }]
  171. })
  172. return rules
  173. }
  174. },
  175. created() {
  176. this.getQuestionBankList()
  177. },
  178. watch: {
  179. 'form.options': {
  180. handler(options) {
  181. const correctLabels = options
  182. .filter(option => option.isCorrect)
  183. .map(option => option.label)
  184. .join(',')
  185. this.form.analysis = correctLabels
  186. },
  187. deep: true
  188. },
  189. 'form.type'() {
  190. this.handleTypeChange()
  191. }
  192. },
  193. methods: {
  194. getQuestionBankList() {
  195. this.loading = true;
  196. listLiveQuestionBank(this.queryParams).then(response => {
  197. this.questionBankList = response.rows
  198. this.total = response.total;
  199. this.loading = false;
  200. })
  201. },
  202. handleSearch() {
  203. this.getQuestionBankList()
  204. },
  205. handleAdd() {
  206. this.dialogTitle = '添加试题'
  207. this.isEdit = false
  208. this.dialogVisible = true
  209. this.resetForm()
  210. },
  211. handleEdit(row) {
  212. this.dialogTitle = '编辑试题'
  213. this.isEdit = true
  214. this.dialogVisible = true
  215. // 先设置基础数据
  216. this.form.id = row.id
  217. this.form.title = row.title
  218. // 先回显选项数据
  219. let options = JSON.parse(row.content)
  220. const answerArray = row.answer.split(',')
  221. this.form.options = options.map(option => ({
  222. label: option.label,
  223. content: option.content,
  224. isCorrect: answerArray.includes(option.label)
  225. }))
  226. // 最后设置题型和答案,因为设置题型会触发handleTypeChange
  227. this.form.type = row.type
  228. this.form.analysis = row.answer
  229. },
  230. submitForm() {
  231. this.$refs.form.validate(valid => {
  232. if (!valid) {
  233. this.$message({
  234. message: '请检查表单填写是否正确',
  235. type: 'warning'
  236. })
  237. return
  238. }
  239. // 验证是否有正确答案
  240. if (!this.form.analysis) {
  241. this.$message({
  242. message: '请设置正确答案',
  243. type: 'warning'
  244. })
  245. return
  246. }
  247. // 验证答案数量
  248. const correctCount = this.form.options.filter(option => option.isCorrect).length
  249. if (this.form.type === 1 && correctCount !== 1) {
  250. this.$message({
  251. message: '单选题必须设置一个正确答案',
  252. type: 'warning'
  253. })
  254. return
  255. }
  256. if (this.form.type === 2 && correctCount < 2) {
  257. this.$message({
  258. message: '多选题至少需要设置两个正确答案',
  259. type: 'warning'
  260. })
  261. return
  262. }
  263. // 构造提交的参数
  264. let params = {
  265. id: this.isEdit ? this.form.id : null,
  266. title: this.form.title,
  267. type: this.form.type,
  268. content: JSON.stringify(this.form.options),
  269. answer: this.form.analysis
  270. }
  271. // TODO: 根据isEdit判断调用添加还是更新接口
  272. if (this.isEdit) {
  273. // 调用更新接口
  274. updateLiveQuestionBank(params).then(response => {
  275. this.msgSuccess("更新成功");
  276. this.getQuestionBankList();
  277. })
  278. } else {
  279. // 调用添加接口
  280. addLiveQuestionBank(params).then(response => {
  281. this.msgSuccess("新增成功");
  282. this.getQuestionBankList();
  283. })
  284. }
  285. this.dialogVisible = false
  286. this.resetForm()
  287. })
  288. },
  289. submitAndContinue() {
  290. this.$refs.form.validate(valid => {
  291. if (!valid) {
  292. this.$message({
  293. message: '请检查表单填写是否正确',
  294. type: 'warning'
  295. })
  296. return
  297. }
  298. // 验证是否有正确答案
  299. if (!this.form.analysis) {
  300. this.$message({
  301. message: '请设置正确答案',
  302. type: 'warning'
  303. })
  304. return
  305. }
  306. // 验证答案数量
  307. const correctCount = this.form.options.filter(option => option.isCorrect).length
  308. if (this.form.type === 1 && correctCount !== 1) {
  309. this.$message({
  310. message: '单选题必须设置一个正确答案',
  311. type: 'warning'
  312. })
  313. return
  314. }
  315. if (this.form.type === 2 && correctCount < 2) {
  316. this.$message({
  317. message: '多选题至少需要设置两个正确答案',
  318. type: 'warning'
  319. })
  320. return
  321. }
  322. // 构造提交的参数
  323. let params = {
  324. title: this.form.title,
  325. type: this.form.type,
  326. content: JSON.stringify(this.form.options),
  327. answer: this.form.analysis
  328. }
  329. // 保存逻辑
  330. addLiveQuestionBank(params).then(response => {
  331. this.msgSuccess("新增成功");
  332. this.getQuestionBankList();
  333. })
  334. this.resetForm()
  335. })
  336. },
  337. addOption() {
  338. if (this.form.options.length >= 6) {
  339. this.$message({
  340. message: '最多只能添加6个选项',
  341. type: 'warning'
  342. })
  343. return
  344. }
  345. const nextLabel = String.fromCharCode('A'.charCodeAt(0) + this.form.options.length)
  346. this.form.options.push({
  347. label: nextLabel,
  348. content: '',
  349. isCorrect: false
  350. })
  351. },
  352. removeOption(index) {
  353. if (this.form.options.length <= 2) {
  354. this.$message({
  355. message: '至少需要保持两个选项',
  356. type: 'warning'
  357. })
  358. return
  359. }
  360. this.form.options.splice(index, 1)
  361. // 重新排列选项标签
  362. this.form.options.forEach((option, idx) => {
  363. option.label = String.fromCharCode('A'.charCodeAt(0) + idx)
  364. })
  365. },
  366. resetForm() {
  367. if (this.$refs.form) {
  368. this.$refs.form.resetFields()
  369. }
  370. // 重置表单数据
  371. this.form = {
  372. id: null,
  373. title: '',
  374. type: 1,
  375. options: [
  376. { label: 'A', content: '', isCorrect: false },
  377. { label: 'B', content: '', isCorrect: false },
  378. { label: 'C', content: '', isCorrect: false },
  379. { label: 'D', content: '', isCorrect: false }
  380. ],
  381. analysis: ''
  382. }
  383. this.isEdit = false
  384. },
  385. handleCorrectChange(currentOption) {
  386. const correctCount = this.form.options.filter(option => option.isCorrect).length
  387. if (this.form.type === 1) { // 单选题
  388. // 如果当前选项被选中,则取消其他选项的选中状态
  389. if (currentOption.isCorrect) {
  390. this.form.options.forEach(option => {
  391. if (option !== currentOption) {
  392. option.isCorrect = false
  393. }
  394. })
  395. }
  396. } else { // 多选题
  397. // 如果取消选中会导致正确答案少于2个,阻止操作
  398. if (!currentOption.isCorrect && correctCount < 2) {
  399. currentOption.isCorrect = true
  400. this.$message({
  401. message: '多选题至少需要2个正确答案',
  402. type: 'warning'
  403. })
  404. }
  405. }
  406. },
  407. handleTypeChange() {
  408. // 如果是编辑状态,不重置答案
  409. if (!this.isEdit) {
  410. // 重置所有选项的正确答案状态
  411. this.form.options.forEach(option => {
  412. option.isCorrect = false
  413. })
  414. this.form.analysis = ''
  415. }
  416. },
  417. handleDelete(row) {
  418. this.$confirm('是否确认删除该试题?', '警告', {
  419. confirmButtonText: '确定',
  420. cancelButtonText: '取消',
  421. type: 'warning'
  422. }).then(() => {
  423. deleteLiveQuestionBank(row.id).then(response => {
  424. this.msgSuccess("删除成功");
  425. this.getQuestionBankList();
  426. })
  427. }).catch(() => {
  428. // 取消删除,不做任何操作
  429. })
  430. },
  431. // 添加获取题型值的方法
  432. getQuestionTypeName(type) {
  433. const found = this.questionTypes.find(item => item.value === type)
  434. return found ? found.label : ''
  435. },
  436. // 修改表格列的显示
  437. formatType(row) {
  438. return this.getQuestionTypeName(row.type)
  439. }
  440. }
  441. }
  442. </script>
  443. <style scoped>
  444. .live-question-bank-container {
  445. padding: 10px 20px;
  446. height: calc(100vh - 84px); /* 减去头部导航的高度 */
  447. }
  448. .question-bank-container {
  449. background-color: #fff;
  450. padding: 10px 20px;
  451. border-radius: 4px;
  452. height: 100%;
  453. }
  454. .question-bank-header {
  455. margin-top: 20px;
  456. display: flex;
  457. justify-content: space-between;
  458. align-items: center;
  459. }
  460. .title {
  461. font-size: 16px;
  462. font-weight: bold;
  463. }
  464. .actions {
  465. display: flex;
  466. align-items: center;
  467. }
  468. /* 修改表格行高 */
  469. ::v-deep .el-table td {
  470. padding: 12px 0;
  471. }
  472. /* 修改表格hover颜色 */
  473. ::v-deep .el-table tbody tr:hover > td {
  474. background-color: #F5F7FA;
  475. }
  476. .option-helper {
  477. display: flex;
  478. align-items: center;
  479. margin-top: 8px;
  480. }
  481. /* 修改弹窗样式 */
  482. ::v-deep .el-dialog {
  483. height: 100%;
  484. margin: 0 !important;
  485. display: flex;
  486. flex-direction: column;
  487. }
  488. ::v-deep .el-dialog__body {
  489. flex: 1;
  490. padding: 30px 40px;
  491. overflow-y: auto;
  492. margin-bottom: 80px; /* 给底部按钮预留空间 */
  493. }
  494. ::v-deep .el-dialog__header {
  495. padding: 20px 40px;
  496. border-bottom: 1px solid #e4e7ed;
  497. }
  498. ::v-deep .el-dialog__footer {
  499. padding: 20px 40px;
  500. border-top: 1px solid #e4e7ed;
  501. position: fixed;
  502. bottom: 0;
  503. width: 100%;
  504. background: #fff;
  505. box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.12);
  506. z-index: 1;
  507. }
  508. ::v-deep .el-form-item__label {
  509. font-weight: normal;
  510. }
  511. ::v-deep .el-textarea__inner {
  512. background-color: #fff;
  513. resize: none;
  514. }
  515. .dialog-footer {
  516. text-align: center;
  517. padding-top: 10px;
  518. }
  519. </style>