editAndUpdate.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. import {
  2. DataTableColumn,
  3. FormInst,
  4. FormItemRule,
  5. FormRules,
  6. NButton,
  7. NCascader,
  8. NDataTable,
  9. NDrawer,
  10. NDrawerContent,
  11. NForm,
  12. NFormItem,
  13. NIcon,
  14. NInput,
  15. NInputNumber,
  16. NModal,
  17. NPageHeader,
  18. NSelect,
  19. NSpace,
  20. NSpin,
  21. NTabPane,
  22. NTabs,
  23. useDialog,
  24. useMessage
  25. } from 'naive-ui'
  26. import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
  27. import { useTabsViewStore } from '@/store/modules/tabsView'
  28. import { useRoute, useRouter } from 'vue-router'
  29. import { getLessonType, lessonType } from '@/views/knowledge-manage/knowledgeTypeData'
  30. import AddQuestionList from '../model/addQuestionList'
  31. import { difficultyCoefficients, questionTypeCode } from '../question-bank'
  32. import styles from '../index.module.less'
  33. import {
  34. examinationKnowledgePointCategoryPage,
  35. unitExaminationDetail,
  36. unitExaminationRandomSave,
  37. unitExaminationSave
  38. } from '../../api'
  39. import { useUserStore } from '@/store/modules/user'
  40. import { DeleteColumnOutlined, DeleteFilled } from '@vicons/antd'
  41. import PreviewUnit from './previewUnit'
  42. import { filterPointCategory } from '..'
  43. type IUnitype = 'normal' | 'random'
  44. interface IQuestion {
  45. /**分值 */
  46. score: null | any
  47. /**题目类型多个逗号分割 */
  48. questionTypeCodes: string
  49. /**难度,可用值:ONE,TWO,THREE */
  50. difficultyCoefficient: string
  51. /** 条件名称(例如考点一) */
  52. name: string
  53. /**题目数量 */
  54. questionNum: null | any
  55. /** */
  56. maxScore: number
  57. /**考点编号 */
  58. categoryId: null | any
  59. /**课程类型 */
  60. courseTypeCode: string
  61. }
  62. function createUnitItem(): IQuestion {
  63. return {
  64. score: null, //分值
  65. questionTypeCodes: '', //题目类型多个逗号分割
  66. difficultyCoefficient: '', // 难度,可用值:ONE,TWO,THREE
  67. name: '', // 条件名称(例如考点一)
  68. questionNum: null, //题目数量
  69. maxScore: 0,
  70. categoryId: null, // 考点编号
  71. courseTypeCode: '' // 课程类型
  72. }
  73. }
  74. export default defineComponent({
  75. name: 'unit-test-index-editAndUpdate',
  76. emits: ['handleSuccess'],
  77. setup(props, { emit }) {
  78. const dialog = useDialog()
  79. const route = useRoute()
  80. const query = route.query
  81. const router = useRouter()
  82. /**题目类型 */
  83. const questions = Object.entries(questionTypeCode).map(([value, label]) => ({ label, value }))
  84. const tabsViewStore = useTabsViewStore()
  85. const gotoBack = () => {
  86. tabsViewStore.closeCurrentTab(route)
  87. router.push({
  88. path: '/educationalManage/unitExamination'
  89. })
  90. }
  91. /**获取考点类型 */
  92. const categorys = ref<any[]>([])
  93. const getType = async () => {
  94. try {
  95. const res: any = await examinationKnowledgePointCategoryPage({
  96. page: 1,
  97. rows: 1000
  98. })
  99. if (Array.isArray(res?.data?.rows)) {
  100. categorys.value = filterPointCategory(res.data.rows, 'children')
  101. }
  102. } catch (error) {}
  103. }
  104. const message = useMessage()
  105. const loading = ref(false)
  106. const formRef = ref<FormInst | null>(null)
  107. const userStore = useUserStore()
  108. const unitType = ref<IUnitype>('normal')
  109. const saveModel = reactive({
  110. operatorId: (userStore.getUserInfo as any)?.id || '',
  111. id: query.unitExaminationId || '',
  112. name: query.name || '', //测验名称
  113. courseTypeCode: query.courseTypeCode || (null as any), // 课程类型
  114. passScore: Number(query.passScore) || (null as any), // 达标分数
  115. timeMinutes: Number(query.timeMinutes) || (null as any) // 测验时长(分钟
  116. })
  117. const pointRef = ref<FormInst | null>()
  118. const pointList = reactive({
  119. questionList: [createUnitItem()] as IQuestion[]
  120. })
  121. const questionListKey = 'questionList'
  122. const setPoint = () => {
  123. const questionListStr = localStorage.getItem(questionListKey) || ''
  124. try {
  125. pointList.questionList = JSON.parse(questionListStr)
  126. } catch (error) {}
  127. }
  128. setPoint()
  129. watch(pointList, () => {
  130. localStorage.setItem(questionListKey, JSON.stringify(pointList.questionList))
  131. })
  132. const rules: FormRules = {
  133. name: [{ required: true, message: '请填写类型名称', trigger: ['input', 'blur'] }],
  134. courseTypeCode: [{ required: true, message: '请选择课程类型', trigger: 'change' }],
  135. passScore: [
  136. { type: 'number', required: true, message: '请填写达标分数', trigger: ['input', 'blur'] }
  137. ],
  138. timeMinutes: [
  139. { type: 'number', required: true, message: '请填写测验时长', trigger: ['input', 'blur'] }
  140. ]
  141. }
  142. // 获取详情
  143. const getDetail = async () => {
  144. if (!saveModel.id) return
  145. loading.value = true
  146. try {
  147. const res: any = await unitExaminationDetail(saveModel.id)
  148. if (Array.isArray(res?.data)) {
  149. res.data.forEach((n: any) => {
  150. let item = {
  151. ...n?.question,
  152. answers: n?.answers || []
  153. }
  154. // console.log('🚀 ~ item', item)
  155. modalData.selectList.push(item)
  156. })
  157. }
  158. } catch (error) {}
  159. loading.value = false
  160. }
  161. onMounted(() => {
  162. getType()
  163. getDetail()
  164. })
  165. const submit = () => {
  166. // console.log(saveModel)
  167. formRef.value?.validate(async (err) => {
  168. if (!err) {
  169. if (!modalData.selectList.length) {
  170. message.error('请添加题目')
  171. return
  172. }
  173. if (saveModel.passScore > totalScore.value) {
  174. message.error('阶段自测的合格分数高于题目的总分值')
  175. return
  176. }
  177. const params: any = {
  178. ...saveModel,
  179. questionList: modalData.selectList
  180. }
  181. let res: any = null
  182. console.log(params)
  183. if (saveModel.id) {
  184. res = await unitExaminationSave(params)
  185. } else {
  186. res = await unitExaminationSave(params)
  187. }
  188. if (res?.code == 200) {
  189. message.success('保存成功')
  190. gotoBack()
  191. // emit('handleSuccess')
  192. } else {
  193. message.warning('保存失败')
  194. }
  195. }
  196. })
  197. }
  198. const modalData = reactive({
  199. open: false,
  200. selectList: [] as any[],
  201. checkList: [] as any[],
  202. previewOpen: false
  203. })
  204. /**已选题目的总分数 */
  205. const totalScore = computed(() => {
  206. return modalData.selectList.reduce((total: Number, n: any) => total + n.totalScore, 0)
  207. })
  208. /**
  209. * 删除随机考点
  210. */
  211. const handleDeleteItem = (index: number) => {
  212. // if (pointList.questionList.length === 1) return message.error('最少保留一个考点')
  213. pointList.questionList.splice(index, 1)
  214. }
  215. const columns = (): DataTableColumn[] => {
  216. return [
  217. {
  218. type: 'selection'
  219. },
  220. {
  221. title: '题目名称',
  222. key: 'name',
  223. width: 230,
  224. render(row: any) {
  225. return (
  226. <div>
  227. <div>{row.name}</div>
  228. </div>
  229. )
  230. }
  231. },
  232. {
  233. title: '题目类型',
  234. key: 'questionTypeCode',
  235. render(row: any) {
  236. return questionTypeCode[row.questionTypeCode]
  237. }
  238. },
  239. {
  240. title: '考点',
  241. key: 'examinationKnowledgePointCategoryName'
  242. },
  243. // {
  244. // title: '课程类型',
  245. // key: 'courseTypeCode',
  246. // render(row: any) {
  247. // return getLessonType(row.courseTypeCode) || '通用类型'
  248. // }
  249. // },
  250. {
  251. title: '难度',
  252. key: 'difficultyCoefficient',
  253. render(row: any) {
  254. return (
  255. difficultyCoefficients.find(
  256. (n: any) => n.value?.toLocaleUpperCase() === row.difficultyCoefficient
  257. )?.label || row.difficultyCoefficient
  258. )
  259. }
  260. },
  261. {
  262. title: '分值',
  263. key: 'totalScore'
  264. },
  265. {
  266. title: '操作',
  267. key: 'action',
  268. width: 180,
  269. render(row: any, rowIndex: number) {
  270. return (
  271. <NSpace>
  272. <NButton
  273. text
  274. type="primary"
  275. disabled={rowIndex === 0}
  276. onClick={() => handleRowMove('up', rowIndex)}
  277. >
  278. 上移
  279. </NButton>
  280. <NButton
  281. text
  282. type="primary"
  283. disabled={rowIndex === modalData.selectList.length - 1}
  284. onClick={() => handleRowMove('down', rowIndex)}
  285. >
  286. 下移
  287. </NButton>
  288. </NSpace>
  289. )
  290. }
  291. }
  292. ]
  293. }
  294. /**删除题目 */
  295. const handleDeleteQuestion = () => {
  296. dialog.warning({
  297. title: '警告',
  298. content: '是否确认删除选中的题目?',
  299. positiveText: '确定',
  300. negativeText: '取消',
  301. onPositiveClick: async () => {
  302. modalData.selectList = modalData.selectList.filter(
  303. (n: any) => !modalData.checkList.includes(n.id)
  304. )
  305. }
  306. })
  307. }
  308. /**表格行移动 */
  309. const handleRowMove = (type: 'up' | 'down', index: number) => {
  310. if (type === 'up') {
  311. modalData.selectList[index] = modalData.selectList.splice(
  312. index - 1,
  313. 1,
  314. modalData.selectList[index]
  315. )[0]
  316. } else {
  317. modalData.selectList[index] = modalData.selectList.splice(
  318. index + 1,
  319. 1,
  320. modalData.selectList[index]
  321. )[0]
  322. }
  323. }
  324. const courseTypeCodeRef = ref()
  325. /** 随机生成题目 */
  326. const checkRamdom = () => {
  327. // courseTypeCodeRef.value.validate()
  328. // if (!saveModel.courseTypeCode) {
  329. // message.error('请选择课程类型')
  330. // return
  331. // }
  332. if (!pointList.questionList.length) {
  333. message.error('请先添加考点')
  334. return
  335. }
  336. if (modalData.selectList.length) {
  337. dialog.warning({
  338. title: '警告',
  339. content: '已有选择的题目,重新生成会清空当前已选的题目,是否继续?',
  340. positiveText: '继续',
  341. negativeText: '取消',
  342. onPositiveClick: async () => {
  343. createUnitTest()
  344. }
  345. })
  346. return
  347. }
  348. createUnitTest()
  349. }
  350. const createUnitTest = async () => {
  351. modalData.selectList = []
  352. pointRef.value?.validate(async (err) => {
  353. if (!err) {
  354. const params: any = pointList.questionList.map((n: any, i: number) => {
  355. return {
  356. ...n,
  357. name: `考点${i + 1}`,
  358. questionTypeCodes: n.questionTypeCodes?.join(',') || ''
  359. }
  360. })
  361. let res: any = await unitExaminationRandomSave(params)
  362. if (Array.isArray(res?.data)) {
  363. modalData.selectList = res.data
  364. message.success('随机生成题目成功')
  365. }
  366. }
  367. })
  368. }
  369. return () => (
  370. <div class={['system-menu-container', styles['unit-test-index-editAndUpdate']]}>
  371. <NPageHeader
  372. on-back={() => gotoBack()}
  373. title={query.name ? (query.name as any) : '新增阶段自测'}
  374. ></NPageHeader>
  375. <div class={['section-container']}>
  376. <NSpin show={loading.value}>
  377. <NForm
  378. ref={formRef}
  379. model={saveModel}
  380. rules={rules}
  381. disabled={query.isLock ? true : false}
  382. requireMarkPlacement="left"
  383. labelPlacement="left"
  384. >
  385. <NSpace itemStyle={{ width: '30%' }}>
  386. <NFormItem label="测验名称" path="name" required>
  387. <NInput
  388. style={{ width: '210px' }}
  389. placeholder="请输入测验名称"
  390. v-model:value={saveModel.name}
  391. ></NInput>
  392. </NFormItem>
  393. <NFormItem ref={courseTypeCodeRef} label="课程类型" path="courseTypeCode" required>
  394. <NSelect
  395. style={{ width: '210px' }}
  396. placeholder="请选择课程类型"
  397. clearable
  398. v-model:value={saveModel.courseTypeCode}
  399. options={lessonType}
  400. />
  401. </NFormItem>
  402. </NSpace>
  403. <NSpace itemStyle={{ width: '30%' }}>
  404. <NFormItem label="合格分数" path="passScore" required>
  405. <NInputNumber
  406. style={{ width: '210px' }}
  407. placeholder="请输入合格分数"
  408. v-model:value={saveModel.passScore}
  409. clearable
  410. showButton={false}
  411. min={0}
  412. max={100}
  413. >
  414. {{
  415. suffix: () => '分'
  416. }}
  417. </NInputNumber>
  418. </NFormItem>
  419. <NFormItem label="测验时长" path="timeMinutes" required>
  420. <NInputNumber
  421. style={{ width: '210px' }}
  422. placeholder="请输入测验时长"
  423. v-model:value={saveModel.timeMinutes}
  424. clearable
  425. showButton={false}
  426. min={0}
  427. >
  428. {{
  429. suffix: () => '分钟'
  430. }}
  431. </NInputNumber>
  432. </NFormItem>
  433. </NSpace>
  434. <NFormItem label="组卷条件" required labelPlacement="top">
  435. <div class={styles.juanWrap} style={{ flex: 1, alignItems: 'center' }}>
  436. <NForm ref={pointRef} model={pointList} labelPlacement="left">
  437. {pointList.questionList.map((item: IQuestion, index: number) => {
  438. return (
  439. <div class={styles.ramdomItem}>
  440. <NSpace itemStyle={{ flex: 1 }}>
  441. <NFormItem
  442. path={`questionList[${index}].categoryId`}
  443. required
  444. rule={{
  445. required: true,
  446. message: '请选择考点',
  447. trigger: ['input', 'blur']
  448. }}
  449. >
  450. {{
  451. default: () => (
  452. <NCascader
  453. v-model:value={item.categoryId}
  454. class={styles.kaodian}
  455. options={categorys.value}
  456. checkStrategy="child"
  457. childrenField="children"
  458. valueField="categoryId"
  459. labelField="name"
  460. expandTrigger="hover"
  461. />
  462. ),
  463. label: () => (
  464. <p class={styles.kaoLabel}>
  465. <span>* </span>考点{index + 1}
  466. </p>
  467. )
  468. }}
  469. </NFormItem>
  470. <NFormItem
  471. label="* 题目数量"
  472. path={`questionList[${index}].questionNum`}
  473. rule={{
  474. type: 'number',
  475. required: true,
  476. message: '请输入题目数量',
  477. trigger: ['input', 'blur']
  478. }}
  479. >
  480. {{
  481. default: () => (
  482. <NInputNumber
  483. style={{ width: '270px' }}
  484. showButton={false}
  485. min={0}
  486. max={100}
  487. v-model:value={item.questionNum}
  488. >
  489. {{
  490. suffix: () => '题'
  491. }}
  492. </NInputNumber>
  493. ),
  494. label: () => (
  495. <p class={styles.kaoLabel}>
  496. <span>* </span>题目数量
  497. </p>
  498. )
  499. }}
  500. </NFormItem>
  501. <NFormItem
  502. label="难度"
  503. required
  504. path={`questionList[${index}].difficultyCoefficient`}
  505. rule={{
  506. required: true,
  507. message: '请选择难度',
  508. trigger: ['input', 'blur']
  509. }}
  510. >
  511. {{
  512. default: () => (
  513. <NSelect
  514. options={difficultyCoefficients}
  515. v-model:value={item.difficultyCoefficient}
  516. />
  517. ),
  518. label: () => (
  519. <p class={styles.kaoLabel}>
  520. <span>* </span>难度
  521. </p>
  522. )
  523. }}
  524. </NFormItem>
  525. <NFormItem
  526. label="总分值"
  527. required
  528. path={`questionList[${index}].score`}
  529. rule={{
  530. type: 'number',
  531. required: true,
  532. message: '请输入分值',
  533. trigger: ['input', 'blur']
  534. }}
  535. >
  536. {{
  537. default: () => (
  538. <NInputNumber
  539. showButton={false}
  540. style={{ width: '270px' }}
  541. min={0}
  542. max={100}
  543. v-model:value={item.score}
  544. >
  545. {{
  546. suffix: () => '分'
  547. }}
  548. </NInputNumber>
  549. ),
  550. label: () => (
  551. <p class={styles.kaoLabel}>
  552. <span>* </span>总分值
  553. </p>
  554. )
  555. }}
  556. </NFormItem>
  557. <NFormItem
  558. label="题型"
  559. required
  560. path={`questionList[${index}].questionTypeCodes`}
  561. rule={{
  562. type: 'array',
  563. required: true,
  564. message: '请选择题目类型',
  565. trigger: ['input', 'blur']
  566. }}
  567. >
  568. {{
  569. default: () => (
  570. <NSelect
  571. multiple
  572. clearable
  573. options={questions}
  574. v-model:value={item.questionTypeCodes}
  575. />
  576. ),
  577. label: () => (
  578. <p class={styles.kaoLabel}>
  579. <span>* </span>题型
  580. </p>
  581. )
  582. }}
  583. </NFormItem>
  584. </NSpace>
  585. <NButton
  586. class={styles.delIcon}
  587. size="tiny"
  588. type="error"
  589. circle
  590. onClick={() => handleDeleteItem(index)}
  591. >
  592. <NIcon component={<DeleteFilled />}></NIcon>
  593. </NButton>
  594. </div>
  595. )
  596. })}
  597. </NForm>
  598. <NSpace style={{ padding: '10px 10px 0 10px' }}>
  599. <NButton
  600. type="warning"
  601. style={{ width: '170px' }}
  602. onClick={() => {
  603. pointList.questionList.push(createUnitItem())
  604. }}
  605. >
  606. + 添加考点
  607. </NButton>
  608. <NButton
  609. type="success"
  610. onClick={checkRamdom}
  611. disabled={query.isLock ? true : false}
  612. >
  613. {modalData.selectList.length
  614. ? '根据考点重新生成题目'
  615. : '根据考点生成测验题目'}
  616. </NButton>
  617. <NButton
  618. type="success"
  619. onClick={() => (modalData.open = true)}
  620. disabled={query.isLock ? true : false}
  621. >
  622. 手动选择题目添加
  623. </NButton>
  624. </NSpace>
  625. </div>
  626. </NFormItem>
  627. </NForm>
  628. <NSpace vertical wrapItem={false}>
  629. <NSpace wrapItem={false} justify="space-between">
  630. <NSpace>
  631. <div style={{ color: 'red' }}>*&nbsp;</div>已选题目列表
  632. <div>
  633. 共 <span style={{ color: 'red' }}>{modalData.selectList.length}</span> 道题目
  634. </div>
  635. <div>
  636. 总分: <span style={{ color: 'red' }}>{totalScore.value}</span> 分
  637. </div>
  638. </NSpace>
  639. <NButton type="error" onClick={handleDeleteQuestion}>
  640. 批量删除
  641. </NButton>
  642. </NSpace>
  643. <NDataTable
  644. columns={columns()}
  645. data={modalData.selectList}
  646. row-key={(row: any) => row.id}
  647. onUpdateCheckedRowKeys={(list) => (modalData.checkList = list)}
  648. ></NDataTable>
  649. </NSpace>
  650. <NSpace style={{ paddingTop: '10px' }}>
  651. {modalData.selectList.length ? (
  652. <NButton onClick={() => (modalData.previewOpen = true)}>预览题目</NButton>
  653. ) : null}
  654. <NButton type="primary" onClick={submit} disabled={query.isLock ? true : false}>
  655. 保存测验
  656. </NButton>
  657. </NSpace>
  658. </NSpin>
  659. </div>
  660. <NModal
  661. preset="dialog"
  662. showIcon={false}
  663. title="选择题目"
  664. v-model:show={modalData.open}
  665. style={{ width: '80vw' }}
  666. >
  667. <div>
  668. <AddQuestionList
  669. selectList={modalData.selectList}
  670. onSelect={(list: any) => {
  671. // console.log(row)
  672. if (list) {
  673. modalData.selectList = modalData.selectList.concat(list)
  674. }
  675. modalData.open = false
  676. }}
  677. />
  678. </div>
  679. </NModal>
  680. <NDrawer v-model:show={modalData.previewOpen} width="80vw">
  681. <NDrawerContent title="预览" closable>
  682. <PreviewUnit list={modalData.selectList} />
  683. </NDrawerContent>
  684. </NDrawer>
  685. </div>
  686. )
  687. }
  688. })