courseConfiguration.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. import {
  2. NAlert,
  3. NButton,
  4. NCard,
  5. NDialog,
  6. NDrawer,
  7. NDrawerContent,
  8. NDropdown,
  9. NEmpty,
  10. NForm,
  11. NFormItem,
  12. NGi,
  13. NGrid,
  14. NIcon,
  15. NInput,
  16. NModal,
  17. NSelect,
  18. NSpace,
  19. NSpin,
  20. NTooltip,
  21. NUpload,
  22. UploadCustomRequestOptions,
  23. useDialog,
  24. useMessage
  25. } from 'naive-ui'
  26. import { defineComponent, onMounted, reactive, ref } from 'vue'
  27. import { useRoute } from 'vue-router'
  28. import {
  29. lessonCoursewareDetailPage,
  30. lessonCoursewareDetailSave,
  31. lessonCoursewareDetailUpdate,
  32. lessonTrainingPage,
  33. lessonCoursewareDetailRemove,
  34. lessonCoursewareExaminationMapperQueryAll,
  35. lessonCoursewareDetailLock,
  36. api_openFileImportInfoSave,
  37. api_removeTraining
  38. } from '../../api'
  39. import styles from '../index.module.less'
  40. import { EditFilled, DeleteFilled, LockFilled, UnlockFilled } from '@vicons/antd'
  41. import CourseKnowledgePoint from '../model/CourseKnowledgePoint'
  42. import SelectAfterClassTraining from '../model/selectAfterClassTraining'
  43. import AddUnitTest from '../model/AddUnitTest'
  44. import TheLink from '@/components/TheLink'
  45. import { api_uploadFile } from '@/plugins/uploadFile'
  46. import Button from 'naive-ui/es/button/src/Button'
  47. export default defineComponent({
  48. name: 'courseConfiguration',
  49. props: {
  50. course: {
  51. type: Object,
  52. default: () => {}
  53. }
  54. },
  55. setup(props, ctx) {
  56. const message = useMessage()
  57. const dialog = useDialog()
  58. const route = useRoute()
  59. const addFormRef = ref()
  60. const addForm = reactive({
  61. open: false,
  62. id: '',
  63. name: '', //课时名称
  64. lessonTargetDesc: '', //课时名称
  65. lessonTrainingId: '', //课后作业
  66. lockEnable: 'true',
  67. accessScope: 0
  68. })
  69. // 课件添加课程
  70. const addCourseware = () => {
  71. addFormRef.value.validate(async (err: any) => {
  72. if (!err) {
  73. const body = {
  74. lessonCoursewareId: route.query.id,
  75. name: addForm.name,
  76. lessonTargetDesc: addForm.lessonTargetDesc,
  77. lockEnable: addForm.lockEnable,
  78. accessScope: addForm.accessScope
  79. }
  80. let res: any
  81. if (addForm.id) {
  82. try {
  83. res = await lessonCoursewareDetailUpdate({
  84. ...body,
  85. id: addForm.id
  86. })
  87. } catch (error) {}
  88. } else {
  89. try {
  90. res = await lessonCoursewareDetailSave(body)
  91. } catch (error) {}
  92. }
  93. if (res?.code == 200) {
  94. message.success('保存成功')
  95. addForm.open = false
  96. getDetail()
  97. }
  98. }
  99. })
  100. }
  101. const state = reactive({
  102. dataList: [] as any,
  103. mapperList: [] as any
  104. })
  105. const getDetail = async () => {
  106. try {
  107. const { data } = await lessonCoursewareDetailPage({
  108. lessonCoursewareId: route.query.id,
  109. page: 1,
  110. rows: 1000
  111. })
  112. if (Array.isArray(data?.rows)) {
  113. state.dataList = data.rows
  114. }
  115. } catch (error) {}
  116. }
  117. /** 查询课件下的所有阶段自测 */
  118. const getMapperAll = async () => {
  119. try {
  120. const { data } = await lessonCoursewareExaminationMapperQueryAll(route.query.id)
  121. if (Array.isArray(data)) {
  122. state.mapperList = data
  123. }
  124. } catch (error) {}
  125. }
  126. onMounted(() => {
  127. getDetail()
  128. getMapperAll()
  129. })
  130. //删除课时
  131. const hanldeDelete = (item: any) => {
  132. const d = dialog.warning({
  133. title: '警告',
  134. content: '是否确认删除此课时?',
  135. positiveText: '确定',
  136. negativeText: '取消',
  137. onPositiveClick: async () => {
  138. d.loading = true
  139. try {
  140. const res: any = await lessonCoursewareDetailRemove(item.id)
  141. if (res?.code == 200) {
  142. message.success('删除成功')
  143. getDetail()
  144. }
  145. } catch (error) {}
  146. d.loading = false
  147. }
  148. })
  149. }
  150. //绑定课后作业
  151. const afterTrain = reactive({
  152. open: false,
  153. id: ''
  154. })
  155. const addAfterTrain = async (row: any) => {
  156. console.log('🚀 ~ row', row)
  157. const body = {
  158. id: addForm.id,
  159. lessonTrainingId: row.id,
  160. name: addForm.name,
  161. lessonTargetDesc: addForm.lessonTargetDesc
  162. }
  163. try {
  164. const res: any = await lessonCoursewareDetailUpdate(body)
  165. if (res?.code == 200) {
  166. message.success('保存成功')
  167. afterTrain.open = false
  168. getDetail()
  169. }
  170. } catch (error) {}
  171. }
  172. // 课程配置
  173. const courseData = reactive({
  174. open: false,
  175. title: '',
  176. item: null
  177. })
  178. //阶段自测
  179. const unitData = reactive({
  180. open: false,
  181. model: {}
  182. })
  183. /** 切换课件是否锁定 */
  184. const toggleLock = (item: any) => {
  185. const d = dialog.warning({
  186. title: '警告',
  187. content: `是否确认${item.lockFlag ? '解锁' : '锁定'}此课时?`,
  188. positiveText: '确定',
  189. negativeText: '取消',
  190. onPositiveClick: async () => {
  191. d.loading = true
  192. try {
  193. const res = await lessonCoursewareDetailLock({ id: item.id, lockFlag: !item.lockFlag })
  194. message.success('操作成功')
  195. getDetail()
  196. } catch (error) {}
  197. d.loading = false
  198. }
  199. })
  200. }
  201. /** 删除选择的作业 */
  202. const hanldeRemoveTraning = (item: any) => {
  203. const d = dialog.warning({
  204. title: '警告',
  205. content: `是否确认删除作业?`,
  206. positiveText: '确定',
  207. negativeText: '取消',
  208. onPositiveClick: async () => {
  209. d.loading = true
  210. try {
  211. const res = await api_removeTraining(item.id)
  212. message.success('删除成功')
  213. getDetail()
  214. } catch (error) {}
  215. d.loading = false
  216. }
  217. })
  218. }
  219. const customRequest_importData = reactive({
  220. loading: false,
  221. dataType: 'EXAMINATION',
  222. importRef: null as any
  223. })
  224. const customRequest_importFile = async (data: UploadCustomRequestOptions) => {
  225. console.log(data.file)
  226. const msg = message.loading('正在上传文件', { duration: 0 })
  227. customRequest_importData.loading = true
  228. try {
  229. const fileUrl = await api_uploadFile(data.file.file, () => {})
  230. const res = await api_openFileImportInfoSave({
  231. dataType: customRequest_importData.dataType,
  232. fileName: data.file.name,
  233. importUrl: fileUrl,
  234. lessonId: route.query.id
  235. })
  236. console.log('🚀 ~ res:', res)
  237. customRequest_importData.loading = false
  238. if (res.data) {
  239. msg.destroy()
  240. // 空表格
  241. if (res.data.insertRow === 0 && res.data.invalidRow === 0) {
  242. message.error('导入失败,表格为空')
  243. return
  244. }
  245. if (res.data.respUrl) {
  246. dialog.error({
  247. title: '信息',
  248. content: () => (
  249. <NSpace>
  250. <div>导入失败,点击下载错误信息</div>
  251. <a href={res.data.respUrl} download>
  252. 下载
  253. </a>
  254. </NSpace>
  255. )
  256. })
  257. return
  258. }
  259. getDetail()
  260. message.success('导入成功')
  261. } else {
  262. message.error('请下载模板后,填写数据再导入')
  263. }
  264. } catch {}
  265. customRequest_importData.loading = false
  266. msg.destroy()
  267. }
  268. return () => (
  269. <div class={styles.courseConfiguration}>
  270. <NSpin show={customRequest_importData.loading}>
  271. <NSpace justify="space-between">
  272. <NSpace>
  273. <NButton
  274. disabled={route.query.isLook ? true : false}
  275. type="primary"
  276. onClick={() => {
  277. addForm.name = ''
  278. addForm.lessonTargetDesc = ''
  279. addForm.lessonTrainingId = ''
  280. addForm.id = ''
  281. addForm.lockEnable = ''
  282. addForm.accessScope = 0
  283. addForm.open = true
  284. }}
  285. >
  286. 添加课程
  287. </NButton>
  288. <NButton
  289. type="primary"
  290. disabled={route.query.isLook ? true : false}
  291. onClick={() => {
  292. unitData.open = true
  293. unitData.model = {}
  294. }}
  295. >
  296. 添加阶段自测
  297. </NButton>
  298. </NSpace>
  299. <NSpace style={{ marginLeft: 'auto' }}>
  300. <NDropdown
  301. size="huge"
  302. trigger="hover"
  303. options={[
  304. {
  305. key: '3',
  306. type: 'render',
  307. render: () => (
  308. <NButton
  309. size="large"
  310. quaternary
  311. tag="a"
  312. //@ts-ignore
  313. href="https://oss.dayaedu.com/daya-docs/%E5%8D%95%E5%85%83%E6%B5%8B%E9%AA%8C%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx"
  314. download
  315. >
  316. 单元测验模板
  317. </NButton>
  318. )
  319. },
  320. {
  321. key: '1',
  322. type: 'render',
  323. render: () => (
  324. <NButton
  325. size="large"
  326. quaternary
  327. tag="a"
  328. //@ts-ignore
  329. href="https://oss.dayaedu.com/daya-docs/%E8%AF%BE%E5%90%8E%E8%AE%AD%E7%BB%83%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx"
  330. download
  331. >
  332. 课后训练模板
  333. </NButton>
  334. )
  335. }
  336. ]}
  337. >
  338. <NButton type="primary" v-auth="downloadUnitQuiz1702251742292344833">
  339. 下载模板
  340. </NButton>
  341. </NDropdown>
  342. <div>
  343. <NUpload
  344. multiple={false}
  345. ref={(el: any) => (customRequest_importData.importRef = el)}
  346. showFileList={false}
  347. accept=".xlsx"
  348. customRequest={customRequest_importFile}
  349. >
  350. <NDropdown
  351. size="huge"
  352. trigger="hover"
  353. options={[
  354. { label: '课件', key: 'COURSEWARE' },
  355. { label: '单元测验', key: 'EXAMINATION' },
  356. { label: '课后训练', key: 'HOMEWORK' },
  357. { label: '素材关联曲目', key: 'COURSEWARE_REF_MATERIAL' }
  358. ]}
  359. onSelect={(key: string) => {
  360. customRequest_importData.dataType = key
  361. console.log(customRequest_importData.importRef)
  362. customRequest_importData.importRef?.clear()
  363. customRequest_importData.importRef?.openOpenFileDialog()
  364. }}
  365. >
  366. <NButton type="primary" onClick={(e: Event) => e.stopPropagation()}>
  367. 导入数据
  368. </NButton>
  369. </NDropdown>
  370. </NUpload>
  371. </div>
  372. </NSpace>
  373. </NSpace>
  374. <NAlert class={styles.title} title="教学内容" />
  375. <NGrid xGap={12} yGap={8} cols={3}>
  376. {state.dataList.map((item: any) => {
  377. const mapper = state.mapperList.find(
  378. (n: any) => n.parentCoursewareDetailId == item.id
  379. )
  380. return (
  381. <>
  382. <NGi>
  383. <NCard
  384. title={item.name}
  385. style={{
  386. '--n-padding-top': '5px',
  387. '--n-padding-bottom': '5px',
  388. height: '100%'
  389. }}
  390. >
  391. {{
  392. default: () => (
  393. <div>
  394. <div style={{ paddingBottom: '12px' }}>{item.lessonTargetDesc}</div>
  395. <table style={{ width: '100%' }}>
  396. <tr>
  397. <td>建议学习时长</td>
  398. <td style={{ 'text-align': 'right' }}>
  399. {item.lessonDurationSecond || '0'}s
  400. </td>
  401. </tr>
  402. <tr>
  403. <td>知识点</td>
  404. <td style={{ 'text-align': 'right' }}>
  405. {item.knowledgePointIds?.split(',').filter(Boolean).length || 0}个
  406. </td>
  407. </tr>
  408. <tr>
  409. <td>课后作业</td>
  410. <td style={{ 'text-align': 'right' }}>
  411. {item.lessonTrainingId ? (
  412. <>
  413. <TheLink
  414. // to={`/#/afterClassTrainingDetail?id=${item.lessonTrainingId}&name=${item.lessonTrainingName}&courseTypeCode=TRUMPET_SINGLE`}
  415. authLink="afterClassTrainingManage1599968711187746818"
  416. to={{
  417. path: '/afterClassTrainingManage',
  418. query: { id: item.lessonTrainingId }
  419. }}
  420. >
  421. {item.lessonTrainingName}
  422. </TheLink>
  423. <NTooltip>
  424. {{
  425. default: () => '删除',
  426. trigger: () => (
  427. <NButton
  428. v-auth="lessonCoursewareDetail/removeTraining1793827126242213890"
  429. quaternary
  430. size="small"
  431. circle
  432. onClick={() => hanldeRemoveTraning(item)}
  433. >
  434. <NIcon component={<DeleteFilled />} />
  435. </NButton>
  436. )
  437. }}
  438. </NTooltip>
  439. </>
  440. ) : (
  441. '无'
  442. )}
  443. </td>
  444. </tr>
  445. </table>
  446. </div>
  447. ),
  448. 'header-extra': () => (
  449. <>
  450. <NTooltip>
  451. {{
  452. default: () => (item.lockFlag ? '解锁' : '锁定'),
  453. trigger: () => (
  454. <NButton
  455. disabled={route.query.isLook ? true : false}
  456. quaternary
  457. circle
  458. onClick={() => toggleLock(item)}
  459. >
  460. <NIcon
  461. component={item.lockFlag ? <LockFilled /> : <UnlockFilled />}
  462. />
  463. </NButton>
  464. )
  465. }}
  466. </NTooltip>
  467. <NTooltip>
  468. {{
  469. default: () => '编辑',
  470. trigger: () => (
  471. <NButton
  472. disabled={route.query.isLook ? true : false}
  473. quaternary
  474. circle
  475. onClick={() => {
  476. addForm.name = item.name
  477. addForm.lessonTargetDesc = item.lessonTargetDesc
  478. addForm.id = item.id
  479. addForm.lockEnable = item.lockEnable + ''
  480. addForm.accessScope = item.accessScope ?? 0
  481. addForm.open = true
  482. }}
  483. >
  484. <NIcon component={<EditFilled />} />
  485. </NButton>
  486. )
  487. }}
  488. </NTooltip>
  489. <NTooltip>
  490. {{
  491. default: () => '删除',
  492. trigger: () => (
  493. <NButton
  494. disabled={route.query.isLook ? true : false}
  495. quaternary
  496. circle
  497. onClick={() => hanldeDelete(item)}
  498. >
  499. <NIcon component={<DeleteFilled />} />
  500. </NButton>
  501. )
  502. }}
  503. </NTooltip>
  504. </>
  505. ),
  506. action: () => (
  507. <NSpace justify="space-around">
  508. <NButton
  509. text
  510. disabled={route.query.isLook ? true : false}
  511. onClick={() => {
  512. courseData.title = item.name
  513. courseData.open = true
  514. courseData.item = item
  515. }}
  516. >
  517. 选择知识点
  518. </NButton>
  519. <NButton
  520. text
  521. disabled={route.query.isLook ? true : false}
  522. onClick={() => {
  523. addForm.id = item.id
  524. addForm.lessonTargetDesc = item.lessonTargetDesc
  525. addForm.name = item.name
  526. afterTrain.open = true
  527. }}
  528. >
  529. 选择课后作业
  530. </NButton>
  531. </NSpace>
  532. )
  533. }}
  534. </NCard>
  535. </NGi>
  536. {mapper && (
  537. <NGi>
  538. <NCard
  539. title={mapper.name}
  540. style={{
  541. '--n-padding-top': '5px',
  542. '--n-padding-bottom': '5px',
  543. '--n-color': 'rgba(238,243,254,1)',
  544. '--n-action-color': 'rgba(238,243,254,1)',
  545. height: '100%'
  546. }}
  547. >
  548. {{
  549. default: () => (
  550. <div>
  551. <div style={{ paddingBottom: '12px' }}>{mapper.desc}</div>
  552. <table style={{ width: '100%' }}>
  553. <tr>
  554. <td>单团学生</td>
  555. <td style={{ 'text-align': 'right' }}>
  556. {mapper?.details?.[0]?.unitExaminationName || '无'}
  557. </td>
  558. </tr>
  559. <tr>
  560. <td>双团学生</td>
  561. <td style={{ 'text-align': 'right' }}>
  562. {mapper?.details?.[1]?.unitExaminationName || '无'}
  563. </td>
  564. </tr>
  565. <tr>
  566. <td>多团学生</td>
  567. <td style={{ 'text-align': 'right' }}>
  568. {mapper?.details?.[2]?.unitExaminationName || '无'}
  569. </td>
  570. </tr>
  571. </table>
  572. </div>
  573. ),
  574. action: () => (
  575. <NSpace justify="space-around">
  576. <NButton
  577. text
  578. disabled={route.query.isLook ? true : false}
  579. onClick={() => {
  580. unitData.open = true
  581. unitData.model = mapper
  582. }}
  583. >
  584. 编辑阶段自测
  585. </NButton>
  586. </NSpace>
  587. )
  588. }}
  589. </NCard>
  590. </NGi>
  591. )}
  592. </>
  593. )
  594. })}
  595. </NGrid>
  596. </NSpin>
  597. {state.dataList.length ? null : <NEmpty />}
  598. {/* 课程编辑 */}
  599. <NModal
  600. preset="dialog"
  601. v-model:show={addForm.open}
  602. title={addForm.id ? '编辑课程' : '添加课程'}
  603. showIcon={false}
  604. >
  605. <NForm ref={addFormRef} model={addForm}>
  606. <NFormItem
  607. label="课程名称"
  608. required
  609. path="name"
  610. rule={[{ required: true, message: '请填写课程名称', trigger: ['blur', 'change'] }]}
  611. >
  612. <NInput v-model:value={addForm.name} />
  613. </NFormItem>
  614. <NFormItem
  615. label="是否需要解锁"
  616. required
  617. path="lockEnable"
  618. rule={[{ required: true, message: '请选择是否解锁', trigger: ['blur', 'change'] }]}
  619. >
  620. <NSelect
  621. v-model:value={addForm.lockEnable}
  622. options={
  623. [
  624. {
  625. label: '是',
  626. value: 'true'
  627. },
  628. {
  629. label: '否',
  630. value: 'false'
  631. }
  632. ] as any
  633. }
  634. clearable
  635. ></NSelect>
  636. </NFormItem>
  637. <NFormItem
  638. label="是否免费"
  639. required
  640. path="accessScope"
  641. rule={[
  642. {
  643. type: 'number',
  644. required: true,
  645. message: '设置是否免费',
  646. trigger: ['blur', 'change']
  647. }
  648. ]}
  649. >
  650. <NSelect
  651. v-model:value={addForm.accessScope}
  652. options={
  653. [
  654. {
  655. label: '是',
  656. value: 0
  657. },
  658. {
  659. label: '否',
  660. value: 1
  661. }
  662. ] as any
  663. }
  664. clearable
  665. ></NSelect>
  666. </NFormItem>
  667. <NFormItem
  668. label="教学目标"
  669. required
  670. path="lessonTargetDesc"
  671. rule={[{ required: true, message: '请填写教学目标', trigger: ['blur', 'change'] }]}
  672. >
  673. <NInput
  674. type="textarea"
  675. rows={3}
  676. v-model:value={addForm.lessonTargetDesc}
  677. maxlength={500}
  678. showCount={true}
  679. />
  680. </NFormItem>
  681. <NSpace justify="end">
  682. <NButton onClick={() => (addForm.open = false)}>取消</NButton>
  683. <NButton type="primary" onClick={() => addCourseware()}>
  684. 确认
  685. </NButton>
  686. </NSpace>
  687. </NForm>
  688. </NModal>
  689. {/* 课后作业 */}
  690. <NModal
  691. preset="dialog"
  692. v-model:show={afterTrain.open}
  693. title="选择课后作业"
  694. showIcon={false}
  695. style={{ width: '80vw' }}
  696. >
  697. <SelectAfterClassTraining
  698. courseTypeCode={props.course?.courseTypeCode}
  699. onHandleSuccess={addAfterTrain}
  700. />
  701. </NModal>
  702. {/* 课程详情编辑 */}
  703. <NDrawer v-model:show={courseData.open} width="80vw">
  704. <NDrawerContent title={courseData.title} closable>
  705. {{
  706. default: () => (
  707. <CourseKnowledgePoint
  708. courseTypeCode={props.course?.courseTypeCode}
  709. item={courseData.item as any}
  710. onHandleSuccess={() => {
  711. getDetail()
  712. }}
  713. />
  714. ),
  715. footer: () => (
  716. <NSpace>
  717. <NButton onClick={() => (courseData.open = false)}>取消</NButton>
  718. <NButton type="primary" onClick={() => (courseData.open = false)}>
  719. 确认
  720. </NButton>
  721. </NSpace>
  722. )
  723. }}
  724. </NDrawerContent>
  725. </NDrawer>
  726. {/* 阶段自测 */}
  727. <NModal
  728. preset="dialog"
  729. v-model:show={unitData.open}
  730. title={(unitData.model as any)?.id ? '编辑阶段自测' : '新增阶段自测'}
  731. showIcon={false}
  732. style={{ width: '500px' }}
  733. >
  734. <AddUnitTest
  735. courseTypeCode={props.course?.courseTypeCode}
  736. list={state.dataList}
  737. item={unitData.model}
  738. onClose={(result) => {
  739. if (result) {
  740. getDetail()
  741. getMapperAll()
  742. }
  743. unitData.open = false
  744. }}
  745. />
  746. </NModal>
  747. </div>
  748. )
  749. }
  750. })