Просмотр исходного кода

feat(课程管理): 新增课程详情页面并优化课程列表样式

- 添加课程详情页面组件,支持视频播放和详情展示
- 重构课程列表卡片样式,增加下拉菜单操作
- 修改Element Plus主题色为蓝色
- 为视频播放组件添加destroy方法暴露
zhanghaijun месяцев назад: 11
Родитель
Сommit
f44f62652d

+ 8 - 0
src/components/core/media/art-video-player/index.vue

@@ -108,4 +108,12 @@
       playerInstance.value.destroy()
     }
   })
+
+  defineExpose({
+    destroy: () => {
+      if (playerInstance.value) {
+        playerInstance.value.destroy()
+      }
+    }
+  })
 </script>

+ 1 - 1
src/config/index.ts

@@ -15,7 +15,7 @@ const appConfig: SystemConfig = {
   },
   // Element Plus 主题
   elementPlusTheme: {
-    primary: '#B48DF3'
+    primary: '#5D87FF'
   },
   // 系统主题
   systemThemeStyles: {

+ 66 - 0
src/views/train/course/course-detail.vue

@@ -0,0 +1,66 @@
+<template>
+  <Dialog>
+    <DetailForm :model="course">
+      <!-- <slot v-if="$slots[item.prop]" :name="item.prop" :form-model="formModel" :model="formModel" :field="item" /> -->
+      <template #guideFilePath="{ model }">
+        <div v-if="model.guideFilePath.endsWith('.mp4')" class="border-art-border-dashed-color w-[80%] border border-solid">
+          <ArtVideoPlayer playerId="my-video-1" :videoUrl="model.guideFilePath" :posterUrl="posterUrl" :autoplay="false" :volume="1" :playbackRates="[0.5, 1, 1.5, 2]" />
+        </div>
+        <div v-else-if="!model.guideFilePath"> 暂无指导文件 </div>
+        <div v-else> {{ model.guideFilePath }} </div>
+      </template>
+    </DetailForm>
+  </Dialog>
+</template>
+<script setup lang="tsx">
+  import { Course } from '@/api/course.api'
+  import { useDialog } from '@/components/dialog'
+  import { useDetailForm } from '@/components/form'
+
+  import lockImg from '@imgs/lock/lock_screen_1.webp'
+
+  // 视频源和封面图片URL
+  const posterUrl = ref(lockImg)
+
+  const course = ref<Course>()
+  const [Dialog, dialogApi] = useDialog({
+    title: `课程详情`,
+    width: '60%',
+    dialogType: 'drawer',
+    modal: true,
+    closeOnClickModal: true,
+    footer: false,
+    destroyOnClose: true
+  })
+  const [DetailForm] = useDetailForm({
+    labelWidth: '120px',
+    schema: [
+      { prop: 'courseID', label: '课程ID', component: 'Input', colSpan: 12 },
+      { prop: 'courseGUID', label: '课程GUID', component: 'Input', colSpan: 12 },
+      { prop: 'courseName', label: '课程名称', component: 'Input', colSpan: 12 },
+      { prop: 'courseDesc', label: '课程描述', component: 'Input', colSpan: 12 },
+      { prop: 'totalDuration', label: '课程总时长', component: 'Input', colSpan: 8 },
+      { prop: 'trainPlan', label: '培训计划', component: 'Input', colSpan: 8 },
+      { prop: 'typeID', label: '分类ID', component: 'Input', colSpan: 8 },
+      { prop: 'externalTypeID', label: '外部分类ID', component: 'Input', colSpan: 8 },
+      { prop: 'lineNo', label: '线路号', component: 'Input', colSpan: 8 },
+      { prop: 'runningMap', label: '运行图', component: 'Input', colSpan: 8 },
+      { prop: 'creator', label: '创建人', component: 'Input', colSpan: 8 },
+      { prop: 'createTime', label: '创建时间', component: 'Input', colSpan: 8 },
+      { prop: 'modifyTime', label: '修改时间', component: 'Input', colSpan: 8 },
+      { prop: 'guide-box', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '指导信息' } },
+      { prop: 'guideDesc', label: '指导描述', component: 'Input', componentProps: { type: 'textarea', rows: 2 }, colSpan: 24 },
+      { prop: 'guideFilePath', label: '指导文件地址', component: 'Input', colSpan: 24 }
+    ]
+  })
+  defineExpose({
+    show(item: Course) {
+      course.value = item
+      dialogApi.setState({
+        title: `课程详情【 ${item.courseName} 】`
+      })
+      dialogApi.open()
+    }
+  })
+</script>
+<style lang="scss" scoped></style>

+ 55 - 46
src/views/train/course/course-list.vue

@@ -40,13 +40,29 @@
             <div class="type-tag">{{ useDateFormat(item.createTime, 'YYYY-MM-DD') }}</div>
           </div>
         </div>
-        <div class="card-bottom space-y-2">
-          <h2>{{ item.courseName }}</h2>
+        <div class="course-foot">
+          <div class="title">{{ item.courseName }}</div>
           <div class="desc">{{ item.courseDesc || '暂无描述' }}</div>
-          <div>
-            <el-button size="small" type="primary" :icon="Edit" @click.stop="handleChange('edit', item)">编辑</el-button>
-            <el-button size="small" type="danger" :icon="Delete" @click.stop="handleChange('delete', item)">删除</el-button>
-            <el-button size="small" type="info" :icon="CopyDocument" @click.stop="handleChange('copy', item)">复制</el-button>
+          <div class="flex h-[32px] flex-row items-center justify-between">
+            <div class="action-desc flex h-full flex-row items-center gap-1">
+              <i class="iconfont-sys">&#xe6f7;</i>
+              <span>{{ useDateFormat(item.createTime, 'YYYY-MM-DD') }}</span>
+            </div>
+            <div class="course-action">
+              <el-dropdown placement="bottom-end" trigger="hover">
+                <div class="action-btn mt-[2px] flex h-[28px] w-[38px] items-center justify-center rounded-md">
+                  <el-icon><MoreFilled /></el-icon>
+                </div>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item :icon="Tickets" @click="handleChange('detail', item)">详情 </el-dropdown-item>
+                    <el-dropdown-item :icon="Edit" @click="handleChange('edit', item)">编辑 </el-dropdown-item>
+                    <el-dropdown-item :icon="Delete" @click="handleChange('delete', item)">删除 </el-dropdown-item>
+                    <el-dropdown-item :icon="CopyDocument" @click="handleChange('copy', item)">复制 </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
           </div>
         </div>
       </div>
@@ -55,10 +71,12 @@
   <div style="margin-top: 16vh" v-if="data.length === 0 && !props.loading">
     <ElEmpty :description="`暂无课程数据`" />
   </div>
+  <courseDetail ref="courseDetailRef" />
 </template>
 <script lang="ts" setup>
-  import { Picture as IconPicture, Delete, CopyDocument, Edit } from '@element-plus/icons-vue'
+  import { Picture as IconPicture, Delete, CopyDocument, Edit, Tickets, MoreFilled } from '@element-plus/icons-vue'
   import { useDateFormat } from '@vueuse/core'
+  import courseDetail from './course-detail.vue'
   import cover1 from '@imgs/cover/img5.webp'
   import cover2 from '@imgs/cover/img2.webp'
   import cover3 from '@imgs/cover/img3.webp'
@@ -92,20 +110,32 @@
     emit('update:checkedList', newChecked)
   }
 
+  // 课程详情
+  const courseDetailRef = ref()
+
   const handleChange = (type: string, item: Course) => {
     switch (type) {
+      case 'detail':
+        courseDetailRef.value.show(item)
+        break
       case 'delete':
-        ElMessageBox.confirm('确定删除课程吗?', '提示', {
+        ElMessageBox.confirm(`确定删除课程【${item.courseName}】吗?`, '提示', {
           confirmButtonText: '确定',
           cancelButtonText: '取消',
           type: 'warning'
         }).then(() => emit('handle', 'delete', item))
         break
+      case 'copy':
+        ElMessageBox.confirm(`确定复制课程【${item.courseName}】吗?`, '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'info'
+        }).then(() => emit('handle', 'copy', item))
+        break
       default:
         emit('handle', type, item)
         break
     }
-    return true
   }
 </script>
 <style lang="scss" scoped>
@@ -145,10 +175,8 @@
       }
     }
   }
-  .card-bottom {
-    padding: 5px 10px;
-
-    h2 {
+  .course-foot {
+    .title {
       font-size: 16px;
       font-weight: 500;
       color: #333;
@@ -156,47 +184,28 @@
       @include ellipsis();
     }
     .desc {
-      font-size: 12px;
+      font-size: 14px;
       font-weight: 500;
       color: var(--art-text-gray-600);
 
       @include ellipsis();
     }
+    .action-desc {
+      font-size: 14px;
+      color: var(--art-text-gray-600);
+    }
+    .action-btn {
+      cursor: pointer;
+      transition: background-color 0.2s ease;
+      border: 1px solid var(--art-border-dashed-color);
 
-    .info {
-      display: flex;
-      justify-content: space-between;
-      width: 100%;
-      height: 25px;
-      margin-top: 6px;
-      line-height: 25px;
-
-      .text {
-        display: flex;
-        align-items: center;
-        color: var(--art-text-gray-600);
-
-        i {
-          margin-right: 5px;
-          font-size: 14px;
-        }
-
-        span {
-          font-size: 13px;
-          color: var(--art-gray-600);
-        }
-
-        .line {
-          width: 1px;
-          height: 12px;
-          margin: 0 15px;
-          background-color: var(--art-border-dashed-color);
-        }
+      &:hover {
+        background-color: var(--art-gray-200);
       }
 
-      .el-button {
-        opacity: 1;
-        transition: all 0.3s;
+      i {
+        // font-size: 20px;
+        color: var(--art-text-gray-700);
       }
     }
   }