index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <template>
  2. <el-row :gutter="20">
  3. <!-- 左侧课程类型树 -->
  4. <el-col :span="5" :xs="24">
  5. <ContentWrap class="h-1/1" style="overflow-x: auto; overflow-y: auto; height: calc(100vh - 100px);" >
  6. <!-- 课程类型切换 -->
  7. <div class="mb-20px">
  8. <el-form-item label="课程类型" class="!mb-0">
  9. <el-select
  10. v-model="queryParams.ctTypeNode"
  11. placeholder="请选择课程类型"
  12. class="!w-full"
  13. @change="handleCtTypeNodeChange"
  14. >
  15. <el-option v-for="courseType in courseTypeList"
  16. :key="courseType.id"
  17. :label="courseType.name"
  18. :value="courseType.id"
  19. />
  20. </el-select>
  21. </el-form-item>
  22. </div>
  23. <!-- 搜索框 -->
  24. <div class="mb-20px">
  25. <el-input
  26. v-model="treeSearchValue"
  27. placeholder="搜索课程类型"
  28. clearable
  29. @input="handleTreeSearch"
  30. class="!w-full"
  31. prefix-icon="ep:search"
  32. />
  33. </div>
  34. <!-- 课程类型树形选择器 -->
  35. <el-tree
  36. v-model="queryParams.courseType"
  37. :data="filteredCourseTypeTree"
  38. :props="{
  39. label: (node) => `${node.ctTypeNode === '0' || node.ctTypeNode === undefined ? node.ctType : node.ctTypeSort + '、' + node.ctType}`,
  40. children: 'children',
  41. value: 'id'
  42. }"
  43. :default-expand-all="true"
  44. node-key="id"
  45. highlight-current
  46. @node-click="handleCourseTypeClick"
  47. />
  48. </ContentWrap>
  49. </el-col>
  50. <el-col :span="19" :xs="24">
  51. <!-- 搜索工作栏 -->
  52. <ContentWrap>
  53. <el-form
  54. class="-mb-15px"
  55. :model="queryParams"
  56. ref="queryFormRef"
  57. :inline="true"
  58. label-width="90px"
  59. >
  60. <el-form-item label="内容类型" prop="courseContentType">
  61. <el-select
  62. v-model="queryParams.courseContentType"
  63. placeholder="请选择内容类型"
  64. clearable
  65. class="!w-240px"
  66. @change="handleQuery"
  67. >
  68. <el-option
  69. v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_COUTNET_TYPE)"
  70. :key="dict.value"
  71. :label="dict.label"
  72. :value="dict.value"
  73. />
  74. </el-select>
  75. </el-form-item>
  76. <el-form-item label="课程名称" prop="courseName">
  77. <el-input
  78. v-model="queryParams.courseName"
  79. placeholder="请输入课程名称"
  80. clearable
  81. @keyup.enter="handleQuery"
  82. class="!w-240px"
  83. />
  84. </el-form-item>
  85. <el-form-item label="是否有检查" prop="courseIsInspect">
  86. <el-select
  87. v-model="queryParams.courseIsInspect"
  88. placeholder="请选择是否有检查"
  89. clearable
  90. class="!w-240px"
  91. @change="handleQuery"
  92. >
  93. <el-option
  94. v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  95. :key="dict.value"
  96. :label="dict.label"
  97. :value="dict.value"
  98. />
  99. </el-select>
  100. </el-form-item>
  101. <el-form-item label="课程标签" prop="courseLabel">
  102. <el-select
  103. v-model="queryParams.courseLabel"
  104. placeholder="请选择课程标签"
  105. clearable
  106. class="!w-240px"
  107. @change="handleQuery"
  108. >
  109. <el-option
  110. v-for="dict in getStrDictOptions(DICT_TYPE.COURSE_LABEL)"
  111. :key="dict.value"
  112. :label="dict.label"
  113. :value="dict.value"
  114. />
  115. </el-select>
  116. </el-form-item>
  117. <el-form-item label="课程状态" prop="courseStatus">
  118. <el-select
  119. v-model="queryParams.courseStatus"
  120. placeholder="请选择课程状态"
  121. clearable
  122. class="!w-240px"
  123. @change="handleQuery"
  124. >
  125. <el-option
  126. v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)"
  127. :key="dict.value"
  128. :label="dict.label"
  129. :value="dict.value"
  130. />
  131. </el-select>
  132. </el-form-item>
  133. <el-form-item>
  134. <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
  135. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  136. <el-button
  137. type="primary"
  138. plain
  139. @click="openForm('create')"
  140. v-hasPermi="['bjdx:course:create']"
  141. >
  142. <Icon icon="ep:plus" class="mr-5px" /> 新增
  143. </el-button>
  144. <el-button
  145. type="success"
  146. plain
  147. @click="handleExport"
  148. :loading="exportLoading"
  149. v-hasPermi="['bjdx:course:export']"
  150. >
  151. <Icon icon="ep:download" class="mr-5px" /> 导出
  152. </el-button>
  153. </el-form-item>
  154. </el-form>
  155. </ContentWrap>
  156. <!-- 列表 -->
  157. <ContentWrap>
  158. <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
  159. <el-table-column label="课程类型" align="center" prop="courseTypeName" />
  160. <el-table-column label="课程名称" align="center" prop="courseName" />
  161. <el-table-column label="内容类型" align="center" prop="courseContentType" >
  162. <template #default="scope">
  163. <dict-tag :type="DICT_TYPE.COURSE_COUTNET_TYPE" :value="scope.row.courseContentType" />
  164. </template>
  165. </el-table-column>
  166. <el-table-column label="课程内容" align="center" prop="courseContent" width="120px">
  167. 【预览查看】
  168. </el-table-column>
  169. <el-table-column label="是否有检查" align="center" prop="courseIsInspect">
  170. <template #default="scope">
  171. <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.courseIsInspect" />
  172. </template>
  173. </el-table-column>
  174. <el-table-column label="课程标签" align="center" prop="courseLabel">
  175. <template #default="scope">
  176. <dict-tag :type="DICT_TYPE.COURSE_LABEL" :value="scope.row.courseLabel" />
  177. </template>
  178. </el-table-column>
  179. <el-table-column label="课程排序" align="center" prop="courseOrder"/>
  180. <el-table-column label="课程状态" align="center" prop="courseStatus">
  181. <template #default="scope">
  182. <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.courseStatus" />
  183. </template>
  184. </el-table-column>
  185. <el-table-column label="操作" align="center" min-width="180px">
  186. <template #default="scope">
  187. <el-button
  188. link
  189. type="primary"
  190. v-if="scope.row.tenantId == getTenantId()"
  191. @click="openForm('update', scope.row.id)"
  192. v-hasPermi="['bjdx:course:update']"
  193. >
  194. 编辑
  195. </el-button>
  196. <el-button
  197. link
  198. type="success"
  199. :disabled="scope.row.courseContentType !== 'video'"
  200. title="仅视频课程可配置"
  201. @click="handleConfig(scope.row.id,scope.row.courseName,scope.row.tenantId)"
  202. >
  203. 配置
  204. </el-button>
  205. <el-button
  206. link
  207. type="danger"
  208. v-if="scope.row.tenantId == getTenantId()"
  209. @click="handleDelete(scope.row.id)"
  210. v-hasPermi="['bjdx:course:delete']"
  211. >
  212. 删除
  213. </el-button>
  214. </template>
  215. </el-table-column>
  216. </el-table>
  217. <!-- 分页 -->
  218. <Pagination
  219. :total="total"
  220. v-model:page="queryParams.pageNo"
  221. v-model:limit="queryParams.pageSize"
  222. @pagination="getList"
  223. />
  224. </ContentWrap>
  225. </el-col>
  226. </el-row>
  227. <!-- 表单弹窗:添加/修改 -->
  228. <CourseForm ref="formRef" @success="getList" />
  229. <!-- 新增预览弹窗 -->
  230. <CoursePreview ref="previewRef" @success="getList" />
  231. </template>
  232. <script setup lang="ts">
  233. import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
  234. import download from '@/utils/download'
  235. import { CourseApi, CourseVO } from '@/api/bjdx/course'
  236. import CourseForm from './CourseForm.vue'
  237. import CoursePreview from '@/views/bjdx/course/CoursePreview.vue'
  238. import { useRouter } from 'vue-router'
  239. import { handleTree } from '@/utils/tree'
  240. import { CourseTypeApi } from '@/api/bjdx/coursetype'
  241. import { getTenantId } from '@/utils/auth'
  242. /** 课程 列表 */
  243. defineOptions({ name: 'Course' })
  244. const message = useMessage() // 消息弹窗
  245. const { t } = useI18n() // 国际化
  246. const loading = ref(true) // 列表的加载中
  247. const list = ref<CourseVO[]>([]) // 列表的数据
  248. const total = ref(0) // 列表的总页数
  249. const queryParams = reactive({
  250. pageNo: 1,
  251. pageSize: 10,
  252. courseName: undefined,
  253. courseContentType: undefined,
  254. courseImagePath: undefined,
  255. courseVideoPath: undefined,
  256. courseContent: undefined,
  257. courseAuthor: undefined,
  258. courseTeacher: undefined,
  259. courseSize: undefined,
  260. courseTime: undefined,
  261. courseIsInspect: undefined,
  262. ctTypeNode: "1",//课程类型
  263. courseType: undefined,
  264. courseTypeName: undefined,
  265. courseLabel: undefined,
  266. courseOrder: undefined,
  267. courseStatus: undefined
  268. })
  269. const queryFormRef = ref() // 搜索的表单
  270. const exportLoading = ref(false) // 导出的加载中
  271. // 新增预览相关变量
  272. const previewRef = ref()
  273. const courseTypeTree = ref() // 树形结构
  274. const treeSearchValue = ref('') // 树形搜索值
  275. // 从字典获取课程类型列表
  276. const courseTypeList = computed(() => {
  277. return getStrDictOptions(DICT_TYPE.COURSE_TYPE).map(dict => ({
  278. id: dict.value,
  279. name: dict.label
  280. }))
  281. })
  282. /** 打开预览弹窗 */
  283. const openPreview = (row) => {
  284. // 使用$nextTick确保组件挂载后调用
  285. previewRef.value?.open(row)
  286. }
  287. /** 查询列表 */
  288. const getList = async () => {
  289. loading.value = true
  290. try {
  291. const data = await CourseApi.getCoursePage(queryParams)
  292. list.value = data.list
  293. total.value = data.total
  294. } finally {
  295. loading.value = false
  296. }
  297. }
  298. /** 搜索按钮操作 */
  299. const handleQuery = async () => {
  300. queryParams.pageNo = 1
  301. getList()
  302. }
  303. /** 处理课程类型节点变更 */
  304. const handleCtTypeNodeChange = (ctTypeNode) => {
  305. queryParams.courseType = undefined
  306. treeSearchValue.value = '' // 清空搜索框
  307. getCourseTypeTree(ctTypeNode).then(() => {
  308. // 选择课程类型后自动搜索
  309. handleQuery()
  310. })
  311. }
  312. /** 处理课程类型节点点击 */
  313. const handleCourseTypeClick = (data) => {
  314. queryParams.courseType = data.id
  315. handleQuery()
  316. }
  317. /** 处理树形搜索 */
  318. const handleTreeSearch = () => {
  319. // 搜索时自动清除当前选中的课程类型
  320. queryParams.courseType = undefined
  321. }
  322. /** 过滤树形数据 */
  323. const filteredCourseTypeTree = computed(() => {
  324. if (!treeSearchValue.value) {
  325. return courseTypeTree.value
  326. }
  327. const filterTree = (nodes, keyword) => {
  328. return nodes.map(node => {
  329. const newNode = { ...node }
  330. // 检查节点是否匹配搜索关键词
  331. const isMatch = newNode.ctType && newNode.ctType.toLowerCase().includes(keyword.toLowerCase())
  332. // 如果有子节点,递归过滤
  333. if (newNode.children && newNode.children.length > 0) {
  334. const filteredChildren = filterTree(newNode.children, keyword)
  335. // 如果节点匹配或有匹配的子节点,则显示该节点
  336. if (isMatch || filteredChildren.length > 0) {
  337. newNode.children = filteredChildren
  338. return newNode
  339. }
  340. return null
  341. }
  342. // 叶子节点如果匹配则显示
  343. return isMatch ? newNode : null
  344. }).filter(Boolean)
  345. }
  346. return filterTree(courseTypeTree.value, treeSearchValue.value)
  347. })
  348. /** 重置按钮操作 */
  349. const resetQuery = () => {
  350. queryFormRef.value.resetFields()
  351. queryParams.ctTypeNode = "1"
  352. queryParams.courseType = undefined
  353. treeSearchValue.value = '' // 清空搜索框
  354. getCourseTypeTree().then(() => {
  355. handleQuery()
  356. })
  357. }
  358. /** 添加/修改操作 */
  359. const formRef = ref()
  360. const openForm = (type: string, id?: number) => {
  361. formRef.value.open(type, queryParams.ctTypeNode, id)
  362. }
  363. /** 删除按钮操作 */
  364. const handleDelete = async (id: number) => {
  365. try {
  366. // 删除的二次确认
  367. await message.delConfirm()
  368. // 发起删除
  369. await CourseApi.deleteCourse(id)
  370. message.success(t('common.delSuccess'))
  371. // 刷新列表
  372. await getList()
  373. } catch {}
  374. }
  375. /** 配置按钮操作 */
  376. const router = useRouter()
  377. const handleConfig = (id: number,courseName: string,tenantId: number) => {
  378. router.push({
  379. name: 'courseConfig',
  380. query: { courseId: id , courseName: courseName,tenantId: tenantId}
  381. })
  382. }
  383. /** 导出按钮操作 */
  384. const handleExport = async () => {
  385. try {
  386. // 导出的二次确认
  387. await message.exportConfirm()
  388. // 发起导出
  389. exportLoading.value = true
  390. const data = await CourseApi.exportCourse(queryParams)
  391. download.excel(data, '课程.xls')
  392. } catch {
  393. } finally {
  394. exportLoading.value = false
  395. }
  396. }
  397. /** 获得课程-类型树 */
  398. const getCourseTypeTree = async (filterCtTypeNode = "1") => {
  399. courseTypeTree.value = []
  400. const data = await CourseTypeApi.getCourseTypeSimpleList()
  401. let filteredData = data
  402. if (filterCtTypeNode !== undefined) {
  403. filteredData = data.filter(item => item.ctTypeNode === '0' || item.ctTypeNode === filterCtTypeNode)
  404. }
  405. const courseTypeItem = courseTypeList.value.find(item => item.id === filterCtTypeNode)
  406. const root = { id: 0, ctType: '课程类型(' + courseTypeItem.name + ')', children: [] }
  407. root.children = handleTree(filteredData, 'id', 'ctParentId')
  408. courseTypeTree.value = [root]
  409. }
  410. /** 初始化 **/
  411. onMounted(() => {
  412. getList()
  413. getCourseTypeTree()
  414. })
  415. </script>
  416. <style scoped>
  417. /* 优化滚动条样式 */
  418. :deep(.custom-scrollbar) {
  419. scrollbar-width: thin;
  420. scrollbar-color: #c0c4cc #f0f2f5;
  421. }
  422. /* 兼容WebKit浏览器(Chrome、Safari等) */
  423. :deep(.custom-scrollbar::-webkit-scrollbar) {
  424. width: 3px;
  425. height: 3px;
  426. }
  427. :deep(.custom-scrollbar::-webkit-scrollbar-track) {
  428. background: #f0f2f5;
  429. border-radius: 3px;
  430. }
  431. :deep(.custom-scrollbar::-webkit-scrollbar-thumb) {
  432. background: #c0c4cc;
  433. border-radius: 3px;
  434. transition: background 0.3s;
  435. }
  436. :deep(.custom-scrollbar::-webkit-scrollbar-thumb:hover) {
  437. background: #909399;
  438. }
  439. </style>