zhanghaijun 1 год назад
Родитель
Сommit
2c09be1228

+ 8 - 19
src/components/form/form.vue

@@ -14,9 +14,9 @@
         <ElCol v-for="item in schemaList" :key="item.prop" :span="item.colSpan || 24" :style="{ display: shouldShow(item) ? 'block' : 'none' }">
           <ElFormItem :prop="item.prop" :label="item.label" :label-width="item.labelWidth || formSchemaRef.labelWidth || '80px'" :rules="item.rules">
             <template v-if="item.label" #label>
-              <span
-                >{{ item.label
-                }}<ElTooltip v-if="item.helpMessage" :content="Array.isArray(item.helpMessage) ? item.helpMessage.join('\n') : item.helpMessage" placement="top">
+              <span>
+                {{ item.label }}
+                <ElTooltip v-if="item.helpMessage" :content="Array.isArray(item.helpMessage) ? item.helpMessage.join('\n') : item.helpMessage" placement="top">
                   <Icon class="help-icon" icon="&#xe636;" /> </ElTooltip
               ></span>
             </template>
@@ -63,10 +63,7 @@
 
   // slot 渲染组件
   const RenderSlot = defineComponent({
-    props: {
-      renderFunc: Function,
-      formModel: Object
-    },
+    props: { renderFunc: Function, formModel: Object },
     setup(props) {
       return () => {
         if (props.renderFunc) {
@@ -77,19 +74,11 @@
     }
   })
 
-  const props = withDefaults(
-    defineProps<{
-      formSchema: FormSchema
-      formMethods?: any
-    }>(),
-    {
-      formSchema: () => {
-        return {
-          schema: []
-        }
-      }
+  const props = withDefaults(defineProps<{ formSchema: FormSchema; formMethods?: any }>(), {
+    formSchema: () => {
+      return { schema: [] }
     }
-  )
+  })
 
   const formModel = reactive<any>({})
   const formSchemaRef = ref<FormSchema>(props.formSchema)

+ 1 - 1
src/router/routes/modules/train.ts

@@ -16,7 +16,7 @@ export const routes: AppRouteRecord[] = [
         meta: { title: '课程列表', icon: 'ant-design:unordered-list-outlined' }
       },
       {
-        path: '/course/run',
+        path: '/course/run/:id',
         name: 'trainer:course-run',
         component: '/train/trainer/course-run',
         meta: { title: '课程运行', icon: 'ant-design:play-circle-outlined' }

+ 0 - 118
src/views/train/course/course-add.vue

@@ -1,118 +0,0 @@
-<template>
-  <div>
-    <BasicForm style="margin-top: 12px" @change="handleChange" />
-  </div>
-</template>
-<script setup lang="ts">
-  import { FormItemSchema, useForm } from '@/components/form'
-  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
-  }
-  const schema: FormItemSchema[] = [
-    {
-      prop: 'divider-input',
-      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: 'divider-select',
-      label: '',
-      labelWidth: '0px',
-      component: 'Divider',
-      colSpan: 24,
-      componentProps: {
-        title: '下拉、单选框、多选框'
-      }
-    },
-    {
-      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: 'divider-dict',
-      label: '',
-      labelWidth: '0px',
-      component: 'Divider',
-      colSpan: 24,
-      componentProps: { title: '字典组件' }
-    },
-    {
-      prop: 'ApiDict-sex-Select',
-      label: '字典组件',
-      helpMessage: 'renderType="Select"',
-      component: 'ApiDict',
-      colSpan: 12,
-      componentProps: { code: 'gender' }
-    }
-  ]
-  const [BasicForm] = useForm({
-    labelPosition: 'right',
-    labelWidth: '150px',
-    schema: schema
-  })
-  const detailData = ref({})
-</script>
-<style lang="scss" scoped></style>

+ 0 - 278
src/views/train/course/course-add2.vue

@@ -1,278 +0,0 @@
-<!-- 表单示例 -->
-<template>
-  <div class="form-example">
-    <h2 class="title">表单组件示例</h2>
-
-    <ElCard class="art-custom-card" shadow="never">
-      <ArtForm
-        ref="formRef"
-        v-model="formData"
-        :items="formItems"
-        :rules="formRules"
-        :defaultExpanded="true"
-        :labelWidth="labelWidth"
-        :labelPosition="labelPosition"
-        :span="span"
-        :gutter="gutter"
-        @reset="handleReset"
-        @submit="handleSubmit"
-      >
-        <template #slots>
-          <ElInput v-model="formData.slots" placeholder="我是插槽渲染出来的组件" />
-        </template>
-      </ArtForm>
-    </ElCard>
-
-    <div class="code">
-      <pre><code>{{ formData }}</code></pre>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-  import { SearchFormItem } from '@/components/core/forms/art-search-bar/index.vue'
-  import { ElMessage, ElUpload, ElButton } from 'element-plus'
-  import type { UploadFile, UploadFiles, UploadUserFile } from 'element-plus'
-
-  interface Emits {
-    (e: 'update:modelValue', value: Record<string, any>): void
-    (e: 'search', params: Record<string, any>): void
-    (e: 'reset'): void
-  }
-  const emit = defineEmits<Emits>()
-
-  // 表单数据双向绑定
-  const formRef = ref()
-
-  // 表单数据
-  const formData = ref({
-    name: undefined,
-    phone: undefined,
-    level: undefined,
-    address: undefined,
-    slots: undefined,
-    date: undefined,
-    daterange: undefined,
-    cascader: undefined,
-    checkboxgroup: undefined,
-    userGender: undefined,
-    iconSelector: undefined,
-    status: undefined,
-    systemName: undefined,
-    fileUpload: [] as UploadUserFile[],
-    imageUpload: [] as UploadUserFile[],
-    multipleFiles: [] as UploadUserFile[],
-    richTextContent: ''
-  })
-
-  // 表单校验规则
-  const formRules = {
-    name: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
-    phone: [
-      { required: true, message: '请输入手机号', trigger: 'blur' },
-      { min: 11, max: 11, message: '请输入11位手机号', trigger: 'blur' },
-      { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
-    ]
-  }
-
-  const labelWidth = ref(100)
-  const labelPosition = ref<'right' | 'left' | 'top'>('right')
-  const span = ref(6)
-  const gutter = ref(12)
-
-  // 动态 options
-  const levelOptions = ref<{ label: string; value: string; disabled?: boolean }[]>([])
-
-  // 共享的选项数据
-  const LEVEL_OPTIONS = [
-    { label: '普通用户', value: 'normal' },
-    { label: 'VIP用户', value: 'vip' },
-    { label: '高级VIP', value: 'svip' },
-    { label: '企业用户', value: 'enterprise', disabled: true }
-  ]
-
-  const GENDER_OPTIONS = [
-    { label: '男', value: '1' },
-    { label: '女', value: '2' }
-  ]
-
-  const DATE_SHORTCUTS = [
-    { text: '今日', value: new Date() },
-    { text: '昨日', value: () => new Date(Date.now() - 86400000) },
-    { text: '一周前', value: () => new Date(Date.now() - 604800000) }
-  ]
-
-  // 创建表单项的工厂函数
-  const createFormItem = (config: any) => config
-
-  // 基础表单项配置
-  const baseFormItems = {
-    username: createFormItem({
-      label: '用户名',
-      key: 'name',
-      type: 'input',
-      placeholder: '请输入用户名',
-      clearable: true
-    }),
-    phone: createFormItem({
-      label: '手机号',
-      key: 'phone',
-      type: 'input',
-      props: { placeholder: '请输入手机号', maxlength: '11' }
-    }),
-    level: createFormItem({
-      label: '用户等级',
-      key: 'level',
-      type: 'select',
-      props: { placeholder: '请选择等级', options: LEVEL_OPTIONS }
-    }),
-    address: createFormItem({
-      label: '地址',
-      key: 'address',
-      type: 'input',
-      placeholder: '请输入地址'
-    }),
-    date: createFormItem({
-      label: '日期',
-      key: 'date',
-      type: 'datetime',
-      props: {
-        style: { width: '100%' },
-        placeholder: '请选择日期',
-        type: 'date',
-        valueFormat: 'YYYY-MM-DD',
-        shortcuts: DATE_SHORTCUTS
-      }
-    }),
-    gender: createFormItem({
-      label: '性别',
-      key: 'userGender',
-      type: 'radiogroup',
-      props: {
-        options: GENDER_OPTIONS
-      }
-    })
-  }
-
-  const userItem = ref<SearchFormItem>({
-    label: '用户名',
-    key: 'name',
-    type: 'input',
-    props: {
-      placeholder: '请输入用户名',
-      clearable: true
-    }
-  })
-
-  // 控制用户名字段是否显示
-  const showUserName = ref(true)
-  // 表单配置
-  const formItems = computed(() => [
-    ...(showUserName.value ? [userItem.value] : []),
-    {
-      ...baseFormItems.phone
-    },
-    {
-      ...baseFormItems.level,
-      props: { placeholder: '请选择等级', options: levelOptions.value }
-    },
-    baseFormItems.address,
-    baseFormItems.date,
-
-    {
-      ...baseFormItems.gender
-    },
-
-    {
-      label: '是否启用',
-      key: 'isEnabled',
-      type: 'switch',
-      props: {
-        placeholder: '请选择是否启用'
-      }
-    },
-    {
-      label: '年龄',
-      key: 'age',
-      type: 'number',
-      slots: {
-        suffix: () => h('span', { style: 'color: #909399; font-size: 12px' }, '岁')
-      }
-    },
-    // 文件上传示例 - 使用 h 函数渲染
-    {
-      label: '文件上传',
-      key: 'multipleFiles',
-      span: 12,
-      type: () =>
-        h(
-          ElUpload,
-          {
-            multiple: true,
-            limit: 5,
-            action: '#',
-            autoUpload: false,
-            showFileList: true,
-            accept: '.pdf,.mp4,.txt',
-            beforeUpload: (file: File) => {
-              console.log('准备上传文件:', file.name)
-              return true
-            },
-            onChange: (file: UploadFile, fileList: UploadFiles) => {
-              console.log('多文件变化:', file, fileList)
-              formData.value.multipleFiles = fileList as UploadUserFile[]
-            },
-            onRemove: (file: UploadFile, fileList: UploadFiles) => {
-              console.log('删除文件:', file, fileList)
-              formData.value.multipleFiles = fileList as UploadUserFile[]
-            },
-            onExceed: (files: File[], fileList: UploadUserFile[]) => {
-              ElMessage.warning(`最多只能上传 5 个文件,当前选择了 ${files.length + fileList.length} 个文件`)
-            }
-          },
-          { default: () => [h(ElButton, { type: 'primary' }, () => '点击上传')] }
-        )
-    }
-  ])
-
-  // 表单处理函数
-  const handleReset = () => {
-    console.log('重置表单')
-    emit('reset')
-  }
-
-  const handleSubmit = async () => {
-    await formRef.value.validate()
-    emit('search', formData.value)
-    console.log('表单数据', formData.value)
-  }
-</script>
-
-<style scoped lang="scss">
-  .form-example {
-    padding-bottom: 20px;
-
-    .title {
-      margin-bottom: 5px;
-      font-size: 18px;
-      font-weight: 500;
-
-      &.m-15 {
-        margin-top: 15px;
-      }
-    }
-
-    .code {
-      padding: 15px;
-      margin-top: 15px;
-      font-size: 14px;
-      background-color: var(--art-main-bg-color);
-      border: 1px solid var(--art-border-color);
-      border-radius: var(--el-border-radius-base);
-    }
-
-    .button-group {
-      margin-top: 15px;
-    }
-  }
-</style>

+ 0 - 37
src/views/train/course/course-card.vue

@@ -1,37 +0,0 @@
-<template>
-  <div>
-    <div class="image-wrapper">
-      <el-image :src="coverImageList[course.typeID] || cover1" fit="cover" loading="lazy">
-        <template #placeholder>
-          <div class="image-placeholder">
-            <ElIcon><Picture /></ElIcon>
-          </div>
-        </template>
-      </el-image>
-    </div>
-    <div class="course-info">
-      <div class="course-name">{{ course.courseName }}</div>
-      <div class="course-desc">{{ course.courseDesc }}</div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-  import { Course } from '@/api/course.api'
-
-  interface Props {
-    course: Course
-    edit: boolean
-  }
-  withDefaults(defineProps<Props>(), {
-    edit: false
-  })
-
-  import cover1 from '@imgs/cover/img1.webp'
-  import cover2 from '@imgs/cover/img2.webp'
-  import cover3 from '@imgs/cover/img3.webp'
-  import cover4 from '@imgs/cover/img4.webp'
-
-  const coverImageList = [cover1, cover2, cover3, cover4]
-</script>
-<style lang="scss" scoped></style>

+ 261 - 0
src/views/train/course/course-list.vue

@@ -0,0 +1,261 @@
+<template>
+  <div class="list">
+    <div class="offset">
+      <div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
+        <!-- 骨架屏 -->
+        <ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
+          <template #template>
+            <div class="top">
+              <ElSkeletonItem variant="image" style="width: 100%; height: 100%; border-radius: 10px" />
+              <div style="padding: 16px 0">
+                <ElSkeletonItem variant="p" style="width: 80%" />
+                <ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
+              </div>
+            </div>
+          </template>
+          <template #default>
+            <div class="top">
+              <ElImage class="cover" :src="item.home_img" lazy fit="cover">
+                <template #error>
+                  <div class="image-slot">
+                    <ElIcon><icon-picture /></ElIcon>
+                  </div>
+                </template>
+              </ElImage>
+
+              <span class="type">{{ item.type_name }}</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>
+            </div>
+          </template>
+        </ElSkeleton>
+      </div>
+    </div>
+  </div>
+  <div style="margin-top: 16vh" v-if="showEmpty">
+    <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 { 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
+  })
+
+  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 toDetail = (item: any) => {
+    console.log('点击了课程详情', item)
+  }
+
+  const toEdit = (item: any) => {
+    router.push({ path: '/course/detail/' + item.id })
+  }
+</script>
+<style lang="scss" scoped>
+  .list {
+    margin-top: 20px;
+
+    .custom-segmented .el-segmented {
+      height: 40px;
+      padding: 6px;
+
+      --el-border-radius-base: 8px;
+    }
+
+    .offset {
+      display: flex;
+      flex-wrap: wrap;
+      width: calc(100% + 20px);
+
+      .item {
+        box-sizing: border-box;
+        width: calc(20% - 20px);
+        margin: 0 20px 20px 0;
+        padding: 20px;
+        cursor: pointer;
+        border: 1px solid var(--art-border-color);
+        border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
+
+        &:hover {
+          .el-button {
+            opacity: 1 !important;
+          }
+        }
+
+        .top {
+          position: relative;
+          aspect-ratio: 16/7;
+
+          .cover {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            background: var(--art-gray-200);
+            border-radius: calc(var(--custom-radius) / 2 + 2px) calc(var(--custom-radius) / 2 + 2px) 0 0;
+
+            .image-slot {
+              font-size: 26px;
+              color: var(--art-gray-400);
+            }
+          }
+
+          .type {
+            position: absolute;
+            top: 5px;
+            right: 5px;
+            padding: 5px 4px;
+            font-size: 12px;
+            color: rgba(#fff, 0.8);
+            background: rgba($color: #000, $alpha: 60%);
+            border-radius: 4px;
+          }
+        }
+
+        .bottom {
+          padding: 5px 10px;
+
+          h2 {
+            font-size: 16px;
+            font-weight: 500;
+            color: #333;
+
+            @include ellipsis();
+          }
+
+          .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);
+              }
+            }
+
+            .el-button {
+              opacity: 1;
+              transition: all 0.3s;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-notebook) {
+    .list {
+      .offset {
+        .item {
+          width: calc(25% - 20px);
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-ipad-pro) {
+    .list {
+      .offset {
+        .item {
+          width: calc(33.333% - 20px);
+
+          .bottom {
+            h2 {
+              font-size: 16px;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-ipad) {
+    .list {
+      .offset {
+        .item {
+          width: calc(50% - 20px);
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-phone) {
+    .list {
+      .offset {
+        .item {
+          width: calc(100% - 20px);
+        }
+      }
+    }
+  }
+</style>

+ 3 - 0
src/views/train/course/detail/course-evaluate.vue

@@ -0,0 +1,3 @@
+<template>
+  <div title="电子考评" />
+</template>

+ 44 - 0
src/views/train/course/detail/course-flow.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="art-full-height w-full">
+    <ProcessDesigner ref="processRef" :data="processData" />
+  </div>
+</template>
+
+<script setup>
+  import ProcessDesigner from '@/components/business/EasyProcess/index.vue'
+  import { ElMessageBox } from 'element-plus'
+  import { onMounted, ref } from 'vue'
+  import mockData from './flow.json'
+
+  // 定义 ProcessDesigner 的实例类型
+  // type ProcessDesignerInstance = InstanceType<typeof ProcessDesigner>
+  // 审批数据
+  // const processData = ref<Record<string, any>>({})
+  // 组件实例 ref
+  // const processRef = ref<ProcessDesignerInstance | null>(null)
+
+  const processData = ref({})
+  const processRef = ref(null)
+
+  onMounted(() => {
+    processData.value = mockData
+  })
+
+  /**
+   * 发布流程
+   */
+  const publish = () => {
+    if (!processRef.value) return
+    processRef.value.validate((valid, messages) => {
+      if (valid) {
+        const result = processRef.value?.getResult()
+        console.info('发布流程:', result)
+        ElMessageBox.alert(JSON.stringify(result, null, 2), '结果')
+      } else {
+        ElMessageBox.alert(messages, '结果')
+      }
+    })
+  }
+</script>
+
+<style lang="scss" scoped></style>

+ 52 - 62
src/views/train/course/detail/index.vue

@@ -1,73 +1,63 @@
-<!--
-@description: 课程监控
-@creationTime: 2025-09-23
--->
 <template>
-  <div class="app-container art-full-height w-full">
-    <div class="header">
-      <el-button type="primary" @click="publish" round>发布</el-button>
-    </div>
-    <div class="body flex-1">
-      <ProcessDesigner ref="processRef" :data="processData" />
-    </div>
+  <div class="page-content course-form">
+    <h2 class="title">课程详情</h2>
+    <BasicForm style="margin-top: 12px" @change="handleChange">
+      <template #courseFlow>
+        <courseFlow />
+      </template>
+      <template #evaluate>
+        <courseEvaluate />
+      </template>
+    </BasicForm>
   </div>
 </template>
-
-<script setup>
-  import ProcessDesigner from '@/components/business/EasyProcess/index.vue'
-  import { ElMessageBox } from 'element-plus'
-  import { onMounted, ref } from 'vue'
-  import mockData from './flow.json'
-
-  // 定义 ProcessDesigner 的实例类型
-  // type ProcessDesignerInstance = InstanceType<typeof ProcessDesigner>
-  // 审批数据
-  // const processData = ref<Record<string, any>>({})
-  // 组件实例 ref
-  // const processRef = ref<ProcessDesignerInstance | null>(null)
-
-  const processData = ref({})
-  const processRef = ref(null)
-
-  onMounted(() => {
-    processData.value = mockData
-  })
-
-  /**
-   * 发布流程
-   */
-  const publish = () => {
-    if (!processRef.value) return
-    processRef.value.validate((valid, messages) => {
-      if (valid) {
-        const result = processRef.value?.getResult()
-        console.info('发布流程:', result)
-        ElMessageBox.alert(JSON.stringify(result, null, 2), '结果')
-      } else {
-        ElMessageBox.alert(messages, '结果')
-      }
-    })
+<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
   }
+  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({})
 </script>
-
 <style lang="scss" scoped>
-  .app-container {
-    display: flex;
-    flex-direction: column;
-    width: 100%;
-    overflow: hidden;
-    border: 1px solid #e8e8e8;
+  .course-form {
+    padding-bottom: 20px;
 
-    .header {
-      display: flex;
-      align-items: center;
-      justify-content: flex-end;
-      padding: 3px 50px;
-    }
+    .title {
+      margin-bottom: 5px;
+      font-size: 18px;
+      font-weight: 500;
 
-    .body {
-      flex: 1;
-      overflow: auto;
+      &.m-15 {
+        margin-top: 15px;
+      }
     }
   }
 </style>

+ 3 - 268
src/views/train/course/index.vue

@@ -1,109 +1,13 @@
 <template>
-  <div class="page-content article-list">
+  <div class="page-content">
     <h2 class="title">课程列表</h2>
     <course-search></course-search>
-    <div class="list">
-      <div class="offset">
-        <div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
-          <!-- 骨架屏 -->
-          <ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
-            <template #template>
-              <div class="top">
-                <ElSkeletonItem variant="image" style="width: 100%; height: 100%; border-radius: 10px" />
-                <div style="padding: 16px 0">
-                  <ElSkeletonItem variant="p" style="width: 80%" />
-                  <ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
-                </div>
-              </div>
-            </template>
-            <template #default>
-              <div class="top">
-                <ElImage class="cover" :src="item.home_img" lazy fit="cover">
-                  <template #error>
-                    <div class="image-slot">
-                      <ElIcon><icon-picture /></ElIcon>
-                    </div>
-                  </template>
-                </ElImage>
-
-                <span class="type">{{ item.type_name }}</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>
-                  <ElButton size="small" @click.stop="toEdit(item)">立即开课</ElButton>
-                </div>
-              </div>
-            </template>
-          </ElSkeleton>
-        </div>
-      </div>
-    </div>
-    <div style="margin-top: 16vh" v-if="showEmpty">
-      <ElEmpty :description="`未找到课程数据`" />
-    </div>
+    <course-list></course-list>
   </div>
 </template>
 <script lang="ts" setup>
   import courseSearch from './course-search.vue'
-
-  import { Picture as IconPicture } from '@element-plus/icons-vue'
-  import { ref, onMounted, computed } from 'vue'
-  import { router } from '@/router'
-  import { useDateFormat } from '@vueuse/core'
-  import { useCommon } from '@/composables/useCommon'
-  import { RoutesAlias } from '@/router/routesAlias'
-
-  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
-  })
-
-  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 toDetail = (item: any) => {
-    console.log('点击了课程详情', item)
-  }
-
-  const toEdit = (item: any) => {
-    router.push({
-      path: RoutesAlias.ArticlePublish,
-      query: { id: item.id }
-    })
-  }
+  import courseList from './course-list.vue'
 </script>
 <style lang="scss" scoped>
   .title {
@@ -115,173 +19,4 @@
       margin-top: 15px;
     }
   }
-  .article-list {
-    .custom-segmented .el-segmented {
-      height: 40px;
-      padding: 6px;
-
-      --el-border-radius-base: 8px;
-    }
-
-    .list {
-      margin-top: 20px;
-
-      .offset {
-        display: flex;
-        flex-wrap: wrap;
-        width: calc(100% + 20px);
-
-        .item {
-          box-sizing: border-box;
-          width: calc(20% - 20px);
-          margin: 0 20px 20px 0;
-          padding: 20px;
-          cursor: pointer;
-          border: 1px solid var(--art-border-color);
-          border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
-
-          &:hover {
-            .el-button {
-              opacity: 1 !important;
-            }
-          }
-
-          .top {
-            position: relative;
-            aspect-ratio: 16/7;
-
-            .cover {
-              display: flex;
-              align-items: center;
-              justify-content: center;
-              width: 100%;
-              height: 100%;
-              object-fit: cover;
-              background: var(--art-gray-200);
-              border-radius: calc(var(--custom-radius) / 2 + 2px) calc(var(--custom-radius) / 2 + 2px) 0 0;
-
-              .image-slot {
-                font-size: 26px;
-                color: var(--art-gray-400);
-              }
-            }
-
-            .type {
-              position: absolute;
-              top: 5px;
-              right: 5px;
-              padding: 5px 4px;
-              font-size: 12px;
-              color: rgba(#fff, 0.8);
-              background: rgba($color: #000, $alpha: 60%);
-              border-radius: 4px;
-            }
-          }
-
-          .bottom {
-            padding: 5px 10px;
-
-            h2 {
-              font-size: 16px;
-              font-weight: 500;
-              color: #333;
-
-              @include ellipsis();
-            }
-
-            .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);
-                }
-              }
-
-              .el-button {
-                opacity: 1;
-                transition: all 0.3s;
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-
-  @media only screen and (max-width: $device-notebook) {
-    .article-list {
-      .list {
-        .offset {
-          .item {
-            width: calc(25% - 20px);
-          }
-        }
-      }
-    }
-  }
-
-  @media only screen and (max-width: $device-ipad-pro) {
-    .article-list {
-      .list {
-        .offset {
-          .item {
-            width: calc(33.333% - 20px);
-
-            .bottom {
-              h2 {
-                font-size: 16px;
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-
-  @media only screen and (max-width: $device-ipad) {
-    .article-list {
-      .list {
-        .offset {
-          .item {
-            width: calc(50% - 20px);
-          }
-        }
-      }
-    }
-  }
-
-  @media only screen and (max-width: $device-phone) {
-    .article-list {
-      .list {
-        .offset {
-          .item {
-            width: calc(100% - 20px);
-          }
-        }
-      }
-    }
-  }
 </style>

+ 258 - 0
src/views/train/trainer/course-list/course-list.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="list">
+    <div class="offset">
+      <div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
+        <!-- 骨架屏 -->
+        <ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
+          <template #template>
+            <div class="top">
+              <ElSkeletonItem variant="image" style="width: 100%; height: 100%; border-radius: 10px" />
+              <div style="padding: 16px 0">
+                <ElSkeletonItem variant="p" style="width: 80%" />
+                <ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
+              </div>
+            </div>
+          </template>
+          <template #default>
+            <div class="top">
+              <ElImage class="cover" :src="item.home_img" lazy fit="cover">
+                <template #error>
+                  <div class="image-slot">
+                    <ElIcon><icon-picture /></ElIcon>
+                  </div>
+                </template>
+              </ElImage>
+
+              <span class="type">{{ item.type_name }}</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>
+                <ElButton size="small" @click.stop="toEdit(item)">立即开课</ElButton>
+              </div>
+            </div>
+          </template>
+        </ElSkeleton>
+      </div>
+    </div>
+  </div>
+  <div style="margin-top: 16vh" v-if="showEmpty">
+    <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 { 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
+  })
+
+  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 toDetail = (item: any) => {
+    console.log('点击了课程详情', item)
+  }
+
+  const toEdit = (item: any) => {
+    router.push({ path: `/trainer/course/run/${item.id}` })
+  }
+</script>
+<style lang="scss" scoped>
+  .list {
+    margin-top: 20px;
+
+    .custom-segmented .el-segmented {
+      height: 40px;
+      padding: 6px;
+
+      --el-border-radius-base: 8px;
+    }
+
+    .offset {
+      display: flex;
+      flex-wrap: wrap;
+      width: calc(100% + 20px);
+
+      .item {
+        box-sizing: border-box;
+        width: calc(20% - 20px);
+        margin: 0 20px 20px 0;
+        padding: 20px;
+        cursor: pointer;
+        border: 1px solid var(--art-border-color);
+        border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
+
+        &:hover {
+          .el-button {
+            opacity: 1 !important;
+          }
+        }
+
+        .top {
+          position: relative;
+          aspect-ratio: 16/7;
+
+          .cover {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            background: var(--art-gray-200);
+            border-radius: calc(var(--custom-radius) / 2 + 2px) calc(var(--custom-radius) / 2 + 2px) 0 0;
+
+            .image-slot {
+              font-size: 26px;
+              color: var(--art-gray-400);
+            }
+          }
+
+          .type {
+            position: absolute;
+            top: 5px;
+            right: 5px;
+            padding: 5px 4px;
+            font-size: 12px;
+            color: rgba(#fff, 0.8);
+            background: rgba($color: #000, $alpha: 60%);
+            border-radius: 4px;
+          }
+        }
+
+        .bottom {
+          padding: 5px 10px;
+
+          h2 {
+            font-size: 16px;
+            font-weight: 500;
+            color: #333;
+
+            @include ellipsis();
+          }
+
+          .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);
+              }
+            }
+
+            .el-button {
+              opacity: 1;
+              transition: all 0.3s;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-notebook) {
+    .list {
+      .offset {
+        .item {
+          width: calc(25% - 20px);
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-ipad-pro) {
+    .list {
+      .offset {
+        .item {
+          width: calc(33.333% - 20px);
+
+          .bottom {
+            h2 {
+              font-size: 16px;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-ipad) {
+    .list {
+      .offset {
+        .item {
+          width: calc(50% - 20px);
+        }
+      }
+    }
+  }
+
+  @media only screen and (max-width: $device-phone) {
+    .list {
+      .offset {
+        .item {
+          width: calc(100% - 20px);
+        }
+      }
+    }
+  }
+</style>

+ 2 - 0
src/views/train/trainer/course-list/index.vue

@@ -3,11 +3,13 @@
   <div class="search-bar">
     <h2 class="title">课程列表</h2>
     <course-search></course-search>
+    <course-list></course-list>
   </div>
 </template>
 
 <script setup lang="ts">
   import courseSearch from './course-search.vue'
+  import courseList from './course-list.vue'
 </script>
 
 <style scoped lang="scss">