Преглед на файлове

feat: 添加课程运行状态检查及错误处理优化

fix(axios): 修复错误消息显示配置问题
refactor(process): 重构流程组件数据传递方式
style(slider): 优化评分滑块样式显示
perf(device): 增加设备状态更新错误处理
zhanghaijun преди 9 месеца
родител
ревизия
4835036d6d

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

@@ -3,7 +3,8 @@ import { Process } from './process.type'
 export const ProcessConstant = {
   KEY_PROCESS_VALIDATOR: 'process_validator',
   KEY_PROCESS_HELPER: 'process_helper',
-  KEY_EDIT: 'process_editable'
+  KEY_EDIT: 'process_editable',
+  KEY_COURSE: 'process_course'
 }
 
 export const generateId = (): string => {

+ 14 - 8
src/components/business/EasyProcess/core/course.scheme.ts

@@ -1,3 +1,17 @@
+export const getDataCommon = (showFile: boolean = false) => {
+  const array = []
+  if (showFile) {
+    array.push({ prop: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 24 })
+    array.push({
+      prop: 'guideFilePath',
+      label: '指导文件地址',
+      component: 'Upload',
+      componentProps: { maxSize: 1024 * 1024 * 5, accept: '.pdf,.mp4,.png', limit: 1 },
+      colSpan: 24
+    })
+  }
+  return [...nodeDataCommon, ...array]
+}
 /**
  * 节点数据定义
  */
@@ -36,14 +50,6 @@ export const nodeDataCommon = [
         { label: '否', value: '0' }
       ]
     }
-  },
-  { prop: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 24 },
-  {
-    prop: 'guideFilePath',
-    label: '指导文件地址',
-    component: 'Upload',
-    componentProps: { maxSize: 1024 * 1024 * 5, accept: '.pdf,.mp4,.png', limit: 1 },
-    colSpan: 24
   }
 ]
 

+ 5 - 5
src/components/business/EasyProcess/core/drawer_course.vue

@@ -8,16 +8,16 @@
 <script setup lang="tsx">
   import { onMounted } from 'vue'
   import { useForm } from '@/components/form'
-  import { nodeDataCommon, nodeDataTrigger } from './course.scheme'
+  import { getDataCommon, nodeDataTrigger } from './course.scheme'
+  import { ProcessConstant } from '../common/process.data'
 
-  const props = withDefaults(defineProps<{ nodeType?: string; nodeId: string; data: Record<string, unknown> }>(), {
-    nodeType: ''
-  })
+  const props = withDefaults(defineProps<{ nodeType?: string; nodeId: string; data: Record<string, unknown> }>(), { nodeType: '' })
 
   const isCourse = computed(() => props.nodeType === 'operation' || props.nodeType === 'fault' || props.nodeType === 'voice' || props.nodeType === 'cognize')
   const isFault = computed(() => props.nodeType === 'fault')
+  const course = inject<any>(ProcessConstant.KEY_COURSE)
 
-  const [CommonForm, commonFormApi] = useForm({ labelWidth: '150px', schema: nodeDataCommon })
+  const [CommonForm, commonFormApi] = useForm({ labelWidth: '150px', schema: getDataCommon(course?.typeID == '1004') })
   const [TriggerForm, triggerFormApi] = useForm({ labelWidth: '150px', schema: nodeDataTrigger })
 
   onMounted(() => {

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

@@ -29,7 +29,7 @@
   import { Process } from './common/process.type'
 
   // 流程数据
-  const props = withDefaults(defineProps<{ data?: string; edit?: boolean }>(), { edit: true })
+  const props = withDefaults(defineProps<{ data?: string; edit?: boolean; course?: any }>(), { edit: true })
 
   const getProcessData = (data: Process.NodeConfig = JSON.parse(props.data || '{}')) => {
     if (data?.nodeType === 'start') return data
@@ -38,6 +38,7 @@
 
   const processHelper = new ProcessHelper(getProcessData(JSON.parse(props.data || '{}') as Process.NodeConfig))
   provide<ProcessHelper>(ProcessConstant.KEY_PROCESS_HELPER, processHelper)
+  provide<any>(ProcessConstant.KEY_COURSE, props.course)
   const processData = computed(() => processHelper.getProcessData())
 
   watch(

+ 6 - 3
src/components/core/forms/art-search-bar/index.vue

@@ -272,13 +272,16 @@
    * 处理重置事件
    */
   const handleReset = () => {
+    handleReset2()
+    emit('reset')
+  }
+
+  const handleReset2 = () => {
     // 重置表单字段(UI 层)
     formInstance.value?.resetFields()
 
     // 清空所有表单项值(包含隐藏项)
     Object.assign(modelValue.value, Object.fromEntries(props.items.map(({ key }) => [key, undefined])))
-
-    emit('reset')
   }
 
   /**
@@ -291,7 +294,7 @@
   defineExpose({
     ref: formInstance,
     validate: (...args: any[]) => formInstance.value?.validate(...args),
-    reset: handleReset
+    reset: handleReset2
   })
 
   // 解构 props 以便在模板中直接使用

+ 1 - 0
src/utils/http/index.ts

@@ -69,6 +69,7 @@ axiosInstance.interceptors.response.use(
     const { code, msg } = response.data
     if (code === ApiStatus.success) return response
     if (code === ApiStatus.unauthorized) handleUnauthorizedError(msg)
+    console.log('请求失败', code, msg)
     throw createHttpError(msg || $t('httpMsg.requestFailed'), code)
   },
   (error) => {

+ 2 - 1
src/utils/http/server.axios.ts

@@ -47,11 +47,12 @@ export class RequestHttp {
           return response
         }
         const res = response.data as ApiResponse
+        const responseConfig = response.config as ExtendedAxiosRequestConfig
         const { code, msg } = res
         if (code === ApiStatus.success) return response
         if (code === ApiStatus.unauthorized) handleUnauthorizedError(msg)
         if (code === ApiStatus.internalServerError) {
-          if (config.showErrorMessage ?? true) {
+          if (responseConfig.showErrorMessage ?? true) {
             ElMessage.error('接口报错:' + (msg || $t('httpMsg.requestFailed')))
           }
         }

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

@@ -13,7 +13,7 @@
     <div v-loading="loading" class="mt-4">
       <BasicForm @change="handleChange">
         <template #courseDataStream>
-          <EasyProcess ref="easyProcessRef" :data="courseDataStream" />
+          <EasyProcess ref="easyProcessRef" :data="courseDataStream" :course="formData" />
         </template>
         <template #evaluate>
           <courseEvaluate :courseId="courseId" />

+ 21 - 17
src/views/train/course/index.vue

@@ -152,23 +152,27 @@
     { id: 1003, status: 3, name: '信息化服务器', icon: 'd_1003_off', title: '离线' }
   ])
   const updateStatus = async () => {
-    const { data } = await request.post<
-      {
-        SysID: number /** 系统ID */
-        SysName: string /** 系统名称 */
-        SysStatus: number /** 系统状态 0不在线 1在线 */
-      }[]
-    >('/serverctrl/subsysstatus', {})
-    if (data?.length) {
-      deviceStatus.value.forEach((item) => {
-        const status = data.find((status) => status.SysID === item.id)
-        if (status) {
-          item.name = status.SysName
-          item.status = status.SysStatus
-          item.title = status.SysStatus === 1 ? '在线' : '离线'
-          item.icon = status.SysStatus === 1 ? 'd_' + status.SysID + '_on' : 'd_' + status.SysID + '_off'
-        }
-      })
+    try {
+      const { data } = await request.post<
+        {
+          SysID: number /** 系统ID */
+          SysName: string /** 系统名称 */
+          SysStatus: number /** 系统状态 0不在线 1在线 */
+        }[]
+      >('/serverctrl/subsysstatus', {}, { showErrorMessage: false, timeout: 6000 })
+      if (data?.length) {
+        deviceStatus.value.forEach((item) => {
+          const status = data.find((status) => status.SysID === item.id)
+          if (status) {
+            item.name = status.SysName
+            item.status = status.SysStatus
+            item.title = status.SysStatus === 1 ? '在线' : '离线'
+            item.icon = status.SysStatus === 1 ? 'd_' + status.SysID + '_on' : 'd_' + status.SysID + '_off'
+          }
+        })
+      }
+    } catch (error) {
+      console.error('更新设备状态失败:', error)
     }
   }
 

+ 1 - 0
src/views/train/course/list/course.plan.ts

@@ -2,6 +2,7 @@ import request from '@/api'
 import { CascaderOption } from 'element-plus'
 import { usePlanStore } from '@/store/modules/plan.store'
 import { useUserStore } from '@/store/modules/user'
+import { ref, computed, toRefs, watch, onMounted } from 'vue'
 
 export interface OptionItem {
   value: string

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

@@ -183,6 +183,10 @@
         break
     }
   }
+  const currentCourseRunning = (currentCourseID: string) => {
+    return props.isRun && runCourseID && String(runCourseID.value) === String(currentCourseID)
+  }
+  defineExpose({ currentCourseRunning })
 </script>
 <style lang="scss" scoped>
   .course-card {

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

@@ -15,6 +15,8 @@
   import { Course } from '@/api/course.api'
   import { useDictStore } from '@/store/modules/dict'
   import { usePlanOptions } from './course.plan'
+  import { ref, reactive, computed } from 'vue'
+  import { ElMessage } from 'element-plus'
 
   const props = withDefaults(defineProps<{ edit?: boolean }>(), { edit: false })
 

+ 5 - 2
src/views/train/course/trainer_run/course_process.vue

@@ -10,11 +10,14 @@
   </div>
 </template>
 <script setup lang="ts">
+  import { Course } from '@/api/course.api'
   import FileView from '../list/file-view.vue'
   import { Process } from '@/components/business/EasyProcess/common/process.type'
-  import { nodeDataCommon, nodeDataTrigger } from '@/components/business/EasyProcess/core/course.scheme'
+  import { getDataCommon, nodeDataTrigger } from '@/components/business/EasyProcess/core/course.scheme'
   import { useDetailForm } from '@/components/form'
 
+  const props = withDefaults(defineProps<{ course?: Course }>(), {})
+
   const processData = ref<Process.NodeConfig>()
   const commonFormModel = ref<Record<string, unknown>>({})
   const triggerFormModel = ref<Record<string, unknown>>({})
@@ -26,7 +29,7 @@
   )
   const isFault = computed(() => processData.value?.nodeType === 'fault')
 
-  const [CommonForm] = useDetailForm({ labelWidth: '150px', schema: nodeDataCommon })
+  const [CommonForm] = useDetailForm({ labelWidth: '150px', schema: getDataCommon(props.course?.typeID == '1004') })
   const [TriggerForm] = useDetailForm({ labelWidth: '150px', schema: nodeDataTrigger })
   const [CourseForm, formApi] = useDetailForm({ labelWidth: '150px' })
 

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

@@ -58,7 +58,7 @@
         </div>
         <div class="flex-1">
           <div class="mb-4 text-lg font-medium">
-            <course-process ref="processFormRef" />
+            <course-process ref="processFormRef" :course="course" />
           </div>
         </div>
       </div>

+ 9 - 2
src/views/train/course/trainer_teacher/index.vue

@@ -2,7 +2,7 @@
 <template>
   <div class="search-bar">
     <course-search ref="courseSearchRef" @search="(params) => getCourseData(params, false)" @reset="() => getCourseData({}, true)"></course-search>
-    <course-list :data="courseData" :loading="loading" :is-run="true">
+    <course-list ref="courseListRef" :data="courseData" :loading="loading" :is-run="true">
       <template #default="{ item }">
         <div class="flex items-center">
           <el-button class="flex-1" type="primary" :icon="DataLine" @click.stop="startCourse(item)">开课</el-button>
@@ -22,7 +22,14 @@
   import { DataLine } from '@element-plus/icons-vue'
   import { router } from '@/router'
 
-  const startCourse = (item: Course) => router.push({ path: '/trainer/course/run/' + item.courseID })
+  const startCourse = (item: Course) => {
+    if (!courseListRef.value?.currentCourseRunning(item.courseID)) {
+      ElMessage.error('请先结束开课课程,再开课')
+      return
+    }
+    router.push({ path: '/trainer/course/run/' + item.courseID })
+  }
+  const courseListRef = ref()
   const recordListRef = ref()
   const showRecordList = (item: Course) => recordListRef.value.show(item.courseID, item.courseName)
 

+ 8 - 4
src/views/train/evaluation/list-log.vue

@@ -33,17 +33,21 @@
       columnsFactory: () => [
         { type: 'globalIndex', width: 80, label: '序号' },
         { prop: 'courseName', label: '课程名称' },
+        // { prop: 'templateID', label: '模板ID' },
+        { prop: 'templateName', label: '模板名称' },
+        { prop: 'categoryName', label: '分类名称' },
+        { prop: 'subCategoryName', label: '子分类名称' },
+        { prop: 'operName', label: '操作人员名称' },
+        { prop: 'operTime', label: '操作时间', formatter: (row: OperLog) => formatSecondsToHHMMSS(Number(row.operTime ?? 0)) },
         // { prop: 'runOperID', label: '运行操作ID' },
         { prop: 'runLogID', label: '运行记录编号' },
         // { prop: 'runCourseID', label: '运行课程ID' },
-        { prop: 'operName', label: '操作名称' },
+        { prop: 'operScore', label: '操作得分' },
         { prop: 'operParam1', label: '操作参数1' },
         { prop: 'operParam2', label: '操作参数2' },
         { prop: 'operParam3', label: '操作参数3' },
         { prop: 'operParam4', label: '操作参数4' },
-        { prop: 'operParam5', label: '操作参数5' },
-        { prop: 'operScore', label: '操作得分' },
-        { prop: 'operTime', label: '操作时间', formatter: (row: OperLog) => formatSecondsToHHMMSS(Number(row.operTime ?? 0)) }
+        { prop: 'operParam5', label: '操作参数5' }
       ]
     }
   })

+ 34 - 1
src/views/train/evaluation/list-referee.vue

@@ -2,7 +2,21 @@
   <ArtTable ref="refereeTableRef" :loading="refereeLoading" :data="refereeData" :columns="refereeColumns" empty-height="360px">
     <!-- 操作得分 -->
     <template #operScore="{ row }">
-      <el-slider v-model="row.operScore" :max="row.evaScore" :min="Math.max(0, row.evaDeduction)" @change="saveRefereeRow(row, 'operScore', '操作得分已修改')" />
+      <div class="my-slider">
+        <div class="my-slider__tooltip" :style="style(row.operScore, Math.max(0, row.evaDeduction), row.evaScore)">
+          <el-button class="my-slider__tooltip-wrapper" size="small">
+            {{ row.operScore }}
+          </el-button>
+        </div>
+        <el-slider
+          v-model="row.operScore"
+          :format-tooltip="(value) => `${value}`"
+          :show-tooltip="false"
+          :min="Math.max(0, row.evaDeduction)"
+          :max="row.evaScore"
+          @change="saveRefereeRow(row, 'operScore', '操作得分已修改')"
+        ></el-slider>
+      </div>
     </template>
     <!-- 是否失格项 -->
     <template #disqualificationItem="{ row }">
@@ -94,6 +108,14 @@
   // ---------- 新增 ----------
   const refereeAddRef = ref()
 
+  const style = (slider: number, min: number, max: number) => {
+    const length = max - min,
+      progress = slider - min,
+      left = (progress / length) * 100
+    return {
+      paddingLeft: `${left}%`
+    }
+  }
   /** -------------------- 生命周期 ----------------------- */
   onMounted(() => {
     refreshRefereeData()
@@ -109,4 +131,15 @@
   .list-referee {
     padding: 10px;
   }
+  .my-slider {
+    padding: 2px 4px;
+    .my-slider__tooltip {
+      text-align: left;
+      .my-slider__tooltip-wrapper {
+        height: 24px;
+        transform: translateX(-50%);
+        top: -50%;
+      }
+    }
+  }
 </style>

+ 2 - 2
vite.config.ts

@@ -15,7 +15,7 @@ import legacy from '@vitejs/plugin-legacy'
 export default ({ mode }: { mode: string }) => {
   const root = process.cwd()
   const env = loadEnv(mode, root)
-  const { VITE_VERSION, VITE_PORT, VITE_API_PROXY_URL } = env
+  const { VITE_VERSION, VITE_PORT } = env
 
   return defineConfig({
     define: {
@@ -26,7 +26,7 @@ export default ({ mode }: { mode: string }) => {
       port: Number(VITE_PORT),
       proxy: {
         '/api': {
-          target: VITE_API_PROXY_URL,
+          target: 'https://haijunit.icu/course/api',
           changeOrigin: true,
           secure: false,
           rewrite: (path) => path.replace(/^\/api/, '')