maintenance.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <template>
  2. <ele-modal
  3. :visible.sync="visible"
  4. :title="dialogTitle"
  5. width="80vw"
  6. append-to-body
  7. :close-on-click-modal="false"
  8. :maxable="true"
  9. @close="cancel"
  10. >
  11. <!-- 题库头部信息 -->
  12. <div class="bank-header">
  13. <el-form
  14. ref="bankForm"
  15. :model="bankForm"
  16. :rules="bankRules"
  17. inline
  18. label-width="120px"
  19. class="bank-form"
  20. >
  21. <el-form-item label="题库名称:" prop="name">
  22. <el-input
  23. v-model="bankForm.name"
  24. placeholder="请输入题库名称"
  25. :disabled="isView"
  26. />
  27. </el-form-item>
  28. <el-form-item label="所属部门:" prop="deptId">
  29. <dept-select
  30. v-model="bankForm.deptId"
  31. placeholder="请选择所属部门"
  32. :disabled="isView"
  33. @changeGroup="onDeptChange"
  34. />
  35. </el-form-item>
  36. </el-form>
  37. </div>
  38. <div class="maintenance-body">
  39. <!-- 左侧题号列表 -->
  40. <div class="left-panel">
  41. <div class="panel-title">题号</div>
  42. <div class="panel-search">
  43. <el-input
  44. v-model="questionSearch"
  45. placeholder="输入关键词搜索试题"
  46. size="small"
  47. >
  48. <el-button
  49. slot="append"
  50. type="primary"
  51. size="small"
  52. @click="handleSearchQuestion"
  53. >搜索</el-button
  54. >
  55. </el-input>
  56. </div>
  57. <el-tree
  58. ref="treeRef"
  59. :data="questionList"
  60. node-key="id"
  61. default-expand-all
  62. highlight-current
  63. :props="{ label: 'label', children: 'children' }"
  64. @node-click="handleQuestionClick"
  65. >
  66. <span class="question-node" slot-scope="{ node, data }">
  67. <span>{{ node.label }}</span>
  68. <el-tooltip
  69. v-if="!isView && data.question"
  70. content="删除试题"
  71. placement="top"
  72. >
  73. <el-button
  74. type="text"
  75. class="question-delete"
  76. icon="el-icon-delete"
  77. aria-label="删除试题"
  78. @click.stop="handleDeleteQuestion(data)"
  79. />
  80. </el-tooltip>
  81. </span>
  82. </el-tree>
  83. </div>
  84. <!-- 右侧试题表单 -->
  85. <div class="right-panel">
  86. <question-form ref="questionFormRef" v-model="form" :is-view="isView" />
  87. </div>
  88. </div>
  89. <template v-slot:footer>
  90. <el-button v-if="!isView" @click="handleReset">重置</el-button>
  91. <el-button
  92. v-if="!isView"
  93. type="primary"
  94. @click="saveNext"
  95. :loading="loading"
  96. >保存并进入下一题</el-button
  97. >
  98. <el-button v-else @click="cancel">关闭</el-button>
  99. </template>
  100. </ele-modal>
  101. </template>
  102. <script>
  103. import DeptSelect from '@/components/CommomSelect/dept-select.vue';
  104. import QuestionForm from '../paperManagement/question-form.vue';
  105. import {
  106. deleteQuestion,
  107. edit as updateQuestionBank,
  108. getById,
  109. getList,
  110. save as saveQuestionBank
  111. } from '@/api/questionBank/questionBank.js';
  112. const questionTypeMap = {
  113. 1: 'SINGLE',
  114. 2: 'MULTIPLE',
  115. 3: 'JUDGE',
  116. 4: 'QUESTION_ANSWER'
  117. };
  118. const questionTypeReverseMap = {
  119. SINGLE: '1',
  120. MULTIPLE: '2',
  121. JUDGE: '3',
  122. QUESTION_ANSWER: '4'
  123. };
  124. const defBankForm = {
  125. id: '',
  126. name: '',
  127. deptId: '',
  128. deptName: ''
  129. };
  130. const defQuestionForm = {
  131. id: '',
  132. sort: '',
  133. content: '',
  134. type: '1',
  135. category: '',
  136. score: '',
  137. options: [
  138. { key: 'A', content: '' },
  139. { key: 'B', content: '' },
  140. { key: 'C', content: '' },
  141. { key: 'D', content: '' }
  142. ],
  143. rightAnswer: '',
  144. rightAnswers: [],
  145. analysis: ''
  146. };
  147. const createQuestionList = () => [];
  148. export default {
  149. components: {
  150. DeptSelect,
  151. QuestionForm
  152. },
  153. data() {
  154. return {
  155. visible: false,
  156. loading: false,
  157. dialogType: 'add',
  158. questionSearch: '',
  159. bankForm: JSON.parse(JSON.stringify(defBankForm)),
  160. bankRules: {
  161. name: [
  162. { required: true, message: '请输入题库名称', trigger: 'blur' }
  163. ],
  164. deptId: [
  165. { required: true, message: '请选择所属部门', trigger: 'change' }
  166. ]
  167. },
  168. form: JSON.parse(JSON.stringify(defQuestionForm)),
  169. questionList: createQuestionList()
  170. };
  171. },
  172. computed: {
  173. dialogTitle() {
  174. const titleMap = {
  175. add: '维护试题',
  176. edit: '维护试题',
  177. view: '查看试题'
  178. };
  179. return titleMap[this.dialogType] || '维护试题';
  180. },
  181. isView() {
  182. return this.dialogType === 'view';
  183. }
  184. },
  185. methods: {
  186. async open(row, type = 'add') {
  187. this.visible = true;
  188. this.dialogType = type;
  189. const questionBankId = row && (row.id || row.questionBankId);
  190. if (questionBankId) {
  191. this.bankForm = {
  192. id: questionBankId,
  193. name: row.questionBankName || row.name || '',
  194. deptId: row.departmentId || row.deptId || '',
  195. deptName: row.departmentName || row.deptName || ''
  196. };
  197. this.loading = true;
  198. try {
  199. await this.loadDetail(questionBankId, type === 'view');
  200. } catch (error) {
  201. this.$message.error(error.message || '获取题库详情失败');
  202. this.cancel();
  203. } finally {
  204. this.loading = false;
  205. }
  206. } else {
  207. this.bankForm = JSON.parse(JSON.stringify(defBankForm));
  208. this.questionList = createQuestionList();
  209. this.form = JSON.parse(JSON.stringify(defQuestionForm));
  210. this.$nextTick(() => {
  211. this.$refs.treeRef && this.$refs.treeRef.setCurrentKey('1');
  212. });
  213. }
  214. this.$nextTick(() => {
  215. this.$refs.bankForm && this.$refs.bankForm.clearValidate();
  216. this.$refs.questionFormRef &&
  217. this.$refs.questionFormRef.clearValidate();
  218. });
  219. },
  220. cancel() {
  221. this.bankForm = JSON.parse(JSON.stringify(defBankForm));
  222. this.form = JSON.parse(JSON.stringify(defQuestionForm));
  223. this.questionList = createQuestionList();
  224. this.visible = false;
  225. },
  226. onDeptChange(deptId, node) {
  227. this.bankForm.deptName = node ? node.name : '';
  228. },
  229. handleSearchQuestion() {
  230. // TODO: 搜索试题
  231. console.log('搜索试题', this.questionSearch);
  232. },
  233. handleQuestionClick(data) {
  234. if (!data.question) return;
  235. this.form = this.mapQuestionToForm(data.question);
  236. this.$nextTick(() => {
  237. this.$refs.questionFormRef &&
  238. this.$refs.questionFormRef.clearValidate();
  239. });
  240. },
  241. async handleDeleteQuestion(data) {
  242. try {
  243. await this.$confirm('确认删除该试题吗?', '提示', {
  244. confirmButtonText: '确定',
  245. cancelButtonText: '取消',
  246. type: 'warning'
  247. });
  248. const message = await deleteQuestion(data.id);
  249. await this.loadDetail(this.bankForm.id, false);
  250. this.$message.success(message || '删除成功');
  251. this.$emit('reload');
  252. } catch (error) {
  253. if (error !== 'cancel' && error !== 'close') {
  254. this.$message.error(error.message || '删除失败');
  255. }
  256. }
  257. },
  258. mapQuestionToForm(question) {
  259. const type = questionTypeReverseMap[question.questionType] || '1';
  260. const options = [...(question.optionList || [])]
  261. .sort((a, b) => Number(a.sortNo) - Number(b.sortNo))
  262. .map((option, index) => ({
  263. key: String.fromCharCode(65 + index),
  264. content: option.optionContent || ''
  265. }));
  266. const correctAnswer = question.correctAnswer || '';
  267. return {
  268. id: question.id || '',
  269. sort: question.sortNo === undefined ? '' : String(question.sortNo),
  270. content: question.questionContent || '',
  271. type,
  272. category:
  273. question.professionalType === undefined
  274. ? ''
  275. : String(question.professionalType),
  276. score:
  277. question.questionScore === undefined
  278. ? ''
  279. : String(question.questionScore),
  280. options,
  281. rightAnswer: type === '2' ? '' : correctAnswer,
  282. rightAnswers:
  283. type === '2' ? correctAnswer.split(',').filter((item) => item) : [],
  284. analysis: question.questionExplanation || ''
  285. };
  286. },
  287. async loadDetail(questionBankId, selectFirstQuestion = true) {
  288. const detail = await getById(questionBankId);
  289. this.bankForm = {
  290. id: detail.id || questionBankId,
  291. name: detail.questionBankName || '',
  292. deptId: detail.departmentId || '',
  293. deptName: detail.departmentName || ''
  294. };
  295. const questions = [...(detail.questionList || [])].sort(
  296. (a, b) => Number(a.sortNo) - Number(b.sortNo)
  297. );
  298. this.questionList = questions.map((question, index) => ({
  299. id: question.id,
  300. label: `第${question.sortNo || index + 1}题`,
  301. question
  302. }));
  303. this.form =
  304. selectFirstQuestion && questions.length
  305. ? this.mapQuestionToForm(questions[0])
  306. : JSON.parse(JSON.stringify(defQuestionForm));
  307. this.$nextTick(() => {
  308. if (selectFirstQuestion && questions.length && this.$refs.treeRef) {
  309. this.$refs.treeRef.setCurrentKey(questions[0].id);
  310. } else if (this.$refs.treeRef) {
  311. this.$refs.treeRef.setCurrentKey(null);
  312. }
  313. });
  314. },
  315. handleReset() {
  316. this.form = JSON.parse(JSON.stringify(defQuestionForm));
  317. this.$refs.questionFormRef &&
  318. this.$refs.questionFormRef.clearValidate();
  319. },
  320. toNumber(value) {
  321. return value === '' || value === null || value === undefined
  322. ? undefined
  323. : Number(value);
  324. },
  325. buildSavePayload() {
  326. const isMultiple = this.form.type === '2';
  327. return {
  328. questionBankId: this.bankForm.id || undefined,
  329. questionBankName: this.bankForm.name,
  330. departmentId: this.bankForm.deptId,
  331. departmentName: this.bankForm.deptName,
  332. question: {
  333. questionId: this.form.id || undefined,
  334. sortNo: this.toNumber(this.form.sort),
  335. questionContent: this.form.content,
  336. questionType: questionTypeMap[this.form.type],
  337. professionalType: this.toNumber(this.form.category),
  338. questionScore: this.toNumber(this.form.score),
  339. correctAnswer: isMultiple
  340. ? this.form.rightAnswers.join(',')
  341. : this.form.rightAnswer,
  342. questionExplanation: this.form.analysis,
  343. optionList: this.form.options.map((item, index) => ({
  344. optionContent: item.content,
  345. sortNo: index + 1
  346. }))
  347. }
  348. };
  349. },
  350. async resolveQuestionBankId(saveResult) {
  351. if (
  352. typeof saveResult === 'number' ||
  353. (typeof saveResult === 'string' && /^\d+$/.test(saveResult))
  354. ) {
  355. return saveResult;
  356. }
  357. const resultId =
  358. typeof saveResult === 'object'
  359. ? saveResult.id || saveResult.questionBankId
  360. : undefined;
  361. if (resultId) {
  362. return resultId;
  363. }
  364. const result = await getList({
  365. questionBankName: this.bankForm.name,
  366. departmentId: this.bankForm.deptId,
  367. pageNum: 1,
  368. size: 20
  369. });
  370. const questionBank = (result.list || []).find(
  371. (item) =>
  372. item.questionBankName === this.bankForm.name &&
  373. String(item.departmentId) === String(this.bankForm.deptId)
  374. );
  375. if (!questionBank) {
  376. throw new Error('题库已保存,但未能获取题库ID');
  377. }
  378. return questionBank.id;
  379. },
  380. saveNext() {
  381. this.$refs.bankForm.validate((bankValid) => {
  382. if (!bankValid) return;
  383. this.$refs.questionFormRef.validate((valid) => {
  384. if (valid) {
  385. this.loading = true;
  386. const payload = this.buildSavePayload();
  387. const saveRequest = this.bankForm.id
  388. ? updateQuestionBank
  389. : saveQuestionBank;
  390. saveRequest(payload)
  391. .then(async (saveResult) => {
  392. if (!this.bankForm.id) {
  393. this.bankForm.id = await this.resolveQuestionBankId(
  394. saveResult
  395. );
  396. }
  397. await this.loadDetail(this.bankForm.id, false);
  398. this.$message.success('保存成功');
  399. this.$emit('reload');
  400. this.$nextTick(() => {
  401. this.$refs.questionFormRef &&
  402. this.$refs.questionFormRef.clearValidate();
  403. });
  404. })
  405. .catch((error) => {
  406. this.$message.error(error.message || '保存失败');
  407. })
  408. .finally(() => {
  409. this.loading = false;
  410. });
  411. }
  412. });
  413. });
  414. }
  415. }
  416. };
  417. </script>
  418. <style lang="scss" scoped>
  419. .bank-header {
  420. padding: 10px 15px 0;
  421. background-color: #f5f7fa;
  422. border-radius: 4px;
  423. margin-bottom: 15px;
  424. .bank-form {
  425. display: flex;
  426. justify-content: center;
  427. flex-wrap: wrap;
  428. }
  429. ::v-deep .el-form-item {
  430. margin-bottom: 10px;
  431. }
  432. }
  433. .maintenance-body {
  434. display: flex;
  435. max-height: calc(90vh - 260px);
  436. min-height: 450px;
  437. }
  438. .left-panel {
  439. width: 260px;
  440. flex-shrink: 0;
  441. border-right: 1px solid #ebeef5;
  442. padding-right: 15px;
  443. margin-right: 15px;
  444. .panel-title {
  445. font-size: 14px;
  446. font-weight: bold;
  447. text-align: center;
  448. padding: 8px 0;
  449. background-color: #409eff;
  450. color: #fff;
  451. border-radius: 4px 4px 0 0;
  452. }
  453. .panel-search {
  454. padding: 10px 0;
  455. ::v-deep .el-input-group__append {
  456. background-color: #409eff;
  457. color: #fff;
  458. border-color: #409eff;
  459. }
  460. }
  461. .question-node {
  462. display: flex;
  463. align-items: center;
  464. justify-content: space-between;
  465. flex: 1;
  466. padding-right: 8px;
  467. }
  468. .question-delete {
  469. color: #f56c6c;
  470. }
  471. }
  472. .right-panel {
  473. flex: 1;
  474. overflow-y: auto;
  475. padding-right: 10px;
  476. }
  477. .question-form {
  478. padding-top: 10px;
  479. }
  480. .danger-text {
  481. color: #f56c6c;
  482. }
  483. </style>