Преглед изворни кода

feat(课程流程): 实现课程运行时的流程交互功能

- 添加流程节点选中事件总线支持
- 为EasyProcess组件增加编辑状态控制
- 实现训练运行页面的流程交互和状态管理
- 添加课程运行相关API接口定义
- 修复课程详情页的schema文件命名错误
zhanghaijun пре 11 месеци
родитељ
комит
69471b16a4

+ 1 - 0
.vscode/settings.json

@@ -51,6 +51,7 @@
     "Cascader",
     "checkboxgroup",
     "iconfont",
+    "Oper",
     "persistedstate",
     "pinia",
     "unplugin",

+ 78 - 0
src/api/course.api.ts

@@ -106,3 +106,81 @@ export class ProcessServer {
   // 语音模板
   public static voiceList = () => request.post<ProcessVoice[]>('/templatevoice/list')
 }
+
+/** 课程运行状态 */
+export interface CourseRunStatus {
+  courseScore: number /** 课程得分 */
+  courseTime: number /** 课程用时单位秒 */
+  pauseFlag: number /** 1:已经暂停,1运行中,-1课程结束 */
+  pauseTotalTime: number /** 暂停累计时长 */
+  runCourseID: number /** 运行课程ID */
+  runCourseName: string /** 运行课程名称 */
+  runDateTime: string /** 运行日期时间 */
+  runLogID: number /** 运行记录编号 */
+}
+/** 课程操作记录 */
+export interface CourseOperationRecord {
+  OperName: string
+  OperParam: string
+  OperParam1: string
+  OperParam2: string
+  OperParam3: string
+  OperParam4: string
+  OperScore: number
+  OperTime: number
+  RunCourseID: number /** 运行课程ID */
+  RunLogID: number /** 运行记录编号 */
+  RunOperID: number
+}
+/** 课程的评分记录 */
+export interface CourseScoreRecord {
+  /** 课程id */
+  courseID: number
+  /** 评价失分 */
+  evaDeduction: number
+  /** 评价映射ID */
+  evaMapID: number
+  /** 评价名称 */
+  evaName: string
+  /** 评价赋分 */
+  evaScore: number
+  /** 评价来源 0来自流程图,1.来自教员自定义 */
+  evaSource: number
+  /** 评价目标角色 */
+  evaTargetRole: number
+  /** 评价类型 0主观评价1客观评价 */
+  evaType: number
+  /** 序号,带插入功能,将插入之前的序号,之前的序号向后累加 */
+  seqNo: number
+}
+// 课程运行接口
+export class CourseRunServer {
+  // 课程运行
+  public static start = (courseId: string) => request.post('/course/run', { courseId: Number(courseId) })
+  // 课程结束
+  public static end = (courseId: string) => request.post('/course/stop', { courseId: Number(courseId) })
+  // 暂停/回复
+  public static pauseOrResume = (courseId: string) => request.post('/course/pause', { courseId: Number(courseId) })
+  // 当前课程的运行状态
+  public static status = () => request.post<CourseRunStatus>('/course/status', {})
+  // 评分列表
+  public static scoreList = (runLogID: number) => request.post<CourseOperationRecord[]>('/eva/record/list', { runLogID: Number(runLogID) })
+  // 设置评分分数
+  public static setScore = (eva: {
+    /** 操作得分 */
+    operScore: number
+    /** 运行记录编号 */
+    runLogID: number
+    /** 运行操作ID */
+    runOperID: number
+  }) => request.post('/eva/record/set', eva)
+
+  // 课程评价列表
+  public static evaList = (courseId: string) => request.post<CourseScoreRecord[]>('/eva/record/list', { courseId: Number(courseId) })
+  // 添加评论条目
+  public static evaAdd = (data: CourseScoreRecord) => request.post('/eva/add', data)
+  // 更新评论条目
+  public static evaUpdate = (data: CourseScoreRecord) => request.post('/eva/update', data)
+  // 删除评论条目
+  public static evaDelete = (courseID: string, seqNo: number) => request.post('/eva/delete', { courseID: Number(courseID), seqNo })
+}

+ 2 - 1
src/components/business/EasyProcess/common/process.data.ts

@@ -2,7 +2,8 @@ import { EasyProcess } from './process.type'
 
 export const ProcessConstant = {
   KEY_PROCESS_VALIDATOR: 'process_validator',
-  KEY_PROCESS_HELPER: 'process_helper'
+  KEY_PROCESS_HELPER: 'process_helper',
+  KEY_EDIT: 'process_editable'
 }
 
 export const generateId = (): string => new Date().getTime() + '' + Math.floor(Math.random() * 1000 + 1)

+ 9 - 2
src/components/business/EasyProcess/core/node_base.vue

@@ -5,7 +5,7 @@
       <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.color || '#FFFFFF'" />
         <div class="ep-node-header-title">{{ config.title }}</div>
-        <Icon class="ep-node-close cursor-pointer text-white" :icon="'&#xe83a;'" v-if="canRemoved" @click.stop="removeNode" />
+        <Icon class="ep-node-close cursor-pointer text-white" :icon="'&#xe83a;'" v-if="canRemoved && isEdit" @click.stop="removeNode" />
       </div>
       <!-- body -->
       <div class="ep-node-body" @click="showNodeDrawer">
@@ -19,13 +19,14 @@
         </div>
       </div>
     </div>
-    <NodeAdd :node="props.node" />
+    <NodeAdd v-if="isEdit" :node="props.node" />
     <DrawerWrap :node="props.node" ref="drawerWrap" />
   </div>
 </template>
 
 <script setup lang="ts" name="BaseNode">
   import { ref, shallowRef, watch, computed, onUnmounted, inject } from 'vue'
+  import { mittBus } from '@/utils/sys'
   import DrawerWrap from './drawer_wrap.vue'
   import NodeAdd from './node_add.vue'
   import { nodeConfigData, ProcessConstant } from '../common/process.data'
@@ -48,6 +49,8 @@
 
   const props = withDefaults(defineProps<{ node: EasyProcess.NodeConfig }>(), {})
 
+  const isEdit = inject<boolean>(ProcessConstant.KEY_EDIT, true)
+
   // 节点配置数据
   const config = ref(nodeConfigData[props.node.nodeType])
   const isStart = computed(() => props.node.nodeType == 'start')
@@ -85,6 +88,10 @@
   // 显示节点配置组件
   const drawerWrap = ref()
   const showNodeDrawer = () => {
+    if (!isEdit) {
+      mittBus.emit('selectProcessNode', props.node)
+      return
+    }
     if (config.value.hasDrawer) {
       drawerWrap.value.show(props.node)
     }

+ 3 - 1
src/components/business/EasyProcess/index.vue

@@ -29,7 +29,7 @@
   import { EasyProcess } from './common/process.type'
 
   // 流程数据
-  const props = withDefaults(defineProps<{ data?: EasyProcess.NodeConfig }>(), {})
+  const props = withDefaults(defineProps<{ data?: EasyProcess.NodeConfig; edit?: boolean }>(), { edit: true })
 
   const getProcessData = (data: EasyProcess.NodeConfig = props.data || blankProcessData()) => {
     if (data?.nodeType === 'start') return data
@@ -48,6 +48,8 @@
       }
     }
   )
+  // 提供可编辑状态
+  provide<boolean>(ProcessConstant.KEY_EDIT, props.edit)
   /* ====== 节点数据验证 ====== start */
   // 注册节点验证器
   const validator = new ProcessValidator()

+ 3 - 0
src/utils/sys/mittBus.ts

@@ -4,6 +4,7 @@
  * mittBus.on('event', callback)
  * mittBus.emit('event', data)
  */
+import { EasyProcess } from '@/components/business/EasyProcess/common/process.type'
 import mitt, { type Emitter } from 'mitt'
 
 // 定义事件类型映射
@@ -18,6 +19,8 @@ type Events = {
   openChat: void
   // 打开锁屏事件 - 无参数
   openLockScreen: void
+  // 流程节点的选中事件 - 可选的节点ID参数
+  selectProcessNode: EasyProcess.NodeConfig | undefined
 }
 
 // 创建类型安全的事件总线实例

+ 2 - 2
src/views/train/course/detail/course-flow.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="art-full-height w-full">
-    <EasyProcess ref="processRef" :data="processData" />
+    <EasyProcess ref="processRef" :data="processData" :edit="edit" />
   </div>
 </template>
 
@@ -9,7 +9,7 @@
   import EasyProcess from '@/components/business/EasyProcess/index.vue'
   import { onMounted, ref } from 'vue'
 
-  const props = withDefaults(defineProps<{ data?: string }>(), {})
+  const props = withDefaults(defineProps<{ data?: string; edit?: boolean }>(), { edit: true })
   // 定义 ProcessDesigner 的实例类型
   type ProcessDesignerInstance = InstanceType<typeof EasyProcess>
   // 审批数据

+ 4 - 9
src/views/train/course/detail/course.schem.ts

@@ -1,11 +1,6 @@
 import { FormItemSchema } from '@/components/form'
 import { FormRules } from 'element-plus'
 
-const options = ref([
-  { label: '选项一', value: 1 },
-  { label: '选项二', value: 2 },
-  { label: '选项三', value: 3 }
-])
 export const rules: FormRules = {
   courseName: [{ required: true, message: '请输入课程名称', trigger: ['blur'] }],
   courseDesc: [{ required: true, message: '请输入课程描述', trigger: ['blur'] }],
@@ -20,7 +15,7 @@ export const schema: FormItemSchema[] = [
   { prop: 'externalTypeID', label: '外部分类ID', component: 'Input', colSpan: 6, defaultValue: '' },
   { prop: 'courseDesc', label: '课程描述', component: 'Input', colSpan: 12 },
   { prop: 'lineNo', label: '线路号', component: 'ApiDict', componentProps: { code: 'line_type' }, colSpan: 6 },
-  { prop: 'runningMap', label: '运行图', component: 'Input', colSpan: 6 },
+  { prop: 'runningMap', label: '运行图', component: 'ApiDict', componentProps: { code: 'running_map' }, colSpan: 6 },
   { prop: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 12 },
   {
     prop: 'guideFilePath',
@@ -41,10 +36,10 @@ export const courseSchema: FormItemSchema[] = [
   { 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: 'typeID', label: '分类ID', component: 'ApiDict', componentProps: { code: 'category' }, colSpan: 6 },
   { prop: 'externalTypeID', label: '外部分类ID', component: 'Input', 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: 'lineNo', label: '线路号', component: 'ApiDict', componentProps: { code: 'line_type' }, colSpan: 6 },
+  { prop: 'runningMap', label: '运行图', component: 'ApiDict', componentProps: { code: 'running_map' }, colSpan: 6 },
   { prop: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 12 },
   { prop: 'guideFilePath', label: '指导文件地址', component: 'Upload', colSpan: 12 },
   { prop: 'creator', label: '创建人', component: 'Span', colSpan: 6 },

+ 2 - 2
src/views/train/course/detail/index.vue

@@ -24,10 +24,11 @@
 <script setup lang="ts">
   import courseFlow from './course-flow.vue'
   import courseEvaluate from './course-evaluate.vue'
-  import { schema, courseSchema, rules } from './course.schem'
+  import { schema, courseSchema, rules } from './course.schema'
   import { Course, CourseServer } from '@/api/course.api'
   import { useForm } from '@/components/form'
   import { router } from '@/router'
+  import { ElMessageBox, ElMessage } from 'element-plus'
 
   // 路由参数
   const route = useRoute()
@@ -81,7 +82,6 @@
   })
 
   const handleCancel = () => {
-    // 二次确认
     ElMessageBox.confirm('确定要取消吗?', '提示', {
       confirmButtonText: '确定',
       cancelButtonText: '取消',

+ 34 - 8
src/views/train/course/trainer_run/index.vue

@@ -18,34 +18,53 @@
     </div>
     <div class="flex h-full flex-1 flex-row gap-4">
       <div class="flex-1 border">
-        <course-flow></course-flow>
+        <course-flow :data="course?.courseDataStream" :edit="false"></course-flow>
       </div>
       <div class="flex flex-col gap-4">
         <div class="h-[180px] w-[320px] border p-4">
-          <el-button v-if="!isRunning" type="primary" @click="start">开始课程</el-button>
-          <el-button v-else type="primary" @click="pause">暂停课程</el-button>
-          <el-button type="primary" @click="reset">结束课程</el-button>
+          <el-button v-if="!isRunning" type="primary" @click="courseStart">开始课程</el-button>
+          <el-button v-else type="primary" @click="coursePause">暂停课程</el-button>
+          <el-button type="primary" @click="courseReset">结束课程</el-button>
         </div>
         <div class="flex-1 border p-4">
-          <div class="mb-4 text-lg font-medium">节点信息</div>
-          <ElEmpty :description="`暂无节点信息`" />
+          <div v-if="processData" class="mb-4 text-lg font-medium">
+            <div>节点名称:{{ processData?.nodeName }}</div>
+          </div>
+          <ElEmpty v-else :description="`暂无节点信息`" />
         </div>
       </div>
     </div>
   </div>
 </template>
 <script lang="ts" setup>
+  import { onMounted, ref, computed } from 'vue'
+  import { useRoute } from 'vue-router'
+  import mittBus from '@/utils/sys/mittBus'
+  import { EasyProcess } from '@/components/business/EasyProcess/common/process.type'
   import cover1 from '@imgs/cover/img1.webp'
   import cover2 from '@imgs/cover/img2.webp'
   import cover3 from '@imgs/cover/img3.webp'
   import { Picture as IconPicture } from '@element-plus/icons-vue'
-  import { Course, CourseServer } from '@/api/course.api'
+  import { Course, CourseServer, CourseRunServer } from '@/api/course.api'
   import courseDetail from './course-detail.vue'
   import courseFlow from '../detail/course-flow.vue'
   import { useTimer } from '@/composables/useTimer'
 
   // 计时器
-  const { hours, minutes, seconds, isRunning, start, pause, reset } = useTimer(new Date())
+  const { hours, minutes, seconds, isRunning, start, pause } = useTimer(new Date())
+
+  const courseStart = async () => {
+    await CourseRunServer.start(courseId.value)
+    start()
+  }
+  const coursePause = async () => {
+    await CourseRunServer.pauseOrResume(courseId.value)
+    pause()
+  }
+  const courseReset = async () => {
+    await CourseRunServer.end(courseId.value)
+    pause()
+  }
 
   // 路由参数
   const route = useRoute()
@@ -59,6 +78,13 @@
     return cover2
   }
 
+  const processData = ref<EasyProcess.NodeConfig>()
+  mittBus.on('selectProcessNode', (node: EasyProcess.NodeConfig | undefined) => {
+    if (node) {
+      processData.value = node
+    }
+  })
+
   onMounted(async () => {
     const { data } = await CourseServer.detail(courseId.value)
     course.value = data