瀏覽代碼

feat: 课程编辑

zhanghaijun 1 年之前
父節點
當前提交
290c9d99f0

+ 27 - 19
src/api/course.api.ts

@@ -2,27 +2,35 @@ import request from '@/api'
 
 /** 课程模型 */
 export interface Course {
-  courseID: number /** 课程ID */
-  courseName: string /** 课程名称 */
-  courseDesc: string /** 课程描述 */
-  typeID: number /** 分类ID */
-  totalDuration: number /** 课程总时长 */
-  createTime: string /** 创建时间 */
-  creator: string /** 创建人 */
-  modifyTime: string /** 修改时间 */
-  courseGUID: string /** 课程GUID */
-  externalTypeID: number /** 外部分类ID */
-  guideDesc: string /** 指导描述 */
-  guideFilePath: string /** 指导文件地址 */
-  lineNo: number /** 线路号 */
-  runningMap: number /** 运行图 */
-  virtualStartTime: number /** 虚拟起始时间 */
+  courseID: number /** 课程ID*/
+  courseName: string /** 课程名称*/
+  courseDesc: string /** 课程描述*/
+  totalDuration: number /** 课程总时长*/
+  typeID: number /** 分类ID*/
+  courseDataStream: string /** 课程数据流*/
+  externalTypeID: number /** 外部分类ID*/
+  courseGUID: string /** 课程GUID*/
+  guideDesc: string /** 指导描述*/
+  guideFilePath: string /** 指导文件地址*/
+  lineNo: number /** 线路号*/
+  runningMap: number /** 运行图*/
+  virtualRole: string /** 虚拟角色,使用逗号隔开*/
+  virtualStartTime: number /** 虚拟起始时间*/
+  creator: string /** 创建人*/
+  createTime: string /** 创建时间*/
+  modifyTime: string /** 修改时间*/
 }
 
 export class CourseServer {
   public static list = (data: any) => request.post<Course[]>('/course/list', data)
-  public static detail = (courseID: number) => request.get<Course>('/course/detail', { courseID })
-  public static save = (data: any) => request.post('/course/save', data)
-  public static update = (data: any) => request.post('/course/update', data)
-  public static delete = (courseID: number) => request.post('/course/delete', { courseID })
+  public static detail = (courseId: number) => request.post<Course>('/course/detail/get', { courseId })
+  public static save = (data: Omit<Course, 'creator' | 'createTime' | 'modifyTime'>) => request.post('/course/add', data)
+  public static update = (data: Omit<Course, 'creator' | 'createTime' | 'modifyTime'>) => request.post('/course/update', data)
+  public static delete = (courseId: number) => request.post('/course/delete', { courseId })
+  // 课程复制
+  public static copy = (courseId: number) => request.post('/course/copy', { courseId })
+  // 课程导入
+  public static import = (file: Blob | File) => request.upload('/course/import', { file })
+  // 课程导出
+  public static export = (courseIds: number[]) => request.post('/course/export', { courseIds })
 }

+ 12 - 3
src/components/business/EasyProcess/node/fault/faultDrawer.vue

@@ -1,4 +1,13 @@
 <template>
-    <!-- 异常触发节点 -->
-    <div> 异常触发配置 </div>
-</template>
+  <!-- 异常触发节点 -->
+  <div> 异常触发配置 </div>
+  <div>
+    <div class="flex flex-row">
+      <span>异常类型</span>
+      <el-select placeholder="请选择异常类型">
+        <el-option label="故障" value="1"></el-option>
+        <el-option label="异常" value="2"></el-option>
+      </el-select>
+    </div>
+  </div>
+</template>

+ 2 - 2
src/components/business/EasyProcess/node/fault/faultNode.vue

@@ -1,3 +1,3 @@
 <template>
-    <div>触发故障</div>
-</template>
+  <div>触发故障</div>
+</template>

+ 2 - 2
src/components/business/EasyProcess/node/voice/voiceNode.vue

@@ -1,3 +1,3 @@
 <template>
-    <div>语言广播</div>
-</template>
+  <div>语言广播</div>
+</template>

+ 2 - 6
src/components/form/components/input.vue

@@ -18,12 +18,8 @@
   }>()
 
   const value = computed({
-    get() {
-      return props.value
-    },
-    set(val) {
-      emit('update:value', val)
-    }
+    get: () => props.value,
+    set: (val) => emit('update:value', val)
   })
 
   // 使用 useSlots 获取所有插槽

+ 5 - 24
src/components/form/form.vue

@@ -144,29 +144,13 @@
           // 根据类型设置不同的默认文本
           switch (componentProps.type) {
             case 'monthrange':
-              return {
-                ...componentProps,
-                startPlaceholder: '开始月份',
-                endPlaceholder: '结束月份'
-              }
+              return { ...componentProps, startPlaceholder: '开始月份', endPlaceholder: '结束月份' }
             case 'yearrange':
-              return {
-                ...componentProps,
-                startPlaceholder: '开始年份',
-                endPlaceholder: '结束年份'
-              }
+              return { ...componentProps, startPlaceholder: '开始年份', endPlaceholder: '结束年份' }
             case 'datetimerange':
-              return {
-                ...componentProps,
-                startPlaceholder: '开始时间',
-                endPlaceholder: '结束时间'
-              }
+              return { ...componentProps, startPlaceholder: '开始时间', endPlaceholder: '结束时间' }
             default:
-              return {
-                ...componentProps,
-                startPlaceholder: '开始日期',
-                endPlaceholder: '结束日期'
-              }
+              return { ...componentProps, startPlaceholder: '开始日期', endPlaceholder: '结束日期' }
           }
         }
         // 如果已经有 startPlaceholder 或 endPlaceholder,则直接返回
@@ -175,10 +159,7 @@
     }
     // 合并属性,如果未提供placeholder则自动生成
     const text = item.component.toLowerCase().includes('select') || item.component.toLowerCase().includes('dict') ? '请选择' : '请输入'
-    return {
-      ...componentProps,
-      placeholder: componentProps.placeholder || `${text}${item.label}`
-    }
+    return { ...componentProps, placeholder: componentProps.placeholder || `${text}${item.label}` }
   }
 
   const componentRef = reactive<Record<string, any>>({})

+ 4 - 3
src/utils/http/server.axios.ts

@@ -1,7 +1,7 @@
 import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
 import { useUserStore } from '@/store/modules/user'
 import { $t } from '@/locales'
-import { ApiResponse, ApiResponsePage, ApiStatus } from './server.type'
+import { ApiResponse, ApiResponsePage, ApiStatus, httpStatus } from './server.type'
 import { createHttpError, handleUnauthorizedError, showError } from './server.help'
 
 /** 扩展 AxiosRequestConfig */
@@ -44,13 +44,14 @@ export class RequestHttp {
       async (response: AxiosResponse<ApiResponse>) => {
         const res = response.data as ApiResponse
         const { code, msg } = res
-        console.log('response', response)
         if (code === ApiStatus.success) return response
         if (code === ApiStatus.unauthorized) handleUnauthorizedError(msg)
         throw new Error(msg || $t('httpMsg.requestFailed'))
       },
-      (error) => {
+      (error: AxiosError) => {
         if (error.response?.status === ApiStatus.unauthorized) handleUnauthorizedError()
+        const message = httpStatus[error.status || 0] || $t('httpMsg.requestFailed')
+        showError(createHttpError(message, ApiStatus.error), config.showErrorMessage ?? true)
         return Promise.reject(error)
       }
     )

+ 56 - 58
src/views/train/course/course-list.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="list">
     <div class="offset">
-      <div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
+      <div class="item" v-for="item in data" :key="item.courseID" @click="toDetail(item)">
         <!-- 骨架屏 -->
-        <ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
+        <ElSkeleton animated :loading="loading" style="width: 100%; height: 100%">
           <template #template>
             <div class="top">
               <ElSkeletonItem variant="image" style="width: 100%; height: 100%; border-radius: 10px" />
@@ -15,7 +15,7 @@
           </template>
           <template #default>
             <div class="top">
-              <ElImage class="cover" :src="item.home_img" lazy fit="cover">
+              <ElImage class="cover" :src="getCover(item)" lazy fit="cover">
                 <template #error>
                   <div class="image-slot">
                     <ElIcon><icon-picture /></ElIcon>
@@ -23,22 +23,17 @@
                 </template>
               </ElImage>
 
-              <span class="type">{{ item.type_name }}</span>
+              <span class="type">{{ useDateFormat(item.createTime, 'YYYY-MM-DD') }}</span>
             </div>
-            <div class="bottom">
-              <h2>{{ item.title }}</h2>
-              <div class="info">
-                <div class="text">
-                  <i class="iconfont-sys">&#xe6f7;</i>
-                  <span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
-                  <div class="line"></div>
-                  <i class="iconfont-sys">&#xe689;</i>
-                  <span>{{ item.count }}</span>
-                </div>
-                <div>
-                  <ElButton size="small" @click.stop="toEdit(item)">编辑</ElButton>
-                  <ElButton size="small">复制</ElButton>
-                </div>
+            <div class="bottom space-y-2">
+              <h2>{{ item.courseName }}</h2>
+              <div class="desc">{{ item.courseDesc }}</div>
+              <div>
+                <el-button size="small" type="primary" :icon="Edit" @click.stop="toEdit(item)">编辑</el-button>
+                <el-button size="small" type="danger" :icon="Delete" @click.stop="deleteCourse(item)">删除</el-button>
+                <el-button size="small" type="info" :icon="CopyDocument" @click.stop="copyCourse(item)">复制</el-button>
+                <el-button size="small" type="text"> 编辑记录 </el-button>
+                <el-button size="small" type="text"> 培训记录 </el-button>
               </div>
             </div>
           </template>
@@ -46,56 +41,52 @@
       </div>
     </div>
   </div>
-  <div style="margin-top: 16vh" v-if="showEmpty">
+  <div style="margin-top: 16vh" v-if="data.length === 0 && !props.loading">
     <ElEmpty :description="`未找到课程数据`" />
   </div>
 </template>
 <script lang="ts" setup>
-  import { Picture as IconPicture } from '@element-plus/icons-vue'
-  import { ref, onMounted, computed } from 'vue'
-  import { router } from '@/router'
+  import { Picture as IconPicture, Delete, CopyDocument, Edit } from '@element-plus/icons-vue'
   import { useDateFormat } from '@vueuse/core'
-  import { useCommon } from '@/composables/useCommon'
-
   import cover1 from '@imgs/cover/img5.webp'
-
-  defineOptions({ name: 'CourseList' })
-
-  const articleList = ref<any[]>([])
-  const isLoading = ref(true)
-
-  const showEmpty = computed(() => {
-    return articleList.value.length === 0 && !isLoading.value
+  import cover2 from '@imgs/cover/img2.webp'
+  import cover3 from '@imgs/cover/img3.webp'
+  import { type Course } from '@/api/course.api'
+  import { ElMessageBox } from 'element-plus'
+
+  const props = withDefaults(defineProps<{ data: Course[]; loading: boolean }>(), {
+    data: () => [],
+    loading: true
   })
 
-  onMounted(() => {
-    getArticleList({ backTop: false })
-  })
-
-  const getArticleList = async ({ backTop = false }) => {
-    isLoading.value = true
-
-    articleList.value = Array.from({ length: 8 }).map((_, index) => ({
-      id: index + 1,
-      title: `课程名称${index + 1}`,
-      count: 56,
-      create_time: '2024-08-26 00:00:00',
-      home_img: cover1,
-      type_name: ['基础操作', '故障处理', '非正常处理'][index % 3]
-    }))
-    isLoading.value = false
-
-    if (backTop) {
-      useCommon().scrollToTop()
+  const emit = defineEmits<{
+    delete: [item: Course]
+    copy: [item: Course]
+    detail: [item: Course]
+    edit: [item: Course]
+  }>()
+
+  const getCover = (item: Course) => {
+    switch (item.typeID) {
+      case 1:
+        return cover1
+      case 2:
+        return cover2
+      case 3:
+        return cover3
+      default:
+        return cover1
     }
   }
-
-  const toDetail = (item: any) => {
-    console.log('点击了课程详情', item)
-  }
-
-  const toEdit = (item: any) => {
-    router.push({ path: '/course/detail/' + item.id })
+  const toDetail = (item: Course) => emit('detail', item)
+  const toEdit = (item: Course) => emit('edit', item)
+  const copyCourse = (item: Course) => emit('copy', item)
+  const deleteCourse = (item: Course) => {
+    ElMessageBox.confirm('确定删除课程吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }).then(() => emit('delete', item))
   }
 </script>
 <style lang="scss" scoped>
@@ -171,6 +162,13 @@
 
             @include ellipsis();
           }
+          .desc {
+            font-size: 12px;
+            font-weight: 500;
+            color: var(--art-text-gray-600);
+
+            @include ellipsis();
+          }
 
           .info {
             display: flex;

+ 34 - 0
src/views/train/course/detail/course.schem.ts

@@ -0,0 +1,34 @@
+import { FormItemSchema, useForm } from '@/components/form'
+
+const options = ref([
+  { label: '选项一', value: '1' },
+  { label: '选项二', value: '2' },
+  { label: '选项三', value: '3' }
+])
+const schema: FormItemSchema[] = [
+  // 课程基本信息
+  { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '课程基本信息' } },
+  { prop: 'courseID', label: '课程ID', component: 'Divider', colSpan: 6 },
+  { prop: 'courseName', label: '课程名称', component: 'Input', colSpan: 6 },
+  { prop: 'totalDuration', label: '课程时长', component: 'InputNumber', colSpan: 6, componentProps: { style: { width: '100%' } } },
+  { prop: 'typeID', label: '分类ID', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
+  { prop: 'courseDesc', label: '课程描述', component: 'Input', colSpan: 12 },
+  { prop: 'externalTypeID', label: '外部分类ID', component: 'Input', colSpan: 6 },
+  { prop: 'courseGUID', label: '课程GUID', component: 'Divider', colSpan: 6 },
+  { prop: 'lineNo', label: '线路号', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
+  { prop: 'runningMap', label: '运行图', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
+  // { prop: 'virtualRole', label: '虚拟角色', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
+  //   { prop: 'virtualStartTime', label: '虚拟起始时间', component: 'Input', colSpan: 6 },
+  { prop: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 12 },
+  { prop: 'guideFilePath', label: '指导文件地址', component: 'Upload', colSpan: 12 },
+  { prop: 'creator', label: '创建人', component: 'Divider', colSpan: 8 },
+  { prop: 'createTime', label: '创建时间', component: 'Divider', colSpan: 8 },
+  { prop: 'modifyTime', label: '修改时间', component: 'Divider', colSpan: 8 },
+  // 课程运行流程
+  { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '课程运行流程' } },
+  { prop: 'courseDataStream', labelWidth: '0px', label: '', component: 'Input', colSpan: 24 },
+  // 电子考评
+  { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '电子考评' } },
+  { prop: 'evaluate', label: '电子考评', component: 'Input', colSpan: 24 }
+]
+export const [BasicForm] = useForm({ labelPosition: 'right', labelWidth: '150px', schema: schema })

+ 54 - 33
src/views/train/course/detail/index.vue

@@ -1,8 +1,15 @@
 <template>
   <div class="page-content course-form">
-    <h2 class="title">课程详情</h2>
-    <BasicForm style="margin-top: 12px" @change="handleChange">
-      <template #courseFlow>
+    <div class="header flex flex-row items-center justify-between">
+      <h2 class="title" v-if="courseNew">新增课程</h2>
+      <h2 class="title" v-else-if="courseEdit">编辑课程</h2>
+      <h2 class="title" v-else>课程详情</h2>
+      <el-space wrap>
+        <el-button v-if="!courseQuery" v-ripple type="primary" @click="addCourse"> 保存课程 </el-button>
+      </el-space>
+    </div>
+    <BasicForm style="margin-top: 12px" :model="courseDetail">
+      <template #courseDataStream>
         <courseFlow />
       </template>
       <template #evaluate>
@@ -12,39 +19,53 @@
   </div>
 </template>
 <script setup lang="ts">
-  import { FormItemSchema, useForm } from '@/components/form'
   import courseFlow from './course-flow.vue'
   import courseEvaluate from './course-evaluate.vue'
-  const options = ref([
-    { label: '选项一', value: '1' },
-    { label: '选项二', value: '2' },
-    { label: '选项三', value: '3' }
-  ])
-  const formState = ref({})
-  const handleChange = (val: any) => {
-    formState.value = val
-    detailData.value = val
+  import { BasicForm } from './course.schem'
+  import { Course, CourseServer } from '@/api/course.api'
+
+  // 路由参数
+  const route = useRoute()
+  const courseId = computed<number>(() => Number(route.params.id))
+  const query = computed(() => route.query)
+
+  const courseNew = computed(() => courseId.value === 0)
+  const courseEdit = computed(() => !courseNew.value && query.value.edit === 'true')
+  const courseQuery = computed(() => !courseNew.value && !courseEdit.value)
+  const courseDetail = ref<Course>({
+    courseID: 0,
+    courseName: '',
+    courseDesc: '',
+    totalDuration: 30,
+    typeID: 0,
+    courseDataStream: '',
+    externalTypeID: 0,
+    courseGUID: '',
+    guideDesc: '',
+    guideFilePath: '',
+    lineNo: 0,
+    runningMap: 0,
+    virtualRole: '',
+    virtualStartTime: 0,
+    creator: '',
+    createTime: '',
+    modifyTime: ''
+  })
+
+  const addCourse = () => {
+    console.log('addCourse', courseDetail.value)
   }
-  const schema: FormItemSchema[] = [
-    // 课程基本信息
-    { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '课程基本信息' } },
-    { prop: 'courseName', label: '课程名称', component: 'Input', colSpan: 8 },
-    { prop: 'courseCode', label: '课程编码', component: 'Input', colSpan: 8 },
-    { prop: 'InputNumber', label: '课程时长', component: 'InputNumber', colSpan: 8, componentProps: { style: { width: '100%' } } },
-    { prop: 'courseDesc', label: '课程描述', component: 'Input', colSpan: 24 },
-    { prop: 'Select', label: '下拉选择(单选)', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
-    { prop: 'Select-multiple', label: '下拉选择(多选)', component: 'Select', colSpan: 6, defaultValue: ['1', '2'], componentProps: { options: options.value, clearable: true, multiple: true } },
-    { prop: 'RadioGroup', label: '单选框', component: 'RadioGroup', colSpan: 6, defaultValue: '1', componentProps: { options: options.value } },
-    { prop: 'CheckboxGroup', label: '多选框', component: 'CheckboxGroup', colSpan: 6, defaultValue: ['1'], componentProps: { options: options.value } },
-    // 课程运行流程
-    { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '课程运行流程' } },
-    { prop: 'courseFlow', labelWidth: '0px', label: '', component: 'Input', colSpan: 24 },
-    // 电子考评
-    { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '电子考评' } },
-    { prop: 'evaluate', label: '电子考评', component: 'Input', colSpan: 24 }
-  ]
-  const [BasicForm] = useForm({ labelPosition: 'right', labelWidth: '150px', schema: schema })
-  const detailData = ref({})
+
+  onMounted(() => {
+    console.log(courseId.value)
+    if (!courseNew.value) {
+      CourseServer.detail(courseId.value)
+        .then(({ data }) => {
+          courseDetail.value = data
+        })
+        .catch(() => ElMessage.error('获取课程数据失败'))
+    }
+  })
 </script>
 <style lang="scss" scoped>
   .course-form {

+ 61 - 1
src/views/train/course/index.vue

@@ -2,12 +2,72 @@
   <div class="page-content">
     <h2 class="title">课程列表</h2>
     <course-search></course-search>
-    <course-list></course-list>
+    <el-card class="art-table-card" shadow="never">
+      <div class="header" id="art-table-header">
+        <el-space wrap>
+          <el-button v-ripple type="primary" :icon="Plus" @click="() => router.push({ path: '/course/detail/0' })"> 新增课程 </el-button>
+          <el-button v-ripple type="default" :icon="Upload"> 导入课程 </el-button>
+          <el-button v-ripple type="default" :icon="Download"> 导出课程 </el-button>
+        </el-space>
+      </div>
+      <course-list
+        :data="courseData"
+        :loading="loading"
+        @copy="(item) => courseHandle('copy', item)"
+        @detail="(item) => courseHandle('detail', item)"
+        @delete="(item) => courseHandle('delete', item)"
+        @edit="(item) => courseHandle('edit', item)"
+      ></course-list>
+    </el-card>
   </div>
 </template>
 <script lang="ts" setup>
+  import { ref } from 'vue'
+  import { CourseServer, type Course } from '@/api/course.api'
   import courseSearch from './course-search.vue'
   import courseList from './course-list.vue'
+  import { Plus, Upload, Download } from '@element-plus/icons-vue'
+  import { router } from '@/router'
+
+  const courseData = ref<Course[]>([])
+  const loading = ref(false)
+
+  onMounted(async () => {
+    getCourseData()
+  })
+
+  const getCourseData = async () => {
+    loading.value = true
+    const { data } = await CourseServer.list({})
+    courseData.value = data!
+    loading.value = false
+  }
+
+  const courseHandle = (type: string, item: Course) => {
+    switch (type) {
+      case 'delete':
+        CourseServer.delete(item.courseID).then(() => {
+          ElMessage.success('删除成功')
+          getCourseData()
+        })
+        break
+      case 'copy':
+        CourseServer.copy(item.courseID).then(() => {
+          ElMessage.success('复制成功')
+          getCourseData()
+        })
+        break
+      case 'detail':
+        router.push({ path: '/course/detail/' + item.courseID, query: { edit: `${false}` } })
+        break
+      case 'edit':
+        router.push({ path: '/course/detail/' + item.courseID, query: { edit: `${true}` } })
+        break
+      default:
+        ElMessage.error('未知操作')
+        break
+    }
+  }
 </script>
 <style lang="scss" scoped>
   .title {