瀏覽代碼

feat: 优化课程管理和故障处理流程

- 新增课程分类、线路和运行图字典数据
- 重构课程详情页,支持流程设计器数据绑定
zhanghaijun 11 月之前
父節點
當前提交
50852644a3
共有 30 個文件被更改,包括 499 次插入 和 547 次删除
  1. 96 0
      develop/dict.json
  2. 7 7
      src/api/course.api.ts
  3. 3 4
      src/api/dict.api.ts
  4. 159 1
      src/assets/img/svg/login_icon.svg
  5. 1 0
      src/assets/img/svg/login_icon1.svg
  6. 15 1
      src/components/business/EasyProcess/common/process.data.ts
  7. 0 49
      src/components/business/EasyProcess/common/validator.js
  8. 1 0
      src/components/business/EasyProcess/index.vue
  9. 1 1
      src/components/business/EasyProcess/node/condition_drawer.vue
  10. 21 49
      src/components/business/EasyProcess/node/fault_drawer.vue
  11. 2 9
      src/components/business/EasyProcess/node/fault_node.vue
  12. 41 24
      src/components/business/EasyProcess/node/operation_drawer.vue
  13. 2 10
      src/components/business/EasyProcess/node/operation_node.vue
  14. 13 5
      src/components/business/EasyProcess/node/voice_drawer.vue
  15. 3 11
      src/components/business/EasyProcess/node/voice_node.vue
  16. 1 1
      src/components/business/meta2d/Meta2dView.vue
  17. 4 4
      src/components/core/views/login/LoginLeftView.vue
  18. 3 9
      src/components/form/components/upload.vue
  19. 3 0
      src/store/modules/dict.ts
  20. 1 1
      src/utils/ui/tabs.ts
  21. 2 4
      src/views/auth/login/index.vue
  22. 0 275
      src/views/auth/login/index1.vue
  23. 1 1
      src/views/dashboard/console/index.vue
  24. 16 2
      src/views/train/course/course-list.vue
  25. 8 11
      src/views/train/course/course-search.vue
  26. 10 10
      src/views/train/course/detail/course.schema.ts
  27. 38 35
      src/views/train/course/detail/index.vue
  28. 6 6
      src/views/train/evaluation/index.vue
  29. 16 0
      src/views/train/fault/demo.vue
  30. 25 17
      src/views/train/fault/index.vue

+ 96 - 0
develop/dict.json

@@ -0,0 +1,96 @@
+{
+  "dicts": [
+    {
+      "code": "gender",
+      "name": "性别",
+      "remark": "字典性别",
+      "sort": 1,
+      "items": [
+        { "value": "1", "label": "男", "remark": "", "sort": 1 },
+        { "value": "2", "label": "女", "remark": "", "sort": 2 }
+      ]
+    },
+    {
+        "code": "course_category",
+        "name": "课程分类",
+        "remark": "字典课程分类",
+        "sort": 1,
+        "items": [
+          { "value": "1001", "label": "基本操作", "remark": "", "sort": 1 },
+          { "value": "1002", "label": "故障处置", "remark": "", "sort": 2 },
+          { "value": "1003", "label": "非正常处置", "remark": "", "sort": 2 }
+        ]
+    },
+    {
+      "code": "course_line",
+      "name": "课程线路",
+      "remark": "字典课程线路",
+      "sort": 1,
+      "items": [
+        { "value": "1001", "label": "成渝高铁", "remark": "", "sort": 1 },
+        { "value": "1002", "label": "京沪高铁", "remark": "", "sort": 2 },
+        { "value": "1003", "label": "京广高铁", "remark": "", "sort": 3 },
+        { "value": "1004", "label": "京哈高铁", "remark": "", "sort": 4 },
+        { "value": "1005", "label": "沪昆高铁", "remark": "", "sort": 5 },
+        { "value": "1006", "label": "京港高铁", "remark": "", "sort": 6 },
+        { "value": "1007", "label": "呼南高铁", "remark": "", "sort": 7 },
+        { "value": "1008", "label": "京昆高铁", "remark": "", "sort": 8 },
+        { "value": "1009", "label": "包海高铁", "remark": "", "sort": 9 },
+        { "value": "1010", "label": "兰西广高铁", "remark": "", "sort": 10 },
+        { "value": "1011", "label": "广深港高铁", "remark": "", "sort": 11 },
+        { "value": "1012", "label": "贵广高铁", "remark": "", "sort": 12 },
+        { "value": "1013", "label": "南广高铁", "remark": "", "sort": 13 },
+        { "value": "1014", "label": "渝厦高铁", "remark": "", "sort": 14 },  
+        { "value": "1015", "label": "集大原高铁", "remark": "", "sort": 15 }
+      ]
+    },
+    {
+        "code": "course_run_map",
+        "name":"课程运行图",
+        "remark": "字典课程运行图",
+        "sort": 1,
+        "items": [
+          { "value": "1001", "label": "一号线运行图", "remark": "", "sort": 1 },
+          { "value": "1002", "label": "二号线运行图", "remark": "", "sort": 2 },
+          { "value": "1003", "label": "三号线运行图", "remark": "", "sort": 3 },
+          { "value": "1004", "label": "四号线运行图", "remark": "", "sort": 4 },
+          { "value": "1005", "label": "五号线运行图", "remark": "", "sort": 5 },
+          { "value": "1006", "label": "六号线运行图", "remark": "", "sort": 6 }
+        ]
+    },
+    {
+      "code": "station_line",
+      "name": "车站号",
+      "remark": "字典车站号",
+      "sort": 2,
+      "items": [
+        { "value": "1", "label": "车站1", "remark": "", "sort": 1 },
+        { "value": "2", "label": "车站2", "remark": "", "sort": 2 },
+        { "value": "3", "label": "车站3", "remark": "", "sort": 3 }
+      ]
+    },
+    {
+      "code": "device_control_type",
+      "name": "设备控制",
+      "remark": "字典设备控制",
+      "sort": 4,
+      "items": [
+        { "value": "0", "label": "分闸", "remark": "", "sort": 1 },
+        { "value": "1", "label": "合闸", "remark": "", "sort": 2 },
+        { "value": "101", "label": "故障", "remark": "", "sort": 3 }
+      ]
+    },
+    {
+      "code": "param_type",
+      "name": "参数类型",
+      "remark": "参数数据类型字典",
+      "sort": 1,
+      "items": [
+        { "value": "1", "label": "数值", "remark": "数字类型参数", "sort": 1 },
+        { "value": "2", "label": "文本", "remark": "字符串类型参数", "sort": 2 },
+        { "value": "3", "label": "布尔", "remark": "真假值类型参数", "sort": 3 },
+        { "value": "4", "label": "数组", "remark": "数组类型参数", "sort": 4 }
+      ]
+    }
+  ]
+}

+ 7 - 7
src/api/course.api.ts

@@ -36,7 +36,7 @@ export class CourseServer {
   }
   public static detail = (courseId: string) => request.post<Course>('/course/detail/get', { courseId: Number(courseId) })
   public static add = (data: Record<string, unknown>) => {
-    return request.post<{ courseId: string }>('/course/add', {
+    return request.post<{ courseID: string }>('/course/add', {
       courseName: data.courseName /** 课程名称*/,
       courseDesc: data.courseDesc /** 课程描述*/,
       totalDuration: Number(data.totalDuration) /** 课程总时长*/,
@@ -46,7 +46,7 @@ export class CourseServer {
       guideDesc: data.guideDesc /** 指导描述*/,
       guideFilePath: data.guideFilePath /** 指导文件地址*/,
       lineNo: Number(data.lineNo) /** 线路号*/,
-      runningMap: data.runningMap /** 运行图*/,
+      runningMap: Number(data.runningMap) /** 运行图*/,
       virtualRole: data.virtualRole /** 虚拟角色,使用逗号隔开*/
     })
   }
@@ -55,15 +55,15 @@ export class CourseServer {
       courseID: Number(data.courseID) /** 课程ID*/,
       courseName: data.courseName /** 课程名称*/,
       courseDesc: data.courseDesc /** 课程描述*/,
-      totalDuration: data.totalDuration /** 课程总时长*/,
-      typeID: data.typeID /** 分类ID*/,
+      totalDuration: Number(data.totalDuration) /** 课程总时长*/,
+      typeID: Number(data.typeID) /** 分类ID*/,
       courseDataStream: data.courseDataStream /** 课程数据流*/,
-      externalTypeID: data.externalTypeID /** 外部分类ID*/,
+      externalTypeID: Number(data.externalTypeID) /** 外部分类ID*/,
       courseGUID: data.courseGUID /** 课程GUID*/,
       guideDesc: data.guideDesc /** 指导描述*/,
       guideFilePath: data.guideFilePath /** 指导文件地址*/,
-      lineNo: data.lineNo /** 线路号*/,
-      runningMap: data.runningMap /** 运行图*/,
+      lineNo: Number(data.lineNo) /** 线路号*/,
+      runningMap: Number(data.runningMap) /** 运行图*/,
       virtualRole: data.virtualRole /** 虚拟角色,使用逗号隔开*/,
       virtualStartTime: data.virtualStartTime /** 虚拟起始时间*/
     })

+ 3 - 4
src/api/dict.api.ts

@@ -3,14 +3,13 @@ import request from '@/api'
 export interface Dict {
   code: string /** 字典编码 */
   name: string /** 字典名称 */
-  remark: string /** 备注 */
-  sort: number /** 排序 */
+  remark?: string /** 备注 */
+  sort?: number /** 排序 */
 }
 export interface DictItem {
-  dictCode: string /** 字典编码 */
   value: string /** 字典项编码 */
   label: string /** 字典项名称 */
-  remark: string /** 备注 */
+  remark?: string /** 备注 */
   sort: number /** 排序 */
 }
 export interface DictList extends Dict {

File diff suppressed because it is too large
+ 159 - 1
src/assets/img/svg/login_icon.svg


File diff suppressed because it is too large
+ 1 - 0
src/assets/img/svg/login_icon1.svg


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

@@ -6,7 +6,10 @@ export const ProcessConstant = {
   KEY_EDIT: 'process_editable'
 }
 
-export const generateId = (): string => new Date().getTime() + '' + Math.floor(Math.random() * 1000 + 1)
+export const generateId = (): string => {
+  const id = new Date().getTime() + '' + Math.floor(Math.random() * 1000 + 1)
+  return String(id).slice(-6)
+}
 
 export const nodeConfigData: Record<Process.NodeType, Process.NodeMeta> = {
   start: {
@@ -73,3 +76,14 @@ export const nodeConfigData: Record<Process.NodeType, Process.NodeMeta> = {
 }
 
 export const blankProcessData = () => nodeConfigData['start'].defaultNode()
+
+export const nodeRoleList = [
+  { value: 'role_default', label: '默认角色', id: '100' },
+  { value: 'role_substation', label: '变电检修员', id: '103' },
+  { value: 'role_catenary', label: '触网检修员', id: '105' },
+  { value: 'role_dispatch', label: '电力调度员', id: '101' },
+  { value: 'role_isolator', label: '隔开检修员', id: '104' },
+  { value: 'role_supply', label: '供电值班员', id: '102' }
+]
+
+export const getNodeRole = (role: string) => nodeRoleList.find((item) => item.label === role)?.value || 'role_default'

+ 0 - 49
src/components/business/EasyProcess/common/validator.js

@@ -1,49 +0,0 @@
-import { reactive } from 'vue'
-export const createValidator = () => {
-  let validator = new Object()
-  validator.nodeRules = new Map()
-  validator.results = reactive(new Map())
-
-  // 注册节点验证规则
-  validator.register = (nodeId, validatorFun) => {
-    if (validatorFun && validatorFun instanceof Function) {
-      validator.nodeRules.set(nodeId, validatorFun)
-    }
-  }
-
-  // 移除节点验证规则
-  validator.remove = (nodeId) => {
-    validator.nodeRules.delete(nodeId)
-    validator.results.delete(nodeId)
-  }
-
-  // 验证节点
-  validator.validate = () => {
-    let valid = true
-    let messages = []
-    validator.nodeRules.forEach((value, key) => {
-      let fun = value
-      if (fun && fun instanceof Function) {
-        let result = fun()
-        if (!result.valid) {
-          valid = false
-          validator.results.set(key, result)
-          messages.push(result.message)
-        } else {
-          validator.results.delete(key)
-        }
-      }
-    })
-    return { valid, messages }
-  }
-
-  // 获取指定节点的结果
-  validator.getResult = (nodeId) => {
-    let result = validator.results.get(nodeId)
-    if (result) {
-      return result
-    }
-  }
-
-  return validator
-}

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

@@ -90,6 +90,7 @@
     justify-content: center;
     width: 100%;
     height: 100%;
+    min-height: 700px;
     padding: 16px;
     overflow: auto;
     background-color: #f5f5f7;

+ 1 - 1
src/components/business/EasyProcess/node/condition_drawer.vue

@@ -40,7 +40,7 @@
       const data = await formApi.getValues()
       formApi.validate().then((valid) => {
         if (valid) {
-          callback(data)
+          callback({ ...data, nodeId: Number(props.node.nodeId) })
         } else {
           ElMessage.error('请检查节点配置是否完整')
         }

+ 21 - 49
src/components/business/EasyProcess/node/fault_drawer.vue

@@ -38,24 +38,17 @@
       { prop: 'name', label: '故障名称', component: 'Input' },
       { prop: 'role', label: '选择角色', component: 'ApiSelect', componentProps: { api: '/role/list', labelField: 'roleName', valueField: 'roleID' } },
       { prop: 'templateID', label: '操作模', component: 'Input' },
-      { prop: 'templateType', label: '操作模板类型', component: 'Input' },
-      { prop: 'category', label: '节点分类', component: 'Input' },
+      // { prop: 'templateType', label: '操作模板类型', component: 'Input' },
+      // { prop: 'category', label: '节点分类', component: 'Input' },
       { prop: 'categoryName', label: '分类名称', component: 'Input' },
-      { prop: 'subCategory', label: '子分类', component: 'Input' },
+      // { prop: 'subCategory', label: '子分类', component: 'Input' },
       { prop: 'subCategoryName', label: '子分类名称', component: 'Input' },
       {
         prop: 'param1Type',
         label: '参数1类型',
-        component: 'Select',
+        component: 'ApiDict',
         colSpan: 10,
-        componentProps: {
-          placeholder: '请选择类型',
-          options: [
-            { label: '文本', value: 1 },
-            { label: '数字', value: 2 },
-            { label: '日期', value: 3 }
-          ]
-        },
+        componentProps: { placeholder: '请选择类型', code: 'param_type' },
         ifShow: () => templateSelected.value?.param1Type != -1
       },
       {
@@ -70,16 +63,9 @@
       {
         prop: 'param2Type',
         label: '参数2类型',
-        component: 'Select',
-        componentProps: {
-          placeholder: '请选择类型',
-          options: [
-            { label: '文本', value: 1 },
-            { label: '数字', value: 2 },
-            { label: '日期', value: 3 }
-          ]
-        },
+        component: 'ApiDict',
         colSpan: 10,
+        componentProps: { placeholder: '请选择类型', code: 'param_type' },
         ifShow: () => templateSelected.value?.param2Type != -1
       },
       {
@@ -94,16 +80,9 @@
       {
         prop: 'param3Type',
         label: '参数3类型',
-        component: 'Select',
+        component: 'ApiDict',
         colSpan: 10,
-        componentProps: {
-          placeholder: '请选择类型',
-          options: [
-            { label: '文本', value: 1 },
-            { label: '数字', value: 2 },
-            { label: '日期', value: 3 }
-          ]
-        },
+        componentProps: { placeholder: '请选择类型', code: 'param_type' },
         ifShow: () => templateSelected.value?.param3Type != -1
       },
       {
@@ -118,16 +97,9 @@
       {
         prop: 'param4Type',
         label: '参数4类型',
-        component: 'Select',
+        component: 'ApiDict',
         colSpan: 10,
-        componentProps: {
-          placeholder: '请选择类型',
-          options: [
-            { label: '文本', value: 1 },
-            { label: '数字', value: 2 },
-            { label: '日期', value: 3 }
-          ]
-        },
+        componentProps: { placeholder: '请选择类型', code: 'param_type' },
         ifShow: () => templateSelected.value?.param4Type != -1
       },
       {
@@ -142,16 +114,9 @@
       {
         prop: 'param5Type',
         label: '参数5类型',
-        component: 'Select',
+        component: 'ApiDict',
         colSpan: 10,
-        componentProps: {
-          placeholder: '请选择类型',
-          options: [
-            { label: '文本', value: 1 },
-            { label: '数字', value: 2 },
-            { label: '日期', value: 3 }
-          ]
-        },
+        componentProps: { placeholder: '请选择类型', code: 'param_type' },
         ifShow: () => templateSelected.value?.param5Type != -1
       },
       {
@@ -171,6 +136,13 @@
       const template = templateList.value.find((item) => item.templateID == val)
       if (template) {
         templateSelected.value = template
+        formApi.setValues({
+          templateName: template.templateName,
+          category: template.category,
+          categoryName: template.categoryName,
+          subCategory: template.subCategory,
+          subCategoryName: template.subCategoryName
+        })
       }
     }
   }
@@ -188,7 +160,7 @@
       const data = await formApi.getValues()
       formApi.validate().then((valid) => {
         if (valid) {
-          callback(data)
+          callback({ ...data, nodeId: Number(props.node.nodeId) })
         } else {
           ElMessage.error('请检查节点配置是否完整')
         }

+ 2 - 9
src/components/business/EasyProcess/node/fault_node.vue

@@ -5,7 +5,7 @@
   </div>
 </template>
 <script setup lang="ts">
-  import { ProcessConstant } from '../common/process.data'
+  import { getNodeRole, ProcessConstant } from '../common/process.data'
   import { ProcessValidator } from '../common/process.helper'
   import { Process } from '../common/process.type'
 
@@ -17,15 +17,8 @@
       nodeData.value = newVal || {}
     }
   )
-  const roleIcon: string[] = ['role_default', 'role_substation', 'role_catenary', 'role_dispatch', 'role_isolator', 'role_supply']
 
-  const getIcon = computed(() => {
-    const role = nodeData.value?.role || 'role_default'
-    if (roleIcon.some((item) => item === role)) {
-      return `svg:${role}`
-    }
-    return 'svg:role_default'
-  })
+  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.role) || '')}`)
 
   const validate = inject<ProcessValidator>(ProcessConstant.KEY_PROCESS_VALIDATOR)
 

+ 41 - 24
src/components/business/EasyProcess/node/operation_drawer.vue

@@ -4,12 +4,7 @@
       <template #templateID="{ model }">
         <el-select-v2
           v-model="model.templateID"
-          :options="
-            templateList.map((item) => ({
-              label: item.templateName,
-              value: item.templateID
-            }))
-          "
+          :options="templateList.map((item) => ({ label: item.templateName, value: item.templateID }))"
           placeholder="请选择操作模板"
           @change="handleTemplateChange"
         />
@@ -37,25 +32,18 @@
     schema: [
       { prop: 'name', label: '操作名称', component: 'Input' },
       { prop: 'role', label: '选择角色', component: 'ApiSelect', componentProps: { api: '/role/list', labelField: 'roleName', valueField: 'roleID' } },
-      { prop: 'templateID', label: '操作模', component: 'Input' },
-      { prop: 'templateType', label: '操作模板类型', component: 'Input' },
-      { prop: 'category', label: '节点分类', component: 'Input' },
+      { prop: 'templateID', label: '操作模板', component: 'Input' },
+      // { prop: 'templateType', label: '操作模板类型', component: 'Input' },
+      // { prop: 'category', label: '节点分类', component: 'Input' },
       { prop: 'categoryName', label: '分类名称', component: 'Input' },
-      { prop: 'subCategory', label: '子分类', component: 'Input' },
+      // { prop: 'subCategory', label: '子分类', component: 'Input' },
       { prop: 'subCategoryName', label: '子分类名称', component: 'Input' },
       {
         prop: 'param1Type',
         label: '参数1类型',
-        component: 'Select',
+        component: 'ApiDict',
         colSpan: 10,
-        componentProps: {
-          placeholder: '请选择类型',
-          options: [
-            { label: '文本', value: 1 },
-            { label: '数字', value: 2 },
-            { label: '日期', value: 3 }
-          ]
-        },
+        componentProps: { code: 'param_type', placeholder: '请选择类型' },
         ifShow: () => templateSelected.value?.param1Type != -1
       },
       {
@@ -71,7 +59,7 @@
         prop: 'param2Type',
         label: '参数2类型',
         component: 'ApiDict',
-        componentProps: { code: 'gender' },
+        componentProps: { code: 'param_type', placeholder: '请选择类型' },
         colSpan: 10,
         ifShow: () => templateSelected.value?.param2Type != -1
       },
@@ -84,7 +72,14 @@
         colSpan: 14,
         ifShow: () => templateSelected.value?.param2Type != -1
       },
-      { prop: 'param3Type', label: '参数3类型', component: 'Select', colSpan: 10, ifShow: () => templateSelected.value?.param3Type != -1 },
+      {
+        prop: 'param3Type',
+        label: '参数3类型',
+        component: 'ApiDict',
+        componentProps: { code: 'param_type', placeholder: '请选择类型' },
+        colSpan: 10,
+        ifShow: () => templateSelected.value?.param3Type != -1
+      },
       {
         prop: 'param3Desc',
         label: '',
@@ -94,7 +89,14 @@
         colSpan: 14,
         ifShow: () => templateSelected.value?.param3Type != -1
       },
-      { prop: 'param4Type', label: '参数4类型', component: 'Select', colSpan: 10, ifShow: () => templateSelected.value?.param4Type != -1 },
+      {
+        prop: 'param4Type',
+        label: '参数4类型',
+        component: 'ApiDict',
+        componentProps: { code: 'param_type', placeholder: '请选择类型' },
+        colSpan: 10,
+        ifShow: () => templateSelected.value?.param4Type != -1
+      },
       {
         prop: 'param4Desc',
         label: '',
@@ -104,7 +106,14 @@
         colSpan: 14,
         ifShow: () => templateSelected.value?.param4Type != -1
       },
-      { prop: 'param5Type', label: '参数5类型', component: 'Select', colSpan: 10, ifShow: () => templateSelected.value?.param5Type != -1 },
+      {
+        prop: 'param5Type',
+        label: '参数5类型',
+        component: 'ApiDict',
+        componentProps: { code: 'param_type', placeholder: '请选择类型' },
+        colSpan: 10,
+        ifShow: () => templateSelected.value?.param5Type != -1
+      },
       {
         prop: 'param5Desc',
         label: '',
@@ -122,6 +131,14 @@
       const template = templateList.value.find((item) => item.templateID == val)
       if (template) {
         templateSelected.value = template
+        formApi.setValues({
+          templateName: template.templateName,
+          templateType: template.templateType,
+          category: template.category,
+          categoryName: template.categoryName,
+          subCategory: template.subCategory,
+          subCategoryName: template.subCategoryName
+        })
       }
     }
   }
@@ -137,7 +154,7 @@
       const data = await formApi.getValues()
       formApi.validate().then((valid) => {
         if (valid) {
-          callback(data)
+          callback({ ...data, nodeId: Number(props.node.nodeId) })
         } else {
           ElMessage.error('请检查节点配置是否完整')
         }

+ 2 - 10
src/components/business/EasyProcess/node/operation_node.vue

@@ -5,7 +5,7 @@
   </div>
 </template>
 <script setup lang="ts">
-  import { ProcessConstant } from '../common/process.data'
+  import { getNodeRole, ProcessConstant } from '../common/process.data'
   import { ProcessValidator } from '../common/process.helper'
   import { Process } from '../common/process.type'
 
@@ -17,15 +17,7 @@
       nodeData.value = newVal || {}
     }
   )
-  const roleIcon: string[] = ['role_default', 'role_substation', 'role_catenary', 'role_dispatch', 'role_isolator', 'role_supply']
-
-  const getIcon = computed(() => {
-    const role = nodeData.value?.role || 'role_default'
-    if (roleIcon.some((item) => item === role)) {
-      return `svg:${role}`
-    }
-    return 'svg:role_default'
-  })
+  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.role) || '')}`)
 
   const validate = inject<ProcessValidator>(ProcessConstant.KEY_PROCESS_VALIDATOR)
 

+ 13 - 5
src/components/business/EasyProcess/node/voice_drawer.vue

@@ -32,16 +32,17 @@
     rules: {
       name: [{ required: true, message: '请输入语音名称', trigger: ['blur'] }],
       role: [{ required: true, message: '请选择角色', trigger: ['change'] }],
-      templateID: [{ required: true, message: '请选择语言模板', trigger: ['change'] }]
+      templateID: [{ required: true, message: '请选择语言模板', trigger: ['change'] }],
+      voiceContent: [{ required: true, message: '请输入语音内容', trigger: ['blur'] }]
     },
     schema: [
       { prop: 'name', label: '语音名称', component: 'Input' },
       { prop: 'role', label: '选择角色', component: 'ApiSelect', componentProps: { api: '/role/list', labelField: 'roleName', valueField: 'roleID' } },
       { prop: 'templateID', label: '语言模板', component: 'Input' },
-      { prop: 'templateType', label: '语音模板类型', component: 'Input' },
-      { prop: 'category', label: '节点分类', component: 'Input' },
+      // { prop: 'templateType', label: '语音模板类型', component: 'Input' },
+      // { prop: 'category', label: '节点分类', component: 'Input' },
       { prop: 'categoryName', label: '分类名称', component: 'Input' },
-      { prop: 'subCategory', label: '子分类', component: 'Input' },
+      // { prop: 'subCategory', label: '子分类', component: 'Input' },
       { prop: 'subCategoryName', label: '子分类名称', component: 'Input' },
       { prop: 'voiceContent', label: '语音内容', component: 'Input' },
       { prop: 'evalTemplate', label: '评估模板', component: 'Input' },
@@ -54,6 +55,13 @@
       const template = templateList.value.find((item) => item.templateID == val)
       if (template) {
         templateSelected.value = template
+        formApi.setValues({
+          templateType: template.templateType,
+          category: template.category,
+          categoryName: template.categoryName,
+          subCategory: template.subCategory,
+          subCategoryName: template.subCategoryName
+        })
       }
     }
   }
@@ -71,7 +79,7 @@
       const data = await formApi.getValues()
       formApi.validate().then((valid) => {
         if (valid) {
-          callback(data)
+          callback({ ...data, nodeId: Number(props.node.nodeId) })
         } else {
           ElMessage.error('请检查节点配置是否完整')
         }

+ 3 - 11
src/components/business/EasyProcess/node/voice_node.vue

@@ -5,7 +5,7 @@
   </div>
 </template>
 <script setup lang="ts">
-  import { ProcessConstant } from '../common/process.data'
+  import { getNodeRole, ProcessConstant } from '../common/process.data'
   import { ProcessValidator } from '../common/process.helper'
   import { Process } from '../common/process.type'
 
@@ -15,18 +15,10 @@
     () => props.node.nodeData,
     (newVal) => {
       nodeData.value = newVal || {}
+      console.log('nodeData.value', nodeData.value)
     }
   )
-
-  const roleIcon: string[] = ['role_default', 'role_substation', 'role_catenary', 'role_dispatch', 'role_isolator', 'role_supply']
-
-  const getIcon = computed(() => {
-    const role = nodeData.value?.role || 'role_default'
-    if (roleIcon.some((item) => item === role)) {
-      return `svg:${role}`
-    }
-    return 'svg:role_default'
-  })
+  const getIcon = computed(() => `svg:${getNodeRole(String(nodeData.value?.role) || '')}`)
 
   const validate = inject<ProcessValidator>(ProcessConstant.KEY_PROCESS_VALIDATOR)
 

+ 1 - 1
src/components/business/meta2d/Meta2dView.vue

@@ -101,7 +101,7 @@
   .meta2d-container {
     z-index: 1;
     width: 100%;
-    height: calc(100vh - 120px);
+    height: calc(100vh - 200px);
     border: 1px solid #ddd;
   }
 </style>

+ 4 - 4
src/components/core/views/login/LoginLeftView.vue

@@ -9,8 +9,8 @@
       <ThemeSvg :src="loginIcon" size="100%" />
     </div>
     <div class="text-wrap">
-      <h1> {{ $t('login.leftView.title') }} </h1>
-      <p> {{ $t('login.leftView.subTitle') }} </p>
+      <h1> {{ '专注教员系统,成就地铁英才' }} </h1>
+      <p> {{ '系统优化体验,管理成就高效' }} </p>
     </div>
     <!-- 几何装饰元素 -->
     <div class="geometric-decorations">
@@ -64,7 +64,7 @@
   .login-left-view {
     position: relative;
     box-sizing: border-box;
-    width: 65vw;
+    width: 50vw;
     height: 100%;
     padding: 15px;
     overflow: hidden;
@@ -453,7 +453,7 @@
     }
 
     @media only screen and (max-width: $device-notebook) {
-      width: 60vw;
+      width: 50vw;
 
       .text-wrap {
         bottom: 40px;

+ 3 - 9
src/components/form/components/upload.vue

@@ -226,23 +226,17 @@
     ...props.headers
   }
   const handleChange = (info: any) => {
+    console.log(info)
     if (!info.file) return
     if (info.status === 'success') {
       // 更新file-list当前file的uid
       fileList.value = fileList.value.map((item: any) => {
         if (item.uid === info.uid) {
-          return {
-            ...item,
-            uid: info.response.data.fileInfoId
-          }
+          return { ...item, uid: info.response.data.fileInfoId }
         }
         return item
       })
-      mValue.value = fileList.value
-        .map((item: any) => {
-          return item.uid
-        })
-        .join(props.separator)
+      mValue.value = fileList.value.map((item: any) => item.uid).join(props.separator)
     } else if (info.status === 'fail') {
       message.error(`${info.name} file upload failed.`)
     }

+ 3 - 0
src/store/modules/dict.ts

@@ -14,6 +14,9 @@ export const useDictStore = defineStore('dict', {
   }),
 
   actions: {
+    getDictWithCode(dictCode: string): DictItem[] {
+      return this.dictMap.get(dictCode) || []
+    },
     async getDictByType(dictCode: string): Promise<DictItem[]> {
       // 如果已经存在缓存,直接返回
       if (this.dictMap.has(dictCode)) {

+ 1 - 1
src/utils/ui/tabs.ts

@@ -11,7 +11,7 @@ export const TAB_CONFIG = {
     openTop: 106,
     closeTop: 60,
     openHeight: 121,
-    closeHeight: 75
+    closeHeight: 110
   },
   'tab-card': {
     openTop: 122,

+ 2 - 4
src/views/auth/login/index.vue

@@ -3,11 +3,9 @@
     <LoginLeftView></LoginLeftView>
 
     <div class="right-wrap">
-      <div class="top-right-wrap">
+      <div class="top-right-wrap" style="display: none">
         <div class="btn theme-btn" @click="toggleTheme">
-          <i class="iconfont-sys">
-            {{ isDark ? '&#xe6b5;' : '&#xe725;' }}
-          </i>
+          <i class="iconfont-sys"> {{ isDark ? '&#xe6b5;' : '&#xe725;' }} </i>
         </div>
         <ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
           <div class="btn language-btn">

+ 0 - 275
src/views/auth/login/index1.vue

@@ -1,275 +0,0 @@
-<template>
-  <div class="login">
-    <LoginLeftView></LoginLeftView>
-
-    <div class="right-wrap">
-      <div class="top-right-wrap">
-        <div class="btn theme-btn" @click="toggleTheme">
-          <i class="iconfont-sys">
-            {{ isDark ? '&#xe6b5;' : '&#xe725;' }}
-          </i>
-        </div>
-        <ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
-          <div class="btn language-btn">
-            <i class="iconfont-sys icon-language">&#xe611;</i>
-          </div>
-          <template #dropdown>
-            <ElDropdownMenu>
-              <div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">
-                <ElDropdownItem :command="lang.value" :class="{ 'is-selected': locale === lang.value }">
-                  <span class="menu-txt">{{ lang.label }}</span>
-                  <i v-if="locale === lang.value" class="iconfont-sys icon-check">&#xe621;</i>
-                </ElDropdownItem>
-              </div>
-            </ElDropdownMenu>
-          </template>
-        </ElDropdown>
-      </div>
-      <div class="header">
-        <ArtLogo class="icon" />
-        <h1>{{ systemName }}</h1>
-      </div>
-      <div class="login-wrap">
-        <div class="form">
-          <h3 class="title">{{ $t('login.title') }}</h3>
-          <p class="sub-title">{{ $t('login.subTitle') }}</p>
-          <ElForm ref="formRef" :model="formData" :rules="rules" @keyup.enter="handleSubmit" style="margin-top: 25px">
-            <ElFormItem prop="account">
-              <ElSelect v-model="formData.account" @change="setupAccount" class="account-select">
-                <ElOption v-for="account in accounts" :key="account.key" :label="account.label" :value="account.key">
-                  <span>{{ account.label }}</span>
-                </ElOption>
-              </ElSelect>
-            </ElFormItem>
-            <ElFormItem prop="username">
-              <ElInput :placeholder="$t('login.placeholder[0]')" v-model.trim="formData.username" />
-            </ElFormItem>
-            <ElFormItem prop="password">
-              <ElInput :placeholder="$t('login.placeholder[1]')" v-model.trim="formData.password" type="password" radius="8px" autocomplete="off" show-password />
-            </ElFormItem>
-            <div class="drag-verify">
-              <div class="drag-verify-content" :class="{ error: !isPassing && isClickPass }">
-                <ArtDragVerify
-                  ref="dragVerify"
-                  v-model:value="isPassing"
-                  :text="$t('login.sliderText')"
-                  textColor="var(--art-gray-800)"
-                  :successText="$t('login.sliderSuccessText')"
-                  :progressBarBg="getCssVar('--el-color-primary')"
-                  background="var(--art-gray-200)"
-                  handlerBg="var(--art-main-bg-color)"
-                />
-              </div>
-              <p class="error-text" :class="{ 'show-error-text': !isPassing && isClickPass }">
-                {{ $t('login.placeholder[2]') }}
-              </p>
-            </div>
-
-            <div class="forget-password">
-              <ElCheckbox v-model="formData.rememberPassword">
-                {{ $t('login.rememberPwd') }}
-              </ElCheckbox>
-              <RouterLink :to="RoutesAlias.ForgetPassword">{{ $t('login.forgetPwd') }}</RouterLink>
-            </div>
-
-            <div style="margin-top: 30px">
-              <ElButton class="login-btn" type="primary" @click="handleSubmit" :loading="loading" v-ripple>
-                {{ $t('login.btnText') }}
-              </ElButton>
-            </div>
-
-            <div class="footer">
-              <p>
-                {{ $t('login.noAccount') }}
-                <RouterLink :to="RoutesAlias.Register">{{ $t('login.register') }}</RouterLink>
-              </p>
-            </div>
-          </ElForm>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-  import AppConfig from '@/config'
-  import { RoutesAlias } from '@/router/routesAlias'
-  import { ElNotification, ElMessage } from 'element-plus'
-  import { useUserStore } from '@/store/modules/user'
-  import { getCssVar } from '@/utils/ui'
-  import { languageOptions } from '@/locales'
-  import { LanguageEnum, SystemThemeEnum } from '@/enums/appEnum'
-  import { useI18n } from 'vue-i18n'
-  import { HttpError } from '@/utils/http/server.error'
-  defineOptions({ name: 'Login' })
-  const dictStore = useDictStore()
-
-  const { t } = useI18n()
-  import { useSettingStore } from '@/store/modules/setting'
-  import type { FormInstance, FormRules } from 'element-plus'
-
-  type AccountKey = 'super' | 'admin' | 'user'
-
-  export interface Account {
-    key: AccountKey
-    label: string
-    userName: string
-    password: string
-    roles: string[]
-  }
-
-  const accounts = computed<Account[]>(() => [
-    {
-      key: 'super',
-      label: t('login.roles.super'),
-      userName: 'superAdmin',
-      password: '123456',
-      roles: ['R_SUPER']
-    },
-    {
-      key: 'admin',
-      label: t('login.roles.admin'),
-      userName: 'admin',
-      password: '123456',
-      roles: ['R_ADMIN']
-    },
-    {
-      key: 'user',
-      label: t('login.roles.user'),
-      userName: 'test',
-      password: '123456',
-      roles: ['R_USER']
-    }
-  ])
-
-  const settingStore = useSettingStore()
-  const { isDark, systemThemeType } = storeToRefs(settingStore)
-
-  const dragVerify = ref()
-
-  const userStore = useUserStore()
-  const router = useRouter()
-  const isPassing = ref(false)
-  const isClickPass = ref(false)
-
-  const systemName = AppConfig.systemInfo.name
-  const formRef = ref<FormInstance>()
-
-  const formData = reactive({
-    account: '',
-    username: '',
-    password: '',
-    rememberPassword: true
-  })
-
-  const rules = computed<FormRules>(() => ({
-    username: [{ required: true, message: t('login.placeholder[0]'), trigger: 'blur' }],
-    password: [{ required: true, message: t('login.placeholder[1]'), trigger: 'blur' }]
-  }))
-
-  const loading = ref(false)
-
-  onMounted(() => {
-    setupAccount('super')
-  })
-
-  // 设置账号
-  const setupAccount = (key: AccountKey) => {
-    const selectedAccount = accounts.value.find((account: Account) => account.key === key)
-    formData.account = key
-    formData.username = selectedAccount?.userName ?? ''
-    formData.password = selectedAccount?.password ?? ''
-  }
-
-  // 登录
-  const handleSubmit = async () => {
-    if (!formRef.value) return
-
-    try {
-      // 表单验证
-      const valid = await formRef.value.validate()
-      if (!valid) return
-
-      // 拖拽验证
-      if (!isPassing.value) {
-        isClickPass.value = true
-        return
-      }
-
-      loading.value = true
-
-      // 登录请求
-      const { username, password } = formData
-
-      const { token, refreshToken } = await UserService.login({ userName: username, password })
-
-      // 验证token
-      if (!token) {
-        throw new Error('Login failed - no token received')
-      }
-
-      // 存储token和用户信息
-      userStore.setToken(token, refreshToken)
-      userStore.setLoginStatus(true)
-      // 这里可以一次性初始化所有字典
-      dictStore.initAllDicts()
-
-      // 登录成功处理
-      showLoginSuccessNotice()
-      router.push('/')
-    } catch (error) {
-      // 处理 HttpError
-      if (error instanceof HttpError) {
-        // console.log(error.code)
-      } else {
-        // 处理非 HttpError
-        ElMessage.error('登录失败,请稍后重试')
-        console.error('[Login] Unexpected error:', error)
-      }
-    } finally {
-      loading.value = false
-      resetDragVerify()
-    }
-  }
-
-  // 重置拖拽验证
-  const resetDragVerify = () => {
-    dragVerify.value.reset()
-  }
-
-  // 登录成功提示
-  const showLoginSuccessNotice = () => {
-    setTimeout(() => {
-      ElNotification({
-        title: t('login.success.title'),
-        type: 'success',
-        duration: 2500,
-        zIndex: 10000,
-        message: `${t('login.success.message')}, ${systemName}!`
-      })
-    }, 150)
-  }
-
-  // 切换语言
-  const { locale } = useI18n()
-
-  const changeLanguage = (lang: LanguageEnum) => {
-    if (locale.value === lang) return
-    locale.value = lang
-    userStore.setLanguage(lang)
-  }
-
-  // 切换主题
-  import { useTheme } from '@/composables/useTheme'
-  import { UserService } from '@/api/sys/user'
-  import { useDictStore } from '@/store/modules/dict'
-
-  const toggleTheme = () => {
-    let { LIGHT, DARK } = SystemThemeEnum
-    useTheme().switchThemeStyles(systemThemeType.value === LIGHT ? DARK : LIGHT)
-  }
-</script>
-
-<style lang="scss" scoped>
-  @use './index';
-</style>

+ 1 - 1
src/views/dashboard/console/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="flex min-h-[400px] items-center justify-center p-4">
+  <div class="flex items-center justify-center p-4">
     <div class="box mb-20 flex h-[400px] w-full max-w-[1100px] flex-col rounded-2xl md:flex-row">
       <!-- 左侧大卡片 -->
       <div class="card h-[250px] w-full bg-cover bg-center shadow-md md:h-full md:w-[350px] md:rounded-r-none" :style="{ backgroundImage: `url(${Image01})` }">

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

@@ -37,18 +37,26 @@
           </ElImage>
           <div class="type">
             <el-checkbox :model-value="checkedCourse.includes(String(item.courseID))" class="hidden" />
-            <div class="type-tag">{{ useDateFormat(item.createTime, 'YYYY-MM-DD') }}</div>
+            <div class="type-tag">{{ item.totalDuration ? item.totalDuration : '0' }}秒</div>
           </div>
         </div>
         <div class="course-foot">
           <div class="title">{{ item.courseName }}</div>
           <div class="desc">{{ item.courseDesc || '暂无描述' }}</div>
           <div class="flex h-[32px] flex-row items-center justify-between">
+            <div class="action-desc flex h-full flex-row items-center gap-1">
+              <i class="iconfont-sys">&#xe6f8;</i>
+              <span>{{ item.courseID }}</span>
+            </div>
             <div class="action-desc flex h-full flex-row items-center gap-1">
               <i class="iconfont-sys">&#xe6f7;</i>
               <span>{{ useDateFormat(item.createTime, 'YYYY-MM-DD') }}</span>
             </div>
-            <div class="course-action">
+            <div v-if="item.creator" class="action-desc flex h-full flex-row items-center gap-1">
+              <i class="iconfont-sys">&#xe6f9;</i>
+              <span>{{ item.creator }}</span>
+            </div>
+            <div class="course-action hidden">
               <el-dropdown placement="bottom-end" trigger="hover">
                 <div class="action-btn mt-[2px] flex h-[28px] w-[38px] items-center justify-center rounded-md">
                   <el-icon><MoreFilled /></el-icon>
@@ -64,6 +72,12 @@
               </el-dropdown>
             </div>
           </div>
+          <el-button-group class="button-group w-full">
+            <el-button type="primary" size="default" class="w-1/4" :icon="Tickets" @click="handleChange('detail', item)">详情 </el-button>
+            <el-button type="primary" size="default" class="w-1/4" :icon="Edit" @click="handleChange('edit', item)">编辑 </el-button>
+            <el-button type="danger" size="default" class="w-1/4" :icon="Delete" @click="handleChange('delete', item)">删除 </el-button>
+            <el-button type="primary" size="default" class="w-1/4" :icon="CopyDocument" @click="handleChange('copy', item)">复制 </el-button>
+          </el-button-group>
         </div>
       </div>
     </div>

+ 8 - 11
src/views/train/course/course-search.vue

@@ -4,10 +4,13 @@
 
 <script setup lang="ts">
   import { Course } from '@/api/course.api'
+  import { useDictStore } from '@/store/modules/dict'
+
+  const dictStore = useDictStore()
 
   // courseID 非必须,其他字段必须
   // 培训计划 非必须
-  interface CourseSearch extends Pick<Course, 'courseID' | 'courseGUID' | 'courseName' | 'courseDesc' | 'typeID' | 'externalTypeID' | 'lineNo' | 'runningMap' | 'creator'> {
+  interface CourseSearch extends Pick<Course, 'courseID' | 'courseName' | 'typeID' | 'lineNo' | 'runningMap' | 'creator'> {
     // 培训计划
     trainPlan: string
   }
@@ -19,11 +22,8 @@
 
   const courseFormData = reactive<CourseSearch>({
     courseID: '',
-    courseGUID: '',
     courseName: '',
-    courseDesc: '',
     typeID: '',
-    externalTypeID: '',
     lineNo: '',
     runningMap: '',
     creator: '',
@@ -32,14 +32,11 @@
 
   const formItems = computed(() => [
     { label: '课程ID', key: 'courseID', type: 'input', placeholder: '请输入课程ID', clearable: true },
-    { label: '课程GUID', key: 'courseGUID', type: 'input', placeholder: '请输入课程GUID', clearable: true },
     { label: '课程名称', key: 'courseName', type: 'input', placeholder: '请输入课程名称', clearable: true },
-    { label: '课程描述', key: 'courseDesc', type: 'input', placeholder: '请输入课程描述', clearable: true },
-    { label: '培训计划', key: 'trainPlan', type: 'input', placeholder: '请输入培训计划', clearable: true },
-    { label: '分类ID', key: 'typeID', type: 'input', placeholder: '请输入分类ID', clearable: true },
-    { label: '外部分类ID', key: 'externalTypeID', type: 'input', placeholder: '请输入外部分类ID', clearable: true },
-    { label: '线路号', key: 'lineNo', type: 'input', placeholder: '请输入线路号', clearable: true },
-    { label: '运行图', key: 'runningMap', type: 'input', placeholder: '请输入运行图', clearable: true },
+    { label: '培训计划', key: 'trainPlan', type: 'datetime', props: { style: { width: '100%' }, placeholder: '请选择日期', type: 'date', valueFormat: 'YYYY-MM-DD' } },
+    { label: '课程分类', key: 'typeID', type: 'select', placeholder: '请选择课程分类', clearable: true, options: dictStore.getDictWithCode('course_category') || [] },
+    { label: '线路号', key: 'lineNo', type: 'select', placeholder: '请输入线路号', clearable: true, options: dictStore.getDictWithCode('course_line') || [] },
+    { label: '运行图', key: 'runningMap', type: 'select', placeholder: '请输入运行图', clearable: true, options: dictStore.getDictWithCode('course_run_map') || [] },
     { label: '创建人', key: 'creator', type: 'input', placeholder: '请输入创建人', clearable: true }
   ])
 

+ 10 - 10
src/views/train/course/detail/course.schema.ts

@@ -9,13 +9,13 @@ export const rules: FormRules = {
 export const schema: FormItemSchema[] = [
   // 课程基本信息
   { prop: '', label: '', labelWidth: '0px', component: 'Divider', colSpan: 24, componentProps: { title: '课程基本信息' } },
-  { prop: 'courseName', label: '课程名称', component: 'Input', colSpan: 6 },
-  { prop: 'totalDuration', label: '课程时长', component: 'InputNumber', colSpan: 6, componentProps: { style: { width: '100%' } }, defaultValue: 30 },
-  { prop: 'typeID', label: '分类ID', component: 'ApiDict', componentProps: { code: 'category' }, colSpan: 6 },
+  { prop: 'courseName', label: '课程名称', component: 'Input', colSpan: 8 },
+  { prop: 'totalDuration', label: '课程时长', labelWidth: '100px', component: 'InputNumber', colSpan: 4, componentProps: { style: { width: '100%' } }, defaultValue: 30 },
+  { prop: 'typeID', label: '课程分类', component: 'ApiDict', componentProps: { code: 'course_category' }, colSpan: 6 },
   { 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: 'ApiDict', componentProps: { code: 'running_map' }, colSpan: 6 },
+  { prop: 'lineNo', label: '线路号', component: 'ApiDict', componentProps: { code: 'course_line' }, colSpan: 6 },
+  { prop: 'runningMap', label: '运行图', component: 'ApiDict', componentProps: { code: 'course_run_map' }, colSpan: 6 },
   { prop: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 12 },
   {
     prop: 'guideFilePath',
@@ -33,13 +33,13 @@ 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: 'courseName', label: '课程名称', component: 'Input', colSpan: 8 },
+  { prop: 'totalDuration', label: '课程时长', labelWidth: '100px', component: 'InputNumber', colSpan: 4, componentProps: { style: { width: '100%' } }, defaultValue: 30 },
   { prop: 'courseDesc', label: '课程描述', component: 'Input', colSpan: 24 },
-  { prop: 'typeID', label: '分类ID', component: 'ApiDict', componentProps: { code: 'category' }, colSpan: 6 },
+  { prop: 'typeID', label: '课程分类', component: 'ApiDict', componentProps: { code: 'course_category' }, colSpan: 6 },
   { prop: 'externalTypeID', label: '外部分类ID', component: 'Input', colSpan: 6 },
-  { prop: 'lineNo', label: '线路号', component: 'ApiDict', componentProps: { code: 'line_type' }, colSpan: 6 },
-  { prop: 'runningMap', label: '运行图', component: 'ApiDict', componentProps: { code: 'running_map' }, colSpan: 6 },
+  { prop: 'lineNo', label: '线路号', component: 'ApiDict', componentProps: { code: 'course_line' }, colSpan: 6 },
+  { prop: 'runningMap', label: '运行图', component: 'ApiDict', componentProps: { code: 'course_run_map' }, colSpan: 6 },
   { prop: 'guideDesc', label: '指导描述', component: 'Input', colSpan: 12 },
   { prop: 'guideFilePath', label: '指导文件地址', component: 'Upload', colSpan: 12 },
   { prop: 'creator', label: '创建人', component: 'Span', colSpan: 6 },

+ 38 - 35
src/views/train/course/detail/index.vue

@@ -12,7 +12,7 @@
     <div v-loading="loading" class="mt-4">
       <BasicForm :formModelInit="courseDetail" @change="handleChange">
         <template #courseDataStream>
-          <EasyProcess ref="easyProcessRef" />
+          <EasyProcess ref="easyProcessRef" :data="courseDataStream" />
         </template>
         <template #evaluate>
           <courseEvaluate />
@@ -35,6 +35,7 @@
   const courseId = computed<string>(() => String(route.params.id))
   const query = computed(() => route.query)
 
+  const courseDataStream = ref('')
   const easyProcessRef = ref()
 
   const courseNew = computed(() => courseId.value === '0')
@@ -43,50 +44,44 @@
   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 [BasicForm, formApi] = useForm({ labelPosition: 'right', labelWidth: '150px', schema: courseNew.value ? schema : courseSchema, rules, showDefaultActions: false })
   const formData = ref<any>({})
   const handleChange = (val: any) => (formData.value = val)
 
   const addCourse = async () => {
     const processData = easyProcessRef.value.getProcess()
-    console.log(processData)
     if (!processData) {
       ElMessage.error('请设计课程流程')
       return
     }
-    formApi
-      .validate()
-      .then(async () => {
-        const courseData = formData.value
-        if (courseNew.value) {
-          const { data } = await CourseServer.add({ ...courseData, courseDataStream: processData })
-          // 保存流程图
-          if (data && data.courseId) {
-            await CourseServer.saveProcess(data.courseId, processData)
-          } else {
-            ElMessage.error('新增课程失败')
-            return
-          }
-          ElMessage.success('新增课程成功')
-          router.push({ path: '/course/list' })
-        } else if (courseEdit.value) {
-          await CourseServer.update({ ...courseData, courseDataStream: processData })
-          // 保存流程图
-          if (courseId.value) {
-            await CourseServer.saveProcess(courseId.value, processData)
-          } else {
-            ElMessage.error('编辑课程失败')
-            return
-          }
-          ElMessage.success('编辑课程成功')
-          router.push({ path: '/course/list' })
+    formApi.validate().then(async () => {
+      const courseData = formData.value
+      if (courseNew.value) {
+        const { data } = await CourseServer.add({ ...courseData, courseDataStream: processData })
+        // 保存流程图
+        if (data && data.courseID) {
+          await CourseServer.saveProcess(data.courseID, processData)
+        } else {
+          ElMessage.error('没有获取到课程ID,无法保存运行流程')
+          return
+        }
+        ElMessage.success('新增课程成功')
+        router.push({ path: '/course/list' })
+      } else if (courseEdit.value) {
+        await CourseServer.update({ ...courseData, courseDataStream: processData })
+        // 保存流程图
+        if (courseId.value) {
+          await CourseServer.saveProcess(courseId.value, processData)
         } else {
-          ElMessage.error('不能编辑课程详情')
+          ElMessage.error('没有获取到课程ID,无法保存运行流程')
+          return
         }
-      })
-      .catch(() => {
-        ElMessage.error('请填写完整课程信息')
-      })
+        ElMessage.success('编辑课程成功')
+        router.push({ path: '/course/list' })
+      } else {
+        ElMessage.error('不能编辑课程详情')
+      }
+    })
   }
 
   onMounted(() => {
@@ -94,7 +89,15 @@
     if (!courseNew.value) {
       CourseServer.detail(courseId.value)
         .then(({ data }) => {
-          courseDetail.value = data
+          courseDataStream.value = data?.courseDataStream || ''
+          courseDetail.value = {
+            ...data,
+            totalDuration: String(data?.totalDuration || 0),
+            typeID: String(data?.typeID || ''),
+            externalTypeID: String(data?.externalTypeID || ''),
+            lineNo: String(data?.lineNo || ''),
+            runningMap: String(data?.runningMap || '')
+          }
           loading.value = false
         })
         .catch(() => ElMessage.error('获取课程数据失败'))

+ 6 - 6
src/views/train/evaluation/index.vue

@@ -4,13 +4,12 @@
     <template v-slot:header>
       <span>评价系统</span>
     </template>
-    <div class="absolute top-0 right-0">
-      <el-space>
-        <el-button type="primary">导出操作记录</el-button>
-        <el-button type="primary">导出裁判打分</el-button>
-      </el-space>
+    <div class="absolute top-[73px] right-[60px] z-[100] flex gap-2">
+      <el-button v-if="activeTab === 'operation'" :icon="Download" type="primary">导出操作记录</el-button>
+      <el-button v-if="activeTab === 'referee'" :icon="Plus" type="primary">新增评价条目</el-button>
+      <el-button v-if="activeTab === 'referee'" :icon="Download" type="primary">导出裁判打分</el-button>
     </div>
-    <el-tabs v-model="activeTab" type="border-card">
+    <el-tabs v-model="activeTab" type="card">
       <el-tab-pane label="操作记录" name="operation">
         <ArtTable ref="operationTableRef" :loading="operationLoading" :data="operationData" :columns="operationColumns" empty-height="360px">
           <!-- 用户信息列 -->
@@ -42,6 +41,7 @@
   import { fetchGetUserList } from '@/api/system-manage'
   import { ElMessageBox } from 'element-plus'
   import { CourseRunServer } from '@/api/course.api'
+  import { Download, Plus } from '@element-plus/icons-vue'
 
   // tabs
   const activeTab = ref<'operation' | 'referee'>('referee')

+ 16 - 0
src/views/train/fault/demo.vue

@@ -0,0 +1,16 @@
+<template>
+  <Dialog> 我是弹窗内容 </Dialog>
+</template>
+<script setup lang="tsx">
+  import { useDialog } from '@/components/dialog'
+  const [Dialog, dialogApi] = useDialog({
+    title: '我是标题',
+    width: '50%'
+  })
+  defineExpose({
+    show() {
+      dialogApi.open()
+    }
+  })
+</script>
+<style lang="scss" scoped></style>

+ 25 - 17
src/views/train/fault/index.vue

@@ -1,24 +1,22 @@
 <template>
-  <el-card class="w-full">
-    <template v-slot:header>
-      <span>设备视图</span>
-    </template>
-    <el-row class="art-full-height w-full">
-      <el-col :span="16">
-        <div class="art-full-height w-full">
-          <Meta2dView :data="data" :preview="true" />
-        </div>
-      </el-col>
-      <el-col :span="8">
-        <div class="art-full-height w-full">
-          <ArtTable ref="tableRef" :loading="loading" :columns="columns" :data="tableData"></ArtTable>
-        </div>
-      </el-col>
-    </el-row>
-  </el-card>
+  <div class="page-content flex flex-col gap-4">
+    <div class="flex h-[32px] w-full items-center justify-between">
+      <h2 class="text-lg font-bold">设备拓扑图</h2>
+      <div class="flex items-center">
+        <el-button type="primary" @click="() => dialogApi.open()">故障设备列表</el-button>
+      </div>
+    </div>
+    <div class="flex-1">
+      <Meta2dView :data="data" :preview="true" />
+    </div>
+    <Dialog>
+      <ArtTable ref="tableRef" :loading="loading" :columns="columns" :data="tableData"></ArtTable>
+    </Dialog>
+  </div>
 </template>
 
 <script lang="ts" setup>
+  import { useDialog } from '@/components/dialog'
   import Meta2dView from '@/components/business/meta2d/Meta2dView.vue'
   import ArtTable from '@/components/core/tables/art-table/index.vue'
   defineOptions({ name: 'Device' })
@@ -26,6 +24,16 @@
   import data from './data.json'
   import { ColumnOption } from '@/types/component'
 
+  const [Dialog, dialogApi] = useDialog({
+    title: '故障设备列表',
+    width: '30%',
+    modal: true,
+    destroyOnClose: true,
+    closeOnClickModal: true,
+    dialogType: 'drawer',
+    footer: false
+  })
+
   // 表格数据
   const tableRef = ref()
   const loading = ref(false)