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

feat(流程设计): 新增故障触发节点功能及优化条件节点配置

zhanghaijun 1 год назад
Родитель
Сommit
20cac0ade3
30 измененных файлов с 1033 добавлено и 284 удалено
  1. 71 1
      src/api/course.api.ts
  2. 8 16
      src/components/business/Drawer/Drawer.vue
  3. 4 4
      src/components/business/EasyProcess/common/data.ts
  4. 18 2
      src/components/business/EasyProcess/common/data.type.d.ts
  5. 16 24
      src/components/business/EasyProcess/node/base/AddNode.vue
  6. 3 3
      src/components/business/EasyProcess/node/base/BaseNode.vue
  7. 64 13
      src/components/business/EasyProcess/node/condition/conditionDrawer.vue
  8. 25 10
      src/components/business/EasyProcess/node/condition/conditionNode.vue
  9. 319 8
      src/components/business/EasyProcess/node/fault/faultDrawer.vue
  10. 45 0
      src/components/business/EasyProcess/node/fault/index.vue
  11. 2 11
      src/components/business/EasyProcess/node/router/routerNode.vue
  12. 2 12
      src/components/core/cards/art-bar-chart-card/index.vue
  13. 1 3
      src/components/core/cards/art-data-list-card/index.vue
  14. 1 7
      src/components/core/cards/art-donut-chart-card/index.vue
  15. 7 39
      src/components/core/forms/art-drag-verify/index.vue
  16. 3 8
      src/components/form/components/divider.vue
  17. 3 3
      src/components/form/detail-form.vue
  18. 15 4
      src/components/form/form.vue
  19. 1 0
      src/components/form/use-form.ts
  20. 5 0
      src/components/icon/svg/tips.svg
  21. 1 1
      src/store/modules/user.ts
  22. 2 2
      src/views/train/course/course-list.vue
  23. 40 14
      src/views/train/course/detail/course.schem.ts
  24. 78 31
      src/views/train/course/detail/index.vue
  25. 63 0
      src/views/train/evaluation/basic.vue
  26. 46 5
      src/views/train/evaluation/index.vue
  27. 124 0
      src/views/train/evaluation/tree.vue
  28. 37 61
      src/views/train/trainer/course-list/course-list.vue
  29. 17 2
      src/views/train/trainer/course-list/index.vue
  30. 12 0
      src/views/train/trainer/course-run/index.vue

+ 71 - 1
src/api/course.api.ts

@@ -24,7 +24,7 @@ export interface Course {
 export class CourseServer {
   public static list = (data: any) => request.post<Course[]>('/course/list', data)
   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 add = (data: Omit<Course, 'courseID' | 'courseGUID' | '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 })
   // 课程复制
@@ -34,3 +34,73 @@ export class CourseServer {
   // 课程导出
   public static export = (courseIds: number[]) => request.post('/course/export', { courseIds })
 }
+
+/** 用户角色 */
+export interface ProcessRole {
+  roleID: number /** 角色ID */
+  roleName: string /** 角色名称 */
+  roleType: number /** 角色类型 */
+}
+/** 操作类模板 */
+export interface ProcessOperate {
+  templateID: number /** 模板ID */
+  templateName: string /** 操作模板名称 */
+  templateType: number /** 操作模板类型 */
+  category: number /** 分类 */
+  categoryName: string /** 分类名称 */
+  param1Desc: string /** 参数1说明 */
+  param1Type: number /** 参数1类型 */
+  param2Desc: string /** 参数2说明 */
+  param2Type: number /** 参数2类型 */
+  param3Desc: string /** 参数3说明 */
+  param3Type: number /** 参数3类型 */
+  param4Desc: string /** 参数4说明 */
+  param4Type: number /** 参数4类型 */
+  param5Desc: string /** 参数5说明 */
+  param5Type: number /** 参数5类型 */
+  subCategory: number /** 子分类 */
+  subCategoryName: string /** 子分类名称 */
+}
+/** 故障类模板 */
+export interface ProcessFault {
+  templateID: number /** 模板ID */
+  templateName: string /** 模板名称 */
+  templateType: number /** 故障模板类型 */
+  category: number /** 分类 */
+  categoryName: string /** 分类名称 */
+  param1Desc: string /** 参数1说明 */
+  param1Type: number /** 参数1类型 */
+  param2Desc: string /** 参数2说明 */
+  param2Type: number /** 参数2类型 */
+  param3Desc: string /** 参数3说明 */
+  param3Type: number /** 参数3类型 */
+  param4Desc: string /** 参数4说明 */
+  param4Type: number /** 参数4类型 */
+  param5Desc: string /** 参数5说明 */
+  param5Type: number /** 参数5类型 */
+  subCategory: number /** 子分类 */
+  subCategoryName: string /** 子分类名称 */
+}
+/** 语音类模板 */
+export interface ProcessVoice {
+  templateID: number /** 模板ID */
+  templateName: string /** 模板名称 */
+  templateType: number /** 语音模板类型 */
+  category: number /** 分类 */
+  categoryName: string /** 分类名称 */
+  subCategory: number /** 子分类 */
+  subCategoryName: string /** 子分类名称 */
+  voiceContent: string /** 语音内容 */
+  evalTemplate: string /** 评估模板 */
+  destRoleID: number /** 目标角色ID */
+}
+export class ProcessServer {
+  // 角色
+  public static roleList = () => request.post<ProcessRole[]>('/role/list')
+  // 操作模板
+  public static operateList = () => request.post<ProcessOperate[]>('/templateoperation/list')
+  // 故障模板
+  public static faultList = () => request.post<ProcessFault[]>('/templatefault/list')
+  // 语音模板
+  public static voiceList = () => request.post<ProcessVoice[]>('/templatevoice/list')
+}

+ 8 - 16
src/components/business/Drawer/Drawer.vue

@@ -3,20 +3,13 @@
     <div class="drawer">
       <!-- header -->
       <div class="drawer-header" v-if="!props.hideHeader">
-        <svg-icon
-          :icon-class="props.icon"
-          class="drawer-header-icon"
-          color="#5a5e66"
-          v-if="props.icon"
-        />
+        <el-icon class="drawer-header-icon" color="#5a5e66" v-if="props.icon">
+          <props.icon />
+        </el-icon>
         <div class="drawer-header-title">{{ props.title }}</div>
-        <svg-icon
-          icon-class="close"
-          class="drawer-header-close"
-          color="#5a5e66"
-          @click="close"
-          v-if="!props.hideCloseBtn"
-        />
+        <el-icon class="drawer-header-close" color="#5a5e66" @click="close" v-if="!props.hideCloseBtn">
+          <Close />
+        </el-icon>
       </div>
       <!-- body -->
       <div class="drawer-body">
@@ -34,6 +27,7 @@
 
 <script setup name="Drawer">
   import { onMounted, onUnmounted } from 'vue'
+  import { Close } from '@element-plus/icons-vue'
 
   const props = defineProps({
     modelValue: {
@@ -85,7 +79,6 @@
 
   const emit = defineEmits(['update:modelValue', 'close'])
   const close = () => {
-    // proxy.$emit("update:modelValue", false)
     emit('update:modelValue', false)
     emit('close')
   }
@@ -96,7 +89,7 @@
     position: fixed;
     width: v-bind('props.width');
     height: 100%;
-    background-color: #ffffff;
+    background: var(--art-main-bg-color);
     box-shadow: -5px -5px 10px 2px rgba(0, 0, 0, 0.2);
     top: 0;
     right: 0;
@@ -123,7 +116,6 @@
     }
     .drawer-header-title {
       flex: 1;
-      color: #5a5e66;
       font-size: 16px;
       font-weight: bold;
     }

+ 4 - 4
src/components/business/EasyProcess/common/data.ts

@@ -18,10 +18,10 @@ export const nodeConfigData: Record<EasyProcess.NodeType, EasyProcess.Node> = {
     defaultNode: {
       nodeName: '路由',
       nodeType: 'router',
-      nodeData: {},
+      nodeData: { value: '1' },
       conditionNodes: [
-        { nodeName: '条件', nodeType: 'condition', isLastCondition: false, nodeData: {} },
-        { nodeName: '条件', nodeType: 'condition', isLastCondition: true, nodeData: {} }
+        { nodeName: '条件', nodeType: 'condition', isLastCondition: false, nodeData: { value: '1' } },
+        { nodeName: '条件', nodeType: 'condition', isLastCondition: true, nodeData: { value: '1' } }
       ]
     }
   },
@@ -32,7 +32,7 @@ export const nodeConfigData: Record<EasyProcess.NodeType, EasyProcess.Node> = {
     canRemoved: true,
     hasDrawer: true,
     icon: { name: '&#xe7b7;' },
-    defaultNode: { nodeName: '条件', nodeType: 'condition', isLastCondition: false, nodeData: {} }
+    defaultNode: { nodeName: '条件', nodeType: 'condition', isLastCondition: false, nodeData: { value: '1' } }
   },
   fault: {
     title: '故障触发',

+ 18 - 2
src/components/business/EasyProcess/common/data.type.d.ts

@@ -30,6 +30,7 @@ export declare namespace EasyProcess {
   export type NodeType = BaseType | (string & { _?: never })
   // 基础节点配置
   export interface BaseNode {
+    nodeID?: number // 节点ID
     nodeName: string
     nodeType: NodeType
     nodeData?: Record<string, unknown>
@@ -54,6 +55,21 @@ export declare namespace EasyProcess {
     isLastCondition: boolean
     childNode?: NodeConfig
   }
-  // 节点配置类型,可以是普通节点、路由节点或条件节点
-  export type NodeConfig = NormalNode | RouterNode | ConditionNode
+  // 故障节点
+  export interface FaultNode extends BaseNode {
+    nodeType: 'fault'
+    nodeData?: Record<string, unknown>
+  }
+  // 语音节点
+  export interface VoiceNode extends BaseNode {
+    nodeType: 'voice'
+    nodeData?: Record<string, unknown>
+  }
+  //操作节点
+  export interface OperationNode extends BaseNode {
+    nodeType: 'operation'
+    nodeData?: Record<string, unknown>
+  }
+  // 节点配置类型,可以是普通节点、路由节点、条件节点、故障节点、语音节点、操作节点
+  export type NodeConfig = NormalNode | RouterNode | ConditionNode | FaultNode | VoiceNode | OperationNode
 }

+ 16 - 24
src/components/business/EasyProcess/node/base/AddNode.vue

@@ -2,15 +2,8 @@
   <div class="ep-node-add">
     <div ref="nodeAddBtn" class="ep-node-add-btn" v-on:mouseenter="showAddSelect(true)" v-on:mouseleave="showAddSelect(false)">
       <Icon class="ep-node-add-btn-icon" :icon="'&#xe83e;'" color="#FFFFFF" />
-      <div ref="nodeAddSelect" :class="['ep-node-add-select-box', nodeAddSelectPostion == 1 ? 'ep-node-add-left' : 'ep-node-add-right']" v-if="isShowAddSelect">
-        <div
-          :class="{ 'ep-node-add-select-item': true, 'ep-node-add-selected': item.selected }"
-          v-for="(item, index) in nodeSelect"
-          :key="index"
-          v-on:click="addNode(item.type)"
-          v-on:mouseenter="addNodeSelected(item, true)"
-          v-on:mouseleave="addNodeSelected(item, false)"
-        >
+      <div ref="nodeAddSelect" :class="['ep-node-add-select-box', 'space-y-3', nodeAddSelectPostion == 1 ? 'ep-node-add-left' : 'ep-node-add-right']" v-if="isShowAddSelect">
+        <div class="ep-node-add-select-item" v-for="(item, index) in nodeSelect" :key="index" v-on:click="addNode(item.type)">
           <Icon class="ep-node-add-select-item-icon" :icon="item.icon.name" :color="item.iconColor" />
           <div class="ep-node-add-select-item-title"> {{ item.title }} </div>
         </div>
@@ -137,10 +130,9 @@
         position: absolute;
         top: 50%;
         z-index: 10;
-        display: flex;
-        gap: 8px;
-        height: 80px;
-        padding: 16px;
+        min-height: 40px;
+        max-width: 200px;
+        padding: 8px;
         background-color: #fff;
         border-radius: 5px;
         box-shadow: 5px 5px 10px 2px rgb(0 0 0 / 20%);
@@ -158,25 +150,25 @@
 
         .ep-node-add-select-item {
           display: flex;
-          flex-direction: column;
+          flex-direction: row;
           align-items: center;
-          justify-content: center;
-          width: 80px;
-          height: 100%;
+          padding: 0 20px;
+          gap: 6px;
+          height: 32px;
+          width: 120px;
           color: #5a5e66;
           cursor: pointer;
           border: 1px solid #ccc;
           border-radius: 8px;
 
-          .ep-node-add-select-item-title {
-            font-size: 14px;
+          &:hover {
+            color: #fff !important;
+            background-color: #1e83e9 !important;
+            .ep-node-add-select-item-icon {
+              transform: scale(1.2);
+            }
           }
         }
-
-        .ep-node-add-selected {
-          color: #fff !important;
-          background-color: #1e83e9 !important;
-        }
       }
 
       .ep-node-add-left {

+ 3 - 3
src/components/business/EasyProcess/node/base/BaseNode.vue

@@ -3,9 +3,9 @@
     <div :class="{ 'ep-node-content': true, 'ep-node-error': isError }">
       <!-- header -->
       <div class="ep-node-header" :style="{ color: config.color, 'background-color': config.bgColor }">
-        <Icon class="ep-node-icon" :icon="config.icon.name" :color="config.icon.name || '#FFFFFF'" />
+        <Icon class="ep-node-icon" :icon="config.icon.name" :color="config.icon.color || '#FFFFFF'" />
         <div class="ep-node-header-title">{{ config.title }}</div>
-        <Icon class="cursor-pointer text-white ep-node-close" :icon="'&#xe83a;'" v-if="canRemoved" @click.stop="removeNode" />
+        <Icon class="ep-node-close cursor-pointer text-white" :icon="'&#xe83a;'" v-if="canRemoved" @click.stop="removeNode" />
       </div>
       <!-- body -->
       <div class="ep-node-body" @click="showNodeDrawer">
@@ -14,7 +14,7 @@
       <!-- 校验错误提示 -->
       <div class="ep-node-error-msg" v-if="isError">
         <div class="ep-node-error-msg-box">
-          <svg-icon icon-class="tips" class="ep-node-error-icon" color="red" @mouseenter="showErrorTips(true)" @mouseleave="showErrorTips(false)" />
+          <icon icon="svg:tips" :width="23" height="23" color="red" @mouseenter="showErrorTips(true)" @mouseleave="showErrorTips(false)" />
           <div class="ep-node-error-tips" v-if="errorTips && errorMsg">{{ errorMsg }}</div>
         </div>
       </div>

+ 64 - 13
src/components/business/EasyProcess/node/condition/conditionDrawer.vue

@@ -1,30 +1,81 @@
 <template>
   <!-- 条件配置 -->
-  <div>
-    <el-form :model="props.config" label-width="80px">
-      <el-form-item label="请假天数">
-        <el-input v-model="props.config.days" />
+  <div class="condition-config">
+    <el-form :model="formModel" label-width="80px">
+      <!-- 条件预览 -->
+      <el-form-item label="当前选择">
+        <el-tag type="primary">{{ selectedOptionLabel }}</el-tag>
+      </el-form-item>
+      <!-- 条件选项 -->
+      <el-form-item label="条件选项">
+        <el-radio-group v-model="optionValue" size="small">
+          <el-radio v-for="item in options" :key="item.value" :value="item.value" :label="item.label" border />
+        </el-radio-group>
       </el-form-item>
     </el-form>
   </div>
 </template>
 
-<script setup name="ConditionDrawer">
-  import { getCurrentInstance, inject } from 'vue'
-  import { KEY_PROCESS_DATA } from '../../config/keys'
+<script setup>
+  import { computed } from 'vue'
 
   const props = defineProps({
     config: {
-      // 传入的流程配置数据
       type: Object,
-      default: {}
+      default: () => ({ value: '1' })
+    }
+  })
+
+  // 使用计算属性创建响应式模型
+  const optionValue = computed({
+    get: () => props.config.value || '1',
+    set: (value) => {
+      emit('update:config', { ...props.config, value })
     }
   })
 
-  const { proxy } = getCurrentInstance()
+  // 定义事件
+  const emit = defineEmits(['update:config'])
 
-  // 获取流程数据
-  const processData = inject(KEY_PROCESS_DATA)
+  // 计算当前选中的选项标签
+  const selectedOptionLabel = computed(() => {
+    const option = options.find((item) => item.value === optionValue.value)
+    return option ? option.label : '未知'
+  })
+
+  // 表单模型(用于el-form绑定)
+  const formModel = computed(() => ({ value: optionValue.value }))
+
+  const options = [
+    { label: '与 (AND)', value: '1' },
+    { label: '或 (OR)', value: '2' },
+    { label: '非 (NOT)', value: '3' }
+  ]
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+  .condition-config {
+    padding: 16px;
+
+    .el-form-item {
+      margin-bottom: 20px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    .el-radio {
+      margin-right: 12px;
+
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+
+    .el-tag {
+      font-size: 14px;
+      padding: 8px 12px;
+    }
+  }
+</style>

+ 25 - 10
src/components/business/EasyProcess/node/condition/conditionNode.vue

@@ -1,8 +1,10 @@
 <template>
   <!-- 条件节点 -->
   <div class="ep-node-condition">
-    <div v-if="!props.node.isLastCondition"> 请假天数: {{ node.nodeData.days }}天 </div>
-    <div v-else> 其他条件进入此流程 </div>
+    <div class="flex flex-row items-center justify-around">
+      <div>条件选项:</div>
+      <el-tag type="primary">{{ selectedOptionLabel }}</el-tag>
+    </div>
   </div>
 </template>
 
@@ -22,21 +24,34 @@
     }
   })
 
+  // 计算当前选中的选项标签
+  const selectedOptionLabel = computed(() => {
+    const option = options.find((item) => item.value === props.node.nodeData.value)
+    return option ? option.label : '未知'
+  })
+
+  const options = [
+    { label: ' 与  (AND) ', value: '1' },
+    { label: ' 或  (OR) ', value: '2' },
+    { label: ' 非  (NOT) ', value: '3' }
+  ]
+
   // 获取流程验证器实例
   const validator = inject(KEY_VALIDATOR)
 
   // 注册验证器
   validator.register(props.tempNodeId, () => {
-    let valid = true
-    if (!props.node.isLastCondition) {
-      if (!props.node.config.days) {
-        valid = false
-      }
+    if (!props.node.nodeData) {
+      return { valid: false, message: '条件数据不能为空' }
+    }
+    const value = props.node.nodeData.value
+    if (!value) {
+      return { valid: false, message: '请选择条件选项' }
     }
-    return {
-      valid: valid,
-      message: '请选择条件'
+    if (!options.find((item) => item.value === value)) {
+      return { valid: false, message: '请选择有效的条件选项' }
     }
+    return { valid: true, message: '' }
   })
 </script>
 

+ 319 - 8
src/components/business/EasyProcess/node/fault/faultDrawer.vue

@@ -1,13 +1,324 @@
 <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>
+    <BasicForm>
+      <!-- 动态参数 -->
+      <template #dynamicParams>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item prop="param1Type">
+              <el-select placeholder="请选择类型">
+                <el-option label="文本" value="1" />
+                <el-option label="数字" value="2" />
+                <el-option label="日期" value="3" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item prop="param1Desc">
+              <el-input placeholder="请输入参数1说明" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </template>
+    </BasicForm>
   </div>
 </template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, onMounted } from 'vue'
+  import type { FormInstance, FormRules } from 'element-plus'
+  import { FormItemSchema, useForm } from '@/components/form'
+
+  const schema: FormItemSchema[] = [
+    {
+      prop: 'roleId',
+      label: '节点角色',
+      component: 'ApiSelect',
+      colSpan: 24,
+      componentProps: {
+        api: '/role/list',
+        labelField: 'roleName',
+        valueField: 'roleID',
+        title: '角色'
+      }
+    },
+    {
+      prop: 'templateID',
+      label: '故障模板',
+      component: 'ApiSelect',
+      colSpan: 24,
+      componentProps: {
+        api: '/templatefault/list',
+        labelField: 'templateName',
+        valueField: 'templateID',
+        title: '故障模板'
+      }
+    },
+    {
+      prop: 'category',
+      label: '节点分类',
+      component: 'Input',
+      colSpan: 24,
+      componentProps: {
+        placeholder: '请输入分类'
+      }
+    },
+    {
+      prop: 'param1Type',
+      label: '节点参数',
+      component: 'Select',
+      colSpan: 10,
+      componentProps: {
+        placeholder: '请选择类型',
+        options: [
+          { label: '文本', value: 1 },
+          { label: '数字', value: 2 },
+          { label: '日期', value: 3 }
+        ]
+      }
+    },
+    {
+      prop: 'param2Desc',
+      label: '',
+      labelWidth: '0px',
+      component: 'Input',
+      colSpan: 14,
+      componentProps: {
+        placeholder: '请输入参数2说明'
+      }
+    },
+    {
+      prop: 'dynamicParams',
+      label: '',
+      labelWidth: '0px',
+      component: 'Input',
+      colSpan: 24,
+      componentProps: {
+        placeholder: '请输入动态参数'
+      }
+    }
+  ]
+  // 表单验证规则
+  const rules: FormRules = {
+    roleId: [{ required: true, message: '请选择节点角色', trigger: 'change' }],
+    templateID: [{ required: true, message: '请选择故障模板', trigger: 'change' }],
+    category: [{ required: true, message: '请选择分类', trigger: 'change' }]
+  }
+  const [BasicForm, formApi] = useForm({ labelWidth: '100px', schema: schema, rules: rules })
+
+  /**
+   * 故障类模板
+   */
+  export interface Fault {
+    templateID: number /** 模板ID */
+    templateName: string /** 模板名称 */
+    category: number /** 分类 */
+    categoryName: string /** 分类名称 */
+    faultLogicMap: number /** 故障逻辑映射 */
+    param1Desc: string /** 参数1说明 */
+    param1Type: number /** 参数1类型 */
+    param2Desc: string /** 参数2说明 */
+    param2Type: number /** 参数2类型 */
+    param3Desc: string /** 参数3说明 */
+    param3Type: number /** 参数3类型 */
+    param4Desc: string /** 参数4说明 */
+    param4Type: number /** 参数4类型 */
+    param5Desc: string /** 参数5说明 */
+    param5Type: number /** 参数5类型 */
+    subCategory: number /** 子分类 */
+    subCategoryName: string /** 子分类名称 */
+  }
+
+  // Props
+  interface Props {
+    config?: Partial<Fault>
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    config: () => ({})
+  })
+
+  // Emits
+  const emit = defineEmits<{
+    'update:config': [value: Partial<Fault>]
+    change: [value: Partial<Fault>]
+  }>()
+
+  // 表单引用
+  const formRef = ref<FormInstance>()
+
+  // 故障数据
+  const fault = reactive<Partial<Fault>>({
+    templateID: 0,
+    templateName: '',
+    category: undefined,
+    categoryName: '',
+    faultLogicMap: undefined,
+    param1Desc: '',
+    param1Type: undefined,
+    param2Desc: '',
+    param2Type: undefined,
+    param3Desc: '',
+    param3Type: undefined,
+    param4Desc: '',
+    param4Type: undefined,
+    param5Desc: '',
+    param5Type: undefined,
+    subCategory: undefined,
+    subCategoryName: '',
+    ...props.config
+  })
+
+  // 选项数据
+  const categoryOptions = ref([
+    { label: '硬件故障', value: 1 },
+    { label: '软件故障', value: 2 },
+    { label: '网络故障', value: 3 },
+    { label: '系统故障', value: 4 }
+  ])
+
+  const subCategoryOptions = ref([
+    { label: 'CPU故障', value: 1, category: 1 },
+    { label: '内存故障', value: 2, category: 1 },
+    { label: '硬盘故障', value: 3, category: 1 },
+    { label: '应用崩溃', value: 1, category: 2 },
+    { label: '服务异常', value: 2, category: 2 },
+    { label: '网络中断', value: 1, category: 3 },
+    { label: '网络延迟', value: 2, category: 3 },
+    { label: '系统资源不足', value: 1, category: 4 },
+    { label: '系统崩溃', value: 2, category: 4 }
+  ])
+
+  const paramTypeOptions = ref([
+    { label: '字符串', value: 1 },
+    { label: '数字', value: 2 },
+    { label: '布尔值', value: 3 },
+    { label: '枚举', value: 4 },
+    { label: '时间', value: 5 }
+  ])
+
+  const faultLogicOptions = ref([
+    { label: '立即触发', value: 1 },
+    { label: '延迟触发', value: 2 },
+    { label: '条件触发', value: 3 },
+    { label: '累计触发', value: 4 }
+  ])
+
+  // 计算属性:过滤子分类选项
+  const filteredSubCategoryOptions = computed(() => {
+    if (!fault.category) return []
+    return subCategoryOptions.value.filter((item) => item.category === fault.category)
+  })
+
+  // 处理方法
+  const handleCategoryChange = (value: number) => {
+    // 清除已选择的子分类
+    fault.subCategory = undefined
+    fault.subCategoryName = ''
+
+    // 设置分类名称
+    const category = categoryOptions.value.find((item) => item.value === value)
+    if (category) {
+      fault.categoryName = category.label
+    }
+  }
+
+  // 表单验证
+  const validate = (): Promise<boolean> => {
+    return new Promise((resolve) => {
+      if (!formRef.value) {
+        resolve(false)
+        return
+      }
+
+      formRef.value.validate((valid) => {
+        resolve(valid)
+      })
+    })
+  }
+
+  // 重置表单
+  const resetForm = () => {
+    if (formRef.value) {
+      formRef.value.resetFields()
+    }
+  }
+
+  // 获取表单数据
+  const getFormData = (): Partial<Fault> => {
+    return { ...fault }
+  }
+
+  // 监听数据变化
+  watch(
+    () => fault,
+    (newValue) => {
+      emit('update:config', newValue)
+      emit('change', newValue)
+    },
+    { deep: true }
+  )
+
+  // 监听子分类变化
+  watch(
+    () => fault.subCategory,
+    (value) => {
+      if (value) {
+        const subCategory = subCategoryOptions.value.find((item) => item.value === value)
+        if (subCategory) {
+          fault.subCategoryName = subCategory.label
+        }
+      }
+    }
+  )
+
+  // 暴露方法给父组件
+  defineExpose({
+    validate,
+    resetForm,
+    getFormData
+  })
+
+  onMounted(() => {
+    // 初始化逻辑
+    console.log('FaultTrigger component mounted')
+  })
+</script>
+
+<style lang="scss" scoped>
+  .fault-trigger-config {
+    padding: 16px;
+
+    .header {
+      font-size: 16px;
+      font-weight: 600;
+      margin-bottom: 20px;
+      color: #303133;
+      padding-bottom: 8px;
+      border-bottom: 1px solid #e4e7ed;
+    }
+
+    .form-container {
+      max-width: 600px;
+
+      .param-row {
+        display: flex;
+        align-items: center;
+
+        .el-input,
+        .el-select {
+          flex-shrink: 0;
+        }
+      }
+
+      .el-form-item {
+        margin-bottom: 20px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+</style>

+ 45 - 0
src/components/business/EasyProcess/node/fault/index.vue

@@ -0,0 +1,45 @@
+<template>
+  <Dialog>
+    <BasicForm />
+  </Dialog>
+</template>
+<script setup lang="tsx">
+  import { useDialog } from '@/components/dialog'
+  import { CrudMethods } from '@/components/crud'
+  import { useForm } from '@/components/form'
+  const record = ref<any>({})
+  const crudApi = ref<CrudMethods>()
+  const [Dialog, dialogApi] = useDialog({
+    title: '我是标题',
+    width: '50%',
+    dialogType: 'drawer',
+    onConfirm() {
+      formApi.validate().then(async () => {
+        const values = await formApi.getValues()
+        console.log(values)
+      })
+    }
+  })
+  const [BasicForm, formApi] = useForm({
+    labelWidth: '120px',
+    schema: [
+      {
+        prop: 'name',
+        label: '姓名',
+        component: 'Input',
+        rules: [{ required: true, message: '请输入姓名' }]
+      }
+    ]
+  })
+  defineExpose({
+    show({ data, getCrudApi }: { data: any; getCrudApi: () => CrudMethods }) {
+      record.value = data
+      crudApi.value = getCrudApi()
+      dialogApi.open()
+      nextTick(() => {
+        formApi.setValues(record.value)
+      })
+    }
+  })
+</script>
+<style lang="scss" scoped></style>

+ 2 - 11
src/components/business/EasyProcess/node/router/routerNode.vue

@@ -3,18 +3,9 @@
   <div class="ep-node-router">
     <div class="ep-condition-node-add" @click="addCondition">添加条件</div>
     <div class="ep-node-router-box">
-      <div
-        v-for="(condition, index) in props.node.conditionNodes"
-        :key="condition.id || index"
-        class="ep-node-router-col"
-      >
+      <div v-for="(condition, index) in props.node.conditionNodes" :key="condition.id || index" class="ep-node-router-col">
         <!-- 生成节点 -->
-        <NodeWrap
-          :node="condition"
-          :conditionNodes="props.node.conditionNodes"
-          :conditionIndex="index"
-          @removeNode="removeCondition(index)"
-        />
+        <NodeWrap :node="condition" :conditionNodes="props.node.conditionNodes" :conditionIndex="index" @removeNode="removeCondition(index)" />
         <!-- 用来遮挡最左列的线 -->
         <template v-if="index == 0">
           <div class="cover-line top-left-cover-line"></div>

+ 2 - 12
src/components/core/cards/art-bar-chart-card/index.vue

@@ -7,20 +7,10 @@
           <p class="value">{{ value }}</p>
           <p class="label">{{ label }}</p>
         </div>
-        <div
-          class="percentage"
-          :class="{ 'is-increase': percentage > 0, 'is-mini-chart': isMiniChart }"
-        >
-          {{ percentage > 0 ? '+' : '' }}{{ percentage }}%
-        </div>
+        <div class="percentage" :class="{ 'is-increase': percentage > 0, 'is-mini-chart': isMiniChart }"> {{ percentage > 0 ? '+' : '' }}{{ percentage }}% </div>
         <div class="date" v-if="date" :class="{ 'is-mini-chart': isMiniChart }">{{ date }}</div>
       </div>
-      <div
-        ref="chartRef"
-        class="chart-container"
-        :class="{ 'is-mini-chart': isMiniChart }"
-        :style="{ height: `calc(${height}rem - 5rem)` }"
-      ></div>
+      <div ref="chartRef" class="chart-container" :class="{ 'is-mini-chart': isMiniChart }" :style="{ height: `calc(${height}rem - 5rem)` }"></div>
     </div>
   </div>
 </template>

+ 1 - 3
src/components/core/cards/art-data-list-card/index.vue

@@ -18,9 +18,7 @@
           <div class="item-time">{{ item.time }}</div>
         </div>
       </ElScrollbar>
-      <ElButton class="more-btn" v-if="showMoreButton" v-ripple @click="handleMore"
-        >查看更多</ElButton
-      >
+      <ElButton class="more-btn" v-if="showMoreButton" v-ripple @click="handleMore">查看更多</ElButton>
     </div>
   </div>
 </template>

+ 1 - 7
src/components/core/cards/art-donut-chart-card/index.vue

@@ -72,13 +72,7 @@
       isEmpty: props.data.every((val) => val === 0)
     },
     checkEmpty: () => props.data.every((val) => val === 0),
-    watchSources: [
-      () => props.data,
-      () => props.color,
-      () => props.radius,
-      () => props.currentValue,
-      () => props.previousValue
-    ],
+    watchSources: [() => props.data, () => props.color, () => props.radius, () => props.currentValue, () => props.previousValue],
     generateOptions: (): EChartsOption => {
       const computedColor = props.color || useChartOps().themeColor
 

+ 7 - 39
src/components/core/forms/art-drag-verify/index.vue

@@ -1,23 +1,8 @@
 <!-- 拖拽验证组件 -->
 <template>
-  <div
-    ref="dragVerify"
-    class="drag_verify"
-    :style="dragVerifyStyle"
-    @mousemove="dragMoving"
-    @mouseup="dragFinish"
-    @mouseleave="dragFinish"
-    @touchmove="dragMoving"
-    @touchend="dragFinish"
-  >
+  <div ref="dragVerify" class="drag_verify" :style="dragVerifyStyle" @mousemove="dragMoving" @mouseup="dragFinish" @mouseleave="dragFinish" @touchmove="dragMoving" @touchend="dragFinish">
     <!-- 进度条 -->
-    <div
-      class="dv_progress_bar"
-      :class="{ goFirst2: isOk }"
-      ref="progressBar"
-      :style="progressBarStyle"
-    >
-    </div>
+    <div class="dv_progress_bar" :class="{ goFirst2: isOk }" ref="progressBar" :style="progressBarStyle"> </div>
 
     <!-- 提示文本 -->
     <div class="dv_text" :style="textStyle" ref="messageRef">
@@ -27,14 +12,7 @@
     </div>
 
     <!-- 滑块处理器 -->
-    <div
-      class="dv_handler dv_handler_bg"
-      :class="{ goFirst: isOk }"
-      @mousedown="dragStart"
-      @touchstart="dragStart"
-      ref="handler"
-      :style="handlerStyle"
-    >
+    <div class="dv_handler dv_handler_bg" :class="{ goFirst: isOk }" @mousedown="dragStart" @touchstart="dragStart" ref="handler" :style="handlerStyle">
       <i class="iconfont-sys" v-html="value ? successIcon : handlerIcon"></i>
     </div>
   </div>
@@ -107,7 +85,7 @@
   }
 
   // 响应式状态定义
-  const state = reactive(<StateType>{
+  const state = reactive<StateType>({
     isMoving: false,
     x: 0,
     isOk: false
@@ -213,9 +191,7 @@
   const progressBarStyle = {
     background: props.progressBarBg,
     height: props.height + 'px',
-    borderRadius: props.circle
-      ? props.height / 2 + 'px 0 0 ' + props.height / 2 + 'px'
-      : props.radius
+    borderRadius: props.circle ? props.height / 2 + 'px 0 0 ' + props.height / 2 + 'px' : props.radius
   }
 
   // 文本样式计算
@@ -237,8 +213,7 @@
       state.isMoving = true
       handler.value.style.transition = 'none'
       // 计算拖拽起始位置
-      state.x =
-        (e.pageX || e.touches[0].pageX) - parseInt(handler.value.style.left.replace('px', ''), 10)
+      state.x = (e.pageX || e.touches[0].pageX) - parseInt(handler.value.style.left.replace('px', ''), 10)
     }
     emit('handlerMove')
   }
@@ -377,14 +352,7 @@
       justify-content: center;
       color: transparent;
       user-select: none;
-      background: linear-gradient(
-        to right,
-        var(--textColor) 0%,
-        var(--textColor) 40%,
-        #fff 50%,
-        var(--textColor) 60%,
-        var(--textColor) 100%
-      );
+      background: linear-gradient(to right, var(--textColor) 0%, var(--textColor) 40%, #fff 50%, var(--textColor) 60%, var(--textColor) 100%);
       -webkit-background-clip: text;
       background-clip: text;
       animation: slidetounlock 2s cubic-bezier(0, 0.2, 1, 1) infinite;

+ 3 - 8
src/components/form/components/divider.vue

@@ -7,14 +7,9 @@
 <script setup lang="ts">
   // import { useChart } from '@/composables/useChart'
 
-  withDefaults(
-    defineProps<{
-      title?: string
-    }>(),
-    {
-      title: ''
-    }
-  )
+  withDefaults(defineProps<{ title?: string }>(), {
+    title: ''
+  })
   // 使用基础的 useChart hook
   // const { isDark } = useChart()
 </script>

+ 3 - 3
src/components/form/detail-form.vue

@@ -12,9 +12,9 @@
         <ElCol v-for="item in schemaList" :key="item.prop" :span="item.colSpan || 24">
           <ElFormItem :prop="item.prop" :label="item.label" :label-width="item.labelWidth || formSchemaRef.labelWidth || '80px'" :class="{ 'divider-form-item': item.component === 'Divider' }">
             <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>

+ 15 - 4
src/components/form/form.vue

@@ -21,7 +21,8 @@
               ></span>
             </template>
             <!-- 字段插槽优先级最高 -->
-            <slot v-if="$slots[item.prop]" :name="item.prop" :form-model="formModel" :model="formModel" :field="item" />
+            <span v-if="item.component == 'Span'" class="form-span">{{ get(formModel, item.prop) }}</span>
+            <slot v-else-if="$slots[item.prop]" :name="item.prop" :form-model="formModel" :model="formModel" :field="item" />
             <template v-else>
               <component
                 style="padding-right: 8px"
@@ -74,13 +75,14 @@
     }
   })
 
-  const props = withDefaults(defineProps<{ formSchema: FormSchema; formMethods?: any }>(), {
+  const props = withDefaults(defineProps<{ formModelInit: any; formSchema: FormSchema; formMethods?: any }>(), {
     formSchema: () => {
       return { schema: [] }
-    }
+    },
+    formModelInit: () => {}
   })
 
-  const formModel = reactive<any>({})
+  const formModel = reactive<any>({ ...props.formModelInit })
   const formSchemaRef = ref<FormSchema>(props.formSchema)
 
   const emit = defineEmits<{
@@ -89,6 +91,15 @@
     (e: 'register', formInstance: FormInstance): void
     (e: 'change', values: any): void
   }>()
+  watch(
+    () => props.formModelInit,
+    (newVal) => {
+      if (newVal) {
+        Object.assign(formModel, newVal)
+      }
+    },
+    { immediate: true, deep: true }
+  )
   // 判断是否需要添加顶部间距
   const shouldAddTopSpacing = computed(() => {
     // 获取第一个显示的组件

+ 1 - 0
src/components/form/use-form.ts

@@ -114,6 +114,7 @@ export function useForm(formSchema: FormSchema) {
           BasicForm,
           {
             ...attrs,
+            formModelInit: attrs.formModelInit || {},
             formSchema: formSchema,
             formMethods: formMethods,
             onRegister: register

Разница между файлами не показана из-за своего большого размера
+ 5 - 0
src/components/icon/svg/tips.svg


+ 1 - 1
src/store/modules/user.ts

@@ -47,7 +47,7 @@ export const useUserStore = defineStore(
      * @param newInfo 新的用户信息
      */
     const setUserInfo = (newInfo: UserItem) => {
-      userInfo.value = newInfo
+      userInfo.value = Object.assign({ roleCodes: [], buttons: [] }, newInfo)
     }
 
     /**

+ 2 - 2
src/views/train/course/course-list.vue

@@ -27,12 +27,12 @@
             </div>
             <div class="bottom space-y-2">
               <h2>{{ item.courseName }}</h2>
-              <div class="desc">{{ item.courseDesc }}</div>
+              <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> -->
                 <el-button size="small" type="text"> 培训记录 </el-button>
               </div>
             </div>

+ 40 - 14
src/views/train/course/detail/course.schem.ts

@@ -1,29 +1,56 @@
-import { FormItemSchema, useForm } from '@/components/form'
+import { FormItemSchema } from '@/components/form'
+import { FormRules } from 'element-plus'
 
 const options = ref([
-  { label: '选项一', value: '1' },
-  { label: '选项二', value: '2' },
-  { label: '选项三', value: '3' }
+  { label: '选项一', value: 1 },
+  { label: '选项二', value: 2 },
+  { label: '选项三', value: 3 }
 ])
-const schema: FormItemSchema[] = [
+export const rules: FormRules = {
+  courseName: [{ required: true, message: '请输入课程名称', trigger: ['blur'] }],
+  courseDesc: [{ required: true, message: '请输入课程描述', trigger: ['blur'] }],
+  totalDuration: [
+    { required: true, message: '请输入课程时长', trigger: ['blur'] },
+    { type: 'number', message: '请输入数字', trigger: ['blur'] },
+    { min: 1, message: '请输入大于等于1的数字', trigger: ['blur'] }
+  ]
+}
+export 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: 'totalDuration', label: '课程时长', component: 'InputNumber', colSpan: 6, componentProps: { style: { width: '100%' } }, defaultValue: 30 },
   { prop: 'typeID', label: '分类ID', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
+  { prop: 'externalTypeID', label: '外部分类ID', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
   { prop: 'courseDesc', label: '课程描述', component: 'Input', colSpan: 12 },
+  { 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: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 12 },
+  { prop: 'guideFilePath', label: '指导文件地址', component: 'Upload', colSpan: 12 },
+  // 课程运行流程
+  { 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 courseSchema: FormItemSchema[] = [
+  // 课程基本信息
+  { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '课程基本信息' } },
+  { prop: 'courseID', label: '课程ID', component: 'Span', colSpan: 6 },
+  { prop: 'courseGUID', label: '课程GUID', component: 'Span', colSpan: 6 },
+  { prop: 'courseName', label: '课程名称', component: 'Input', colSpan: 6 },
+  { prop: 'totalDuration', label: '课程时长', component: 'InputNumber', colSpan: 6, componentProps: { style: { width: '100%' } }, defaultValue: 30 },
+  { prop: 'courseDesc', label: '课程描述', component: 'Input', colSpan: 24 },
+  { prop: 'typeID', label: '分类ID', component: 'Select', colSpan: 6, defaultValue: '1', componentProps: { options: options.value, clearable: true } },
   { 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: 'creator', label: '创建人', component: 'Span', colSpan: 6 },
+  { prop: 'createTime', label: '创建时间', component: 'Span', colSpan: 6 },
+  { prop: 'modifyTime', label: '修改时间', component: 'Span', colSpan: 6 },
   // 课程运行流程
   { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '课程运行流程' } },
   { prop: 'courseDataStream', labelWidth: '0px', label: '', component: 'Input', colSpan: 24 },
@@ -31,4 +58,3 @@ const schema: FormItemSchema[] = [
   { 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 })

+ 78 - 31
src/views/train/course/detail/index.vue

@@ -8,21 +8,25 @@
         <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>
-        <courseEvaluate />
-      </template>
-    </BasicForm>
+    <div v-loading="loading" class="mt-4">
+      <BasicForm :formModelInit="courseDetail" @change="handleChange">
+        <template #courseDataStream>
+          <courseFlow />
+        </template>
+        <template #evaluate>
+          <courseEvaluate />
+        </template>
+      </BasicForm>
+    </div>
   </div>
 </template>
 <script setup lang="ts">
   import courseFlow from './course-flow.vue'
   import courseEvaluate from './course-evaluate.vue'
-  import { BasicForm } from './course.schem'
+  import { schema, courseSchema, rules } from './course.schem'
   import { Course, CourseServer } from '@/api/course.api'
+  import { useForm } from '@/components/form'
+  import { router } from '@/router'
 
   // 路由参数
   const route = useRoute()
@@ -32,38 +36,81 @@
   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 loading = ref(true)
+  const courseDetail = ref<Course | undefined>(undefined)
+
+  const [BasicForm, formApi] = useForm({ labelPosition: 'right', labelWidth: '150px', schema: courseNew.value ? schema : courseSchema, rules })
+  const formData = ref<any>({})
+  const handleChange = (val: any) => (formData.value = val)
 
-  const addCourse = () => {
-    console.log('addCourse', courseDetail.value)
+  const addCourse = async () => {
+    formApi
+      .validate()
+      .then((data2) => {
+        console.log('validate success', data2)
+        const data = formData.value
+        if (courseNew.value) {
+          CourseServer.add({
+            courseName: data.courseName,
+            courseDesc: data.courseDesc,
+            totalDuration: Number(data.totalDuration || 10),
+            typeID: Number(data.typeID || 1),
+            courseDataStream: data.courseDataStream,
+            externalTypeID: Number(data.externalTypeID || 1),
+            guideDesc: data.guideDesc,
+            guideFilePath: data.guideFilePath,
+            lineNo: Number(data.lineNo || 1),
+            runningMap: Number(data.runningMap || 1),
+            virtualRole: data.virtualRole,
+            virtualStartTime: Number(data.virtualStartTime || 0)
+          })
+            .then(() => {
+              ElMessage.success('新增课程成功')
+              router.push({ path: '/course/list' })
+            })
+            .catch(() => ElMessage.error('新增课程失败'))
+        } else if (courseEdit.value) {
+          CourseServer.update({
+            courseID: data.courseID,
+            courseName: data.courseName,
+            courseDesc: data.courseDesc,
+            totalDuration: Number(data.totalDuration || 10),
+            typeID: Number(data.typeID || 1),
+            courseDataStream: data.courseDataStream,
+            externalTypeID: Number(data.externalTypeID || 1),
+            courseGUID: data.courseGUID,
+            guideDesc: data.guideDesc,
+            guideFilePath: data.guideFilePath,
+            lineNo: Number(data.lineNo || 1),
+            runningMap: Number(data.runningMap || 1),
+            virtualRole: data.virtualRole,
+            virtualStartTime: Number(data.virtualStartTime || 0)
+          })
+            .then(() => {
+              ElMessage.success('新增课程成功')
+              router.push({ path: '/course/list' })
+            })
+            .catch(() => ElMessage.error('新增课程失败'))
+        } else {
+          ElMessage.error('不能编辑课程详情')
+        }
+      })
+      .catch(() => {
+        ElMessage.error('请检查课程信息')
+      })
   }
 
   onMounted(() => {
-    console.log(courseId.value)
+    loading.value = true
     if (!courseNew.value) {
       CourseServer.detail(courseId.value)
         .then(({ data }) => {
           courseDetail.value = data
+          loading.value = false
         })
         .catch(() => ElMessage.error('获取课程数据失败'))
+    } else {
+      loading.value = false
     }
   })
 </script>

+ 63 - 0
src/views/train/evaluation/basic.vue

@@ -0,0 +1,63 @@
+<!-- 基础表格 -->
+<template>
+  <div class="user-page art-full-height">
+    <ElCard class="art-table-card" shadow="never" style="margin-top: 0">
+      <!-- 表格 -->
+      <ArtTable
+        rowKey="id"
+        :show-table-header="false"
+        :loading="loading"
+        :data="data"
+        :columns="columns"
+        :pagination="pagination"
+        @pagination:size-change="handleSizeChange"
+        @pagination:current-change="handleCurrentChange"
+      >
+      </ArtTable>
+    </ElCard>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { useTable } from '@/composables/useTable'
+  import { fetchGetUserList } from '@/api/system-manage'
+
+  defineOptions({ name: 'UserMixedUsageExample' })
+
+  const { data, columns, loading, pagination, handleSizeChange, handleCurrentChange } = useTable({
+    core: {
+      apiFn: fetchGetUserList,
+      apiParams: {
+        current: 1,
+        size: 20,
+        userName: '',
+        userPhone: '',
+        userEmail: ''
+      },
+      columnsFactory: () => [
+        {
+          prop: 'id',
+          label: 'ID'
+        },
+        {
+          prop: 'nickName',
+          label: '昵称'
+        },
+        {
+          prop: 'userGender',
+          label: '性别',
+          sortable: true,
+          formatter: (row) => row.userGender || '未知'
+        },
+        {
+          prop: 'userPhone',
+          label: '手机号'
+        },
+        {
+          prop: 'userEmail',
+          label: '邮箱'
+        }
+      ]
+    }
+  })
+</script>

+ 46 - 5
src/views/train/evaluation/index.vue

@@ -1,8 +1,49 @@
 <template>
-  <div>
-    <h2></h2>
-    <div>正在开发中,敬请期待</div>
-  </div>
+  <!-- 表格区域 -->
+  <ElCard class="art-table-card" shadow="never" style="margin-top: 0">
+    <ArtTableHeader :loading="loading" @refresh="() => refreshData()" layout="refresh,size,fullscreen,settings" fullClass="art-table-card">
+      <template #left>
+        <h4 class="font-bold">评价系统</h4>
+      </template>
+    </ArtTableHeader>
+
+    <ArtTable ref="tableRef" :loading="loading" :data="data" :columns="columns" empty-height="360px">
+      <!-- 用户信息列 -->
+      <template #avatar="{ row }">
+        <div class="user-info">
+          <ElAvatar :src="row.avatar" :size="40" />
+          <div class="user-details">
+            <p class="user-name">{{ row.userName }}</p>
+            <p class="user-email">{{ row.userEmail }}</p>
+          </div>
+        </div>
+      </template>
+    </ArtTable>
+  </ElCard>
 </template>
-<script lang="ts" setup></script>
+
+<script setup lang="ts">
+  import { ref } from 'vue'
+  import { useTable } from '@/composables/useTable'
+  import { fetchGetUserList } from '@/api/system-manage'
+
+  const tableRef = ref()
+
+  const { data, loading, columns, refreshData } = useTable({
+    core: {
+      apiFn: (params) => fetchGetUserList(params),
+      columnsFactory: () => [
+        { type: 'globalIndex', width: 80, label: '序号' },
+        { prop: '1', label: '阶段', minWidth: 200 },
+        { prop: '2', label: '关联模块', width: 200 },
+        { prop: '3', label: '评价对象', minWidth: 200 },
+        { prop: '4', label: '评价顶点', minWidth: 200 },
+        { prop: '5', label: '评价类型', minWidth: 200 },
+        { prop: '6', label: '配分分数', width: 100 },
+        { prop: '7', label: '得分分数', width: 100 }
+      ]
+    }
+  })
+</script>
+
 <style lang="scss" scoped></style>

+ 124 - 0
src/views/train/evaluation/tree.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="art-full-height">
+    <div class="tree-container">
+      <div class="left-sidebar">
+        <ElCard class="art-table-card" shadow="never" style="margin-top: 0">
+          <b>左右布局示例:</b>
+          <p style="margin-top: 20px">例如:分类、树形结构等</p>
+        </ElCard>
+      </div>
+
+      <div class="right-content art-full-height">
+        <UserSearch v-model="defaultFilter" />
+
+        <ElCard class="art-table-card" shadow="never">
+          <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+            <template #left>
+              <ElSpace wrap>
+                <ElButton @click="showButtons = !showButtons" v-ripple type="primary" plain>{{ showButtons ? '收起' : '展开' }}按钮组</ElButton>
+                <ElButton v-show="showButtons" v-ripple v-for="value in 12" :key="value">表格自适应</ElButton>
+              </ElSpace>
+            </template>
+          </ArtTableHeader>
+
+          <ArtTable rowKey="id" :loading="loading" :data="data" :columns="columns" :pagination="pagination" @pagination:size-change="handleSizeChange" @pagination:current-change="handleCurrentChange">
+          </ArtTable>
+        </ElCard>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { useTable } from '@/composables/useTable'
+  import { fetchGetUserList } from '@/api/system-manage'
+  import { ElButton, ElCard } from 'element-plus'
+  import UserSearch from '@/views/system/user/modules/user-search.vue'
+
+  defineOptions({ name: 'TreeTable' })
+
+  const showButtons = ref(false)
+
+  // 表单搜索初始值
+  const defaultFilter = ref({
+    name: undefined
+  })
+
+  const { data, columns, columnChecks, loading, pagination, refreshData, handleSizeChange, handleCurrentChange } = useTable({
+    core: {
+      apiFn: fetchGetUserList,
+      apiParams: {
+        current: 1,
+        size: 20,
+        userName: '',
+        userPhone: '',
+        userEmail: ''
+      },
+      columnsFactory: () => [
+        {
+          prop: 'id',
+          label: 'ID'
+        },
+        {
+          prop: 'nickName',
+          label: '昵称'
+        },
+        {
+          prop: 'userGender',
+          label: '性别',
+          sortable: true,
+          formatter: (row) => row.userGender || '未知'
+        },
+        {
+          prop: 'userPhone',
+          label: '手机号'
+        },
+        {
+          prop: 'userEmail',
+          label: '邮箱'
+        }
+      ]
+    }
+  })
+</script>
+
+<style lang="scss" scoped>
+  .tree-container {
+    box-sizing: border-box;
+    display: flex;
+    gap: 16px;
+    height: 100%;
+
+    .left-sidebar {
+      flex-shrink: 0;
+      width: 230px;
+      height: 100%;
+    }
+
+    .right-content {
+      flex-grow: 1;
+      min-width: 0;
+      height: 100%;
+    }
+
+    .art-table-card {
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+    }
+  }
+
+  @media screen and (max-width: $device-ipad) {
+    .tree-container {
+      display: block;
+      gap: 0;
+      height: auto;
+
+      .left-sidebar {
+        width: 100%;
+        height: auto;
+        margin-bottom: 20px;
+      }
+    }
+  }
+</style>

+ 37 - 61
src/views/train/trainer/course-list/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">
         <!-- 骨架屏 -->
-        <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,27 +15,20 @@
           </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>
                   </div>
                 </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>
-                <ElButton size="small" @click.stop="toEdit(item)">立即开课</ElButton>
+            <div class="bottom space-y-2">
+              <h2>{{ item.courseName }}</h2>
+              <div class="desc">{{ item.courseDesc || '暂无描述' }}</div>
+              <div>
+                <el-button type="primary" :icon="DataLine" @click.stop="startCourse(item)" style="width: 100%">开课</el-button>
               </div>
             </div>
           </template>
@@ -43,69 +36,45 @@
       </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, DataLine } from '@element-plus/icons-vue'
   import { useDateFormat } from '@vueuse/core'
-  import { useCommon } from '@/composables/useCommon'
-
   import cover1 from '@imgs/cover/img5.webp'
+  import cover2 from '@imgs/cover/img2.webp'
+  import cover3 from '@imgs/cover/img3.webp'
+  import { type Course } from '@/api/course.api'
+  import { router } from '@/router'
 
-  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 props = withDefaults(defineProps<{ data: Course[]; loading: boolean }>(), {
+    data: () => [],
+    loading: true
   })
 
-  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 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: `/trainer/course/run/${item.id}` })
+  const startCourse = (item: Course) => {
+    router.push({ path: '/trainer/course/run/' + item.courseID })
   }
 </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;
@@ -168,6 +137,13 @@
 
             @include ellipsis();
           }
+          .desc {
+            font-size: 12px;
+            font-weight: 500;
+            color: var(--art-text-gray-600);
+
+            @include ellipsis();
+          }
 
           .info {
             display: flex;

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

@@ -1,15 +1,30 @@
 <!-- 表格搜索栏示例 -->
 <template>
   <div class="search-bar">
-    <h2 class="title">课程列表</h2>
+    <h2 class="title">教员系统</h2>
     <course-search></course-search>
-    <course-list></course-list>
+    <course-list :data="courseData" :loading="loading"></course-list>
   </div>
 </template>
 
 <script setup lang="ts">
   import courseSearch from './course-search.vue'
   import courseList from './course-list.vue'
+  import { Course, CourseServer } from '@/api/course.api'
+
+  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
+  }
 </script>
 
 <style scoped lang="scss">

+ 12 - 0
src/views/train/trainer/course-run/index.vue

@@ -34,6 +34,18 @@
 </template>
 <script lang="ts" setup>
   import cover1 from '@imgs/cover/img5.webp'
+  import { Picture as IconPicture } from '@element-plus/icons-vue'
+  import { Course, CourseServer } from '@/api/course.api'
+
+  // 路由参数
+  const route = useRoute()
+  const courseId = computed(() => Number(route.params.id))
+  const course = ref<Course>()
+
+  onMounted(async () => {
+    const { data } = await CourseServer.detail(courseId.value)
+    course.value = data
+  })
 </script>
 <style lang="scss" scoped>
   .border {