浏览代码

新增工作流 工作流模板 sop管理

pm 4 月之前
父节点
当前提交
20500698bd
共有 66 个文件被更改,包括 8715 次插入1071 次删除
  1. 16 0
      package-lock.json
  2. 1 0
      package.json
  3. 49 0
      src/api/custonWorkflow/index.ts
  4. 62 0
      src/api/custonWorkflow/step.ts
  5. 59 0
      src/api/custonWorkflow/stepTemplate.ts
  6. 9 10
      src/api/email/notify/index.ts
  7. 8 9
      src/api/email/templates/index.ts
  8. 1 0
      src/api/infra/file/index.ts
  9. 1 2
      src/api/material/blacklist/index.ts
  10. 1 1
      src/api/material/checkRecord/index.ts
  11. 0 43
      src/api/material/information/index.ts
  12. 2 2
      src/api/material/manualException/index.ts
  13. 4 4
      src/api/material/plan/index.ts
  14. 2 2
      src/api/material/statistics/index.ts
  15. 52 0
      src/api/sop/index.ts
  16. 47 0
      src/api/sop/sopPoint.ts
  17. 61 0
      src/api/sop/sopStep.ts
  18. 49 0
      src/api/sop/sopUser.ts
  19. 8 1
      src/api/system/user/index.ts
  20. 二进制
      src/assets/images/添加.png
  21. 二进制
      src/assets/images/返回.png
  22. 2 2
      src/components/TinyMCE/index.vue
  23. 1 1
      src/main.ts
  24. 169 8
      src/router/modules/remaining.ts
  25. 2 1
      src/styles/index.scss
  26. 1 0
      src/utils/dict.ts
  27. 118 0
      src/views/CustomStepTemplate/CS/StepTemplateDetail.vue
  28. 625 0
      src/views/CustomStepTemplate/CS/index.vue
  29. 113 0
      src/views/CustomWorkflow/CW/CheckView.vue
  30. 140 0
      src/views/CustomWorkflow/CW/CreateView.vue
  31. 118 0
      src/views/CustomWorkflow/CW/TableStepDetail.vue
  32. 760 0
      src/views/CustomWorkflow/CW/TableView.vue
  33. 353 0
      src/views/CustomWorkflow/CW/TemplateAdd.vue
  34. 93 0
      src/views/CustomWorkflow/CW/UpdateView.vue
  35. 1083 0
      src/views/CustomWorkflow/CW/WorkFlowView.vue
  36. 261 0
      src/views/CustomWorkflow/CW/index.vue
  37. 8 7
      src/views/Exceptions/manualException/index.vue
  38. 4 3
      src/views/dv/lotoStation/index.vue
  39. 2 259
      src/views/dv/technology/technologyDetail/DeviceDetail.vue
  40. 11 6
      src/views/email/emailNotify/EmailNotifyForm.vue
  41. 14 8
      src/views/email/emailNotify/index.vue
  42. 0 131
      src/views/email/emailTemplates/EmailTemplateForm.vue
  43. 0 188
      src/views/email/emailTemplates/index.vue
  44. 44 55
      src/views/material/Inspectionrecords/index.vue
  45. 118 182
      src/views/material/blacklist/BlacklistForm.vue
  46. 8 8
      src/views/material/blacklist/index.vue
  47. 9 18
      src/views/material/coll/index.vue
  48. 31 15
      src/views/material/information/BindDialog.vue
  49. 53 17
      src/views/material/information/index.vue
  50. 39 13
      src/views/material/inspectionplan/PlanForm.vue
  51. 40 47
      src/views/material/inspectionplan/index.vue
  52. 10 4
      src/views/material/inventory/index.vue
  53. 1 3
      src/views/material/lockers/index.vue
  54. 787 0
      src/views/sopm/sop/CreateSop.vue
  55. 718 0
      src/views/sopm/sop/ModeView/TableView.vue
  56. 1083 0
      src/views/sopm/sop/ModeView/WorkFlowView.vue
  57. 79 0
      src/views/sopm/sop/SetModeStep.vue
  58. 13 0
      src/views/sopm/sop/SetPoint.vue
  59. 13 0
      src/views/sopm/sop/SetUser.vue
  60. 805 0
      src/views/sopm/sop/UpdateSop.vue
  61. 191 0
      src/views/sopm/sop/index.vue
  62. 2 7
      src/views/statisticians/LockerOne/LockerChange.vue
  63. 3 3
      src/views/system/marsdept/index.vue
  64. 275 0
      src/views/system/user/FaceOrFingerForm.vue
  65. 28 6
      src/views/system/user/UserForm.vue
  66. 55 5
      src/views/system/user/index.vue

+ 16 - 0
package-lock.json

@@ -16,6 +16,7 @@
         "@microsoft/fetch-event-source": "^2.0.1",
         "@tinymce/tinymce-vue": "^6.2.0",
         "@videojs-player/vue": "^1.0.0",
+        "@vue-flow/core": "^1.45.0",
         "@vueuse/core": "^10.9.0",
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^5.1.10",
@@ -6275,6 +6276,21 @@
         "path-browserify": "^1.0.1"
       }
     },
+    "node_modules/@vue-flow/core": {
+      "version": "1.45.0",
+      "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.45.0.tgz",
+      "integrity": "sha512-+Qd4fTnCfrhfYQzlHyf5Jt7rNE4PlDnEJEJZH9v6hDZoTOeOy1RhS85cSxKYxdsJ31Ttj2v3yabhoVfBf+bmJA==",
+      "dependencies": {
+        "@vueuse/core": "^10.5.0",
+        "d3-drag": "^3.0.0",
+        "d3-interpolate": "^3.0.1",
+        "d3-selection": "^3.0.0",
+        "d3-zoom": "^3.0.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.0"
+      }
+    },
     "node_modules/@vue/babel-helper-vue-transform-on": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz",

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "@microsoft/fetch-event-source": "^2.0.1",
     "@tinymce/tinymce-vue": "^6.2.0",
     "@videojs-player/vue": "^1.0.0",
+    "@vue-flow/core": "^1.45.0",
     "@vueuse/core": "^10.9.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",

+ 49 - 0
src/api/custonWorkflow/index.ts

@@ -0,0 +1,49 @@
+// src/api/mes/modeStation/modeStation.ts
+import request from '@/config/axios'
+
+export interface modeStationVO {
+  modeId?: number
+  modeName: string
+  modeCode: string
+  isPreset?: string
+  modeTitle?: string
+  modeDescription?: string
+  isColockSupport?: string
+  createTime?: Date
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  modeName?: string
+  modeTitle?: string
+  isPreset?: string
+}
+
+// 查询工作流模式列表
+export const getWorkflowModePage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/workflow-mode/getWorkflowModePage', params })
+}
+
+// 获取工作流模式详细信息
+export const selectWorkflowModeById = async (id: number) => {
+  return await request.get({ url: '/iscs/workflow-mode/selectWorkflowModeById', params: { id: id } })
+}
+
+
+// 新增工作流模式
+export const insertWorkflowMode = async (data: modeStationVO) => {
+  return await request.post({ url: '/iscs/workflow-mode/insertWorkflowMode', data })
+}
+
+// 修改工作流模式
+export const updateWorkflowMode = async (data: modeStationVO) => {
+  return await request.put({ url: '/iscs/workflow-mode/updateWorkflowMode', data })
+}
+
+// 删除工作流模式
+export const deleteWorkflowModeList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/workflow-mode/deleteWorkflowModeList?ids='+ids,
+  })
+}

+ 62 - 0
src/api/custonWorkflow/step.ts

@@ -0,0 +1,62 @@
+// src/api/mes/modeStation/modeStation.ts
+import request from '@/config/axios'
+
+export interface modeStationVO {
+  modeId?: number
+  createTime?: Date
+  stepTemplateId: number,
+  stepIndex: number,
+  stepName: string,
+  stepTitle: string,
+  stepDescription: string,
+  confirmType:number,
+  confirmRoleCode: string,
+  confirmUser:number,
+  enableCancelJob: boolean,
+  enableSetLocker: boolean,
+  enableSetColocker: boolean,
+  enableAddColocker: boolean,
+  gotoStepAfterAddingColocker:number,
+  enableReduceColocker: boolean,
+  enableLock: boolean,
+  enableColock: boolean,
+  enableReleaseColock: boolean,
+  enableUnlock: boolean,
+  enableEndJob: boolean,
+
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  stepName?: string
+  stepTitle?: string
+}
+
+// 查询工作步骤列表
+export const getWorkflowStepPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/workflow-step/getWorkflowStepPage', params })
+}
+
+// 获取工作步骤详细信息
+export const selectWorkflowStepById = async (id: number) => {
+  return await request.get({ url: '/iscs/workflow-step/selectWorkflowStepById', params: { id: id } })
+}
+
+
+// 新增工作步骤
+export const insertWorkflowStep = async (data: modeStationVO) => {
+  return await request.post({ url: '/iscs/workflow-step/insertWorkflowStep', data })
+}
+
+// 修改工作步骤
+export const updateWorkflowStep = async (data: modeStationVO) => {
+  return await request.post({ url: '/iscs/workflow-step/updateWorkflowStep', data })
+}
+
+// 删除工作步骤
+export const deleteWorkflowStepList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/workflow-step/deleteWorkflowStepList?ids='+ids,
+  })
+}

+ 59 - 0
src/api/custonWorkflow/stepTemplate.ts

@@ -0,0 +1,59 @@
+// src/api/mes/modeStation/modeStation.ts
+import request from '@/config/axios'
+
+export interface modeStationVO {
+  stepCode: number,
+  isPreset: boolean,
+  stepName: string,
+  stepTitle: string,
+  stepDescription: string,
+  confirmType:number,
+  confirmRoleCode: string,
+  confirmUser:number,
+  enableCancelJob: boolean,
+  enableSetLocker: boolean,
+  enableSetColocker: boolean,
+  enableAddColocker: boolean,
+  gotoStepAfterAddingColocker:number,
+  enableReduceColocker: boolean,
+  enableLock: boolean,
+  enableColock: boolean,
+  enableReleaseColock: boolean,
+  enableUnlock: boolean,
+  enableEndJob: boolean,
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  stepName?: string
+  stepTitle?: string
+}
+
+// 查询工作步骤模板列表
+export const getWorkflowStepTemplatePage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/workflow-step-template/getWorkflowStepTemplatePage', params })
+}
+
+// 获取工作步骤模板详细信息
+export const selectWorkflowStepTemplateById = async (id: number) => {
+  return await request.get({ url: '/iscs/workflow-step-template/selectWorkflowStepTemplateById', params: { id: id } })
+}
+
+
+// 新增工作步骤模板
+export const insertWorkflowStepTemplate = async (data: modeStationVO) => {
+  return await request.post({ url: '/iscs/workflow-step-template/insertWorkflowStepTemplate', data })
+}
+
+// 修改工作步骤模板
+export const updateWorkflowStepTemplate = async (data: modeStationVO) => {
+  return await request.put({ url: '/iscs/workflow-step-template/updateWorkflowStepTemplate', data })
+}
+
+// 删除工作步骤模板
+export const deleteWorkflowStepTemplateList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/workflow-step-template/deleteWorkflowStepTemplateList?ids='+ids,
+  })
+}

+ 9 - 10
src/api/email/notify/index.ts

@@ -22,31 +22,30 @@ export interface PageParam {
 
 // 查看系统邮件提醒周期配置-分页
 export const listIsMailNotifyConfigPage = async (params: PageParam) => {
-  return await request.get({ url: '/iscs/notify/getIsMailNotifyConfigPage', params })
+  return await request.get({ url: '/iscs/mail-notify-config/getMailNotifyConfigPage', params })
 }
 
 // 新增系统邮件提醒周期配置
 export const addIsMailNotifyConfig = async (data: MailNotifyConfigVO) => {
-  return await request.post({ url: '/iscs/notify/insertIsMailNotifyConfig', data })
+  return await request.post({ url: '/iscs/mail-notify-config/insertMailNotifyConfig', data })
 }
 
 // 删除系统邮件提醒周期配置
-export const deleteIsMailNotifyConfig = async (configId: number) => {
-  return await request.post({
-    url: '/iscs/notify/deleteIsMailNotifyConfigByConfigIds',
-    params: { configIds: configId }
+export const deleteIsMailNotifyConfig = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/mail-notify-config/deleteMailNotifyConfigList?ids='+ids,
   })
 }
 
 // 修改系统邮件提醒周期配置
 export const updateIsMailNotifyConfig = async (data: MailNotifyConfigVO) => {
-  return await request.post({ url: '/iscs/notify/updateIsMailNotifyConfig', data })
+  return await request.put({ url: '/iscs/mail-notify-config/updateMailNotifyConfig', data })
 }
 
 // 获取系统邮件提醒周期配置详细信息
-export const getIsMailNotifyConfigById = async (configId: number) => {
+export const getIsMailNotifyConfigById = async (id: number) => {
   return await request.get({
-    url: '/iscs/notify/selectIsMailNotifyConfigById',
-    params: { configId }
+    url: '/iscs/mail-notify-config/selectMailNotifyConfigById',
+    params: { id }
   })
 }

+ 8 - 9
src/api/email/templates/index.ts

@@ -22,31 +22,30 @@ export interface PageParam {
 
 // 查询邮件模板列表
 export const listEmailTemplates = async (params: PageParam) => {
-  return await request.get({ url: '/iscs/template/getIsMailTemplatePage', params })
+  return await request.get({ url: '/iscs/mail-send-task-item/getMailSendTaskItemPage', params })
 }
 
 // 查询邮件模板详细
-export const getEmailTemplatesInfo = async (templateId: number) => {
+export const getEmailTemplatesInfo = async (id: number) => {
   return await request.get({
-    url: '/iscs/template/selectIsMailTemplateById',
-    params: { templateId }
+    url: '/iscs/mail-send-task-item/selectMailSendTaskItemById',
+    params: { id }
   })
 }
 
 // 新增邮件模板
 export const addEmailTemplates = async (data: EmailTemplateVO) => {
-  return await request.post({ url: '/iscs/template/insertIsMailTemplate', data })
+  return await request.post({ url: '/iscs/mail-send-task-item/insertMailSendTaskItem', data })
 }
 
 // 修改邮件模板
 export const updateEmailTemplates = async (data: EmailTemplateVO) => {
-  return await request.post({ url: '/iscs/template/updateIsMailTemplate', data })
+  return await request.put({ url: '/iscs/mail-send-task-item/updateMailSendTaskItem', data })
 }
 
 // 删除邮件模板
-export const delEmailTemplates = async (templateIds: number) => {
+export const delEmailTemplates = async (ids: number) => {
   return await request.post({
-    url: '/iscs/template/deleteIsMailTemplateByTemplateCodes',
-    params: { templateIds }
+    url: '/iscs/mail-send-task-item/deleteMailSendTaskItemList?ids='+ids,
   })
 }

+ 1 - 0
src/api/infra/file/index.ts

@@ -37,5 +37,6 @@ export const createFile = (data: any) => {
 
 // 上传文件
 export const updateFile = (data: any) => {
+  // return request.upload({ url: '/iscs/user-characteristic/insertUserFingerprintDat', data })
   return request.upload({ url: '/infra/file/upload', data })
 }

+ 1 - 2
src/api/material/blacklist/index.ts

@@ -20,7 +20,6 @@ export interface BlacklistVO {
 export const listBlacklist = async (params: BlacklistQuery) => {
   return await request.get({ url: '/iscs/blacklist/getBlacklistPage', params })
 }
-
 // 查询黑名单详情
 export const getBlacklistInfo = async (id: number) => {
   return await request.get({ url: '/iscs/blacklist/selectBlacklistById', params: { id: id } })
@@ -38,7 +37,7 @@ export const updateBlacklist = async (data: BlacklistVO) => {
 
 // 删除黑名单
 export const delBlacklist = async (ids: number) => {
-  return await request.post({
+  return await request.delete({
     url: '/iscs/blacklist/deleteBlacklistList?ids='+ids,
 
   })

+ 1 - 1
src/api/material/checkRecord/index.ts

@@ -53,5 +53,5 @@ export const deleteCheckRecord = async (ids: number) => {
 }
 // 导出物资检查记录
 export const exportCheckRecord = (params: any) => {
-  return request.download({ url: '/iscs/materials-check-record/exportMaterialsCheckRecordExcel', params })
+  return request.get({ url: '/iscs/materials-check-record/exportMaterialsCheckRecordExcel', params })
 }

+ 0 - 43
src/api/material/information/index.ts

@@ -106,24 +106,6 @@ export const updateMaterialsLoan = async (data: MaterialsLoanVO[]) => {
   })
 }
 
-// 物资更换记录
-export const insertMaterialsChangeRecord = async (data: MaterialsChangeVO) => {
-  return await request.post({ url: '/iscs/change/insertIsMaterialsChangeRecord', data })
-}
-
-// 物资绑定与解绑
-export const updateMaterialsBindingRemove = async (data: MaterialsBindingVO) => {
-  return await request.post({ url: '/iscs/cabinet/updateMaterialsBindingRemove', data })
-}
-
-// 新增物资检查记录
-export const insertMaterialsCheckRecord = async (data: MaterialsCheckVO[]) => {
-  return await request.post({
-    url: '/iscs/check/insertIsMaterialsCheckRecord',
-    data: { list: data }
-  })
-}
-
 // 查询异常还错柜子的物资
 export const getExMaterials = async (params: MaterialsQuery) => {
   return await request.get({ url: '/iscs/materials/getExMaterials', params })
@@ -137,30 +119,5 @@ export const getExMaterialType = async (params: { materialsId: number }) => {
   })
 }
 
-// 确认更换物资
-export const insertReplaceRecord = async (data: MaterialsChangeVO[]) => {
-  return await request.post({
-    url: '/iscs/hardware/material-api/insertReplaceRecord',
-    data: { list: data }
-  })
-}
 
-// 新增物资柜开门超时异常
-export const insertCabinetOpenTimeout = async (data: { cabinetId: number }) => {
-  return await request.post({ url: '/iscs/hardware/material-api/insertCabinetOpenTimeout', data })
-}
 
-// 解除物资柜开门超时异常
-export const updateCabinetOpenTimeout = async (data: { cabinetId: number }) => {
-  return await request.post({ url: '/iscs/hardware/material-api/updateCabinetOpenTimeout', data })
-}
-
-// 新增物资柜开柜门记录
-export const insertCabinetOpenRecord = async (data: { cabinetId: number }) => {
-  return await request.post({ url: '/iscs/hardware/material-api/insertCabinetOpenRecord', data })
-}
-
-// 更新物资柜关闭柜门记录
-export const updateCabinetClose = async (data: { cabinetId: number }) => {
-  return await request.post({ url: '/iscs/hardware/material-api/updateCabinetClose', data })
-}

+ 2 - 2
src/api/material/manualException/index.ts

@@ -42,12 +42,12 @@ export interface ManualExceptionVO {
 
 // 查询人工传入异常列表
 export const listManualException = async (params: ManualExceptionQuery) => {
-  return await request.get({ url: '/iscs/exception/getIsExceptionPage', params })
+  return await request.get({ url: '/iscs/exception/getExceptionPage', params })
 }
 
 // 查询人工传入异常详情
 export const getExceptionInfo = async (id: number) => {
-  return await request.get({ url: '/iscs/exception/selectIsExceptionById', params: { exceptionId: id } })
+  return await request.get({ url: '/iscs/exception/selectExceptionById', params: { exceptionId: id } })
 }
 
 // 新增人工传入异常

+ 4 - 4
src/api/material/plan/index.ts

@@ -57,7 +57,7 @@ export const updatePlan = async (data: PlanVO) => {
 
 // 删除物资检查计划
 export const deletePlan = async (ids: number) => {
-  return await request.post({
+  return await request.delete({
     url: '/iscs/materials-check-plan/deleteMaterialsCheckPlanList?ids='+ids,
   })
 }
@@ -67,15 +67,15 @@ export const getCheckPlanCabinetList = async (params: PlanQuery) => {
   return await request.get({ url: '/iscs/materials-plan-cabinet/getMaterialsPlanCabinetPage', params })
 }
 
-// 更新自动创建检查计划配置
+// 更新自动创建检查计划配置(接口在邮件提醒周期配置里)
 export const updateAutomaticConfig = async (data: AutoConfigVO) => {
-  return await request.post({ url: '/iscs/notify/updateAutomaticConfig', data })
+  return await request.post({ url: '/iscs/mail-notify-config/updateAutomaticConfig', data })
 }
 
 // 查询自动创建检查计划配置
 export const selectIsMailNotifyConfigByCode = async (params: { configCode: string }) => {
   return await request.get({
-    url: '/iscs/notify/selectIsMailNotifyConfigByCode',
+    url: '/iscs/mail-notify-config/selectMailNotifyConfigByCode',
     params
   })
 }

+ 2 - 2
src/api/material/statistics/index.ts

@@ -150,6 +150,6 @@ export const exportClaimAndReturn = async (params) => {
   return await request.post({ url: `/iscs/statistics-api/exportClaimAndReturn`, params })
 }
 //导出物资盘点
-export const exportMaterialInventory = async (params) => {
-  return await request.post({ url: `/iscs/statistics-api/exportMaterialInventory`, params })
+export const exportMaterialInventory = async () => {
+  return await request.post({ url: `/iscs/statistics-api/exportMaterialInventory` })
 }

+ 52 - 0
src/api/sop/index.ts

@@ -0,0 +1,52 @@
+
+import request from '@/config/axios'
+
+export interface sopVO {
+  id: number,
+  sopCode: string,
+  sopName: string,
+  sopType: number,
+  workstationId: number,
+  machineryId: number,
+  sopContent: string,
+  sopStatus: number,
+  sopIndex: number,
+  remark: string
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  sopName?: string
+  sopType?: string
+  machineryId?: string
+  workstationId: number,
+}
+
+// 查询SOP列表
+export const getSopPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/sop/getSopPage', params })
+}
+
+// 获取SOP详细信息
+export const selectSopById = async (id: number) => {
+  return await request.get({ url: '/iscs/sop/selectSopById', params: { id: id } })
+}
+
+
+// 新增SOP
+export const insertSop = async (data: sopVO) => {
+  return await request.post({ url: '/iscs/sop/insertSop', data })
+}
+
+// 修改SOP
+export const updateSop = async (data: sopVO) => {
+  return await request.put({ url: '/iscs/sop/updateSop', data })
+}
+
+// 删除SOP
+export const deleteSopList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/sop/deleteSopList?ids='+ids,
+  })
+}

+ 47 - 0
src/api/sop/sopPoint.ts

@@ -0,0 +1,47 @@
+
+import request from '@/config/axios'
+
+export interface SOPVo {
+  id: number,
+  sopId: number,
+  groupId: number,
+  pointId: number,
+  prePointId: number,
+  remark: string,
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+}
+
+// 查询SopPoint列表
+export const getSopWorkflowStepPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/sop-points/getSopPointsPage', params })
+}
+// 查询SopPointlist
+export const getSopPointsList = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/sop-points/getSopPointsList', params })
+}
+// 获取SopPoint详细信息
+export const selectSopPointsById = async (id: number) => {
+  return await request.get({ url: '/iscs/sop-points/selectSopPointsById', params: { id: id } })
+}
+
+
+// 新增SopPoint
+export const insertSopPoints = async (data: SOPVo) => {
+  return await request.post({ url: '/iscs/sop-points/insertSopPoints', data })
+}
+
+// 修改SopPoint
+export const updateSopPoints= async (data: SOPVo) => {
+  return await request.put({ url: '/iscs/sop-points/updateSopPoints', data })
+}
+
+// 删除SopPoint
+export const deleteSopPointsList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/sop-points/deleteSopPointsList?ids='+ids,
+  })
+}

+ 61 - 0
src/api/sop/sopStep.ts

@@ -0,0 +1,61 @@
+
+import request from '@/config/axios'
+
+export interface SOPVo {
+  id: number,
+  sopId: number,
+  stepId: number,
+  stepIndex: number,
+  stepName: string,
+  stepIcon: string,
+  stepTitle: string,
+  stepTitleShort: string,
+  stepDescription: string,
+  confirmType: number,
+  confirmRoleCode: string,
+  confirmUser: number,
+  enableCancelJob: boolean,
+  enableSetLocker: boolean,
+  enableSetColocker: boolean,
+  enableAddColocker: boolean,
+  gotoStepAfterAddingColocker: number,
+  enableReduceColocker: boolean,
+  enableLock: boolean,
+  enableColock: boolean,
+  enableReleaseColock: boolean,
+  enableUnlock: boolean,
+  enableEndJob: boolean
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+}
+
+// 查询SOP步骤列表
+export const getSopWorkflowStepPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/sop-workflow-step/getSopWorkflowStepPage', params })
+}
+
+// 获取SOP步骤详细信息
+export const selectSopWorkflowStepById = async (id: number) => {
+  return await request.get({ url: '/iscs/sop-workflow-step/selectSopWorkflowStepById', params: { id: id } })
+}
+
+
+// 新增SOP步骤
+export const insertSopWorkflowStep = async (data: SOPVo) => {
+  return await request.post({ url: '/iscs/sop-workflow-step/insertSopWorkflowStep', data })
+}
+
+// 修改SOP步骤
+export const updateSopWorkflowStep= async (data: SOPVo) => {
+  return await request.put({ url: '/iscs/sop-workflow-step/updateSopWorkflowStep', data })
+}
+
+// 删除SOP步骤
+export const deleteSopWorkflowStepList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/sop-workflow-step/deleteSopWorkflowStepList?ids='+ids,
+  })
+}

+ 49 - 0
src/api/sop/sopUser.ts

@@ -0,0 +1,49 @@
+
+import request from '@/config/axios'
+
+export interface SOPVo {
+  id: number,
+  sopId: number,
+  groupId: number,
+  userId: number,
+  userName: string,
+  userType: number,
+  userRole: string,
+  remark: string
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+}
+
+// 查询SopUser列表
+export const getSopUserPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/sop-user/getSopUserPage', params })
+}
+// 查询SopUserList
+export const getSopUserList = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/sop-user/getSopUserList', params })
+}
+// 获取SopUser详细信息
+export const selectSopUserById = async (id: number) => {
+  return await request.get({ url: '/iscs/sop-user/selectSopUserById', params: { id: id } })
+}
+
+
+// 新增SopUser
+export const insertSopUser = async (data: SOPVo) => {
+  return await request.post({ url: '/iscs/sop-user/insertSopUser', data })
+}
+
+// 修改SopUser
+export const updateSopUser= async (data: SOPVo) => {
+  return await request.put({ url: '/iscs/sop-user/updateSopUser', data })
+}
+
+// 删除SopUser
+export const deleteSopUserList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/sop-user/deleteSopUserList?ids='+ids,
+  })
+}

+ 8 - 1
src/api/system/user/index.ts

@@ -21,7 +21,14 @@ export interface UserVO {
 export const getUserPage = (params: PageParam) => {
   return request.get({ url: '/system/user/page', params })
 }
-
+// 查询用户管理查询特征值(指纹 面部)
+export const getUserType = (params: PageParam) => {
+  return request.get({ url: '/iscs/user-characteristic/getUserCharacteristicPage', params })
+}
+//删除用户指纹 人脸
+export const deleteUserFaceOrFinger = (ids: number) => {
+  return request.delete({ url: '/iscs/user-characteristic/deleteUserCharacteristicList?ids=' + ids })
+}
 // 查询用户详情
 export const getUser = (id: number) => {
   return request.get({ url: '/system/user/get?id=' + id })

二进制
src/assets/images/添加.png


二进制
src/assets/images/返回.png


+ 2 - 2
src/components/TinyMCE/index.vue

@@ -122,8 +122,8 @@ const handleFilePicker = (callback: Function, value: string, meta: any) => {
 // TinyMCE配置
 const init = reactive({
   selector: `#${tinymceId.value}`,
-  language_url: '/tinymce/langs/zh-Hans.js',
-  language: 'zh-Hans',
+  language_url: '/tinymce/langs/zh-CN.js',
+  language: 'zh_CN',
   skin_url: '/tinymce/skins/ui/oxide',
   content_css: '/tinymce/skins/content/default/content.css',
   menubar: true,

+ 1 - 1
src/main.ts

@@ -68,7 +68,7 @@ const setupAll = async () => {
   app.use(VueKonva)
   app.use(VueDOMPurifyHTML)
   app.component('TinyMCE', TinyMCE)
-
+  app.config.warnHandler = () => null
   app.mount('#app')
 }
 

+ 169 - 8
src/router/modules/remaining.ts

@@ -1,6 +1,6 @@
-import { Layout } from '@/utils/routerHelper'
+import {Layout} from '@/utils/routerHelper'
 
-const { t } = useI18n()
+const {t} = useI18n()
 /**
  * redirect: noredirect        当设置 noredirect 的时候该路由在面包屑导航中不可被点击
  * name:'router-name'          设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
@@ -104,6 +104,167 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+  {
+    path: '/CustomWorkflow',
+    component: Layout,
+    name: 'CustomWorkflow',
+    meta: {
+      hidden: true,
+    },
+    children: [
+      {
+        path: 'CW/create',
+        component: () => import('@/views/CustomWorkflow/CW/CreateView.vue'),
+        name: 'CreateView',
+        meta: {
+          title: '自定义作业流程新增',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/CustomWorkflow/CW/CreateView'
+        }
+      },
+      {
+        path: 'CW/update',
+        component: () => import('@/views/CustomWorkflow/CW/UpdateView.vue'),
+        name: 'UpdateView',
+        meta: {
+          title: '自定义作业流程修改',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/CustomWorkflow/CW/UpdateView'
+        }
+      },
+      {
+        path: 'CW/view',
+        component: () => import('@/views/CustomWorkflow/CW/CheckView.vue'),
+        name: 'CheckView',
+        meta: {
+          title: '自定义作业流程查看',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/CustomWorkflow/CW/CheckView'
+        },
+
+      },
+      {
+        path: 'CW/TableStepDetail',
+        component: () => import('@/views/CustomWorkflow/CW/TableStepDetail.vue'),
+        name: 'TableStepDetail',
+        meta: {
+          title: '步骤操作说明',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/CustomWorkflow/CW/TableStepDetail'
+        }
+      },
+    ]
+  },
+  {
+    path: '/CustomStepTemplate',
+    component: Layout,
+    name: 'CustomStepTemplate',
+    meta: {
+      hidden: true,
+    },
+    children: [
+      {
+        path: 'CustomStepTemplate/CS/StepTemplateDetail',
+        component: () => import('@/views/CustomStepTemplate/CS/StepTemplateDetail.vue'),
+        name: 'StepTemplateDetail',
+        meta: {
+          title: '步骤操作说明',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/CustomStepTemplate/CS/StepTemplateDetail'
+        }
+      },
+    ]
+  },
+  {
+    path: '/sopm',
+    component: Layout,
+    name: 'sopm',
+    meta: {
+      hidden: true,
+    },
+    children: [
+      {
+        path: 'sopm/sop/CreateSop',
+        component: () => import('@/views/sopm/sop/CreateSop.vue'),
+        name: 'CreateSop',
+        meta: {
+          title: 'sop新增',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/sopm/sop/CreateSop'
+        }
+      },
+      {
+        path: 'sopm/sop/UpdateSop',
+        component: () => import('@/views/sopm/sop/UpdateSop.vue'),
+        name: 'UpdateSop',
+        meta: {
+          title: 'sop修改',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/sopm/sop/UpdateSop'
+        }
+      },
+      {
+        path: 'sopm/sop/SetModeStep',
+        component: () => import('@/views/sopm/sop/SetModeStep.vue'),
+        name: 'SetModeStep',
+        meta: {
+          title: '设置模式步骤',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/sopm/sop/SetModeStep'
+        }
+      },
+      {
+        path: 'sopm/sop/SetPoint',
+        component: () => import('@/views/sopm/sop/SetPoint.vue'),
+        name: 'SetPoint',
+        meta: {
+          title: '设置点位',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/sopm/sop/SetPoint'
+        }
+      },
+      {
+        path: 'sopm/sop/SetUser',
+        component: () => import('@/views/sopm/sop/SetUser.vue'),
+        name: 'SetUser',
+        meta: {
+          title: '设置人员',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/sopm/sop/SetUser'
+        }
+      },
+    ]
+  },
   {
     path: '/material',
     component: Layout,
@@ -546,13 +707,13 @@ const remainingRouter: AppRouteRecordRaw[] = [
         path: 'order/detail/:id(\\d+)',
         component: () => import('@/views/mall/trade/order/detail/index.vue'),
         name: 'TradeOrderDetail',
-        meta: { title: '订单详情', icon: 'ep:view', activeMenu: '/mall/trade/order' }
+        meta: {title: '订单详情', icon: 'ep:view', activeMenu: '/mall/trade/order'}
       },
       {
         path: 'after-sale/detail/:id(\\d+)',
         component: () => import('@/views/mall/trade/afterSale/detail/index.vue'),
         name: 'TradeAfterSaleDetail',
-        meta: { title: '退款详情', icon: 'ep:view', activeMenu: '/mall/trade/after-sale' }
+        meta: {title: '退款详情', icon: 'ep:view', activeMenu: '/mall/trade/after-sale'}
       }
     ]
   },
@@ -560,7 +721,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     path: '/member',
     component: Layout,
     name: 'MemberCenter',
-    meta: { hidden: true },
+    meta: {hidden: true},
     children: [
       {
         path: 'user/detail/:id',
@@ -578,7 +739,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     path: '/pay',
     component: Layout,
     name: 'pay',
-    meta: { hidden: true },
+    meta: {hidden: true},
     children: [
       {
         path: 'cashier',
@@ -595,7 +756,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
   {
     path: '/diy',
     name: 'DiyCenter',
-    meta: { hidden: true },
+    meta: {hidden: true},
     component: Layout,
     children: [
       {
@@ -626,7 +787,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     path: '/crm',
     component: Layout,
     name: 'CrmCenter',
-    meta: { hidden: true },
+    meta: {hidden: true},
     children: [
       {
         path: 'clue/detail/:id',

+ 2 - 1
src/styles/index.scss

@@ -2,7 +2,8 @@
 @use './FormCreate/index.scss';
 @use './theme.scss';
 @use 'element-plus/theme-chalk/dark/css-vars.css';
-
+@import "@vue-flow/core/dist/style.css";
+@import "@vue-flow/core/dist/theme-default.css";
 .reset-margin [class*='el-icon'] + span {
   margin-left: 2px !important;
 }

+ 1 - 0
src/utils/dict.ts

@@ -248,6 +248,7 @@ export enum DICT_TYPE {
   //============== ISCS - 新增模块 =====================
   MES_MACHINERY_STATUS = 'mes_machinery_status',
   POWER_TYPE = 'power_type',
+  SYS_STEP_CONFIRMTYPE='sys_step_confirmType',
   POINT_TYPE = 'point_type',
   SOP_STATUS = 'sop_status',
   SOP_TYPE = 'sop_type',

+ 118 - 0
src/views/CustomStepTemplate/CS/StepTemplateDetail.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="step-detail-page">
+    <!-- 顶部操作栏 -->
+    <div class="action-bar">
+      <el-button @click="handleCancel">返回</el-button>
+      <el-button type="primary" @click="handleSave" :loading="saveLoading">保存</el-button>
+    </div>
+
+    <!-- 富文本编辑器 -->
+    <div class="editor-container">
+      <TinyMCE
+        v-model:value="stepDescription"
+        :height="700"
+        placeholder="请输入内容..."
+        @update:value="handleContentChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import TinyMCE from "@/components/TinyMCE/index.vue"
+import { updateWorkflowStepTemplate } from '@/api/custonWorkflow/stepTemplate'
+
+const route = useRoute()
+const router = useRouter()
+
+const stepDescription = ref('')
+const saveLoading = ref(false)
+const stepData = ref(null) // 存储完整的步骤数据
+
+// 内容变化处理
+const handleContentChange = (content: string) => {
+  // console.log('内容变化:', content)
+  stepDescription.value = content
+}
+
+// 保存操作
+const handleSave = async () => {
+  try {
+    saveLoading.value = true
+
+    // 更新步骤数据中的描述字段
+    const updateData = {
+      ...stepData.value,
+      stepDescription: stepDescription.value
+    }
+
+    // 调用更新接口
+    await updateWorkflowStepTemplate(updateData)
+
+    ElMessage.success('保存成功')
+
+
+  } catch (error) {
+    console.error('保存失败:', error)
+    ElMessage.error('保存失败,请稍后重试')
+  } finally {
+    saveLoading.value = false
+  }
+}
+
+// 取消操作
+const handleCancel = () => {
+  // 如果有内容变化,提示用户
+  if (stepDescription.value !== stepData.value?.stepDescription) {
+    ElMessageBox.confirm(
+      '内容已修改,确定要取消吗?',
+      '确认取消',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '继续编辑',
+        type: 'warning'
+      }
+    ).then(() => {
+
+      router.back()
+    }).catch(() => {
+      // 用户选择继续编辑,不做任何操作
+    })
+  } else {
+    router.back()
+  }
+}
+
+// 页面初始化
+onMounted(() => {
+  const tempData = localStorage.getItem('templateData')
+  if (tempData) {
+    stepData.value = JSON.parse(tempData)
+    stepDescription.value = stepData.value.stepDescription || ''
+    // 清除临时数据
+    localStorage.removeItem('templateData')
+  }
+
+})
+</script>
+
+<style scoped lang="scss">
+.step-detail-page {
+  padding: 20px;
+
+  .action-bar {
+    margin-bottom: 20px;
+    display: flex;
+    gap: 10px;
+    justify-content: flex-end;
+  }
+
+  .editor-container {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+  }
+}
+</style>

+ 625 - 0
src/views/CustomStepTemplate/CS/index.vue

@@ -0,0 +1,625 @@
+<template>
+  <div class="workflow-page">
+    <!-- 顶部按钮区域 -->
+    <ContentWrap>
+      <!-- 搜索工作栏 -->
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="100px"
+      >
+        <el-form-item label="标题" prop="stepTitle">
+          <el-input
+            v-model="queryParams.stepTitle"
+            placeholder="请输入隔离点名称"
+            clearable
+            class="!w-240px"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />
+            重置
+          </el-button>
+          <el-button type="primary" @click="addRow">
+            <el-icon>
+              <Plus />
+            </el-icon>
+            添加
+          </el-button>
+          <el-button type="danger" plain :disabled="!hasSelection" @click="deleteSelected()">
+            <Icon icon="ep:delete" class="mr-5px" />
+            删除
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 表格区域 -->
+    <div class="table-container">
+      <el-table
+        ref="tableRef"
+        :data="tableData"
+        @selection-change="handleSelectionChange"
+        border
+        stripe
+        row-key="id"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="是否预置" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.isPreset" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+        <el-table-column label="图标" width="120" align="center">
+          <template #default="{ row }">
+            <el-popover placement="bottom" trigger="click" width="230">
+              <!-- 图标选择面板 -->
+              <div style="display: flex; flex-wrap: wrap; gap: 8px">
+                <div
+                  v-for="icon in iconOptions"
+                  :key="icon.value"
+                  @click="() => selectIcon(row, icon.value)"
+                  :style="{
+                    border: row.stepIcon === icon.value ? '2px solid #409EFF' : '1px solid #ccc',
+                    borderRadius: '4px',
+                    padding: '2px',
+                    cursor: 'pointer'
+                  }"
+                >
+                  <img
+                    :src="icon.value"
+                    :alt="icon.name"
+                    style="width: 28px; height: 28px; object-fit: contain"
+                  />
+                </div>
+              </div>
+
+              <!-- 触发元素:当前图标或按钮 -->
+              <template #reference>
+                <div style="cursor: pointer; display: inline-block">
+                  <img
+                    v-if="row.stepIcon"
+                    :src="row.stepIcon"
+                    style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid #ccc"
+                  />
+                  <el-button v-else type="primary" size="small">选择图标</el-button>
+                </div>
+              </template>
+            </el-popover>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="标题" width="180">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.stepTitle"
+              placeholder="标题"
+              size="small"
+              @blur="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="标题缩写" width="120">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.stepTitleShort"
+              placeholder="标题缩写"
+              size="small"
+              @blur="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="确认方式" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.confirmType"
+              placeholder="请选择确认方式"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STEP_CONFIRMTYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="确认角色" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.confirmRoleCode"
+              placeholder="请选择确认角色"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+              @clear="handleRoleClear(row)"
+            >
+              <el-option
+                v-for="dict in RoleOptions"
+                :key="dict.code"
+                :label="dict.name"
+                :value="dict.code"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="确认人员" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.confirmUser"
+              placeholder="请选择确认人员"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in UserOptions"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="步骤操作说明" width="120" align="center">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="viewStepDetail(row)"> 查看</el-button>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="取消作业" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableCancelJob" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="设置锁定人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableSetLocker" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="设置共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableSetColocker" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="添加共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableAddColocker" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="添加共锁人后跳转步骤" width="180">
+          <template #default="{ row }">
+            <el-input-number
+              v-model="row.gotoStepAfterAddingColocker"
+              :min="1"
+              size="small"
+              placeholder="步骤号"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="减少共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableReduceColocker" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="上锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableLock" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="共锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableColock" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="解除共锁" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableReleaseColock" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="解锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableUnlock" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="结束作业" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableEndJob" @change="saveRowData(row)" />
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick } from 'vue'
+import { Plus, DocumentAdd, Delete } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import Sortable from 'sortablejs'
+
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration'
+import {
+  getWorkflowStepTemplatePage,
+  insertWorkflowStepTemplate,
+  updateWorkflowStepTemplate,
+  deleteWorkflowStepTemplateList
+} from '@/api/custonWorkflow/stepTemplate'
+import { getRolePage } from '@/api/system/role'
+import { getRoleUser } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  stepTitle: undefined
+})
+const route = useRoute()
+const router = useRouter()
+// 响应式数据
+const tableData = ref([])
+const selectedRows = ref([])
+const tableRef = ref()
+
+// 计算属性
+const hasSelection = computed(() => selectedRows.value.length > 0)
+// 表格图标切换
+const selectIcon = (row, iconUrl) => {
+  row.stepIcon = iconUrl
+  saveRowData(row) // 你已有的接口保存方法
+}
+// 查看步骤详情
+const viewStepDetail = (row) => {
+  // 临时存储步骤数据
+  console.log(row,'存储数据')
+  localStorage.setItem('templateData', JSON.stringify(row))
+
+  // 跳转到详情页面
+  router.push({
+    name: 'StepTemplateDetail',
+    query: {
+      id: row.id,
+    }
+  })
+}
+let stepCounter = 1
+// 生成新的表格行
+const createNewRow = () => {
+  const stepNumber = stepCounter++
+  return {
+    stepCode: `step_${stepNumber}`,
+    isPreset: undefined,
+    stepName: `步骤${stepNumber}`,
+    stepIcon: undefined,
+    stepTitle: undefined,
+    stepTitleShort: undefined,
+    stepDescription: undefined,
+    confirmType: undefined,
+    confirmRoleCode: undefined,
+    confirmUser: undefined,
+    enableCancelJob: undefined,
+    enableSetLocker: undefined,
+    enableSetColocker: undefined,
+    enableAddColocker: undefined,
+    gotoStepAfterAddingColocker: undefined,
+    enableReduceColocker: undefined,
+    enableLock: undefined,
+    enableColock: undefined,
+    enableReleaseColock: undefined,
+    enableUnlock: undefined,
+    enableEndJob: undefined,
+    id: undefined
+  }
+}
+
+// 添加新行
+const addRow = () => {
+  const newRow = createNewRow()
+
+  // 先插入表格
+  tableData.value.push(newRow)
+
+  // 然后保存(传的就是表格里的对象引用)
+  saveRowData(newRow)
+}
+
+
+
+// 删除选中行
+const deleteSelected = async () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请先选择要删除的行')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除选中的 ${selectedRows.value.length} 行数据吗?`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+
+    // 获取选中行中已有 id 的(过滤未保存的行)
+    const selectedWithId = selectedRows.value.filter((row) => row.id)
+    const selectedIds = selectedWithId.map((row) => row.id)
+
+    // 删除已有 id 的记录
+    if (selectedIds.length > 0) {
+      await deleteWorkflowStepTemplateList(selectedIds)
+    }
+
+    // 无论是否调用接口,前端都要同步移除这些行
+    tableData.value = tableData.value.filter((row) => !selectedRows.value.includes(row))
+    selectedRows.value = []
+
+    ElMessage.success('删除成功')
+  } catch (error) {
+    console.error('删除失败', error)
+    ElMessage.error('删除失败,请稍后重试')
+  }
+}
+
+// 选择变化处理
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection
+}
+
+// 行拖拽
+const initRowDrop = () => {
+  nextTick(() => {
+    const tbody = document.querySelector('.el-table__body-wrapper tbody')
+    if (tbody) {
+      Sortable.create(tbody, {
+        animation: 150,
+        handle: 'tr',
+        ghostClass: 'sortable-ghost',
+        chosenClass: 'sortable-chosen',
+        dragClass: 'sortable-drag',
+        onEnd: async ({ newIndex, oldIndex }) => {
+          if (newIndex !== undefined && oldIndex !== undefined && newIndex !== oldIndex) {
+            const movedRow = tableData.value.splice(oldIndex, 1)[0]
+            tableData.value.splice(newIndex, 0, movedRow)
+
+            for (let i = 0; i < tableData.value.length; i++) {
+              tableData.value[i].id = i + 1
+              await saveRowData(tableData.value[i])
+            }
+
+            ElMessage.success('拖拽排序完成')
+          }
+        }
+      })
+    }
+  })
+}
+
+// 自动保存单行数据
+const saveRowData = async (row) => {
+  try {
+    if (!row) return
+
+    if (!row.id) {
+      const res = await insertWorkflowStepTemplate(row)
+      if (res) {
+        // 写回 id,保持响应式引用不变
+        row.id = res
+        ElMessage.success('新增步骤成功')
+      }
+    } else {
+      await updateWorkflowStepTemplate(row)
+      // 为了给确认人员传递查询条件
+      if (row.confirmRoleCode) {
+        await InitUser(row)
+      }
+
+      ElMessage.success('更新步骤成功')
+    }
+  } catch (error) {
+    console.error('保存步骤失败', error)
+    ElMessage.error('保存失败,请稍后重试')
+  }
+}
+
+// 角色获取
+const RoleOptions = ref()
+const InitRole = async () => {
+  const data = await getRolePage({ pageNo: 1, pageSize: 10 })
+  RoleOptions.value = data.list
+}
+// 人员获取
+const UserOptions = ref()
+const InitUser = async (row) => {
+  console.log(row, 'row')
+  try {
+    const data = await getRoleUser(row.confirmRoleCode)
+    UserOptions.value = data.map((row) => {
+      return {
+        label: row.nickname,
+        value: row.id
+      }
+    })
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 在您的组件中添加
+const iconOptions = ref([])
+
+// 获取图标选项
+const loadIconOptions = async () => {
+  const icons = await getIcons()
+  if (icons && icons.length > 0) {
+    iconOptions.value = icons
+    console.log('图标选项已加载:', iconOptions.value)
+  }
+}
+// 获取步骤基础图标
+const getIcons = async () => {
+  try {
+    const sysAttrKey1 = 'icon.step.all'
+    const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
+    console.log(iconRes, '获取到的图标配置')
+
+    if (iconRes && iconRes.sysAttrValue) {
+      // 将逗号分隔的字符串转换为数组
+      const iconKeys = iconRes.sysAttrValue.split(',')
+      console.log('图标键值列表:', iconKeys)
+
+      // 批量获取每个图标的具体值
+      const iconValues = await getIconValues(iconKeys)
+      console.log('所有图标值:', iconValues)
+
+      return iconValues
+    }
+  } catch (error) {
+    console.error('获取图标失败:', error)
+  }
+}
+
+// 批量获取图标值
+const getIconValues = async (iconKeys) => {
+  const iconValues = []
+
+  // 方法1:串行请求(推荐,避免并发过多)
+  for (const key of iconKeys) {
+    try {
+      const iconData = await getIsSystemAttributeByKey(key.trim())
+      if (iconData && iconData.sysAttrValue) {
+        iconValues.push({
+          id: iconData.id,
+          key: key.trim(),
+          value: iconData.sysAttrValue,
+          name: iconData.sysAttrName || key.trim()
+        })
+      }
+    } catch (error) {
+      console.error(`获取图标 ${key} 失败:`, error)
+    }
+  }
+
+  return iconValues
+}
+/** 查询按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  initTableData()
+}
+/** 重置按钮操作 */
+const queryFormRef = ref()
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+// 初始化如果添加过步骤需要回显出来
+const initTableData = async () => {
+  try {
+    const data = await getWorkflowStepTemplatePage({
+      pageNo: 1,
+      pageSize: -1
+    })
+
+    if (Array.isArray(data.list)) {
+      // 按 stepIndex 从小到大排序
+      tableData.value = data.list
+    } else {
+      tableData.value = []
+    }
+  } catch (error) {
+    tableData.value = []
+  }
+}
+// 确认角色清空操作
+const handleRoleClear = (row) => {
+  row.confirmRoleCode = ''
+  row.confirmUser = ''
+}
+
+// 组件挂载时初始化
+onMounted(() => {
+  initRowDrop() //行拖拽数据更新
+  loadIconOptions() //获取步骤图标信息
+  InitRole() //初始化角色数据
+  initTableData() //初始化步骤表格里的数据
+})
+// 监听 confirmRoleCode 的变化
+watch(
+  () => tableData.value,
+  async (newTableData) => {
+    if (newTableData) {
+      const data = await UserApi.getUserPage({ pageNo: 1, pageSize: -1 })
+      console.log(data, 'user')
+      UserOptions.value = data.list.map((row) => {
+        return {
+          label: row.nickname,
+          value: row.id
+        }
+      })
+    }
+  },
+  { immediate: true, deep: true }
+)
+</script>
+
+<style scoped lang="scss">
+.workflow-page {
+  padding: 20px;
+
+  .button-group {
+    margin-bottom: 20px;
+    display: flex;
+    gap: 10px;
+
+    .el-button {
+      display: flex;
+      align-items: center;
+      gap: 5px;
+    }
+  }
+
+  .table-container {
+    .el-table {
+      .el-input,
+      .el-select,
+      .el-input-number {
+        width: 100%;
+      }
+
+      .el-textarea {
+        .el-textarea__inner {
+          resize: vertical;
+        }
+      }
+    }
+  }
+}
+</style>

+ 113 - 0
src/views/CustomWorkflow/CW/CheckView.vue

@@ -0,0 +1,113 @@
+
+<template>
+  <div>
+    <ContentWrap>
+      <el-form
+        ref="formRef"
+        v-loading="formLoading"
+        :model="formData"
+        label-width="100px"
+      >
+        <el-form-item label="模式名称" prop="name">
+          <el-input
+            style="width: 300px"
+            v-model="formData.modeName"
+            placeholder="请输入模式名称"
+            :disabled="true"
+          />
+        </el-form-item>
+        <el-form-item label="标题" prop="name">
+          <el-input
+            style="width: 300px"
+            v-model="formData.modeTitle"
+            placeholder="请输入标题"
+            :disabled="true"
+          />
+        </el-form-item>
+        <el-form-item label="支持共锁" prop="isColockSupport">
+          <el-checkbox v-model="formData.isColockSupport" :value="true" :disabled="true" />
+        </el-form-item>
+        <el-form-item label="描述" prop="modeDescription">
+          <el-input
+            style="width: 550px"
+            v-model="formData.modeDescription"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入描述内容"
+            maxlength="500"
+            show-word-limit
+            :disabled="true"
+          />
+        </el-form-item>
+        <el-button type="primary" style="margin-left: 500px" :disabled="true">保 存</el-button>
+        <el-button :disabled="true">取 消</el-button>
+      </el-form>
+
+      <el-form-item label="模式步骤" prop="step" style="margin-left: 30px" />
+      <el-radio-group
+        v-model="tabPosition"
+        class="mb-15px"
+        @change="handleTabChange"
+
+      >
+        <el-radio-button label="first" >表格视图</el-radio-button>
+        <el-radio-button label="second" >流程视图</el-radio-button>
+      </el-radio-group>
+
+      <ContentWrap>
+        <TableView v-if="tabPosition == 'first'" :drag-enabled="false" :read-only="true" />
+        <WorkflowView v-else :read-only="true" />
+      </ContentWrap>
+    </ContentWrap>
+  </div>
+</template>
+
+<script setup lang="ts">
+import TableView from './TableView.vue'
+import WorkflowView from './WorkFlowView.vue'
+import * as ModeApi from '@/api/custonWorkflow/index'
+import { useI18n } from 'vue-i18n'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useRoute } from 'vue-router'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const route = useRoute()
+
+const formLoading = ref(false)
+const tabPosition = ref('first')
+
+const formData = ref({
+  modeName: undefined,
+  modeTitle: undefined,
+  isColockSupport: false,
+  modeDescription: undefined
+})
+
+// 切换标签页(其实现在也禁用了)
+const handleTabChange = (newTab: string) => {
+  tabPosition.value = newTab
+}
+
+// 获取原始数据
+const getInfo = async () => {
+  const data = await ModeApi.selectWorkflowModeById(route.query.id)
+  formData.value = data
+}
+
+// 禁用了保存按钮,所以方法可以保留不调用
+// const SaveWorkflowMode = async () => {
+//   const data = await ModeApi.updateWorkflowMode(formData.value)
+//   if (data) {
+//     message.success(t('common.createSuccess'))
+//   }
+// }
+
+onMounted(() => {
+  getInfo()
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 140 - 0
src/views/CustomWorkflow/CW/CreateView.vue

@@ -0,0 +1,140 @@
+<template>
+  <div>
+    <ContentWrap>
+      <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="100px">
+        <el-form-item label="模式名称" prop="name">
+          <el-input style="width: 300px" v-model="formData.modeName" placeholder="请输入模式名称" />
+        </el-form-item>
+        <el-form-item label="标题" prop="name">
+          <el-input style="width: 300px" v-model="formData.modeTitle" placeholder="请输入标题" />
+        </el-form-item>
+
+        <el-form-item label="支持共锁" prop="isColockSupport">
+          <el-checkbox v-model="formData.isColockSupport" :value="true" />
+        </el-form-item>
+
+        <el-form-item label="描述" prop="modeDescription">
+          <el-input
+            style="width: 550px"
+            v-model="formData.modeDescription"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入描述内容"
+            maxlength="500"
+            show-word-limit
+          />
+        </el-form-item>
+        <el-button type="primary" style="margin-left: 500px" @click="SaveWorkflowMode"
+          >保 存
+        </el-button>
+        <el-button @click="goBack">取 消</el-button>
+      </el-form>
+      <el-form-item label="模式步骤" prop="step" style="margin-left: 30px" />
+      <el-radio-group v-model="tabPosition" class="mb-15px" @change="handleTabChange">
+        <el-radio-button label="first">表格视图</el-radio-button>
+        <el-radio-button label="second">流程视图</el-radio-button>
+      </el-radio-group>
+      <ContentWrap>
+        <TableView
+          v-if="tabPosition == 'first'"
+          :enableStepTable="enableStepTable"
+          :modeId="formData.id"
+        />
+        <WorkflowView v-else :enableStepTable="enableStepTable" :modeId="formData.id" />
+      </ContentWrap>
+    </ContentWrap>
+  </div>
+</template>
+<script setup lang="ts">
+import TableView from './TableView.vue'
+import WorkflowView from './WorkFlowView.vue'
+import * as ModeApi from '@/api/custonWorkflow/index'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const router = useRouter()
+const formLoading = ref(false) // 表单的加载中
+const tabPosition = ref('first')
+const enableStepTable = ref(false) //新增控制表格新增 模板导入 删除不能点击或者出现
+const formData = ref({
+  modeName: undefined,
+  modeTitle: undefined,
+  isColockSupport: false,
+  modeDescription: undefined,
+  isPreset: undefined
+})
+// 监听数据变化,设置未保存标记
+const hasUnsavedChanges = ref(false) // 标记是否有未保存的更改
+// 方法定义
+const handleTabChange = (newTab: string) => {
+  tabPosition.value = newTab
+}
+
+const SaveWorkflowMode = async () => {
+  try {
+    let data
+    let successMessage
+
+    if (formData.value.id) {
+      // 有 id,调用修改接口
+      data = await ModeApi.updateWorkflowMode(formData.value)
+      successMessage = t('common.updateSuccess')
+    } else {
+      // 没有 id,调用新增接口
+      data = await ModeApi.insertWorkflowMode(formData.value)
+      successMessage = t('common.createSuccess')
+      enableStepTable.value = true
+      // 新增成功后,将返回的 id 保存到 formData 中
+      if (data) {
+        formData.value.id = data
+      }
+    }
+
+    if (data) {
+      message.success(successMessage)
+      // 保存成功后重置未保存标记
+      hasUnsavedChanges.value = false
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败,请稍后重试')
+  }
+}
+// 监听数据变化,设置未保存标记
+watch(
+  () => ({
+    modeName: formData.value.modeName,
+    modeTitle: formData.value.modeTitle,
+    isColockSupport: formData.value.isColockSupport,
+    modeDescription: formData.value.modeDescription,
+    isPreset: formData.value.isPreset
+  }),
+  () => {
+    hasUnsavedChanges.value = true
+  },
+  { deep: true }
+)
+// 路由离开守卫
+onBeforeRouteLeave((to, from, next) => {
+  if (hasUnsavedChanges.value) {
+    ElMessageBox.confirm('当前页面有未保存的更改,是否继续离开?', '提示', {
+      confirmButtonText: '继续离开',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+      .then(() => {
+        next()
+      })
+      .catch(() => {
+        next(false)
+      })
+  } else {
+    next()
+  }
+})
+const goBack = () => {
+  router.push('/CustomWorkflow/CW')
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 118 - 0
src/views/CustomWorkflow/CW/TableStepDetail.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="step-detail-page">
+    <!-- 顶部操作栏 -->
+    <div class="action-bar">
+      <el-button @click="handleCancel">返回</el-button>
+      <el-button type="primary" @click="handleSave" :loading="saveLoading">保存</el-button>
+    </div>
+
+    <!-- 富文本编辑器 -->
+    <div class="editor-container">
+      <TinyMCE
+        v-model:value="stepDescription"
+        :height="700"
+        placeholder="请输入内容..."
+        @update:value="handleContentChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import TinyMCE from "@/components/TinyMCE/index.vue"
+import { updateWorkflowStep } from '@/api/custonWorkflow/step'
+
+const route = useRoute()
+const router = useRouter()
+
+const stepDescription = ref('')
+const saveLoading = ref(false)
+const stepData = ref(null) // 存储完整的步骤数据
+
+// 内容变化处理
+const handleContentChange = (content: string) => {
+  // console.log('内容变化:', content)
+  stepDescription.value = content
+}
+
+// 保存操作
+const handleSave = async () => {
+  try {
+    saveLoading.value = true
+
+    // 更新步骤数据中的描述字段
+    const updateData = {
+      ...stepData.value,
+      stepDescription: stepDescription.value
+    }
+
+    // 调用更新接口
+    await updateWorkflowStep(updateData)
+
+    ElMessage.success('保存成功')
+
+
+  } catch (error) {
+    console.error('保存失败:', error)
+    ElMessage.error('保存失败,请稍后重试')
+  } finally {
+    saveLoading.value = false
+  }
+}
+
+// 取消操作
+const handleCancel = () => {
+  // 如果有内容变化,提示用户
+  if (stepDescription.value !== stepData.value?.stepDescription) {
+    ElMessageBox.confirm(
+      '内容已修改,确定要取消吗?',
+      '确认取消',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '继续编辑',
+        type: 'warning'
+      }
+    ).then(() => {
+
+      router.back()
+    }).catch(() => {
+      // 用户选择继续编辑,不做任何操作
+    })
+  } else {
+    router.back()
+  }
+}
+
+// 页面初始化
+onMounted(() => {
+  const tempData = localStorage.getItem('tempStepData')
+  if (tempData) {
+    stepData.value = JSON.parse(tempData)
+    stepDescription.value = stepData.value.stepDescription || ''
+    // 清除临时数据
+    localStorage.removeItem('tempStepData')
+  }
+
+})
+</script>
+
+<style scoped lang="scss">
+.step-detail-page {
+  padding: 20px;
+
+  .action-bar {
+    margin-bottom: 20px;
+    display: flex;
+    gap: 10px;
+    justify-content: flex-end;
+  }
+
+  .editor-container {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+  }
+}
+</style>

+ 760 - 0
src/views/CustomWorkflow/CW/TableView.vue

@@ -0,0 +1,760 @@
+<template>
+  <div class="workflow-page">
+    <!-- 顶部按钮区域 -->
+    <div class="button-group">
+      <el-button
+        type="primary"
+        @click="addRow"
+        :disabled="DisableCheckView || !props.enableStepTable||props.isPreset"
+      >
+        <el-icon>
+          <Plus />
+        </el-icon>
+        添加
+      </el-button>
+      <el-button
+        type="success"
+        @click="addFromTemplate"
+        :disabled="DisableCheckView || !props.enableStepTable||props.isPreset"
+      >
+        <el-icon>
+          <DocumentAdd />
+        </el-icon>
+        从模板添加
+      </el-button>
+<!--      这里修改的时候 如果是预置的工作流禁止修改-->
+      <el-button
+        type="danger"
+        @click="deleteSelected"
+        :disabled="isDeleteDisabled||props.isPreset"
+      >
+        <el-icon>
+          <Delete />
+        </el-icon>
+        删除
+      </el-button>
+    </div>
+    <TemplateAddDialog
+      v-model:visible="templateDialogVisible"
+      @select-template="handleTemplateSelect"
+    />
+    <!-- 表格区域 -->
+    <div class="table-container">
+      <el-table
+        ref="tableRef"
+        :data="tableData"
+        @selection-change="handleSelectionChange"
+        border
+        stripe
+        row-key="id"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="{ row }">
+            {{ row.stepIndex }}
+          </template>
+        </el-table-column>
+
+        <el-table-column label="图标" width="120" align="center">
+          <template #default="{ row }">
+            <el-popover placement="bottom" trigger="click" width="230">
+              <!-- 图标选择面板 -->
+              <div style="display: flex; flex-wrap: wrap; gap: 8px">
+                <div
+                  v-for="icon in iconOptions"
+                  :key="icon.value"
+                  @click="() => selectIcon(row, icon.value)"
+                  :style="{
+                    border: row.stepIcon === icon.value ? '2px solid #409EFF' : '1px solid #ccc',
+                    borderRadius: '4px',
+                    padding: '2px',
+                    cursor: 'pointer'
+                  }"
+                >
+                  <img
+                    :src="icon.value"
+                    :alt="icon.name"
+                    style="width: 28px; height: 28px; object-fit: contain"
+                  />
+                </div>
+              </div>
+
+              <!-- 触发元素:当前图标或按钮 -->
+              <template #reference>
+                <div style="cursor: pointer; display: inline-block">
+                  <img
+                    v-if="row.stepIcon"
+                    :src="row.stepIcon"
+                    style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid #ccc"
+                  />
+                  <el-button v-else type="primary" size="small">选择图标</el-button>
+                </div>
+              </template>
+            </el-popover>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="标题" width="180">
+          <template #default="{ row }">
+            <el-input
+              :disabled="DisableCheckView"
+              v-model="row.stepTitle"
+              placeholder="标题"
+              size="small"
+              @blur="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="标题缩写" width="120">
+          <template #default="{ row }">
+            <el-input
+              :disabled="DisableCheckView"
+              v-model="row.stepTitleShort"
+              placeholder="标题缩写"
+              size="small"
+              @blur="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="确认方式" width="130">
+          <template #default="{ row }">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="row.confirmType"
+              placeholder="请选择确认方式"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STEP_CONFIRMTYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="确认角色" width="130">
+          <template #default="{ row }">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="row.confirmRoleCode"
+              placeholder="请选择确认角色"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+              @clear="handleRoleClear(row)"
+            >
+              <el-option
+                v-for="dict in RoleOptions"
+                :key="dict.code"
+                :label="dict.name"
+                :value="dict.code"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="确认人员" width="130">
+          <template #default="{ row }">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="row.confirmUser"
+              placeholder="请选择确认人员"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in UserOptions"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="步骤操作说明" width="120" align="center">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="viewStepDetail(row)">查看</el-button>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="取消作业" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableCancelJob"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="设置锁定人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableSetLocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="设置共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableSetColocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="添加共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableAddColocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="添加共锁人后跳转步骤" width="180">
+          <template #default="{ row }">
+            <el-input-number
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.gotoStepAfterAddingColocker"
+              :min="1"
+              size="small"
+              placeholder="步骤号"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="减少共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableReduceColocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="上锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableLock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="共锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableColock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="解除共锁" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableReleaseColock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="解锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableUnlock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="结束作业" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableEndJob" @change="saveRowData(row)" :disabled="DisableCheckView||props.isPreset"/>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick } from 'vue'
+import { Plus, DocumentAdd, Delete } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import Sortable from 'sortablejs'
+
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
+import {
+  getWorkflowStepPage,
+  insertWorkflowStep,
+  updateWorkflowStep,
+  deleteWorkflowStepList
+} from '@/api/custonWorkflow/step'
+import { getRolePage } from '@/api/system/role'
+import { getRoleUser } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import TemplateAddDialog from './TemplateAdd.vue'
+
+const route = useRoute()
+const router = useRouter()
+// 响应式数据
+const tableData = ref([])
+const selectedRows = ref([])
+const tableRef = ref()
+const templateDialogVisible = ref(false)
+const props = defineProps({
+  enableStepTable: {
+    type: Boolean,
+    required: true
+  },
+  modeId: {
+    type: [String, Number],
+    default: null
+  },
+  isPreset:{
+    type: Boolean,
+    required: true
+  }
+})
+// 使用 computed 来处理复杂的禁用逻辑
+const isDeleteDisabled = computed(() => {
+  // 没有选中数据
+  if (!hasSelection.value) {
+    return true
+  }
+
+  // 处于查看模式
+  if (DisableCheckView.value) {
+    return true
+  }
+
+  // 是预设模式
+  if (props.isPreset) {
+    return true
+  }
+
+  // 步骤表格被禁用
+  if (!props.enableStepTable) {
+    return true
+  }
+
+  return false
+})
+// 计算属性
+const hasSelection = computed(() => selectedRows.value.length > 0)
+// 表格图标切换
+const selectIcon = (row, iconUrl) => {
+  row.stepIcon = iconUrl
+  saveRowData(row) // 你已有的接口保存方法
+}
+// 查看步骤详情
+const viewStepDetail = (row) => {
+  // 临时存储步骤数据
+  localStorage.setItem('tempStepData', JSON.stringify(row))
+
+  // 跳转到详情页面
+  router.push({
+    name: 'TableStepDetail',
+    query: {
+      stepId: row.id,
+      modeId: route.query.id
+    }
+  })
+}
+// 生成新的表格行
+const createNewRow = () => {
+  return {
+    modeId: route.query.id,
+    stepTemplateId: undefined,
+    stepIndex: 0,
+    stepName: undefined,
+    stepTitle: undefined,
+    stepTitleShort: undefined,
+    stepDescription: undefined,
+    confirmType: undefined,
+    confirmRoleCode: undefined,
+    confirmUser: undefined,
+    enableCancelJob: undefined,
+    enableSetLocker: undefined,
+    enableSetColocker: undefined,
+    enableAddColocker: undefined,
+    gotoStepAfterAddingColocker: undefined,
+    enableReduceColocker: undefined,
+    enableLock: undefined,
+    enableColock: undefined,
+    enableReleaseColock: undefined,
+    enableUnlock: undefined,
+    enableEndJob: undefined,
+    id: undefined,
+    stepIcon: undefined
+  }
+}
+// 从模板添加
+const addFromTemplate = () => {
+  templateDialogVisible.value = true
+}
+// 接收模板选择结果
+const handleTemplateSelect = async (templateData) => {
+  console.log(templateData, '子组件传递的数据')
+
+  try {
+    // 遍历数组中的每个模板对象
+    for (const template of templateData) {
+      console.log('处理模板:', template)
+
+      // 计算新的 stepIndex
+      const newStepIndex = tableData.value.length + 1
+
+      // 创建新行,只映射需要的字段
+      const newRow = {
+        ...createNewRow(),
+        stepTitle: template.stepTitle,
+        stepTitleShort: template.stepTitleShort,
+        stepDescription: template.stepDescription,
+        stepIcon: template.stepIcon,
+        confirmType: template.confirmType,
+        confirmRoleCode: template.confirmRoleCode,
+        confirmUser: template.confirmUser,
+        enableCancelJob: template.enableCancelJob,
+        enableSetLocker: template.enableSetLocker,
+        enableSetColocker: template.enableSetColocker,
+        enableAddColocker: template.enableAddColocker,
+        gotoStepAfterAddingColocker: template.gotoStepAfterAddingColocker,
+        enableReduceColocker: template.enableReduceColocker,
+        enableLock: template.enableLock,
+        enableColock: template.enableColock,
+        enableReleaseColock: template.enableReleaseColock,
+        enableUnlock: template.enableUnlock,
+        enableEndJob: template.enableEndJob,
+        stepIndex: newStepIndex,
+        modeId: route.query.id || props.modeId
+      }
+      console.log('创建的新行数据:', newRow)
+      // 插入表格
+      tableData.value.push(newRow)
+      // 保存到后端
+      await saveRowData(newRow)
+    }
+    ElMessage.success(`成功添加 ${templateData.length} 个模板`)
+  } catch (error) {
+    console.error('添加模板失败:', error)
+    ElMessage.error('添加模板失败')
+  }
+}
+// 添加新行
+const addRow = () => {
+  const newRow = createNewRow()
+
+  // 计算 stepIndex(推荐使用最大值 + 1)
+  const maxStepIndex =
+    tableData.value.length > 0 ? Math.max(...tableData.value.map((r) => r.stepIndex || 0)) : 0
+  newRow.stepIndex = maxStepIndex + 1
+
+  // 先插入表格
+  tableData.value.push(newRow)
+
+  // 然后保存(传的就是表格里的对象引用)
+  saveRowData(newRow)
+}
+
+// 删除选中行
+const deleteSelected = async () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请先选择要删除的行')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除选中的 ${selectedRows.value.length} 行数据吗?`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+
+    // 获取选中行中已有 id 的(过滤未保存的行)
+    const selectedWithId = selectedRows.value.filter((row) => row.id)
+    const selectedIds = selectedWithId.map((row) => row.id)
+
+    // 删除已有 id 的记录
+    if (selectedIds.length > 0) {
+      await deleteWorkflowStepList(selectedIds)
+    }
+
+    // 无论是否调用接口,前端都要同步移除这些行
+    tableData.value = tableData.value.filter((row) => !selectedRows.value.includes(row))
+    selectedRows.value = []
+
+    ElMessage.success('删除成功')
+  } catch (error) {
+    console.error('删除失败', error)
+    ElMessage.error('删除失败,请稍后重试')
+  }
+}
+
+// 选择变化处理
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection
+}
+
+// 行拖拽
+const initRowDrop = () => {
+  nextTick(() => {
+    const tbody = document.querySelector('.el-table__body-wrapper tbody')
+    if (tbody) {
+      Sortable.create(tbody, {
+        animation: 150,
+        handle: 'tr',
+        ghostClass: 'sortable-ghost',
+        chosenClass: 'sortable-chosen',
+        dragClass: 'sortable-drag',
+        onEnd: async ({ newIndex, oldIndex }) => {
+          if (newIndex !== undefined && oldIndex !== undefined && newIndex !== oldIndex) {
+            const movedRow = tableData.value.splice(oldIndex, 1)[0]
+            tableData.value.splice(newIndex, 0, movedRow)
+
+            for (let i = 0; i < tableData.value.length; i++) {
+              tableData.value[i].stepIndex = i + 1
+              await saveRowData(tableData.value[i])
+            }
+
+            ElMessage.success('拖拽排序完成')
+          }
+        }
+      })
+    }
+  })
+}
+
+// 自动保存单行数据
+const saveRowData = async (row) => {
+  try {
+    if (!row) return
+
+    if (!row.id) {
+      const res = await insertWorkflowStep(row)
+      if (res) {
+        // 写回 id,保持响应式引用不变
+        row.id = res
+        createNewRow()
+        ElMessage.success('新增步骤成功')
+      }
+    } else {
+      await updateWorkflowStep(row)
+      // 为了给确认人员传递查询条件
+      if (row.confirmRoleCode) {
+        await InitUser(row)
+      }
+
+      ElMessage.success('更新步骤成功')
+    }
+  } catch (error) {
+    console.error('保存步骤失败', error)
+    ElMessage.error('保存失败,请稍后重试')
+  }
+}
+
+// 角色获取
+const RoleOptions = ref()
+const InitRole = async () => {
+  const data = await getRolePage({ pageNo: 1, pageSize: 10 })
+  RoleOptions.value = data.list
+}
+// 人员获取
+const UserOptions = ref()
+const InitUser = async (row) => {
+  console.log(row, 'row')
+  try {
+    const data = await getRoleUser(row.confirmRoleCode)
+    UserOptions.value = data.map((row) => {
+      return {
+        label: row.nickname,
+        value: row.id
+      }
+    })
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 在您的组件中添加
+const iconOptions = ref([])
+
+// 获取图标选项
+const loadIconOptions = async () => {
+  const icons = await getIcons()
+  if (icons && icons.length > 0) {
+    iconOptions.value = icons
+    console.log('图标选项已加载:', iconOptions.value)
+  }
+}
+// 获取步骤基础图标
+const getIcons = async () => {
+  try {
+    const sysAttrKey1 = 'icon.step.all'
+    const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
+    console.log(iconRes, '获取到的图标配置')
+
+    if (iconRes && iconRes.sysAttrValue) {
+      // 将逗号分隔的字符串转换为数组
+      const iconKeys = iconRes.sysAttrValue.split(',')
+      console.log('图标键值列表:', iconKeys)
+
+      // 批量获取每个图标的具体值
+      const iconValues = await getIconValues(iconKeys)
+      console.log('所有图标值:', iconValues)
+
+      return iconValues
+    }
+  } catch (error) {
+    console.error('获取图标失败:', error)
+  }
+}
+
+// 批量获取图标值
+const getIconValues = async (iconKeys) => {
+  const iconValues = []
+
+  // 方法1:串行请求(推荐,避免并发过多)
+  for (const key of iconKeys) {
+    try {
+      const iconData = await getIsSystemAttributeByKey(key.trim())
+      if (iconData && iconData.sysAttrValue) {
+        iconValues.push({
+          id: iconData.id,
+          key: key.trim(),
+          value: iconData.sysAttrValue,
+          name: iconData.sysAttrName || key.trim()
+        })
+      }
+    } catch (error) {
+      console.error(`获取图标 ${key} 失败:`, error)
+    }
+  }
+
+  return iconValues
+}
+// 初始化如果添加过步骤需要回显出来
+const initTableData = async () => {
+  try {
+    if (props.modeId || route.query.id) {
+      const data = await getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: route.query.id || props.modeId
+      })
+
+      if (Array.isArray(data.list)) {
+        // 按 stepIndex 从小到大排序
+        tableData.value = data.list.sort((a, b) => {
+          const aIndex = a.stepIndex || 0
+          const bIndex = b.stepIndex || 0
+          return aIndex - bIndex
+        })
+
+        console.log(
+          '初始化数据完成,已按 stepIndex 排序:',
+          tableData.value.map((row) => row.stepIndex)
+        )
+      } else {
+        tableData.value = []
+      }
+    }
+  } catch (error) {
+    console.error('初始化表格数据失败:', error)
+    ElMessage.error('加载步骤数据失败')
+    tableData.value = []
+  }
+}
+// 确认角色清空操作
+const handleRoleClear = (row) => {
+  row.confirmRoleCode = ''
+  row.confirmUser = ''
+}
+const DisableCheckView = ref()
+// 组件挂载时初始化
+onMounted(() => {
+  if (route.query.type == 'view') {
+    DisableCheckView.value = true
+  } else {
+    DisableCheckView.value = false
+  }
+  initRowDrop() //行拖拽数据更新
+  loadIconOptions() //获取步骤图标信息
+  InitRole() //初始化角色数据
+  initTableData() //初始化步骤表格里的数据
+})
+// 监听 confirmRoleCode 的变化
+watch(
+  () => tableData.value,
+  async (newTableData) => {
+    if (newTableData) {
+      const data = await UserApi.getUserPage({ pageNo: 1, pageSize: -1 })
+      console.log(data, 'user')
+      UserOptions.value = data.list.map((row) => {
+        return {
+          label: row.nickname,
+          value: row.id
+        }
+      })
+    }
+  },
+  { immediate: true, deep: true }
+)
+</script>
+
+<style scoped lang="scss">
+.workflow-page {
+  padding: 20px;
+
+  .button-group {
+    margin-bottom: 20px;
+    display: flex;
+    gap: 10px;
+
+    .el-button {
+      display: flex;
+      align-items: center;
+      gap: 5px;
+    }
+  }
+
+  .table-container {
+    .el-table {
+      .el-input,
+      .el-select,
+      .el-input-number {
+        width: 100%;
+      }
+
+      .el-textarea {
+        .el-textarea__inner {
+          resize: vertical;
+        }
+      }
+    }
+  }
+}
+</style>

+ 353 - 0
src/views/CustomWorkflow/CW/TemplateAdd.vue

@@ -0,0 +1,353 @@
+<template>
+  <Dialog  v-model="visibleRef" title="模板步骤" width="1300">
+    <div class="workflow-page">
+      <!-- 表格区域 -->
+      <div class="table-container">
+        <el-table
+          ref="tableRef"
+          :data="tableData"
+          @selection-change="handleSelectionChange"
+          border
+          stripe
+          row-key="id"
+        >
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column label="图标" width="120" align="center">
+            <template #default="{ row }">
+              <el-popover placement="bottom" trigger="click" width="230">
+                <!-- 图标选择面板 -->
+                <div style="display: flex; flex-wrap: wrap; gap: 8px">
+                  <div
+                    v-for="icon in iconOptions"
+                    :key="icon.value"
+                    @click="() => selectIcon(row, icon.value)"
+                    :style="{
+                      border: row.stepIcon === icon.value ? '2px solid #409EFF' : '1px solid #ccc',
+                      borderRadius: '4px',
+                      padding: '2px',
+                      cursor: 'pointer'
+                    }"
+                  >
+                    <img
+                      :src="icon.value"
+                      :alt="icon.name"
+                      style="width: 28px; height: 28px; object-fit: contain"
+                    />
+                  </div>
+                </div>
+
+                <!-- 触发元素:当前图标或按钮 -->
+                <template #reference>
+                  <div style="cursor: pointer; display: inline-block">
+                    <img
+                      v-if="row.stepIcon"
+                      :src="row.stepIcon"
+                      style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid #ccc"
+                    />
+                    <el-button v-else type="primary" size="small">选择图标</el-button>
+                  </div>
+                </template>
+              </el-popover>
+            </template>
+          </el-table-column>
+
+          <el-table-column label="标题" width="180" prop="stepTitle" />
+
+          <el-table-column label="标题缩写" width="120" prop="stepTitleShort" />
+
+          <el-table-column label="确认方式" width="130">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.SYS_STEP_CONFIRMTYPE" :value="scope.row.confirmType" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="确认角色" width="150">
+            <template #default="{ row }">
+              <el-select v-model="row.confirmRoleCode" placeholder="" disabled>
+                <el-option
+                  v-for="dict in RoleOptions"
+                  :key="dict.code"
+                  :label="dict.name"
+                  :value="dict.code"
+                />
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column label="确认人员" width="130">
+            <template #default="{ row }">
+              <el-select v-model="row.confirmUser" placeholder="" disabled>
+                <el-option
+                  v-for="dict in UserOptions"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column label="步骤操作说明" width="120" align="center">
+            <el-button type="primary" link> 查看</el-button>
+          </el-table-column>
+
+          <el-table-column label="取消作业" width="100" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableCancelJob" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="设置锁定人" width="120" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableSetLocker" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="设置共锁人" width="120" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableSetColocker" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="添加共锁人" width="120" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableAddColocker" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="添加共锁人后跳转步骤" width="180">
+            <template #default="{ row }">
+              <el-input-number
+                v-model="row.gotoStepAfterAddingColocker"
+                :min="1"
+                size="small"
+                placeholder="步骤号"
+              />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="减少共锁人" width="120" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableReduceColocker" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="上锁" width="80" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableLock" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="共锁" width="80" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableColock" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="解除共锁" width="100" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableReleaseColock" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="解锁" width="80" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableUnlock" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="结束作业" width="100" align="center">
+            <template #default="{ row }">
+              <el-checkbox v-model="row.enableEndJob" />
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+    <template #footer>
+      <el-button type="primary" @click="handleConfirm">确 定</el-button>
+      <el-button @click="handleClose">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration'
+import { getWorkflowStepTemplatePage } from '@/api/custonWorkflow/stepTemplate'
+import { getRolePage } from '@/api/system/role'
+import * as UserApi from '@/api/system/user'
+defineOptions({ name: 'TemplateAddDialog' })
+
+// 响应式数据
+const tableData = ref([])
+
+const tableRef = ref()
+const props = defineProps({
+  visible: Boolean
+})
+const emit = defineEmits(['update:visible', 'select-template'])
+const visibleRef = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+})
+// 控制选中项
+const selectedRows = ref([])
+const handleSelectionChange = (rows) => {
+  selectedRows.value = rows
+}
+// 点击确定时回传数据
+const handleConfirm = () => {
+  emit('select-template', selectedRows.value)
+  emit('update:visible', false)
+}
+
+const handleClose = () => {
+  emit('update:visible', false)
+}
+// 表格图标切换
+const selectIcon = (row, iconUrl) => {
+  row.stepIcon = iconUrl
+}
+
+// 角色获取
+const RoleOptions = ref()
+const InitRole = async () => {
+  const data = await getRolePage({ pageNo: 1, pageSize: 10 })
+  RoleOptions.value = data.list
+}
+// 人员获取
+const UserOptions = ref()
+const InitUser = async () => {
+
+  try {
+    const data=await UserApi.getUserPage({ pageNo: 1, pageSize: -1 })
+    UserOptions.value = data.list.map((row) => {
+      return {
+        label: row.nickname,
+        value: row.id
+      }
+    })
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 在您的组件中添加
+const iconOptions = ref([])
+
+// 获取图标选项
+const loadIconOptions = async () => {
+  const icons = await getIcons()
+  if (icons && icons.length > 0) {
+    iconOptions.value = icons
+    console.log('图标选项已加载:', iconOptions.value)
+  }
+}
+// 获取步骤基础图标
+const getIcons = async () => {
+  try {
+    const sysAttrKey1 = 'icon.step.all'
+    const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
+    console.log(iconRes, '获取到的图标配置')
+
+    if (iconRes && iconRes.sysAttrValue) {
+      // 将逗号分隔的字符串转换为数组
+      const iconKeys = iconRes.sysAttrValue.split(',')
+      console.log('图标键值列表:', iconKeys)
+
+      // 批量获取每个图标的具体值
+      const iconValues = await getIconValues(iconKeys)
+      console.log('所有图标值:', iconValues)
+
+      return iconValues
+    }
+  } catch (error) {
+    console.error('获取图标失败:', error)
+  }
+}
+
+// 批量获取图标值
+const getIconValues = async (iconKeys) => {
+  const iconValues = []
+
+  // 方法1:串行请求(推荐,避免并发过多)
+  for (const key of iconKeys) {
+    try {
+      const iconData = await getIsSystemAttributeByKey(key.trim())
+      if (iconData && iconData.sysAttrValue) {
+        iconValues.push({
+          id: iconData.id,
+          key: key.trim(),
+          value: iconData.sysAttrValue,
+          name: iconData.sysAttrName || key.trim()
+        })
+      }
+    } catch (error) {
+      console.error(`获取图标 ${key} 失败:`, error)
+    }
+  }
+
+  return iconValues
+}
+
+// 初始化如果添加过步骤需要回显出来
+const initTableData = async () => {
+  try {
+    const data = await getWorkflowStepTemplatePage({
+      pageNo: 1,
+      pageSize: -1
+    })
+
+    if (Array.isArray(data.list)) {
+      // 按 stepIndex 从小到大排序
+      tableData.value = data.list
+    } else {
+      tableData.value = []
+    }
+  } catch (error) {
+    tableData.value = []
+  }
+}
+
+// 组件挂载时初始化
+onMounted(() => {
+  loadIconOptions() //获取步骤图标信息
+  InitRole() //初始化角色数据
+  initTableData() //初始化步骤表格里的数据
+  InitUser()
+})
+
+</script>
+
+<style scoped lang="scss">
+.workflow-page {
+  padding: 20px;
+
+  .button-group {
+    margin-bottom: 20px;
+    display: flex;
+    gap: 10px;
+
+    .el-button {
+      display: flex;
+      align-items: center;
+      gap: 5px;
+    }
+  }
+
+  .table-container {
+    .el-table {
+      .el-input,
+      .el-select,
+      .el-input-number {
+        width: 100%;
+      }
+
+      .el-textarea {
+        .el-textarea__inner {
+          resize: vertical;
+        }
+      }
+    }
+  }
+}
+</style>

+ 93 - 0
src/views/CustomWorkflow/CW/UpdateView.vue

@@ -0,0 +1,93 @@
+<template>
+  <div>
+    <ContentWrap>
+      <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="100px">
+        <el-form-item label="模式名称" prop="name">
+          <el-input style="width: 300px" v-model="formData.modeName" placeholder="请输入模式名称" />
+        </el-form-item>
+        <el-form-item label="标题" prop="name">
+          <el-input style="width: 300px" v-model="formData.modeTitle" placeholder="请输入标题" />
+        </el-form-item>
+
+        <el-form-item label="支持共锁" prop="isColockSupport">
+          <el-checkbox v-model="formData.isColockSupport" :value="true" :disabled="formData.isPreset"/>
+        </el-form-item>
+
+        <el-form-item label="描述" prop="modeDescription">
+          <el-input
+            style="width: 550px"
+            v-model="formData.modeDescription"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入描述内容"
+            maxlength="500"
+            show-word-limit
+          />
+        </el-form-item>
+        <el-button type="primary" style="margin-left: 500px" @click="SaveWorkflowMode"
+          >保 存
+        </el-button>
+        <el-button @click="goBack">取 消</el-button>
+      </el-form>
+      <el-form-item label="模式步骤" prop="step" style="margin-left: 30px" />
+      <el-radio-group v-model="tabPosition" class="mb-15px" @change="handleTabChange">
+        <el-radio-button label="first">表格视图</el-radio-button>
+        <el-radio-button label="second">流程视图</el-radio-button>
+      </el-radio-group>
+      <ContentWrap>
+        <TableView
+          v-if="tabPosition == 'first'"
+          :enableStepTable="enableStepTable"
+          :isPreset="formData.isPreset"
+        />
+        <WorkflowView v-else :enableStepTable="enableStepTable" :isPreset="formData.isPreset" />
+      </ContentWrap>
+    </ContentWrap>
+  </div>
+</template>
+<script setup lang="ts">
+import TableView from './TableView.vue'
+import WorkflowView from './WorkFlowView.vue'
+import * as ModeApi from '@/api/custonWorkflow/index'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const route = useRoute()
+const router = useRouter()
+const formLoading = ref(false) // 表单的加载中
+const tabPosition = ref('first')
+const formData = ref({
+  modeName: undefined,
+  modeTitle: undefined,
+  isColockSupport: false,
+  modeDescription: undefined,
+  isPreset: undefined
+})
+const enableStepTable = ref(true) //新增控制表格新增 模板导入 删除不能点击或者出现 修改不用改变此值
+
+// 方法定义
+const handleTabChange = (newTab: string) => {
+  tabPosition.value = newTab
+}
+
+//修改先获取原本保存的内容
+const getInfo = async () => {
+  const data = await ModeApi.selectWorkflowModeById(route.query.id)
+  formData.value = data
+}
+const SaveWorkflowMode = async () => {
+  const data = await ModeApi.updateWorkflowMode(formData.value)
+  if (data) {
+    message.success(t('common.updateSuccess'))
+  }
+}
+
+const goBack = () => {
+  router.push('/CustomWorkflow/CW')
+}
+onMounted(() => {
+  getInfo()
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 1083 - 0
src/views/CustomWorkflow/CW/WorkFlowView.vue

@@ -0,0 +1,1083 @@
+<template>
+  <div style="padding: 20px">
+    <!-- 顶部按钮栏 -->
+    <div style="margin-bottom: 10px; display: flex; gap: 10px">
+      <el-button type="primary" @click="handleAddNode" :disabled="DisableCheckView||props.isPreset">添加</el-button>
+      <el-button
+        type="success"
+        @click="addFromTemplate"
+        :disabled="DisableCheckView || !props.enableStepTable||props.isPreset"
+      >
+        <el-icon>
+          <DocumentAdd />
+        </el-icon>
+        从模板添加
+      </el-button>
+      <el-button
+        type="danger"
+        @click="handleDeleteNode"
+        :disabled="!selectedNodeId || DisableCheckView||props.isPreset"
+      >
+        删除
+      </el-button>
+    </div>
+
+    <!-- VueFlow 主画布 -->
+    <VueFlow style="width: 100%; height: 600px">
+      <template #node-default="{ id, data }">
+        <div class="custom-node">
+          <div class="node-content">
+            <!-- 图标显示 -->
+            <div style="font-size: 30px">
+              <img
+                v-if="data.stepIcon && data.stepIcon.startsWith('http')"
+                :src="data.stepIcon"
+                :alt="data.stepTitleShort"
+                style="width: 40px; height: 40px; object-fit: contain"
+              />
+              <span v-else>{{ data.stepIcon || '📋' }}</span>
+            </div>
+            <div style="font-weight: bold; font-size: 14px">
+              {{ data.stepTitleShort || '无标题' }}
+            </div>
+            <div style="font-size: 25px">
+              {{ String.fromCharCode(9311 + (data.stepIndex || 1)) }}
+            </div>
+          </div>
+
+          <!-- 四个连接点 -->
+          <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top" />
+          <Handle
+            type="source"
+            position="bottom"
+            :id="`${id}-bottom`"
+            class="handle handle-bottom"
+          />
+          <Handle type="target" position="left" :id="`${id}-left`" class="handle handle-left" />
+          <Handle type="source" position="right" :id="`${id}-right`" class="handle handle-right" />
+        </div>
+      </template>
+    </VueFlow>
+
+    <!-- 节点表单内容 -->
+    <div
+      v-if="showForm && selectedNodeId"
+      style="margin-top: 20px; padding: 20px; border: 1px solid #ccc"
+    >
+      <h3>节点配置({{ selectedNodeId }})</h3>
+      <div style="display: flex; gap: 20px">
+        <el-form label-width="155px">
+          <el-form-item label="图标">
+            <el-popover placement="bottom" trigger="click" width="230">
+              <!-- 图标选择面板 -->
+              <div style="display: flex; flex-wrap: wrap; gap: 8px">
+                <div
+                  v-for="icon in iconOptions"
+                  :key="icon.value"
+                  @click="() => selectIcon(formData, icon.value)"
+                  :style="{
+                    border:
+                      formData.stepIcon === icon.value ? '2px solid #409EFF' : '1px solid #ccc',
+                    borderRadius: '4px',
+                    padding: '2px',
+                    cursor: 'pointer'
+                  }"
+                >
+                  <img
+                    :src="icon.value"
+                    :alt="icon.name"
+                    style="width: 28px; height: 28px; object-fit: contain"
+                  />
+                </div>
+              </div>
+
+              <!-- 触发元素:当前图标或按钮 -->
+              <template #reference>
+                <div style="cursor: pointer; display: inline-block">
+                  <img
+                    v-if="formData.stepIcon"
+                    :src="formData.stepIcon"
+                    style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid #ccc"
+                  />
+                  <el-button v-else type="primary" size="small">选择图标</el-button>
+                </div>
+              </template>
+            </el-popover>
+          </el-form-item>
+
+          <el-form-item label="标题">
+            <el-input
+              :disabled="DisableCheckView"
+              v-model="formData.stepTitle"
+              placeholder="标题"
+              size="small"
+              @blur="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="标题缩写">
+            <el-input
+              :disabled="DisableCheckView"
+              v-model="formData.stepTitleShort"
+              placeholder="标题缩写"
+              size="small"
+              @blur="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="确认方式">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="formData.confirmType"
+              placeholder="请选择确认方式"
+              @change="handleFormChange"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STEP_CONFIRMTYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="确认角色" v-if="formData.confirmType == '2'">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="formData.confirmRoleCode"
+              placeholder="请选择确认角色"
+              @change="handleFormChange"
+              filterable
+              clearable
+              @clear="handleRoleClear(formData)"
+            >
+              <el-option
+                v-for="dict in RoleOptions"
+                :key="dict.code"
+                :label="dict.name"
+                :value="dict.code"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="确认人员" v-if="formData.confirmType == '2'">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="formData.confirmUser"
+              placeholder="请选择确认人员"
+              @change="handleFormChange"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in UserOptions"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="取消作业">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableCancelJob"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="设置锁定人">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableSetLocker"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="设置共锁人">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableSetColocker"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="添加共锁人">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableAddColocker"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="添加共锁人后跳转步骤">
+            <el-input-number
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.gotoStepAfterAddingColocker"
+              :min="1"
+              size="small"
+              placeholder="步骤号"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="减少共锁人">
+            <el-checkbox
+              v-model="formData.enableReduceColocker"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="上锁">
+            <el-checkbox
+              v-model="formData.enableLock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="共锁">
+            <el-checkbox
+              v-model="formData.enableColock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="解除共锁">
+            <el-checkbox
+              v-model="formData.enableReleaseColock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="解锁">
+            <el-checkbox
+              v-model="formData.enableUnlock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="结束作业">
+            <el-checkbox
+              v-model="formData.enableEndJob"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+        </el-form>
+
+        <!-- 右侧富文本 -->
+        <div style="flex: 1">
+          <label style="font-weight: bold; display: block; margin-bottom: 8px">步骤操作说明</label>
+          <TinyMCE
+            :disabled="DisableCheckView"
+            v-model:value="formData.stepDescription"
+            :height="700"
+            placeholder="请输入内容..."
+            @change="handleFormChange"
+            @update:value="handleContentChange"
+          />
+        </div>
+      </div>
+    </div>
+    <!--    从模板添加-->
+    <TemplateAddDialog
+      v-model:visible="templateDialogVisible"
+      @select-template="handleTemplateSelect"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch, onMounted, nextTick } from 'vue'
+import { VueFlow, useVueFlow, Position, addEdge, Handle } from '@vue-flow/core'
+import '@vue-flow/core/dist/style.css'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useRoute } from 'vue-router'
+import TinyMCE from '@/components/TinyMCE/index.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
+import {
+  getWorkflowStepPage,
+  insertWorkflowStep,
+  updateWorkflowStep,
+  deleteWorkflowStepList
+} from '@/api/custonWorkflow/step'
+
+import { getRolePage } from '@/api/system/role'
+import { getRoleUser } from '@/api/system/user'
+import { DocumentAdd } from '@element-plus/icons-vue'
+import TemplateAddDialog from '@/views/CustomWorkflow/CW/TemplateAdd.vue'
+
+const route = useRoute()
+const {
+  addNodes,
+  addEdges,
+  onNodeClick,
+  onNodeContextMenu,
+  removeNodes,
+  findNode,
+  zoomTo,
+  updateNode,
+  onConnect
+} = useVueFlow()
+const props = defineProps({
+  enableStepTable: {
+    type: Boolean,
+    required: true
+  },
+  modeId: {
+    type: [String, Number],
+    default: null
+  },
+  isPreset:{
+    type: Boolean,
+    required: true
+  }
+
+})
+const selectedNodeId = ref<string | null>(null)
+const showForm = ref(false)
+const nodeIdCounter = ref(1)
+const nodes = ref([])
+const edges = ref([]) // 存储连接线
+const iconOptions = ref([])
+const RoleOptions = ref([])
+const UserOptions = ref([])
+
+// 默认数据结构
+const formData = reactive({
+  modeId: route.query.id || props.modeId,
+  stepTemplateId: undefined,
+  stepIndex: 0,
+  stepName: undefined,
+  stepTitle: undefined,
+  stepTitleShort: undefined,
+  stepDescription: undefined,
+  confirmType: undefined,
+  confirmRoleCode: undefined,
+  confirmUser: undefined,
+  enableCancelJob: undefined,
+  enableSetLocker: undefined,
+  enableSetColocker: undefined,
+  enableAddColocker: undefined,
+  gotoStepAfterAddingColocker: undefined,
+  enableReduceColocker: undefined,
+  enableLock: undefined,
+  enableColock: undefined,
+  enableReleaseColock: undefined,
+  enableUnlock: undefined,
+  enableEndJob: undefined,
+  id: undefined,
+  stepIcon: undefined
+})
+
+// 从模板添加
+const templateDialogVisible = ref(false)
+const addFromTemplate = () => {
+  templateDialogVisible.value = true
+}
+// 接收模板选择结果
+const handleTemplateSelect = (templateNodeData) => {
+  const { template } = templateNodeData
+
+  const id = `node-${nodeIdCounter.value++}`
+  const stepIndex = getNextAvailableIndex()
+
+  const newNode = {
+    id,
+    position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
+    width: 100,
+    height: 150,
+    data: {
+      modeId: route.query.id || props.modeId,
+      stepIcon: template.stepIcon || '',
+      stepTitleShort: template.stepTitleShort || template.stepName || '',
+      stepTitle: template.stepTitle || template.stepName || '',
+      stepDescription: template.stepDescription || '',
+      stepIndex: stepIndex,
+      index: stepIndex,
+      // 其他模板字段
+      confirmType: template.confirmType || 1,
+      confirmRoleCode: template.confirmRoleCode || '',
+      confirmUser: template.confirmUser || null,
+      enableCancelJob: template.enableCancelJob || false,
+      enableSetLocker: template.enableSetLocker || false,
+      enableSetColocker: template.enableSetColocker || false,
+      enableAddColocker: template.enableAddColocker || false,
+      gotoStepAfterAddingColocker: template.gotoStepAfterAddingColocker || null,
+      enableReduceColocker: template.enableReduceColocker || false,
+      enableLock: template.enableLock || false,
+      enableColock: template.enableColock || false,
+      enableReleaseColock: template.enableReleaseColock || false,
+      enableUnlock: template.enableUnlock || false,
+      enableEndJob: template.enableEndJob || false
+    },
+    style: {
+      width: '130px',
+      height: '180px',
+      borderRadius: '12px',
+      border: '1px solid #999',
+      textAlign: 'center',
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+      padding: '10px',
+      backgroundColor: '#fff'
+    },
+    draggable: true
+  }
+
+  addNodes(newNode)
+  nodes.value.push(newNode)
+
+  console.log('从模板创建的新节点:', newNode)
+}
+// 防抖函数
+const debounce = (func, wait) => {
+  let timeout
+  return function executedFunction(...args) {
+    const later = () => {
+      clearTimeout(timeout)
+      func(...args)
+    }
+    clearTimeout(timeout)
+    timeout = setTimeout(later, wait)
+  }
+}
+// 表格图标切换
+const selectIcon = (formData, iconUrl) => {
+  formData.stepIcon = iconUrl
+  saveFormData(formData) // 你已有的接口保存方法
+}
+
+// 初始化步骤数据并渲染节点
+const initTableData = async () => {
+  try {
+    if (props.modeId || route.query.id) {
+      const data = await getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: route.query.id || props.modeId
+      })
+
+      if (Array.isArray(data.list) && data.list.length > 0) {
+        // 按 stepIndex 从小到大排序
+        const sortedData = data.list.sort((a, b) => {
+          const aIndex = a.stepIndex || 0
+          const bIndex = b.stepIndex || 0
+          return aIndex - bIndex
+        })
+
+        console.log(
+          '初始化数据完成,已按 stepIndex 排序:',
+          sortedData.map((row) => row.stepIndex)
+        )
+
+        // 渲染节点
+        renderNodesFromData(sortedData)
+
+        // 渲染连接线
+        renderEdgesFromData(sortedData)
+      } else {
+        console.log('没有找到步骤数据')
+      }
+    }
+  } catch (error) {
+    console.error('初始化表格数据失败:', error)
+    ElMessage.error('加载步骤数据失败')
+  }
+}
+
+// 初始化数据渲染节点 - 修正版本
+const renderNodesFromData = (data) => {
+  // 清空现有节点
+  nodes.value = []
+
+  data.forEach((item, index) => {
+    const nodeId = `node-${item.id || Date.now() + index}`
+
+    const newNode = {
+      id: nodeId,
+      position: {
+        x: 100 + index * 200,
+        y: 100
+      },
+      width: 100,
+      height: 150,
+      data: {
+        stepIcon: item.stepIcon,
+        stepTitleShort: item.stepTitleShort,
+        stepIndex: item.stepIndex || index + 1, // 使用 stepIndex
+        index: item.stepIndex || index + 1, // 保持兼容性
+        // 保存完整的数据用于表单编辑
+        stepData: item
+      },
+      style: {
+        width: '130px',
+        height: '180px',
+        borderRadius: '12px',
+        border: '1px solid #999',
+        textAlign: 'center',
+        display: 'flex',
+        flexDirection: 'column',
+        alignItems: 'center',
+        justifyContent: 'space-between',
+        padding: '10px',
+        backgroundColor: '#fff'
+      },
+      draggable: true
+    }
+
+    addNodes(newNode)
+    nodes.value.push(newNode)
+  })
+}
+
+// 渲染连接线 - 新增函数
+const renderEdgesFromData = (data) => {
+  // 清空现有连接线
+  edges.value = []
+
+  // 根据 stepIndex 顺序创建连接线
+  for (let i = 0; i < data.length - 1; i++) {
+    const currentStep = data[i]
+    const nextStep = data[i + 1]
+
+    const sourceNodeId = `node-${currentStep.id}`
+    const targetNodeId = `node-${nextStep.id}`
+
+    // 创建连接线,从右侧连接到左侧
+    const edge = {
+      id: `edge-${currentStep.id}-${nextStep.id}`,
+      source: sourceNodeId,
+      target: targetNodeId,
+      sourceHandle: `${sourceNodeId}-right`, // 从右侧连接点出发
+      targetHandle: `${targetNodeId}-left`, // 连接到左侧连接点
+      type: 'smoothstep',
+      style: { stroke: '#333', strokeWidth: 2 },
+      markerEnd: {
+        type: 'arrowclosed',
+        width: 20,
+        height: 20,
+        color: '#333'
+      }
+    }
+
+    addEdges(edge)
+    edges.value.push(edge)
+
+    console.log('创建连接线:', edge)
+  }
+}
+
+// 获取下一个可用的索引 - 修正版本
+function getNextAvailableIndex() {
+  if (nodes.value.length === 0) {
+    return 1
+  }
+
+  // 使用 stepIndex 而不是 index
+  const usedIndexes = nodes.value.map((node) => node.data.stepIndex || 1)
+  let nextIndex = 1
+  while (usedIndexes.includes(nextIndex)) {
+    nextIndex++
+  }
+
+  return nextIndex
+}
+
+// 新增节点
+function handleAddNode() {
+  const id = `node-${nodeIdCounter.value++}`
+  const stepIndex = getNextAvailableIndex() // 获取下一个 stepIndex
+
+  const newNode = {
+    id,
+    position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
+    width: 100,
+    height: 150,
+    data: {
+      stepIcon: '',
+      stepTitleShort: '',
+      stepIndex: stepIndex, // 使用 stepIndex
+      index: stepIndex // 保持兼容性
+    },
+    style: {
+      width: '130px',
+      height: '180px',
+      borderRadius: '12px',
+      border: '1px solid #999',
+      textAlign: 'center',
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+      padding: '10px',
+      backgroundColor: '#fff'
+    },
+    draggable: true
+  }
+
+  addNodes(newNode)
+  nodes.value.push(newNode)
+
+  selectedNodeId.value = id
+  showForm.value = true
+
+  // 重置表单数据,并设置 stepIndex
+  resetFormData()
+  formData.stepIndex = stepIndex
+}
+
+// 删除操作
+function handleDeleteNode() {
+  if (selectedNodeId.value) {
+    // 获取要删除的节点数据
+    const nodeToDelete = nodes.value.find((node) => node.id === selectedNodeId.value)
+
+    if (nodeToDelete && nodeToDelete.data.stepData && nodeToDelete.data.stepData.id) {
+      // 显示确认对话框
+      ElMessageBox.confirm(
+        `确定要删除步骤"${nodeToDelete.data.stepTitleShort || '未命名'}"吗?`,
+        '确认删除',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      )
+        .then(() => {
+          // 用户确认删除
+          deleteWorkflowStepList([nodeToDelete.data.stepData.id])
+            .then(() => {
+              // 删除相关的连接线
+              const relatedEdges = edges.value.filter(
+                (edge) =>
+                  edge.source === selectedNodeId.value || edge.target === selectedNodeId.value
+              )
+
+              relatedEdges.forEach((edge) => {
+                edges.value = edges.value.filter((e) => e.id !== edge.id)
+              })
+
+              // 删除成功,从界面移除节点
+              removeNodes([selectedNodeId.value])
+
+              const index = nodes.value.findIndex((node) => node.id === selectedNodeId.value)
+              if (index !== -1) {
+                nodes.value.splice(index, 1)
+              }
+
+              selectedNodeId.value = null
+              showForm.value = false
+
+              setTimeout(() => {
+                reorderNodeIndexes()
+              }, 100)
+
+              ElMessage.success('删除成功')
+            })
+            .catch((error) => {
+              console.error('删除失败:', error)
+              ElMessage.error('删除失败,请稍后重试')
+            })
+        })
+        .catch(() => {
+          // 用户取消删除
+          ElMessage.info('已取消删除')
+        })
+    } else {
+      // 如果节点没有对应的数据库ID,直接从界面删除
+      removeNodes([selectedNodeId.value])
+
+      const index = nodes.value.findIndex((node) => node.id === selectedNodeId.value)
+      if (index !== -1) {
+        nodes.value.splice(index, 1)
+      }
+
+      selectedNodeId.value = null
+      showForm.value = false
+
+      setTimeout(() => {
+        reorderNodeIndexes()
+      }, 100)
+    }
+  }
+}
+
+// 重置表单数据
+const resetFormData = () => {
+  Object.assign(formData, {
+    modeId: route.query.id,
+    stepTemplateId: undefined,
+    stepIndex: 0,
+    stepName: undefined,
+    stepTitle: undefined,
+    stepTitleShort: undefined,
+    stepDescription: undefined,
+    confirmType: undefined,
+    confirmRoleCode: undefined,
+    confirmUser: undefined,
+    enableCancelJob: undefined,
+    enableSetLocker: undefined,
+    enableSetColocker: undefined,
+    enableAddColocker: undefined,
+    gotoStepAfterAddingColocker: undefined,
+    enableReduceColocker: undefined,
+    enableLock: undefined,
+    enableColock: undefined,
+    enableReleaseColock: undefined,
+    enableUnlock: undefined,
+    enableEndJob: undefined,
+    id: undefined,
+    stepIcon: undefined
+  })
+}
+
+// 表单变化处理(防抖) - 修正版本
+const handleFormChange = debounce(async () => {
+  if (selectedNodeId.value) {
+    // 更新节点显示
+    updateNode(selectedNodeId.value, (node) => {
+      node.data.stepIcon = formData.stepIcon
+      node.data.stepTitleShort = formData.stepTitleShort
+      node.data.stepIndex = formData.stepIndex // 同步 stepIndex
+    })
+
+    // 更新本地节点数组
+    const localNode = nodes.value.find((n) => n.id === selectedNodeId.value)
+    if (localNode) {
+      localNode.data.stepIcon = formData.stepIcon
+      localNode.data.stepTitleShort = formData.stepTitleShort
+      localNode.data.stepIndex = formData.stepIndex // 同步 stepIndex
+    }
+
+    // 保存数据
+    await saveFormData(formData)
+  }
+}, 1000)
+
+// 自动保存单行数据
+const saveFormData = async (row) => {
+  try {
+    if (!row) return
+    // 如果没有 id,则执行新增
+    if (!row.id) {
+      const res = await insertWorkflowStep(row)
+      if (res && res) {
+        row.id = res
+        console.log('新增步骤成功:', res)
+        ElMessage.success(`新增步骤成功`)
+      }
+    } else {
+      // 有 id 则执行更新
+      console.log('准备更新步骤:', row)
+
+      await updateWorkflowStep(row)
+      ElMessage.success(`更新步骤成功`)
+    }
+  } catch (error) {
+    console.error('保存步骤失败', error)
+    ElMessage.error('保存失败,请稍后重试')
+  }
+}
+
+// 节点点击处理 - 修正版本
+onNodeClick(({ node }) => {
+  if (selectedNodeId.value === node.id) {
+    showForm.value = false
+    selectedNodeId.value = null
+  } else {
+    selectedNodeId.value = node.id
+    showForm.value = true
+
+    // 更新表单数据
+    const nodeData = node.data
+    if (nodeData.stepData) {
+      // 如果有完整数据,使用完整数据
+      Object.assign(formData, nodeData.stepData)
+    } else {
+      // 否则使用节点显示数据
+      formData.stepIcon = nodeData.stepIcon || ''
+      formData.stepTitleShort = nodeData.stepTitleShort || ''
+      formData.stepIndex = nodeData.stepIndex || 1 // 同步 stepIndex
+    }
+  }
+})
+
+onNodeContextMenu(({ node, event }) => {
+  event.preventDefault()
+  selectedNodeId.value = node.id
+  showForm.value = true
+})
+
+// 获取当前所有节点并重新排序索引 - 修正版本
+function reorderNodeIndexes() {
+  console.log('重新排序前的节点:', nodes.value)
+
+  const sortedNodes = [...nodes.value].sort((a, b) => {
+    if (Math.abs(a.position.y - b.position.y) < 50) {
+      return a.position.x - b.position.x
+    }
+    return a.position.y - b.position.y
+  })
+
+  sortedNodes.forEach((node, arrayIndex) => {
+    const newStepIndex = arrayIndex + 1
+    updateNode(node.id, (nodeData) => {
+      nodeData.data.stepIndex = newStepIndex
+      nodeData.data.index = newStepIndex // 保持兼容性
+    })
+
+    const localNode = nodes.value.find((n) => n.id === node.id)
+    if (localNode) {
+      localNode.data.stepIndex = newStepIndex
+      localNode.data.index = newStepIndex // 保持兼容性
+    }
+  })
+
+  console.log('重新排序后的节点:', nodes.value)
+}
+
+// 角色获取
+const InitRole = async () => {
+  try {
+    const data = await getRolePage({ pageNo: 1, pageSize: -1 })
+    RoleOptions.value = data.list
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 人员获取
+const InitUser = async () => {
+  try {
+    const data = await getRoleUser(formData.confirmRoleCode)
+    UserOptions.value = data.map((item) => {
+      return {
+        label: item.nickname,
+        value: item.id
+      }
+    })
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 获取图标选项
+const loadIconOptions = async () => {
+  try {
+    const icons = await getIcons()
+    if (icons && icons.length > 0) {
+      iconOptions.value = icons
+      console.log('图标选项已加载:', iconOptions.value)
+    }
+  } catch (error) {
+    console.error('加载图标选项失败:', error)
+  }
+}
+
+// 获取步骤基础图标
+const getIcons = async () => {
+  try {
+    const sysAttrKey1 = 'icon.step.all'
+    const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
+    console.log(iconRes, '获取到的图标配置')
+
+    if (iconRes && iconRes.sysAttrValue) {
+      const iconKeys = iconRes.sysAttrValue.split(',')
+      console.log('图标键值列表:', iconKeys)
+
+      const iconValues = await getIconValues(iconKeys)
+      console.log('所有图标值:', iconValues)
+
+      return iconValues
+    }
+  } catch (error) {
+    console.error('获取图标失败:', error)
+  }
+}
+
+// 批量获取图标值
+const getIconValues = async (iconKeys) => {
+  const iconValues = []
+
+  for (const key of iconKeys) {
+    try {
+      const iconData = await getIsSystemAttributeByKey(key.trim())
+      if (iconData && iconData.sysAttrValue) {
+        iconValues.push({
+          id: iconData.id,
+          key: key.trim(),
+          value: iconData.sysAttrValue,
+          name: iconData.sysAttrName || key.trim()
+        })
+      }
+    } catch (error) {
+      console.error(`获取图标 ${key} 失败:`, error)
+    }
+  }
+
+  return iconValues
+}
+
+// 内容变化
+const handleContentChange = (content: string) => {
+  console.log('内容变化:', content)
+  handleFormChange() // 触发保存
+}
+
+// 节点连接线 - 修正版本
+onConnect((params) => {
+  console.log('连接参数:', params)
+
+  // 创建新的连接线,确保使用正确的 sourceHandle 和 targetHandle
+  const newEdge = {
+    id: `e-${params.source}-${params.target}`,
+    source: params.source,
+    target: params.target,
+    sourceHandle: params.sourceHandle, // 使用实际的 sourceHandle
+    targetHandle: params.targetHandle, // 使用实际的 targetHandle
+    type: 'smoothstep',
+    style: { stroke: '#333', strokeWidth: 2 },
+    markerEnd: {
+      type: 'arrowclosed',
+      width: 20,
+      height: 20,
+      color: '#333'
+    }
+  }
+
+  console.log('创建连接线:', newEdge)
+  addEdges(newEdge)
+  edges.value.push(newEdge)
+})
+
+const DisableCheckView = ref()
+// 组件挂载时初始化
+onMounted(async () => {
+  if (route.query.type == 'view') {
+    DisableCheckView.value = true
+  } else {
+    DisableCheckView.value = false
+  }
+
+  zoomTo(1.1)
+
+  // 并行执行初始化
+  await Promise.all([loadIconOptions(), InitRole(), initTableData()])
+})
+// 确认角色清空操作
+const handleRoleClear = (formData) => {
+  formData.confirmRoleCode = ''
+  formData.confirmUser = ''
+}
+// 监听 confirmRoleCode 的变化
+watch(
+  () => formData.confirmRoleCode,
+  async (newRoleCode) => {
+    if (newRoleCode) {
+      await InitUser(newRoleCode)
+    }
+  },
+  { immediate: true } // immediate: true 表示在组件挂载时立即执行一次
+)
+</script>
+
+<style scoped lang="scss">
+.custom-node {
+  position: relative;
+  width: 125px;
+  height: 180px;
+  background-color: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px;
+  box-sizing: border-box;
+}
+
+.node-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+}
+
+// 连接点样式
+.handle {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+  position: absolute;
+  z-index: 10;
+}
+
+.handle:hover {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+.handle-top {
+  top: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-bottom {
+  bottom: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-left {
+  left: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.handle-right {
+  right: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+// 全局样式,确保连接点可见
+:deep(.vue-flow__handle) {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+}
+
+:deep(.vue-flow__handle:hover) {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+// 连接线样式
+:deep(.vue-flow__edge-path) {
+  stroke: #333;
+  stroke-width: 2;
+}
+
+:deep(.vue-flow__edge) {
+  z-index: 1;
+}
+
+// 箭头样式
+:deep(.vue-flow__edge-marker) {
+  fill: #333;
+}
+</style>

+ 261 - 0
src/views/CustomWorkflow/CW/index.vue

@@ -0,0 +1,261 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="模式名称" prop="modeName">
+        <el-input
+          v-model="queryParams.modeName"
+          placeholder="请输入模式名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="标题" prop="modeTitle">
+        <el-input
+          v-model="queryParams.modeTitle"
+          placeholder="请输入标题"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="支持共锁" prop="isColockSupport">
+        <el-select
+          v-model="queryParams.isColockSupport"
+          placeholder="请选择支持共锁"
+          clearable
+          class="!w-240px"
+        >
+          <el-option label="是" value="true" />
+          <el-option label="否" value="false" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['iscs:mode:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />
+          新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="modeList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" prop="id" width="80" align="center" >
+        <template #default="{ row }">
+          <span :style="{ color: row.isPreset ? 'red' : 'black' }">
+            {{ row.id }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="模式名称" prop="modeName">
+        <template #default="{ row }">
+          <span :style="{ color: row.isPreset ? 'red' : 'black' }">
+            {{ row.modeName }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="标题" prop="modeTitle" />
+      <el-table-column label="描述" prop="modeDescription" :show-overflow-tooltip="true" />
+      <el-table-column label="支持共锁" prop="isColockSupport" width="100" align="center">
+        <template #default="{ row }">
+          <el-checkbox
+            :model-value="Boolean(row.isColockSupport)"
+            :disabled="true"
+            class="black-checkbox"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建人" prop="createBy" width="120" />
+      <el-table-column label="创建时间" prop="createTime" width="180" align="center">
+        <template #default="{ row }">
+          {{ formatDate(row.createTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="更新人" prop="updateBy" width="120" />
+      <el-table-column label="更新时间" prop="updateTime" width="180" align="center">
+        <template #default="{ row }">
+          {{ formatDate(row.updateTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="200">
+        <template #default="{ row }">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('view', row.id)"
+            v-hasPermi="['iscs:mode:view']"
+          >
+            查看
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', row.id)"
+            v-hasPermi="['iscs:mode:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(row.id)"
+            v-hasPermi="['iscs:mode:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页组件 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import * as ModeApi from '@/api/custonWorkflow/index'
+import { formatDate } from '@/utils/formatTime'
+
+const router = useRouter()
+
+defineOptions({ name: 'ModeManagement' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const modeList = ref([]) // 列表的数据
+const total = ref(0) // 总条数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  modeName: undefined,
+  modeTitle: undefined,
+  isColockSupport: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const ids = ref() // 选中的ID数组
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map((item) => item.id)
+}
+
+/** 查询模式列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ModeApi.getWorkflowModePage(queryParams)
+    modeList.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.pageNo = 1
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改/查看操作 */
+const openForm = (type: string, id?: number) => {
+  if (type === 'create') {
+    router.push({
+      name: 'CreateView',
+      query: { id: id, type: 'create' }
+    })
+  } else if (type === 'update') {
+    router.push({
+      name: 'UpdateView',
+      query: { id: id, type: 'update', }
+    })
+  } else if (type === 'view') {
+    router.push({
+      name: 'CheckView',
+      query: { id: id, type: 'view' }
+    })
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ModeApi.deleteWorkflowModeList(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>
+<style scoped>
+/* 未选中状态:黑色边框,白色背景,无对勾 */
+.black-checkbox :deep(.el-checkbox__input.is-disabled .el-checkbox__inner) {
+  background-color: #ffffff !important;
+  border-color: #000000 !important;
+  opacity: 1 !important;
+}
+
+/* 选中状态:黑色边框,白色背景,黑色对勾 */
+.black-checkbox :deep(.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner) {
+  background-color: #ffffff !important;
+  border-color: #000000 !important;
+  opacity: 1 !important;
+}
+
+/* 选中状态的对勾图标:黑色 */
+.black-checkbox :deep(.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after) {
+  border-color: #000000 !important;
+  opacity: 1 !important;
+}
+
+/* 未选中状态的对勾图标:隐藏 */
+.black-checkbox :deep(.el-checkbox__input.is-disabled .el-checkbox__inner::after) {
+  border-color: transparent !important;
+  opacity: 0 !important;
+}
+</style>

+ 8 - 7
src/views/Exceptions/manualException/index.vue

@@ -141,8 +141,8 @@
     </el-table>
     <Pagination
       v-model:total="total"
-      v-model:page="queryParams.current"
-      v-model:limit="queryParams.size"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
   </ContentWrap>
@@ -156,6 +156,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import * as ExceptionApi from '@/api/material/manualException'
 import ExceptionDetail from './ExceptionDetail.vue'
+import {listManualException} from "@/api/material/manualException";
 
 defineOptions({ name: 'MaterialManualException' })
 
@@ -202,8 +203,8 @@ const dateShortcuts = [
 
 // 查询参数
 const queryParams = reactive({
-  current: 1,
-  size: 10,
+  pageNo: 1,
+  pageSize: 10,
   exceptionCategory: undefined,
   exceptionDescription: undefined,
   exceptionLevel: undefined,
@@ -245,7 +246,7 @@ const getList = async () => {
       queryParams.endHandleTime = dateFormatter(handleTime.value[1])
     }
     const data = await ExceptionApi.listManualException(queryParams)
-    exceptionList.value = data.records
+    exceptionList.value = data.list
     total.value = data.total
   } finally {
     loading.value = false
@@ -254,7 +255,7 @@ const getList = async () => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.current = 1
+  queryParams.pageNo = 1
   getList()
 }
 
@@ -284,7 +285,7 @@ const handleClearTime2 = (value: [Date, Date] | null) => {
 
 /** 查看详情 */
 const handleCheck = (row: any) => {
-  detailRef.value?.open(row.exceptionId)
+  detailRef.value?.open(row.id)
 }
 
 /** 初始化 */

+ 4 - 3
src/views/dv/lotoStation/index.vue

@@ -26,9 +26,9 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="设备/工艺" prop="lotoId">
+      <el-form-item label="设备/工艺" prop="machineryId">
         <el-tree-select
-          v-model="queryParams.lotoId"
+          v-model="queryParams.machineryId"
           :data="machineryOptions"
           :props="{ label: 'machineryName', value: 'id', children: 'children' }"
           placeholder="选择设备/工艺"
@@ -141,7 +141,8 @@ const queryParams = reactive({
   pageSize: 10,
   lotoName: undefined,
   workstationId: undefined,
-  lotoId: undefined
+  lotoId: undefined,
+  machineryId:undefined,
 })
 
 const queryFormRef = ref() // 搜索的表单

+ 2 - 259
src/views/dv/technology/technologyDetail/DeviceDetail.vue

@@ -15,99 +15,16 @@
       <MapData :machinery-id="route.query.machineryId" />
     </div>
 
-    <!-- 设备表单弹窗 -->
-    <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
-      <el-form
-        ref="formRef"
-        v-loading="formLoading"
-        :model="formData"
-        :rules="formRules"
-        label-width="120px"
-      >
-        <el-form-item label="上级" prop="parentId">
-          <el-tree-select
-            v-model="formData.parentId"
-            :data="machineryOptions"
-            :props="defaultProps"
-            placeholder="选择上级"
-          />
-        </el-form-item>
 
-        <el-form-item label="设备/工艺名称" prop="machineryName">
-          <el-input v-model="formData.machineryName" placeholder="请输入设备/工艺名称" />
-        </el-form-item>
-
-<!--        <el-row>-->
-<!--          <el-col :span="18">-->
-<!--            <el-form-item label="设备/工艺编号" prop="machineryCode">-->
-<!--              <el-input v-model="formData.machineryCode" placeholder="请输入设备/工艺编号" />-->
-<!--            </el-form-item>-->
-<!--          </el-col>-->
-<!--          <el-col :span="6">-->
-<!--            <el-form-item label-width="30">-->
-<!--              <el-switch-->
-<!--                v-model="autoGenFlag"-->
-<!--                active-text="自动生成"-->
-<!--                @change="handleAutoGenChange"-->
-<!--              />-->
-<!--            </el-form-item>-->
-<!--          </el-col>-->
-<!--        </el-row>-->
-
-        <el-form-item label="岗位" prop="workstationId">
-          <el-tree-select
-            v-model="formData.workstationId"
-            :data="workstationOptions"
-            :props="defaultProps"
-            placeholder="请选择岗位"
-          />
-        </el-form-item>
-
-        <el-form-item label="所属电柜" prop="lotoId">
-          <el-select
-            v-model="formData.lotoId"
-            placeholder="请选择所属电柜"
-            class="!w-300px"
-          >
-            <el-option
-              v-for="dict in lotoOptions"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-            />
-          </el-select>
-        </el-form-item>
-
-        <el-form-item label="设备/工艺类型" prop="machineryType">
-          <el-input v-model="formData.machineryType" placeholder="请输入设备/工艺类型" />
-        </el-form-item>
-
-        <el-form-item label="工艺图" prop="machineryImg">
-          <ImageUpload
-            v-model="formData.machineryImg"
-            :limit="1"
-            :file-size="5"
-            @onUploaded="handleImgUploaded"
-            @onRemoved="handleImgRemoved"
-          />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="dialogVisible = false">取 消</el-button>
-      </template>
-    </Dialog>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive, onMounted, watch } from 'vue'
+import { ref } from 'vue'
 import { useRoute } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import * as TechnologyApi from '@/api/dv/technology'
-import * as LotoApi from '@/api/dv/lotoStation'
-import { genCode } from '@/api/system/autocode/rule'
+
 import MapData from './MapData.vue'
 import Tinymce from '@/components/TinyMCE/index.vue'
 
@@ -117,183 +34,9 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const route = useRoute()
 
-// 数据列表相关
-const loading = ref(false)
 const tabPosition = ref('deviceInfo')
-const multiple = ref(true)
-const showSearch = ref(true)
-const isExpandAll = ref(true)
-
-// 表单相关
-const dialogVisible = ref(false)
-const dialogTitle = ref('')
-const formLoading = ref(false)
-const formRef = ref()
-const formData = ref({
-  machineryId: undefined,
-  parentId: undefined,
-  machineryName: '',
-  machineryCode: '',
-  workstationId: undefined,
-  lotoId: undefined,
-  machineryType: '',
-  machineryImg: undefined
-})
-
-// 表单校验规则
-const formRules = reactive({
-  machineryName: [{ required: true, message: '设备/工艺名称不能为空', trigger: 'blur' }],
-  // machineryCode: [{ required: true, message: '设备/工艺编号不能为空', trigger: 'blur' }],
-  workstationId: [{ required: true, message: '岗位不能为空', trigger: 'blur' }],
-  lotoId: [{ required: true, message: '电柜不能为空', trigger: 'blur' }]
-})
-
-// 选项数据
-const workstationOptions = ref([])
-const machineryOptions = ref([])
-const lotoOptions = ref([])
-const defaultProps = {
-  children: 'children',
-  label: 'label'
-}
-
-// 自动生成编码
-const autoGenFlag = ref(false)
-
-// 监听岗位变化
-watch(() => formData.value.workstationId, (newVal) => {
-  if (newVal) {
-    getLotoList()
-  }
-})
-
-/** 获取电柜列表 */
-const getLotoList = async () => {
-  try {
-    const res = await LotoApi.listLoto({
-      pageSize: -1,
-      workstationId: formData.value.workstationId
-    })
-    lotoOptions.value = res.data.records.map(item => ({
-      value: item.lotoId,
-      label: item.lotoName
-    }))
-  } catch (error) {
-    console.error('获取电柜列表失败:', error)
-  }
-}
-
-/** 获取选项数据 */
-const getOptions = async () => {
-  // 获取岗位树
-  const marsRes = await TechnologyApi.listMarsDept({ pageSize: -1 })
-  workstationOptions.value = marsRes.data.records
-
-  // 获取设备工艺树
-  const techRes = await TechnologyApi.listTechnology({ pageSize: -1 })
-  machineryOptions.value = techRes.data.records
-
-  // 获取电柜列表
-  await getLotoList()
-}
-
-/** 打开表单 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  resetForm()
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await TechnologyApi.getTechnologyInfo(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-
-/** 新增 */
-const handleAdd = (row?: any) => {
-  formData.value.parentId = row?.machineryId || 0
-  open('create')
-}
-
-/** 修改 */
-const handleUpdate = (row: any) => {
-  open('update', row.machineryId)
-}
-
-/** 删除 */
-const handleDelete = async (row: any) => {
-  await message.confirm('确认删除数据项?')
-  try {
-    await TechnologyApi.delTechnology(row.machineryId)
-    message.success(t('common.delSuccess'))
-    getOptions()
-  } catch {}
-}
 
-/** 提交表单 */
-const submitForm = async () => {
-  if (!formRef.value) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  formLoading.value = true
-  try {
-    const data = formData.value
-    if (data.machineryId) {
-      await TechnologyApi.updateTechnology(data)
-      message.success(t('common.updateSuccess'))
-    } else {
-      await TechnologyApi.addTechnology(data)
-      message.success(t('common.createSuccess'))
-    }
-    dialogVisible.value = false
-    getOptions()
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 自动生成编码 */
-// const handleAutoGenChange = async (val: boolean) => {
-//   if (val) {
-//     const res = await genCode('TECHNOLOGY_CODE')
-//     formData.value.machineryCode = res.data
-//   } else {
-//     formData.value.machineryCode = ''
-//   }
-// }
-
-/** 图片上传成功 */
-const handleImgUploaded = (urls: string[]) => {
-  formData.value.machineryImg = urls[0]
-}
-
-/** 图片移除 */
-const handleImgRemoved = () => {
-  formData.value.machineryImg = undefined
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    machineryId: undefined,
-    parentId: undefined,
-    machineryName: '',
-    machineryCode: '',
-    workstationId: undefined,
-    lotoId: undefined,
-    machineryType: '',
-    machineryImg: undefined
-  }
-  formRef.value?.resetFields()
-  autoGenFlag.value = false
-}
 
-onMounted(() => {
-  getOptions()
-})
 </script>
 
 <style scoped>

+ 11 - 6
src/views/email/emailNotify/EmailNotifyForm.vue

@@ -14,9 +14,9 @@
         <el-select v-model="formData.templateCode">
           <el-option
             v-for="item in templatesList"
-            :key="item.templateCode"
-            :label="item.templateName"
-            :value="item.templateCode"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
           />
         </el-select>
       </el-form-item>
@@ -54,7 +54,7 @@
 <script lang="ts" setup>
 import { CommonStatusEnum } from '@/utils/constants'
 import * as EmailNotifyApi from '@/api/email/notify/index'
-import * as EmailTemplateApi from '@/api/email/templates/index'
+import * as MailTemplateApi from "@/api/system/mail/template";
 
 defineOptions({ name: 'EmailNotifyForm' })
 
@@ -128,8 +128,13 @@ const open = async (type: string, id?: number) => {
     pageNo: 1,
     pageSize: -1
   }
-  const res = await EmailTemplateApi.listEmailTemplates(data)
-  templatesList.value = res.records
+  const res = await MailTemplateApi.getMailTemplatePage(data)
+  templatesList.value = res.list.map((item) => {
+    return {
+      label: item.name,
+      value: item.code,
+    }
+  })
   // 修改时,设置数据
   if (id) {
     formLoading.value = true

+ 14 - 8
src/views/email/emailNotify/index.vue

@@ -60,7 +60,7 @@
           />
         </template>
       </el-table-column>
-      <el-table-column prop="templateName" label="邮件模板" />
+      <el-table-column prop="templateCode" label="邮件模板编码" />
       <el-table-column prop="reminderTime" label="提醒时长">
         <template #default="{ row }">
           {{ formattedTime(row.reminderTime) }}
@@ -71,7 +71,7 @@
           <el-button
             link
             type="primary"
-            @click="openForm('update', row.configId)"
+            @click="openForm('update', row.id)"
             v-hasPermi="['iscs:notify:update']"
           >
             修改
@@ -79,7 +79,7 @@
           <el-button
             link
             type="danger"
-            @click="handleDelete(row.configId)"
+            @click="handleDelete(row.id)"
             v-hasPermi="['iscs:notify:delete']"
           >
             删除
@@ -94,8 +94,6 @@
 </template>
 
 <script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
 import * as EmailNotifyApi from '@/api/email/notify/index'
 import EmailNotifyForm from './EmailNotifyForm.vue'
 
@@ -114,13 +112,17 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 const refreshTable = ref(true) // 重新渲染表格状态
-
+const ids=ref()
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.id)
+}
 /** 查询邮件提醒列表 */
 const getList = async () => {
   loading.value = true
   try {
     const data = await EmailNotifyApi.listIsMailNotifyConfigPage(queryParams)
-    templatesNotifyList.value = data.records
+    templatesNotifyList.value = data.list
   } finally {
     loading.value = false
   }
@@ -160,7 +162,11 @@ const handleDelete = async (id: number) => {
 /** 是否激活状态修改 */
 const handleFrozenChange = async (row: any) => {
   try {
-    await EmailNotifyApi.updateIsMailNotifyConfig(row)
+    const data={
+      ...row,
+      status:'1'
+    }
+    await EmailNotifyApi.updateIsMailNotifyConfig(data)
     message.success(row.status === '1' ? '激活成功' : '取消激活')
   } catch {}
 }

+ 0 - 131
src/views/email/emailTemplates/EmailTemplateForm.vue

@@ -1,131 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="600">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="120px"
-    >
-      <el-form-item label="邮件模板编号" prop="templateCode">
-        <el-input
-          :disabled="formType === 'update'"
-          v-model="formData.templateCode"
-          placeholder="请输入邮件模板编号"
-        />
-      </el-form-item>
-      <el-form-item label="邮件模板名称" prop="templateName">
-        <el-input
-          v-model="formData.templateName"
-          placeholder="请输入邮件模板名称"
-        />
-      </el-form-item>
-      <el-form-item label="邮件模板标题" prop="templateTitle">
-        <el-input
-          v-model="formData.templateTitle"
-          placeholder="请输入邮件模板标题"
-        />
-      </el-form-item>
-      <el-form-item label="邮件模板内容" prop="templateContent">
-        <el-input
-          type="textarea"
-          :rows="20"
-          v-model="formData.templateContent"
-          placeholder="请输入邮件模板内容"
-        />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-
-<script lang="ts" setup>
-import { CommonStatusEnum } from '@/utils/constants'
-import * as EmailTemplateApi from '@/api/email/templates/index'
-
-defineOptions({ name: 'EmailTemplateForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  templateId: undefined,
-  templateCode: '',
-  templateName: '',
-  templateTitle: '',
-  templateContent: ''
-})
-
-const formRules = reactive({
-  templateCode: [{ required: true, message: '邮件模板编号不能为空', trigger: 'blur' }],
-  templateName: [{ required: true, message: '邮件模板名称不能为空', trigger: 'blur' }],
-  templateTitle: [{ required: true, message: '邮件模板标题不能为空', trigger: 'blur' }],
-  templateContent: [{ required: true, message: '邮件模板内容不能为空', trigger: 'blur' }]
-})
-
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      const data = await EmailTemplateApi.getEmailTemplatesInfo(id)
-      formData.value = data
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value
-    if (formType.value === 'create') {
-      await EmailTemplateApi.addEmailTemplates(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await EmailTemplateApi.updateEmailTemplates(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    templateId: undefined,
-    templateCode: '',
-    templateName: '',
-    templateTitle: '',
-    templateContent: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 188
src/views/email/emailTemplates/index.vue

@@ -1,188 +0,0 @@
-<template>
-  <!-- 搜索工作栏 -->
-  <ContentWrap>
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="100px"
-    >
-      <el-form-item label="邮件模板名称" prop="templateName">
-        <el-input
-          v-model="queryParams.templateName"
-          placeholder="请输入邮件模板名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="邮件模板标题" prop="templateTitle">
-        <el-input
-          v-model="queryParams.templateTitle"
-          placeholder="请输入邮件模板标题"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['iscs:template:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table
-      v-loading="loading"
-      :data="templatesList"
-      @selection-change="handleSelectionChange"
-    >
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column prop="templateCode" label="邮件模板编号" />
-      <el-table-column prop="templateName" label="邮件模板名称" />
-      <el-table-column prop="templateTitle" label="邮件模板标题" />
-      <el-table-column prop="templateContent" label="内容" width="120">
-        <template #default="{ row }">
-          <el-button link type="primary" @click="handleCheck(row)">查看</el-button>
-        </template>
-      </el-table-column>
-      <el-table-column
-        label="新增时间"
-        align="center"
-        prop="createTime"
-        width="150"
-      >
-        <template #default="{ row }">
-          <span>{{ row.createTime }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" width="150">
-        <template #default="{ row }">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', row.templateId)"
-            v-hasPermi="['iscs:template:update']"
-          >
-            修改
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(row.templateId)"
-            v-hasPermi="['iscs:template:delete']"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <EmailTemplateForm ref="formRef" @success="getList" />
-
-  <!-- 查看内容弹窗 -->
-  <Dialog v-model="checkDialogVisible" title="内容查看" width="600">
-    <el-input
-      type="textarea"
-      v-model="checkContent"
-      :rows="20"
-      readonly
-    />
-    <template #footer>
-      <el-button @click="checkDialogVisible = false">关 闭</el-button>
-    </template>
-  </Dialog>
-</template>
-
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-import * as EmailTemplateApi from '@/api/email/templates/index'
-import EmailTemplateForm from './EmailTemplateForm.vue'
-
-defineOptions({ name: 'EmailTemplate' })
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-
-const loading = ref(true) // 列表的加载中
-const templatesList = ref([]) // 列表的数据
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  templateName: undefined,
-  templateTitle: undefined
-})
-const queryFormRef = ref() // 搜索的表单
-const refreshTable = ref(true) // 重新渲染表格状态
-
-// 查看内容相关
-const checkDialogVisible = ref(false)
-const checkContent = ref('')
-
-/** 查询邮件模板列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await EmailTemplateApi.listEmailTemplates(queryParams)
-    templatesList.value = data.records
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryParams.pageNo = 1
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
-
-/** 查看内容操作 */
-const handleCheck = (row: any) => {
-  checkContent.value = row.templateContent
-  checkDialogVisible.value = true
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await EmailTemplateApi.delEmailTemplates(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 初始化 **/
-onMounted(async () => {
-  await getList()
-})
-</script>

+ 44 - 55
src/views/material/Inspectionrecords/index.vue

@@ -46,7 +46,7 @@
         <el-tree-select
           v-model="queryParams.materialsTypeId"
           :data="materialstypeOptions"
-          :props="{ label: 'materialsTypeName', value: 'materialsTypeId' }"
+          :props="{ label: 'materialsTypeName', value: 'id' }"
           placeholder="请选择物资类型"
           class="!w-240px"
         />
@@ -189,25 +189,17 @@ import * as RecordApi from '@/api/material/checkRecord'
 import * as CabinetApi from '@/api/material/information/index'
 import * as TypeApi from '@/api/material/type'
 import download from '@/utils/download'
-import {exportCheckRecord} from "@/api/material/checkRecord";
+
+import {getDateRange} from "@/utils/formatTime";
 
 
 defineOptions({ name: 'InspectionRecords' })
 
-const props = defineProps({
-  cabinetId: {
-    type: String,
-    required: false
-  },
-  planId: {
-    type: String,
-    required: false
-  },
-  planName: {
-    type: String,
-    required: false
-  }
-})
+const props = defineProps<{
+  planName?: string
+  planId?: string | number
+  cabinetId?: string | number
+}>()
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -218,7 +210,12 @@ const total = ref(0) // 列表的总页数
 const cabinets = ref([]) // 物资柜数据
 const materialstypeOptions = ref([]) // 物资类型选项
 const visibleSelect = ref(false) // 是否显示物资柜选择
-
+// 在 setup 中定义
+const route = useRoute()
+const router = useRouter()
+const emit = defineEmits<{
+  recordId: [recordId: string | number]
+}>()
 // 查询参数
 const queryParams = reactive({
   pageNo: 1,
@@ -245,30 +242,33 @@ const getList = async () => {
   loading.value = true
   try {
     // 处理时间范围
-    if (Array.isArray(createTime.value) && createTime.value.length === 2) {
-      queryParams.startTime = formatDate(createTime.value[0])
-      queryParams.endTime = formatDate(createTime.value[1])
-    }
-    // 处理路由参数
-    const route = useRoute()
-    if (route.query.planName) {
-      queryParams.planName = route.query.planName
-    }
-    if (route.query.planId) {
-      queryParams.planId = route.query.planId
+    if (Array.isArray(createTime.value) && createTime.value.length == 2) {
+      const [startTime, endTime] = getDateRange(createTime.value[0], createTime.value[1])
+      queryParams.startTime = startTime
+      queryParams.endTime = endTime
     }
-    if (route.query.cabinetId) {
-      queryParams.cabinetId = route.query.cabinetId
-    }
-    // 处理 props
-    if (props.planName) {
-      queryParams.planName = props.planName
-    }
-    if (props.planId) {
-      queryParams.planId = props.planId
-    }
-    if (props.cabinetId) {
-      queryParams.cabinetId = props.cabinetId
+    // 定义字段配置,包含类型转换规则
+    const paramConfig = {
+      planName: { type: 'string' },
+      planId: { type: 'number' },
+      cabinetId: { type: 'number' },
+      materialsCabinetId: { type: 'number' }
+    } as const
+
+    // 批量处理参数
+    Object.entries(paramConfig).forEach(([field, { type }]) => {
+      const value = props[field] ?? route.query[field]
+
+      if (value !== undefined && value !== null && value !== '') {
+        queryParams[field] = type === 'number' ? Number(value) : String(value)
+      } else {
+        queryParams[field] = null
+      }
+    })
+
+    // 处理 cabinetId 的特殊逻辑
+    if (route.query.cabinetId || props.cabinetId) {
+      visibleSelect.value = true
     }
     const data = await RecordApi.listCheckRecord(queryParams)
     list.value = data.list
@@ -319,18 +319,6 @@ const resetQuery = () => {
   handleQuery()
 }
 
-/** 格式化日期 */
-const formatDate = (date: Date) => {
-  if (!date) return null
-  const year = date.getFullYear()
-  const month = String(date.getMonth() + 1).padStart(2, '0')
-  const day = String(date.getDate()).padStart(2, '0')
-  const hours = String(date.getHours()).padStart(2, '0')
-  const minutes = String(date.getMinutes()).padStart(2, '0')
-  const seconds = String(date.getSeconds()).padStart(2, '0')
-  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
-}
-
 /** 清空时间 */
 const handleClearTime = (value: any) => {
   if (value == null) {
@@ -371,15 +359,16 @@ const handleExport = async () => {
 
 /** 初始化 **/
 onMounted(async () => {
-  if (props.cabinetId) {
-    visibleSelect.value = true
-  }
   await Promise.all([
     getList(),
     getMaterialTypes(),
     getCabinets()
   ])
+
 })
+
+
+
 </script>
 
 <style lang="scss" scoped>

+ 118 - 182
src/views/material/blacklist/BlacklistForm.vue

@@ -1,80 +1,18 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="1300">
-    <div>
-      <el-row :gutter="24">
-        <el-col :span="11" style="margin-left: 20px">
-          <!-- 穿梭框左边 -->
-          <el-input
-            v-model="searchQueryLeft"
-            placeholder="请输入姓名"
-            @input="searchQueryLeftInput"
-            class="mb-5px"
-          />
-          <el-table
-            ref="leftTableRef"
-            :data="leftTableData"
-            height="486"
-            @select="handleLeftSelect"
-            @select-all="handleLeftSelectAll"
-            row-key="id"
-          >
-            <el-table-column type="selection" width="55" align="center" />
-            <el-table-column label="用户编号" align="center" prop="userId" />
-            <el-table-column label="姓名" align="center" prop="nickName" />
-            <el-table-column label="登录名" align="center" prop="userName" />
-          </el-table>
-          <Pagination
-            v-model:total="leftTotal"
-            v-model:page="leftQueryParams.current"
-            v-model:limit="leftQueryParams.size"
-            @pagination="getLeftList"
-          />
-        </el-col>
-
-        <div style="width: 56px; height: 100%; float: left">
-          <el-button
-            style="margin-top: 25vh"
-            @click="handleMoveRight"
-            type="primary"
-            :disabled="!selectedLeftData.length"
-          >
-            <Icon icon="ep:arrow-right" />
-          </el-button>
-          <br />
-          <el-button
-            @click="handleMoveLeft"
-            type="primary"
-            :disabled="!selectedRightData.length"
-            style="margin-left: 0; margin-top: 10px"
-          >
-            <Icon icon="ep:arrow-left" />
-          </el-button>
-        </div>
-
-        <el-col :span="11">
-          <!-- 穿梭框右边 -->
-          <el-input
-            v-model="searchQueryRight"
-            placeholder="请输入姓名"
-            @input="searchQueryRightInput"
-            class="mb-5px"
-          />
-          <el-table
-            ref="rightTableRef"
-            :data="rightTableData"
-            height="488"
-            @select="handleRightSelect"
-            @select-all="handleRightSelectAll"
-            row-key="id"
-          >
-            <el-table-column type="selection" width="55" align="center" />
-            <el-table-column label="用户编号" align="center" prop="userId" />
-            <el-table-column label="姓名" align="center" prop="nickName" />
-            <el-table-column label="登录名" align="center" prop="userName" />
-          </el-table>
-        </el-col>
-      </el-row>
-    </div>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="700">
+
+    <el-transfer
+      v-model="selectedKeys"
+      :data="transferData"
+      :titles="['可选用户', '已选用户']"
+      :button-texts="['移除', '添加']"
+      :filterable="true"
+      :filter-placeholder="'请输入姓名或登录名'"
+      :filter-method="filterMethod"
+      :left-default-checked="[]"
+      :right-default-checked="[]"
+
+    />
 
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -95,24 +33,9 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中
 
-// 左侧表格数据
-const leftTableRef = ref()
-const leftTableData = ref([])
-const leftTotal = ref(0)
-const leftQueryParams = reactive({
-  current: 1,
-  size: 10,
-  nickName: undefined
-})
-const searchQueryLeft = ref('')
-const selectedLeftData = ref([])
-
-// 右侧表格数据
-const rightTableRef = ref()
-const rightTableData = ref([])
-const originalRightData = ref([])
-const searchQueryRight = ref('')
-const selectedRightData = ref([])
+// 穿梭框数据
+const transferData = ref([])
+const selectedKeys = ref([])
 
 /** 打开弹窗 */
 const open = async (type: string) => {
@@ -126,90 +49,30 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 /** 获取左侧列表数据 */
 const getLeftList = async () => {
   const data = {
-    ...leftQueryParams,
-    nickName: searchQueryLeft.value
+    pageNo: 1,
+    pageSize: -1, // 获取所有数据
+    nickName: undefined
   }
   const res = await BlacklistApi.listWhitelist(data)
-  leftTableData.value = res.records
-  leftTotal.value = res.total
-}
-
-/** 左侧搜索 */
-const searchQueryLeftInput = (value: string) => {
-  searchQueryLeft.value = value
-  getLeftList()
-}
-
-/** 右侧搜索 */
-const searchQueryRightInput = (value: string) => {
-  if (!originalRightData.value.length) {
-    originalRightData.value = [...rightTableData.value]
-  }
-  if (value) {
-    rightTableData.value = originalRightData.value.filter((item) =>
-      item.nickName.includes(value)
-    )
-  } else {
-    rightTableData.value = [...originalRightData.value]
-  }
-}
-
-/** 左侧选中 */
-const handleLeftSelect = (selection: any[]) => {
-  selectedLeftData.value = selection
-}
 
-/** 左侧全选 */
-const handleLeftSelectAll = (selection: any[]) => {
-  selectedLeftData.value = selection
+  // 转换为穿梭框需要的格式
+  transferData.value = (res.list || []).map((item: any) => ({
+    key: item.userId,
+    label: `${item.nickName} (${item.userName})`,
+    disabled: false,
+    userId: item.userId,
+    nickName: item.nickName,
+    userName: item.userName
+  }))
 }
 
-/** 右侧选中 */
-const handleRightSelect = (selection: any[]) => {
-  selectedRightData.value = selection
-}
-
-/** 右侧全选 */
-const handleRightSelectAll = (selection: any[]) => {
-  selectedRightData.value = selection
-}
-
-/** 移动到右侧 */
-const handleMoveRight = () => {
-  rightTableData.value = rightTableData.value.concat(selectedLeftData.value)
-  removeFromLeftTable(selectedLeftData.value)
-  selectedLeftData.value = []
-}
-
-/** 移动到左侧 */
-const handleMoveLeft = () => {
-  leftTableData.value = leftTableData.value.concat(selectedRightData.value)
-  removeFromRightTable(selectedRightData.value)
-  selectedRightData.value = []
-}
-
-/** 从左侧表格移除 */
-const removeFromLeftTable = (data: any[]) => {
-  if (data.length && leftTableData.value.length) {
-    data.forEach((item) => {
-      const index = leftTableData.value.findIndex((row) => row.nickName === item.nickName)
-      if (index > -1) {
-        leftTableData.value.splice(index, 1)
-      }
-    })
-  }
-}
-
-/** 从右侧表格移除 */
-const removeFromRightTable = (data: any[]) => {
-  if (data.length && rightTableData.value.length) {
-    data.forEach((item) => {
-      const index = rightTableData.value.findIndex((row) => row.nickName === item.nickName)
-      if (index > -1) {
-        rightTableData.value.splice(index, 1)
-      }
-    })
+/** 过滤方法 */
+const filterMethod = (query: string, item: any) => {
+  if (query === '') {
+    return true
   }
+  return item.nickName.toLowerCase().includes(query.toLowerCase()) ||
+    item.userName.toLowerCase().includes(query.toLowerCase())
 }
 
 /** 提交表单 */
@@ -217,10 +80,16 @@ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成
 const submitForm = async () => {
   formLoading.value = true
   try {
-    const data = rightTableData.value.map((item) => ({
+    // 获取右侧已选中的数据
+    const selectedData = transferData.value.filter((item: any) =>
+      selectedKeys.value.includes(item.key)
+    )
+
+    const data = selectedData.map((item: any) => ({
       userId: item.userId,
       module: '2'
     }))
+
     await BlacklistApi.addBlacklist(data)
     message.success(t('common.createSuccess'))
     dialogVisible.value = false
@@ -232,18 +101,85 @@ const submitForm = async () => {
 
 /** 重置表单 */
 const resetForm = () => {
-  leftTableData.value = []
-  rightTableData.value = []
-  originalRightData.value = []
-  searchQueryLeft.value = ''
-  searchQueryRight.value = ''
-  selectedLeftData.value = []
-  selectedRightData.value = []
+  transferData.value = []
+  selectedKeys.value = []
 }
 </script>
 
 <style scoped lang="scss">
-.mb-5px {
-  margin-bottom: 5px;
+:deep(.el-transfer) {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+
+  .el-transfer-panel {
+    width: 50%;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+  }
+
+  .el-transfer-panel__header {
+    background-color: #f5f7fa;
+    border-bottom: 1px solid #dcdfe6;
+    padding: 12px 16px;
+    font-weight: 500;
+  }
+
+  .el-transfer-panel__body {
+    height: 450px;
+  }
+
+  .el-transfer-panel__list {
+    height: 350px;
+    padding: 0;
+  }
+
+  .el-transfer-panel__item {
+    padding: 10px 16px;
+    border-bottom: 1px solid #f0f0f0;
+    cursor: pointer;
+
+    &:hover {
+      background-color: #f5f7fa;
+    }
+
+    &.is-checked {
+      background-color: #ecf5ff;
+      color: #409eff;
+    }
+  }
+
+  .el-transfer__buttons {
+    width: 10%;
+    padding: 0 20px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    height: 400px;
+  }
+
+  .el-transfer__button {
+    margin: 10px 0;
+    width: 100%;
+  }
+
+  .el-transfer-panel__filter {
+    padding: 10px 16px;
+    border-bottom: 1px solid #dcdfe6;
+
+    .el-input__wrapper {
+      box-shadow: 0 0 0 1px #dcdfe6 inset;
+    }
+  }
+}
+
+:deep(.el-checkbox) {
+  margin-right: 8px;
+}
+
+:deep(.el-transfer-panel__empty) {
+  padding: 30px 0;
+  text-align: center;
+  color: #909399;
 }
 </style>

+ 8 - 8
src/views/material/blacklist/index.vue

@@ -78,8 +78,8 @@
 
     <Pagination
       v-model:total="total"
-      v-model:page="queryParams.current"
-      v-model:limit="queryParams.size"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
   </ContentWrap>
@@ -106,8 +106,8 @@ const multiple = ref(true) // 非多个禁用
 
 // 查询参数
 const queryParams = reactive({
-  current: 1,
-  size: 10,
+  pageNo: 1,
+  pageSize: 10,
   userName: undefined,
   nickName: undefined
 })
@@ -118,7 +118,7 @@ const getList = async () => {
   loading.value = true
   try {
     const data = await BlacklistApi.listBlacklist(queryParams)
-    list.value = data.records
+    list.value = data.list
     total.value = data.total
   } finally {
     loading.value = false
@@ -127,7 +127,7 @@ const getList = async () => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.current = 1
+  queryParams.pageNo = 1
   getList()
 }
 
@@ -139,7 +139,7 @@ const resetQuery = () => {
 
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: any[]) => {
-  ids.value = selection.map((item) => item.recordId)
+  ids.value = selection.map((item) => item.id)
   multiple.value = !selection.length
 }
 
@@ -151,7 +151,7 @@ const openForm = (type: string) => {
 
 /** 删除按钮操作 */
 const handleDelete = async (row?: any) => {
-  const recordIds = row?.recordId || ids.value
+  const recordIds = row?.id || ids.value
   try {
     // 删除的二次确认
     await message.delConfirm()

+ 9 - 18
src/views/material/coll/index.vue

@@ -150,6 +150,7 @@ import { handleTree } from '@/utils/tree'
 import * as LoanApi from '@/api/material/loan/index'
 import * as CabinetApi from '@/api/material/lockers/index'
 import * as TypeApi from '@/api/material/type/index'
+import {getDateRange} from "@/utils/formatTime";
 
 
 // 定义组件 props
@@ -203,12 +204,14 @@ const getList = async () => {
   try {
     // 处理时间范围
     if (Array.isArray(loanTime.value) && loanTime.value.length === 2) {
-      queryParams.loanTimeStart = formatDate(loanTime.value[0])
-      queryParams.loanTimeEnd = formatDate(loanTime.value[1])
+      const [startTime, endTime] = getDateRange(loanTime.value[0], loanTime.value[1])
+      queryParams.loanTimeStart = startTime
+      queryParams.loanTimeEnd = endTime
     }
     if (Array.isArray(restitutionTime.value) && restitutionTime.value.length === 2) {
-      queryParams.restitutionTimeStart = formatDate(restitutionTime.value[0])
-      queryParams.restitutionTimeEnd = formatDate(restitutionTime.value[1])
+      const [startTime, endTime] = getDateRange(restitutionTime.value[0], restitutionTime.value[1])
+      queryParams.restitutionTimeStart = startTime
+      queryParams.restitutionTimeEnd = endTime
     }
     const data = await LoanApi.listLoan(queryParams)
     list.value = data.list
@@ -262,19 +265,7 @@ const handleQueryStatus = () => {
   getList()
 }
 
-/** 格式化日期 */
-const formatDate = (date: Date) => {
-  if (date && date instanceof Date && !isNaN(date.getTime())) {
-    const year = date.getFullYear()
-    const month = String(date.getMonth() + 1).padStart(2, '0')
-    const day = String(date.getDate()).padStart(2, '0')
-    const hours = String(date.getHours()).padStart(2, '0')
-    const minutes = String(date.getMinutes()).padStart(2, '0')
-    const seconds = String(date.getSeconds()).padStart(2, '0')
-    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
-  }
-  return null
-}
+
 
 /** 清空领取时间 */
 const handleClearLoanTime = (value: any) => {
@@ -294,7 +285,7 @@ const handleClearRestitutionTime = (value: any) => {
 
 /** 初始化 **/
 onMounted(async () => {
-  queryParams.loanFromId = props.cabinetId || null
+  queryParams.loanFromId = Number(props.cabinetId) || null
   if (props.cabinetId) {
     visibleSelect.value = true
   }

+ 31 - 15
src/views/material/information/BindDialog.vue

@@ -4,6 +4,7 @@
       v-loading="loading"
       :data="dialogMatList"
       @selection-change="handleSelectionChange"
+      style="height: 550px"
     >
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="物资编号" align="center" prop="materialsId" />
@@ -42,17 +43,18 @@
       </el-table-column>
     </el-table>
 
-    <Pagination
-      v-model:total="total"
-      v-model:page="queryParams.current"
-      v-model:limit="queryParams.size"
-      @pagination="getList"
-    />
-
-    <template #footer>
-      <el-button type="primary" @click="submitBind">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
+    <div class="dialog-footer">
+      <Pagination
+        v-model:total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+      <div class="dialog-buttons">
+        <el-button type="primary" @click="submitBind">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </div>
   </Dialog>
 </template>
 
@@ -81,8 +83,8 @@ const materialsIds = ref([]) // 选中的数据
 
 // 查询参数
 const queryParams = reactive({
-  current: 1,
-  size: 10,
+  pageNo: 1,
+  pageSize: 10,
   materialsCabinetId: 0
 })
 
@@ -98,7 +100,7 @@ const getList = async () => {
   loading.value = true
   try {
     const data = await MaterialApi.listMaterials(queryParams)
-    dialogMatList.value = data.records
+    dialogMatList.value = data.list
     total.value = data.total
   } finally {
     loading.value = false
@@ -107,7 +109,7 @@ const getList = async () => {
 
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: any[]) => {
-  materialsIds.value = selection.map((item) => item.materialsId)
+  materialsIds.value = selection.map((item) => item.id)
 }
 
 /** 提交绑定 */
@@ -125,3 +127,17 @@ const submitBind = async () => {
   } catch {}
 }
 </script>
+<style scoped lang="scss">
+.dialog-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 0;
+  margin-top: 10px;
+
+  .dialog-buttons {
+    display: flex;
+    gap: 8px;
+  }
+}
+</style>

+ 53 - 17
src/views/material/information/index.vue

@@ -41,7 +41,7 @@
         <el-tree-select
           v-model="queryParams.materialsTypeId"
           :data="machinerytypeOptions"
-          :props="{ label: 'materialsTypeName', value: 'materialsTypeId' }"
+          :props="{ label: 'materialsTypeName', value: 'id' }"
           placeholder="请选择物资类型"
           class="!w-240px"
         />
@@ -162,7 +162,7 @@
           type="primary"
           plain
           @click="handleImport"
-          v-hasPermi="['iscs:materials:import']"
+          v-hasPermi="['iscs:materials:create']"
         >
           <Icon icon="ep:upload" class="mr-5px" /> 批量新增
         </el-button>
@@ -297,7 +297,7 @@
 
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
+import { getDateRange} from '@/utils/formatTime'
 import { handleTree } from '@/utils/tree'
 import * as MaterialApi from '@/api/material/information'
 import * as CabinetApi from "@/api/material/lockers/index";
@@ -312,8 +312,8 @@ defineOptions({ name: 'MaterialInformation' })
 
 const props = defineProps({
   cabinetId: {
-    type: String,
-    required: false
+    type: [String, Number],
+    default: null
   }
 })
 
@@ -337,7 +337,7 @@ const queryParams = reactive({
   materialsName: undefined,
   materialsRfid: undefined,
   loanState: undefined,
-  materialsCabinetId: undefined,
+  materialsCabinetId: null as string | number | null,
   materialsTypeId: undefined,
   startExpirationDate: undefined,
   endExpirationDate: undefined,
@@ -404,26 +404,58 @@ const filteredPropertyValues = computed(() => {
     (value) => value.propertyId === queryParams.propertyId
   )
 })
-
+const route=useRoute()
 // 初始化
 onMounted(async () => {
-  queryParams.materialsCabinetId = props.cabinetId || null
-  if (props.cabinetId) {
-    visibleSelect.value = true
-  }
   await getList()
   await getTreeselect()
   await materialsCabinets()
+
+  // 设置初始值
+  setInitialCabinetId()
 })
 
+// 设置初始物资柜ID
+const setInitialCabinetId = () => {
+  if (props.cabinetId) {
+    queryParams.materialsCabinetId = Number(props.cabinetId)
+    visibleSelect.value = true
+  } else if (route.query.cabinetId) {
+    queryParams.materialsCabinetId = Number(route.query.cabinetId)
+  } else {
+    queryParams.materialsCabinetId = null
+  }
+}
+
+// 监听物资柜数据变化,确保值能正确回显
+watch(
+  () => cabinets.value,
+  (newCabinets) => {
+    if (newCabinets && newCabinets.length > 0) {
+      // 如果当前没有选中值,但有默认值,则设置
+      if (!queryParams.materialsCabinetId) {
+        setInitialCabinetId()
+      }
+    }
+  },
+  { immediate: true }
+)
+
 /** 查询物资列表 */
 const getList = async () => {
   loading.value = true
   try {
-    if (Array.isArray(createTime.value) && createTime.value.length === 2) {
-      queryParams.startExpirationDate = dateFormatter(createTime.value[0])
-      queryParams.endExpirationDate = dateFormatter(createTime.value[1])
+    if (Array.isArray(createTime.value) && createTime.value.length == 2) {
+      const [startTime, endTime] = getDateRange(createTime.value[0], createTime.value[1])
+      queryParams.startExpirationDate = startTime
+      queryParams.endExpirationDate = endTime
+    }
+    if(route.query.cabinetId){
+      queryParams.materialsCabinetId = Number(route.query.cabinetId)
+    }else {
+      queryParams.materialsCabinetId =null
     }
+
     const data = await MaterialApi.listMaterials(queryParams)
     materialsList.value = data.list
     total.value = data.total
@@ -437,7 +469,7 @@ const getTreeselect = async () => {
   const data = await TypeApi.listType({ pageNo: 1, pageSize: -1 })
   machinerytypeOptions.value = handleTree(
     data.list,
-    'materialsTypeId',
+    'id',
     'parentId',
     'children'
   )
@@ -473,7 +505,11 @@ const resetQuery = () => {
   queryParams.materialsCode = undefined
   queryParams.materialsName = undefined
   queryParams.loanState = undefined
-  queryParams.materialsCabinetId = undefined
+  if(route.query.cabinetId) {
+    queryParams.materialsCabinetId = Number(route.query.cabinetId)
+  }else {
+    queryParams.materialsCabinetId = undefined
+  }
   queryParams.materialsTypeId = undefined
   queryParams.startExpirationDate = undefined
   queryParams.endExpirationDate = undefined
@@ -486,7 +522,7 @@ const resetQuery = () => {
 
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: any[]) => {
-  ids.value = selection.map((item) => item.materialsId)
+  ids.value = selection.map((item) => item.id)
   single.value = selection.length !== 1
   multiple.value = !selection.length
 }

+ 39 - 13
src/views/material/inspectionplan/PlanForm.vue

@@ -14,7 +14,9 @@
         <el-tree-select
           v-model="formData.workstationId"
           :data="workstationOptions"
-          :props="{ label: 'workstationName', value: 'workstationId' }"
+          check-strictly
+          :render-after-expand="false"
+          :props="{ label: 'workstationName', value: 'id' }"
           placeholder="请选择所属区域"
         />
       </el-form-item>
@@ -67,11 +69,9 @@
 <script lang="ts" setup>
 import * as PlanApi from '@/api/material/plan'
 import * as CabinetApi from '@/api/material/information/index'
-import {getMaterialsCabinets} from "@/api/material/information/index";
-import {getPlanInfo} from "@/api/material/plan";
-
-// import * as DeptApi from '@/api/system/marsdept'
-
+import * as MarsDeptApi from '@/api/system/marsdept'
+import * as UserApi from '@/api/system/user'
+import { handleTree } from '@/utils/tree'
 defineOptions({ name: 'PlanForm' })
 
 const props = defineProps({
@@ -123,7 +123,7 @@ const open = async (type: string, row?: any) => {
   if (row) {
     formLoading.value = true
     try {
-      const data = await PlanApi.getPlanInfo(row.planId)
+      const data = await PlanApi.getPlanInfo(row.id)
       formData.value = data
     } finally {
       formLoading.value = false
@@ -192,21 +192,47 @@ const formatDate = (date: Date) => {
 const disabledDate = (time: Date) => {
   return time.getTime() < Date.now() - 8.64e7
 }
+/** 获取检察员列表  */
+const getCheckUsers = async () => {
+  const data = await UserApi.getRoleUser('check')
+  planPersonOption.value = data.map((item) => ({
+    label: item.nickname,
+    value: item.id
+  }))
+}
+// 岗位下拉
+const getOtherList = async () => {
+  try {
+    const data = await MarsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 })
+    if (data && data.list) {
+      workstationOptions.value = handleTree(data.list, 'id', 'parentId')
+    } else {
+      workstationOptions.value = []
+    }
+  } catch (error) {
+    console.error('获取区域数据失败:', error)
+    workstationOptions.value = []
+  }
+}
 
+onMounted(() => {
+  getOtherList()
+  getCheckUsers()
+})
 /** 监听所属区域变更 */
 watch(
   () => formData.value.workstationId,
   async (newVal) => {
     if (newVal) {
       const data = await CabinetApi.getMaterialsCabinets({
-        current: 1,
-        size: -1,
+        pageNo: 1,
+        pageSize: -1,
         workstationId: newVal
       })
-      if (data?.records) {
-        formData.value.cabinetIds = data.records.map((item) => item.cabinetId)
-        cabinets.value = data.records.map((item) => ({
-          value: item.cabinetId,
+      if (data?.list) {
+        // formData.value.cabinetIds = data.list.map((item) => item.id)
+        cabinets.value = data.list.map((item) => ({
+          value: item.id,
           label: item.cabinetName
         }))
       }

+ 40 - 47
src/views/material/inspectionplan/index.vue

@@ -8,7 +8,7 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item label="计划名称" prop="planName" v-if="!visibleSelect">
+      <el-form-item label="计划名称" prop="planName" v-if="!visibleSelect" >
         <el-input
           v-model="queryParams.planName"
           placeholder="请输入计划名称"
@@ -63,7 +63,7 @@
       <el-form-item label="状态" prop="status">
         <el-select v-model="queryParams.status" placeholder="请选择状态" class="!w-240px">
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.CHECKING_STATUS)"
+            v-for="dict in getStrDictOptions(DICT_TYPE.CHECKING_STATUS)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
@@ -103,8 +103,8 @@
             style="transform: scale(1.5)"
           />
           <span style="margin-left: 6%">启动自动创建</span>
-        </el-col>
-        <el-col :span="4">
+        </el-col >
+        <el-col :span="4" style="margin-top: 8px">
           <el-form-item label="计划频率">
             <el-select
               v-model="autoConfig.planFrequency"
@@ -119,7 +119,7 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="4">
+        <el-col :span="4" style="margin-top: 8px">
           <el-form-item label="计划日期">
             <el-select
               v-model="autoConfig.planDate"
@@ -138,7 +138,7 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="5.5">
+        <el-col :span="5.5" style="margin-top: 8px">
           <el-form-item label="检察员">
             <el-select
               v-model="autoConfig.checkUserId"
@@ -157,7 +157,7 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="5.5" style="margin-top: 8px">
+        <el-col :span="5.5" style="margin-top: 11px">
           <span>(*自动创建的检查计划,覆盖所有物资柜)</span>
         </el-col>
       </el-row>
@@ -167,16 +167,16 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="计划编号" align="center" prop="planId" />
-      <el-table-column label="计划名称" align="center" prop="planName" />
+      <el-table-column label="计划编号" align="center" prop="id" />
+      <el-table-column label="计划名称" align="center" prop="planName" width="220"/>
       <el-table-column label="物资柜" align="center" prop="cabinetName">
-        <template #default="scope">
-          <el-tooltip :content="scope.row.cabinetName" placement="top">
-            <span class="text-primary cursor-pointer" @click="handlePlanDetail(scope.row)">
-              {{ getDisplayText(scope.row.cabinetName) }}
-            </span>
-          </el-tooltip>
-        </template>
+<!--        <template #default="scope">-->
+<!--          <el-tooltip :content="scope.row.cabinetName" placement="top">-->
+<!--            <span class="text-primary cursor-pointer" @click="handlePlanDetail(scope.row)">-->
+<!--              {{ getDisplayText(scope.row.cabinetName) }}-->
+<!--            </span>-->
+<!--          </el-tooltip>-->
+<!--        </template>-->
       </el-table-column>
       <el-table-column label="计划日期" align="center" prop="planDate" />
       <el-table-column label="检察员" align="center" prop="checkUserName" />
@@ -232,21 +232,25 @@
 </template>
 
 <script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import * as PlanApi from '@/api/material/plan'
 import * as CabinetApi from '@/api/material/lockers/index'
 import * as UserApi from '@/api/system/user'
-import * as MarsDeptApi from '@/api/system/marsdept'
+
 import PlanForm from './PlanForm.vue'
 import PlanDetailDialog from './PlanDetailDialog.vue'
 import CheckRecordDialog from './CheckRecordDialog.vue'
-import {listMaterialsCabinet} from "@/api/material/lockers/index";
-import {getRoleUser, getUserPage} from "@/api/system/user";
+import {getDateRange} from "@/utils/formatTime";
+import {selectIsMailNotifyConfigByCode} from "@/api/material/plan";
 
 
 defineOptions({ name: 'InspectionPlan' })
-
+// 在 setup 中定义
+const route = useRoute()
+const router = useRouter()
+const emit = defineEmits<{
+  planId: [planId: string | number]
+}>()
 const props = defineProps({
   cabinetId: {
     type: String,
@@ -295,9 +299,10 @@ const getList = async () => {
   loading.value = true
   try {
     // 处理时间范围
-    if (Array.isArray(createTime.value) && createTime.value.length === 2) {
-      queryParams.startTime = formatDate(createTime.value[0])
-      queryParams.endTime = formatDate(createTime.value[1])
+    if (Array.isArray(createTime.value) && createTime.value.length == 2) {
+      const [startTime, endTime] = getDateRange(createTime.value[0], createTime.value[1])
+      queryParams.startTime = startTime
+      queryParams.endTime = endTime
     }
     const data = await PlanApi.listPlan(queryParams)
     list.value = data.list
@@ -333,7 +338,7 @@ const getCheckUsers = async () => {
 /** 获取自动配置信息 */
 const getAutoConfigInfo = async () => {
   const data = await PlanApi.selectIsMailNotifyConfigByCode({
-    templateCode: 'CHECK_PLAN'
+    templateCode: 'test_01'
   })
   autoConfig.planDate = data.planDate
   autoConfig.planFrequency = data.planFrequency
@@ -402,19 +407,7 @@ const resetQuery = () => {
   handleQuery()
 }
 
-/** 格式化日期 */
-const formatDate = (date: Date) => {
-  if (date && date instanceof Date && !isNaN(date.getTime())) {
-    const year = date.getFullYear()
-    const month = String(date.getMonth() + 1).padStart(2, '0')
-    const day = String(date.getDate()).padStart(2, '0')
-    const hours = String(date.getHours()).padStart(2, '0')
-    const minutes = String(date.getMinutes()).padStart(2, '0')
-    const seconds = String(date.getSeconds()).padStart(2, '0')
-    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
-  }
-  return null
-}
+
 
 /** 清空时间 */
 const handleClearTime = (value: any) => {
@@ -425,10 +418,10 @@ const handleClearTime = (value: any) => {
 }
 
 /** 获取显示文本 */
-const getDisplayText = (cabinetName: string) => {
-  const names = cabinetName.split(',')
-  return `${names.length}个`
-}
+// const getDisplayText = (cabinetName: string) => {
+//   const names = cabinetName.split(',')
+//   return `${names.length}个`
+// }
 
 /** 添加/修改操作 */
 const formRef = ref()
@@ -447,14 +440,14 @@ const recordRef = ref()
 const handleViewRecord = (row: any) => {
   if (props.cabinetId) {
     emit('planId', {
-      planId: row.planId,
+      planId: row.id,
       planName: row.planName
     })
   } else {
     router.push({
       path: '/material/Inspectionrecords',
       query: {
-        planId: row.planId,
+        planId: row.id,
         planName: row.planName
       }
     })
@@ -467,7 +460,7 @@ const handleDelete = async (row: any) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await PlanApi.deletePlan(row.planId)
+    await PlanApi.deletePlan(row.id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -476,7 +469,7 @@ const handleDelete = async (row: any) => {
 
 /** 初始化 **/
 onMounted(async () => {
-  queryParams.cabinetId = props.cabinetId || null
+  queryParams.cabinetId = Number(props.cabinetId) || null
   if (props.cabinetId) {
     visibleSelect.value = true
   }

+ 10 - 4
src/views/material/inventory/index.vue

@@ -95,6 +95,7 @@
 
 import * as StatisticApi from '@/api/material/statistics/index'
 import download from '@/utils/download'
+import {exportMaterialInventory} from "@/api/material/statistics/index";
 defineOptions({ name: 'MaterialInventory' })
 
 const { t } = useI18n() // 国际化
@@ -180,10 +181,15 @@ const getList = async () => {
 }
 
 /** 导出按钮操作 */
-const handleExport = () => {
-  download('iscs/statistics-api/exportMaterialInventory', {
-    currentTab: currentTab.value
-  }, `物资盘点_${new Date().getTime()}.xlsx`)
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+
+    const data = await StatisticApi.exportMaterialInventory()
+    download.excel(data, '物资盘点.xls')
+  } catch {
+  }
 }
 
 /** 监听Tab变化 */

+ 1 - 3
src/views/material/lockers/index.vue

@@ -412,7 +412,7 @@ const getException = async (): Promise<void> => {
       status: 0
     })
 
-    const unClosedCabinets = exceptionResponse.data.records || []
+    const unClosedCabinets = exceptionResponse.list || []
     let unClosedCabinetDetails: [] = []
 
     if (unClosedCabinets.length > 0) {
@@ -518,8 +518,6 @@ const showExTable = (): void => {
   exceptionTableVisible.value = !exceptionTableVisible.value
 }
 
-
-
 // 物资柜点击事件
 const handleCabinetClick = (cabinet: any): void => {
   router.push({

+ 787 - 0
src/views/sopm/sop/CreateSop.vue

@@ -0,0 +1,787 @@
+<template>
+  <div>
+    <!--    sop表单-->
+    <ContentWrap>
+      <el-collapse v-model="activeName" accordion>
+        <el-collapse-item name="1">
+          <template #title>
+            <div style="display: flex; align-items: center; gap: 8px">
+              <el-icon size="20" style="margin-left: 10px">
+                <InfoFilled />
+              </el-icon>
+              <span style="font-size: 18px">SOP创建步骤</span>
+            </div>
+          </template>
+
+          <div style="padding-left: 20px">
+            <div>1、设置SOP的基本信息</div>
+            <div>2、确定流程模式信息</div>
+            <div>3、确定点位及锁定分组</div>
+            <div>4、确定共锁人列表(有共锁)</div>
+          </div>
+        </el-collapse-item>
+      </el-collapse>
+
+      <!-- 自定义边框容器 与sop表单 -->
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">基本信息</span>
+        </div>
+        <div class="tab-content">
+          <el-form
+            class="-mb-15px"
+            :model="SopForm"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-row>
+              <el-col :span="4">
+                <el-form-item label="SOP名称" prop="sopName">
+                  <el-input
+                    v-model="SopForm.sopName"
+                    placeholder="请输入sop名称"
+                    clearable
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="2">
+                <el-checkbox v-model="SopAutoName">自动生成</el-checkbox>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="SOP区域" prop="workstationId">
+                  <el-tree-select
+                    v-model="SopForm.workstationId"
+                    :data="workstationOption"
+                    :props="{ label: 'workstationName', value: 'id', children: 'children' }"
+                    placeholder="选择岗位"
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="工艺设备" prop="machineryId">
+                  <el-tree-select
+                    v-model="SopForm.machineryId"
+                    :data="machineryOptions"
+                    :props="{ label: 'machineryName', value: 'id', children: 'children' }"
+                    placeholder="选择设备/工艺"
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="SOP类型" prop="sopType">
+                  <el-select
+                    v-model="SopForm.sopType"
+                    placeholder="请选择SOP类型"
+                    clearable
+                    class="!w-240px"
+                    @change="handleSopTypeChange"
+                  >
+                    <el-option
+                      v-for="dict in getIntDictOptions(DICT_TYPE.SOP_TYPE)"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="dict.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-form-item label="流程模式" prop="modeId">
+                <el-select
+                  v-model="SopForm.modeId"
+                  placeholder="请选择流程模式"
+                  clearable
+                  class="!w-240px"
+                  @change="handleModeChange"
+                >
+                  <el-option
+                    v-for="dict in ModeOption"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-row>
+          </el-form>
+        </div>
+      </div>
+    </ContentWrap>
+    <!--流程步骤画布-->
+    <ContentWrap v-if="Visible">
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">流程设置</span>
+          <div class="set-btn" @click="goSetting('SetModeStep',SopForm.id)">设置</div>
+        </div>
+        <div class="tab-content">
+          <!-- VueFlow 主画布 -->
+          <VueFlow style="width: 100%; height: 300px">
+            <template #node-default="{ id, data }">
+              <div class="custom-node">
+                <div class="node-content">
+                  <!-- 图标显示 -->
+                  <div style="font-size: 30px">
+                    <img
+                      v-if="data.stepIcon && data.stepIcon.startsWith('http')"
+                      :src="data.stepIcon"
+                      :alt="data.stepTitleShort"
+                      style="width: 40px; height: 40px; object-fit: contain"
+                    />
+                    <span v-else>{{ data.stepIcon || '📋' }}</span>
+                  </div>
+                  <div style="font-weight: bold; font-size: 14px">
+                    {{ data.stepTitleShort || '无标题' }}
+                  </div>
+                  <div style="font-size: 25px">
+                    {{ String.fromCharCode(9311 + (data.stepIndex || 1)) }}
+                  </div>
+                </div>
+
+                <!-- 四个连接点 -->
+                <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top" />
+                <Handle
+                  type="source"
+                  position="bottom"
+                  :id="`${id}-bottom`"
+                  class="handle handle-bottom"
+                />
+                <Handle
+                  type="target"
+                  position="left"
+                  :id="`${id}-left`"
+                  class="handle handle-left"
+                />
+                <Handle
+                  type="source"
+                  position="right"
+                  :id="`${id}-right`"
+                  class="handle handle-right"
+                />
+              </div>
+            </template>
+          </VueFlow>
+        </div>
+      </div>
+    </ContentWrap>
+    <!--    点位设置 -->
+    <ContentWrap v-if="Visible">
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">点位设置</span>
+          <div class="set-btn"  @click="goSetting('SetPoint',SopForm.id)">设置</div>
+        </div>
+        <div class="tab-content" style="height: 300px">
+          <div class="point_center_box">
+            <img src="../../../assets/images/添加.png" alt=""  @click="goSetting('SetPoint',SopForm.id)"/>
+            <span style="color: red">*请添加需要进行隔离的点位</span>
+          </div>
+        </div>
+      </div>
+    </ContentWrap>
+    <!--    人员设置 -->
+    <ContentWrap v-if="Visible">
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">人员设置</span>
+          <div class="set-btn" @click="goSetting('SetUser',SopForm.id)">设置</div>
+        </div>
+        <div class="tab-content" style="display: flex;height: 300px">
+          <div class="left_box">
+            <div class="tab-header">
+              <span class="tab-title">锁定人</span>
+            </div>
+            <div class="point_center_box">
+              <img src="../../../assets/images/添加.png" alt="" @click="goSetting('SetUser',SopForm.id)"/>
+              <span>请添加参与锁定的人员</span>
+            </div>
+
+          </div>
+          <div class="right_box">
+            <div class="tab-header">
+              <span class="tab-title">共锁人</span>
+            </div>
+            <div class="point_center_box">
+              <img src="../../../assets/images/添加.png" alt="" @click="goSetting('SetUser',SopForm.id)"/>
+              <span>请添加参与共锁的人员</span>
+            </div>
+
+          </div>
+        </div>
+      </div>
+    </ContentWrap >
+    <div class="bottom-btn">
+      <el-button @click="submit">
+        <el-icon>
+          <Check />
+        </el-icon>
+        确 定
+      </el-button>
+
+      <el-button @click="cancel">
+        <el-icon>
+          <Close />
+        </el-icon>
+        取 消
+      </el-button>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { Check, Close } from '@element-plus/icons-vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { InfoFilled } from '@element-plus/icons-vue'
+import * as TechnologyApi from '@/api/dv/technology'
+import * as MarsDeptApi from '@/api/system/marsdept/index'
+import * as ModeApi from '@/api/custonWorkflow/index'
+import * as ModeStepApi from '@/api/custonWorkflow/step'
+import * as SopApi from '@/api/sop/index'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+import { handleTree } from '@/utils/tree'
+import { ref } from 'vue'
+import { Handle, useVueFlow, VueFlow } from '@vue-flow/core'
+
+const SopForm = reactive({
+  sopName: '',
+  sopType: '',
+  machineryId: '',
+  modeId: '',
+  workstationId: '',
+  id: undefined
+})
+const SopAutoName = ref(false)
+const activeName = ref('1')
+const machineryOptions = ref()
+const workstationOption = ref()
+const ModeOption = ref()
+const Visible = ref(false) //控制底部流程 点位 人员模块的显示
+const nodes = ref([]) //储存节点
+const edges = ref([]) // 存储连接线
+// 创建查找映射
+const workstationMap = new Map()
+const machineryMap = new Map()
+const router = useRouter()
+const route = useRoute()
+// 添加数据修改标记
+const hasUnsavedChanges = ref(false)
+const { addNodes, addEdges, setEdges, setNodes } = useVueFlow()
+//跳转设置对应页面
+const goSetting = (type,sopId) => {
+  if(type=='SetModeStep'){
+    router.push({
+      name:'SetModeStep',
+      query:{
+        sopId:sopId,
+      }
+    })
+  }else if(type=='SetPoint'){
+    router.push({
+      name:'SetPoint',
+      query:{
+        sopId:sopId,
+      }
+    })
+  }else if(type=='SetUser'){
+    router.push({
+      name:'SetUser',
+      query:{
+        sopId:sopId,
+      }
+    })
+  }
+}
+// 获取基本信息
+const getOtherList = async () => {
+  try {
+    // 获取岗位数据
+    const deptRes = await MarsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 })
+    workstationOption.value = handleTree(deptRes.list, 'id', 'parentId')
+    buildWorkstationMap(deptRes.list)
+
+    // 获取设备/工艺数据
+    const techRes = await TechnologyApi.listTechnology({ pageNo: 1, pageSize: -1 })
+    const data = techRes.list.filter((item) => item.machineryType == '工艺')
+    machineryOptions.value = handleTree(data, 'id', 'parentId')
+    buildMachineryMap(data)
+
+    // 获取工作流模式数据
+    const modeRes = await ModeApi.getWorkflowModePage({ pageNo: 1, pageSize: -1 })
+    ModeOption.value = modeRes.list.map((item) => ({
+      label: item.modeName,
+      value: item.id
+    }))
+
+    console.log('数据加载完成')
+  } catch (error) {
+    console.error('获取数据失败:', error)
+    ElMessage.error('获取数据失败')
+  }
+}
+
+// 流程模式切换
+const handleModeChange = async (value) => {
+  console.log(value, 'value')
+
+  SopForm.modeId = value
+  // 清空画布
+  await clearCanvasProperly()
+  if (SopForm.modeId) {
+    const data = await ModeStepApi.getWorkflowStepPage({
+      pageNo: 1,
+      pageSize: -1,
+      modeId: SopForm.modeId
+    })
+
+    if (Array.isArray(data.list) && data.list.length > 0) {
+      // 按 stepIndex 从小到大排序
+      const sortedData = data.list.sort((a, b) => {
+        const aIndex = a.stepIndex || 0
+        const bIndex = b.stepIndex || 0
+        return aIndex - bIndex
+      })
+      // 渲染节点
+      renderNodesFromData(sortedData)
+      // 渲染连接线
+      renderEdgesFromData(sortedData)
+    }
+  }
+}
+// 独立的清空画布函数
+const clearCanvasProperly = async () => {
+  setNodes([])
+  setEdges([])
+  nodes.value = [] // 同步响应式数据(如果有自定义 nodes)
+  edges.value = []
+}
+// 初始化数据渲染节点 - 修正版本
+const renderNodesFromData = (data) => {
+  // 清空现有节点
+  nodes.value = []
+
+  data.forEach((item, index) => {
+    const nodeId = `node-${item.id || Date.now() + index}`
+
+    const newNode = {
+      id: nodeId,
+      position: {
+        x: 100 + index * 200,
+        y: 100
+      },
+      width: 100,
+      height: 150,
+      data: {
+        stepIcon: item.stepIcon,
+        stepTitleShort: item.stepTitleShort,
+        stepIndex: item.stepIndex || index + 1, // 使用 stepIndex
+        index: item.stepIndex || index + 1, // 保持兼容性
+        // 保存完整的数据用于表单编辑
+        stepData: item
+      },
+      style: {
+        width: '130px',
+        height: '180px',
+        borderRadius: '12px',
+        border: '1px solid #999',
+        textAlign: 'center',
+        display: 'flex',
+        flexDirection: 'column',
+        alignItems: 'center',
+        justifyContent: 'space-between',
+        padding: '10px',
+        backgroundColor: '#fff'
+      },
+      draggable: true
+    }
+
+    addNodes(newNode)
+    nodes.value.push(newNode)
+  })
+}
+
+// 渲染连接线 - 新增函数
+const renderEdgesFromData = (data) => {
+  // 清空现有连接线
+  edges.value = []
+
+  // 根据 stepIndex 顺序创建连接线
+  for (let i = 0; i < data.length - 1; i++) {
+    const currentStep = data[i]
+    const nextStep = data[i + 1]
+
+    const sourceNodeId = `node-${currentStep.id}`
+    const targetNodeId = `node-${nextStep.id}`
+
+    // 创建连接线,从右侧连接到左侧
+    const edge = {
+      id: `edge-${currentStep.id}-${nextStep.id}`,
+      source: sourceNodeId,
+      target: targetNodeId,
+      sourceHandle: `${sourceNodeId}-right`, // 从右侧连接点出发
+      targetHandle: `${targetNodeId}-left`, // 连接到左侧连接点
+      type: 'smoothstep',
+      style: { stroke: '#333', strokeWidth: 2 },
+      markerEnd: {
+        type: 'arrowclosed',
+        width: 20,
+        height: 20,
+        color: '#333'
+      }
+    }
+
+    addEdges(edge)
+    edges.value.push(edge)
+
+    console.log('创建连接线:', edge)
+  }
+}
+// 监听表单数据变化
+watch(
+  () => SopForm,
+  () => {
+    hasUnsavedChanges.value = true
+  },
+  { deep: true }
+)
+// 保存成功后重置标记
+const submit = async () => {
+  try {
+    let data
+    let successMessage
+
+    if (SopForm.id) {
+      // 修改操作
+      data = await SopApi.updateSop(SopForm)
+      successMessage = t('common.updateSuccess')
+
+      if (data) {
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+        Visible.value = true
+      }
+    } else {
+      // 新增操作
+      data = await SopApi.insertSop(SopForm)
+      successMessage = t('common.createSuccess')
+
+      if (data) {
+        // 新增成功后,获取完整数据
+        try {
+          const selectData = await SopApi.selectSopById(data)
+          if (selectData) {
+            // 正确更新 ref 的值
+            SopForm = { ...SopForm, ...selectData }
+          }
+        } catch (selectError) {
+          console.warn('获取详情失败,但不影响保存:', selectError)
+          // 即使获取详情失败,也设置 id
+          SopForm.id = data
+        }
+
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+        Visible.value = true
+      }
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败')
+  }
+}
+
+// 取消操作
+const cancel = async () => {
+  if (hasUnsavedChanges.value) {
+    try {
+      await ElMessageBox.confirm('当前页面有未保存的更改,确定要离开吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+      // 用户确认离开
+      router.push('/sopm/sop')
+    } catch {
+      // 用户取消离开
+      console.log('用户取消离开')
+    }
+  } else {
+    // 没有未保存的更改,直接离开
+    router.push('/sopm/sop')
+  }
+}
+
+// 初始化
+onMounted(() => {
+  getOtherList()
+})
+
+// 构建岗位查找映射
+const buildWorkstationMap = (list) => {
+  const buildMap = (items) => {
+    for (const item of items) {
+      workstationMap.set(item.id, item.workstationName)
+      if (item.children && item.children.length > 0) {
+        buildMap(item.children)
+      }
+    }
+  }
+  buildMap(list)
+}
+
+// 构建工艺查找映射
+const buildMachineryMap = (list) => {
+  const buildMap = (items) => {
+    for (const item of items) {
+      machineryMap.set(item.id, item.machineryName || item.name)
+      if (item.children && item.children.length > 0) {
+        buildMap(item.children)
+      }
+    }
+  }
+  buildMap(list)
+}
+//SopType改变函数
+const handleSopTypeChange = (value) => {
+  SopForm.sopType = value
+}
+// 使用 watchEffect 更简洁
+watchEffect(() => {
+  const { workstationId, machineryId, sopType } = SopForm
+  const autoName = SopAutoName.value
+
+  if (autoName && workstationId && machineryId && sopType) {
+    generateSopName()
+  }
+})
+// 监听 workstationId 变化
+watchEffect(async () => {
+  const newWorkstationId = SopForm.workstationId
+  if (newWorkstationId) {
+    // console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
+    SopForm.machineryId = null
+    await getMachineryData(newWorkstationId)
+  }
+})
+
+// 获取工艺数据的函数
+const getMachineryData = async (workstationId) => {
+  try {
+    const techRes = await TechnologyApi.listTechnology({
+      pageNo: 1,
+      pageSize: -1,
+      workstationId: workstationId // 传递岗位ID参数
+    })
+
+    const data = techRes.list.filter((item) => item.machineryType == '工艺')
+    machineryOptions.value = handleTree(data, 'id', 'parentId')
+  } catch (error) {
+    console.error('获取工艺数据失败:', error)
+    ElMessage.error('获取工艺数据失败')
+  }
+}
+// 生成 SOP 名称
+const generateSopName = () => {
+  try {
+    const workstationName = workstationMap.get(SopForm.workstationId)
+    const machineryName = machineryMap.get(SopForm.machineryId)
+    const sopTypeName = getSopTypeName(SopForm.sopType)
+    console.log(workstationName, machineryName, sopTypeName, 'bbbbb')
+    SopForm.sopName = `${workstationName}-${machineryName}-${sopTypeName}`
+
+    console.log('自动生成的 SOP 名称:', SopForm.sopName)
+  } catch (error) {
+    console.error('生成 SOP 名称失败:', error)
+  }
+}
+
+// 获取 SOP 类型名称
+const getSopTypeName = (sopType) => {
+  const sopTypeOptions = getIntDictOptions(DICT_TYPE.SOP_TYPE)
+  const typeOption = sopTypeOptions.find((option) => option.value === sopType)
+  return typeOption ? typeOption.label : '未知类型'
+}
+</script>
+
+<style scoped lang="scss">
+.custom-tabs-container {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  margin-top: 20px;
+}
+
+.tab-header {
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #dcdfe6;
+  padding: 12px 20px;
+  border-radius: 4px 4px 0 0;
+}
+
+.tab-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #303133;
+}
+
+.set-btn {
+  width: 60px;
+  height: 30px;
+  border: 1px solid black;
+  border-radius: 6px;
+  text-align: center;
+  line-height: 30px;
+  float: right;
+}
+
+.tab-content {
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 0 0 4px 4px;
+}
+
+.point_center_box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+
+  img {
+    width: 80px;
+    height: 80px;
+  }
+}
+
+.left_box {
+  width: 500px;
+  margin-right: 10px;
+  display: flex;
+  flex-direction: column;
+  img {
+    width: 80px;
+    height: 80px;
+  }
+}
+
+.right_box {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+
+  img {
+    width: 80px;
+    height: 80px;
+
+  }
+}
+
+.bottom-btn {
+  width: 100%;
+  height: 40px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.custom-node {
+  position: relative;
+  width: 125px;
+  height: 180px;
+  background-color: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px;
+  box-sizing: border-box;
+}
+
+.node-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+}
+
+//连接点样式
+
+.handle {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+  position: absolute;
+  z-index: 10;
+}
+
+.handle:hover {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+.handle-top {
+  top: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-bottom {
+  bottom: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-left {
+  left: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.handle-right {
+  right: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+//连接点全局样式
+:deep(.vue-flow__handle) {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+}
+
+:deep(.vue-flow__handle:hover) {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+//连接线样式
+:deep(.vue-flow__edge-path) {
+  stroke: #333;
+  stroke-width: 2;
+}
+
+:deep(.vue-flow__edge) {
+  z-index: 1;
+}
+
+// 箭头样式
+:deep(.vue-flow__edge-marker) {
+  fill: #333;
+}
+</style>

+ 718 - 0
src/views/sopm/sop/ModeView/TableView.vue

@@ -0,0 +1,718 @@
+<template>
+  <div class="workflow-page">
+    <!-- 表格区域 -->
+    <div class="table-container">
+      <el-table
+        ref="tableRef"
+        :data="tableData"
+        @selection-change="handleSelectionChange"
+        border
+        stripe
+        row-key="id"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="{ row }">
+            {{ row.stepIndex }}
+          </template>
+        </el-table-column>
+
+        <el-table-column label="图标" width="120" align="center">
+          <template #default="{ row }">
+            <el-popover placement="bottom" trigger="click" width="230">
+              <!-- 图标选择面板 -->
+              <div style="display: flex; flex-wrap: wrap; gap: 8px">
+                <div
+                  v-for="icon in iconOptions"
+                  :key="icon.value"
+                  @click="() => selectIcon(row, icon.value)"
+                  :style="{
+                    border: row.stepIcon === icon.value ? '2px solid #409EFF' : '1px solid #ccc',
+                    borderRadius: '4px',
+                    padding: '2px',
+                    cursor: 'pointer'
+                  }"
+                >
+                  <img
+                    :src="icon.value"
+                    :alt="icon.name"
+                    style="width: 28px; height: 28px; object-fit: contain"
+                  />
+                </div>
+              </div>
+
+              <!-- 触发元素:当前图标或按钮 -->
+              <template #reference>
+                <div style="cursor: pointer; display: inline-block">
+                  <img
+                    v-if="row.stepIcon"
+                    :src="row.stepIcon"
+                    style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid #ccc"
+                  />
+                  <el-button v-else type="primary" size="small">选择图标</el-button>
+                </div>
+              </template>
+            </el-popover>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="标题" width="180">
+          <template #default="{ row }">
+            <el-input
+
+              v-model="row.stepTitle"
+              placeholder="标题"
+              size="small"
+              @blur="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="标题缩写" width="120">
+          <template #default="{ row }">
+            <el-input
+
+              v-model="row.stepTitleShort"
+              placeholder="标题缩写"
+              size="small"
+              @blur="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="确认方式" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.confirmType"
+              placeholder="请选择确认方式"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STEP_CONFIRMTYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="确认角色" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.confirmRoleCode"
+              placeholder="请选择确认角色"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+              @clear="handleRoleClear(row)"
+            >
+              <el-option
+                v-for="dict in RoleOptions"
+                :key="dict.code"
+                :label="dict.name"
+                :value="dict.code"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="确认人员" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.confirmUser"
+              placeholder="请选择确认人员"
+              @change="saveRowData(row)"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in UserOptions"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="步骤操作说明" width="120" align="center">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="viewStepDetail(row)">查看</el-button>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="取消作业" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableCancelJob"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="设置锁定人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableSetLocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="设置共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableSetColocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="添加共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableAddColocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="添加共锁人后跳转步骤" width="180">
+          <template #default="{ row }">
+            <el-input-number
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.gotoStepAfterAddingColocker"
+              :min="1"
+              size="small"
+              placeholder="步骤号"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="减少共锁人" width="120" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableReduceColocker"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="上锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableLock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="共锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableColock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="解除共锁" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableReleaseColock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="解锁" width="80" align="center">
+          <template #default="{ row }">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="row.enableUnlock"
+              @change="saveRowData(row)"
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column label="结束作业" width="100" align="center">
+          <template #default="{ row }">
+            <el-checkbox v-model="row.enableEndJob" @change="saveRowData(row)" :disabled="DisableCheckView||props.isPreset"/>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick } from 'vue'
+import { Plus, DocumentAdd, Delete } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import Sortable from 'sortablejs'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
+import {
+  getWorkflowStepPage,
+  insertWorkflowStep,
+  updateWorkflowStep,
+  deleteWorkflowStepList
+} from '@/api/custonWorkflow/step'
+import { getRolePage } from '@/api/system/role'
+import { getRoleUser } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import TemplateAddDialog from './TemplateAdd.vue'
+
+const route = useRoute()
+const router = useRouter()
+// 响应式数据
+const tableData = ref([])
+const selectedRows = ref([])
+const tableRef = ref()
+const templateDialogVisible = ref(false)
+const props = defineProps({
+  enableStepTable: {
+    type: Boolean,
+    required: true
+  },
+  modeId: {
+    type: [String, Number],
+    default: null
+  },
+  isPreset:{
+    type: Boolean,
+    required: true
+  }
+})
+// 使用 computed 来处理复杂的禁用逻辑
+const isDeleteDisabled = computed(() => {
+  // 没有选中数据
+  if (!hasSelection.value) {
+    return true
+  }
+
+  // 处于查看模式
+  if (DisableCheckView.value) {
+    return true
+  }
+
+  // 是预设模式
+  if (props.isPreset) {
+    return true
+  }
+
+  // 步骤表格被禁用
+  if (!props.enableStepTable) {
+    return true
+  }
+
+  return false
+})
+// 计算属性
+const hasSelection = computed(() => selectedRows.value.length > 0)
+// 表格图标切换
+const selectIcon = (row, iconUrl) => {
+  row.stepIcon = iconUrl
+  saveRowData(row) // 你已有的接口保存方法
+}
+// 查看步骤详情
+const viewStepDetail = (row) => {
+  // 临时存储步骤数据
+  localStorage.setItem('tempStepData', JSON.stringify(row))
+
+  // 跳转到详情页面
+  router.push({
+    name: 'TableStepDetail',
+    query: {
+      stepId: row.id,
+      modeId: route.query.id
+    }
+  })
+}
+// 生成新的表格行
+const createNewRow = () => {
+  return {
+    modeId: route.query.id,
+    stepTemplateId: undefined,
+    stepIndex: 0,
+    stepName: undefined,
+    stepTitle: undefined,
+    stepTitleShort: undefined,
+    stepDescription: undefined,
+    confirmType: undefined,
+    confirmRoleCode: undefined,
+    confirmUser: undefined,
+    enableCancelJob: undefined,
+    enableSetLocker: undefined,
+    enableSetColocker: undefined,
+    enableAddColocker: undefined,
+    gotoStepAfterAddingColocker: undefined,
+    enableReduceColocker: undefined,
+    enableLock: undefined,
+    enableColock: undefined,
+    enableReleaseColock: undefined,
+    enableUnlock: undefined,
+    enableEndJob: undefined,
+    id: undefined,
+    stepIcon: undefined
+  }
+}
+// 从模板添加
+const addFromTemplate = () => {
+  templateDialogVisible.value = true
+}
+// 接收模板选择结果
+const handleTemplateSelect = async (templateData) => {
+  console.log(templateData, '子组件传递的数据')
+
+  try {
+    // 遍历数组中的每个模板对象
+    for (const template of templateData) {
+      console.log('处理模板:', template)
+
+      // 计算新的 stepIndex
+      const newStepIndex = tableData.value.length + 1
+
+      // 创建新行,只映射需要的字段
+      const newRow = {
+        ...createNewRow(),
+        stepTitle: template.stepTitle,
+        stepTitleShort: template.stepTitleShort,
+        stepDescription: template.stepDescription,
+        stepIcon: template.stepIcon,
+        confirmType: template.confirmType,
+        confirmRoleCode: template.confirmRoleCode,
+        confirmUser: template.confirmUser,
+        enableCancelJob: template.enableCancelJob,
+        enableSetLocker: template.enableSetLocker,
+        enableSetColocker: template.enableSetColocker,
+        enableAddColocker: template.enableAddColocker,
+        gotoStepAfterAddingColocker: template.gotoStepAfterAddingColocker,
+        enableReduceColocker: template.enableReduceColocker,
+        enableLock: template.enableLock,
+        enableColock: template.enableColock,
+        enableReleaseColock: template.enableReleaseColock,
+        enableUnlock: template.enableUnlock,
+        enableEndJob: template.enableEndJob,
+        stepIndex: newStepIndex,
+        modeId: route.query.id || props.modeId
+      }
+      console.log('创建的新行数据:', newRow)
+      // 插入表格
+      tableData.value.push(newRow)
+      // 保存到后端
+      await saveRowData(newRow)
+    }
+    ElMessage.success(`成功添加 ${templateData.length} 个模板`)
+  } catch (error) {
+    console.error('添加模板失败:', error)
+    ElMessage.error('添加模板失败')
+  }
+}
+// 添加新行
+const addRow = () => {
+  const newRow = createNewRow()
+
+  // 计算 stepIndex(推荐使用最大值 + 1)
+  const maxStepIndex =
+    tableData.value.length > 0 ? Math.max(...tableData.value.map((r) => r.stepIndex || 0)) : 0
+  newRow.stepIndex = maxStepIndex + 1
+
+  // 先插入表格
+  tableData.value.push(newRow)
+
+  // 然后保存(传的就是表格里的对象引用)
+  saveRowData(newRow)
+}
+
+// 删除选中行
+const deleteSelected = async () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请先选择要删除的行')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除选中的 ${selectedRows.value.length} 行数据吗?`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+
+    // 获取选中行中已有 id 的(过滤未保存的行)
+    const selectedWithId = selectedRows.value.filter((row) => row.id)
+    const selectedIds = selectedWithId.map((row) => row.id)
+
+    // 删除已有 id 的记录
+    if (selectedIds.length > 0) {
+      await deleteWorkflowStepList(selectedIds)
+    }
+
+    // 无论是否调用接口,前端都要同步移除这些行
+    tableData.value = tableData.value.filter((row) => !selectedRows.value.includes(row))
+    selectedRows.value = []
+
+    ElMessage.success('删除成功')
+  } catch (error) {
+    console.error('删除失败', error)
+    ElMessage.error('删除失败,请稍后重试')
+  }
+}
+
+// 选择变化处理
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection
+}
+
+// 行拖拽
+const initRowDrop = () => {
+  nextTick(() => {
+    const tbody = document.querySelector('.el-table__body-wrapper tbody')
+    if (tbody) {
+      Sortable.create(tbody, {
+        animation: 150,
+        handle: 'tr',
+        ghostClass: 'sortable-ghost',
+        chosenClass: 'sortable-chosen',
+        dragClass: 'sortable-drag',
+        onEnd: async ({ newIndex, oldIndex }) => {
+          if (newIndex !== undefined && oldIndex !== undefined && newIndex !== oldIndex) {
+            const movedRow = tableData.value.splice(oldIndex, 1)[0]
+            tableData.value.splice(newIndex, 0, movedRow)
+
+            for (let i = 0; i < tableData.value.length; i++) {
+              tableData.value[i].stepIndex = i + 1
+              await saveRowData(tableData.value[i])
+            }
+
+            ElMessage.success('拖拽排序完成')
+          }
+        }
+      })
+    }
+  })
+}
+
+// 自动保存单行数据
+const saveRowData = async (row) => {
+  try {
+    if (!row) return
+
+    if (!row.id) {
+      const res = await insertWorkflowStep(row)
+      if (res) {
+        // 写回 id,保持响应式引用不变
+        row.id = res
+        createNewRow()
+        ElMessage.success('新增步骤成功')
+      }
+    } else {
+      await updateWorkflowStep(row)
+      // 为了给确认人员传递查询条件
+      if (row.confirmRoleCode) {
+        await InitUser(row)
+      }
+
+      ElMessage.success('更新步骤成功')
+    }
+  } catch (error) {
+    console.error('保存步骤失败', error)
+    ElMessage.error('保存失败,请稍后重试')
+  }
+}
+
+// 角色获取
+const RoleOptions = ref()
+const InitRole = async () => {
+  const data = await getRolePage({ pageNo: 1, pageSize: 10 })
+  RoleOptions.value = data.list
+}
+// 人员获取
+const UserOptions = ref()
+const InitUser = async (row) => {
+  console.log(row, 'row')
+  try {
+    const data = await getRoleUser(row.confirmRoleCode)
+    UserOptions.value = data.map((row) => {
+      return {
+        label: row.nickname,
+        value: row.id
+      }
+    })
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 在您的组件中添加
+const iconOptions = ref([])
+
+// 获取图标选项
+const loadIconOptions = async () => {
+  const icons = await getIcons()
+  if (icons && icons.length > 0) {
+    iconOptions.value = icons
+    console.log('图标选项已加载:', iconOptions.value)
+  }
+}
+// 获取步骤基础图标
+const getIcons = async () => {
+  try {
+    const sysAttrKey1 = 'icon.step.all'
+    const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
+    console.log(iconRes, '获取到的图标配置')
+
+    if (iconRes && iconRes.sysAttrValue) {
+      // 将逗号分隔的字符串转换为数组
+      const iconKeys = iconRes.sysAttrValue.split(',')
+      console.log('图标键值列表:', iconKeys)
+
+      // 批量获取每个图标的具体值
+      const iconValues = await getIconValues(iconKeys)
+      console.log('所有图标值:', iconValues)
+
+      return iconValues
+    }
+  } catch (error) {
+    console.error('获取图标失败:', error)
+  }
+}
+
+// 批量获取图标值
+const getIconValues = async (iconKeys) => {
+  const iconValues = []
+
+  // 方法1:串行请求(推荐,避免并发过多)
+  for (const key of iconKeys) {
+    try {
+      const iconData = await getIsSystemAttributeByKey(key.trim())
+      if (iconData && iconData.sysAttrValue) {
+        iconValues.push({
+          id: iconData.id,
+          key: key.trim(),
+          value: iconData.sysAttrValue,
+          name: iconData.sysAttrName || key.trim()
+        })
+      }
+    } catch (error) {
+      console.error(`获取图标 ${key} 失败:`, error)
+    }
+  }
+
+  return iconValues
+}
+// 初始化如果添加过步骤需要回显出来
+const initTableData = async () => {
+  try {
+    if (props.modeId || route.query.id) {
+      const data = await getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: route.query.id || props.modeId
+      })
+
+      if (Array.isArray(data.list)) {
+        // 按 stepIndex 从小到大排序
+        tableData.value = data.list.sort((a, b) => {
+          const aIndex = a.stepIndex || 0
+          const bIndex = b.stepIndex || 0
+          return aIndex - bIndex
+        })
+
+        console.log(
+          '初始化数据完成,已按 stepIndex 排序:',
+          tableData.value.map((row) => row.stepIndex)
+        )
+      } else {
+        tableData.value = []
+      }
+    }
+  } catch (error) {
+    console.error('初始化表格数据失败:', error)
+    ElMessage.error('加载步骤数据失败')
+    tableData.value = []
+  }
+}
+// 确认角色清空操作
+const handleRoleClear = (row) => {
+  row.confirmRoleCode = ''
+  row.confirmUser = ''
+}
+const DisableCheckView = ref()
+// 组件挂载时初始化
+onMounted(() => {
+  if (route.query.type == 'view') {
+    DisableCheckView.value = true
+  } else {
+    DisableCheckView.value = false
+  }
+  initRowDrop() //行拖拽数据更新
+  loadIconOptions() //获取步骤图标信息
+  InitRole() //初始化角色数据
+  initTableData() //初始化步骤表格里的数据
+})
+// 监听 confirmRoleCode 的变化
+watch(
+  () => tableData.value,
+  async (newTableData) => {
+    if (newTableData) {
+      const data = await UserApi.getUserPage({ pageNo: 1, pageSize: -1 })
+      console.log(data, 'user')
+      UserOptions.value = data.list.map((row) => {
+        return {
+          label: row.nickname,
+          value: row.id
+        }
+      })
+    }
+  },
+  { immediate: true, deep: true }
+)
+</script>
+
+<style scoped lang="scss">
+.workflow-page {
+  padding: 20px;
+
+  .button-group {
+    margin-bottom: 20px;
+    display: flex;
+    gap: 10px;
+
+    .el-button {
+      display: flex;
+      align-items: center;
+      gap: 5px;
+    }
+  }
+
+  .table-container {
+    .el-table {
+      .el-input,
+      .el-select,
+      .el-input-number {
+        width: 100%;
+      }
+
+      .el-textarea {
+        .el-textarea__inner {
+          resize: vertical;
+        }
+      }
+    }
+  }
+}
+</style>

+ 1083 - 0
src/views/sopm/sop/ModeView/WorkFlowView.vue

@@ -0,0 +1,1083 @@
+<template>
+  <div style="padding: 20px">
+    <!-- 顶部按钮栏 -->
+    <div style="margin-bottom: 10px; display: flex; gap: 10px">
+      <el-button type="primary" @click="handleAddNode" :disabled="DisableCheckView||props.isPreset">添加</el-button>
+      <el-button
+        type="success"
+        @click="addFromTemplate"
+        :disabled="DisableCheckView || !props.enableStepTable||props.isPreset"
+      >
+        <el-icon>
+          <DocumentAdd />
+        </el-icon>
+        从模板添加
+      </el-button>
+      <el-button
+        type="danger"
+        @click="handleDeleteNode"
+        :disabled="!selectedNodeId || DisableCheckView||props.isPreset"
+      >
+        删除
+      </el-button>
+    </div>
+
+    <!-- VueFlow 主画布 -->
+    <VueFlow style="width: 100%; height: 600px">
+      <template #node-default="{ id, data }">
+        <div class="custom-node">
+          <div class="node-content">
+            <!-- 图标显示 -->
+            <div style="font-size: 30px">
+              <img
+                v-if="data.stepIcon && data.stepIcon.startsWith('http')"
+                :src="data.stepIcon"
+                :alt="data.stepTitleShort"
+                style="width: 40px; height: 40px; object-fit: contain"
+              />
+              <span v-else>{{ data.stepIcon || '📋' }}</span>
+            </div>
+            <div style="font-weight: bold; font-size: 14px">
+              {{ data.stepTitleShort || '无标题' }}
+            </div>
+            <div style="font-size: 25px">
+              {{ String.fromCharCode(9311 + (data.stepIndex || 1)) }}
+            </div>
+          </div>
+
+          <!-- 四个连接点 -->
+          <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top" />
+          <Handle
+            type="source"
+            position="bottom"
+            :id="`${id}-bottom`"
+            class="handle handle-bottom"
+          />
+          <Handle type="target" position="left" :id="`${id}-left`" class="handle handle-left" />
+          <Handle type="source" position="right" :id="`${id}-right`" class="handle handle-right" />
+        </div>
+      </template>
+    </VueFlow>
+
+    <!-- 节点表单内容 -->
+    <div
+      v-if="showForm && selectedNodeId"
+      style="margin-top: 20px; padding: 20px; border: 1px solid #ccc"
+    >
+      <h3>节点配置({{ selectedNodeId }})</h3>
+      <div style="display: flex; gap: 20px">
+        <el-form label-width="155px">
+          <el-form-item label="图标">
+            <el-popover placement="bottom" trigger="click" width="230">
+              <!-- 图标选择面板 -->
+              <div style="display: flex; flex-wrap: wrap; gap: 8px">
+                <div
+                  v-for="icon in iconOptions"
+                  :key="icon.value"
+                  @click="() => selectIcon(formData, icon.value)"
+                  :style="{
+                    border:
+                      formData.stepIcon === icon.value ? '2px solid #409EFF' : '1px solid #ccc',
+                    borderRadius: '4px',
+                    padding: '2px',
+                    cursor: 'pointer'
+                  }"
+                >
+                  <img
+                    :src="icon.value"
+                    :alt="icon.name"
+                    style="width: 28px; height: 28px; object-fit: contain"
+                  />
+                </div>
+              </div>
+
+              <!-- 触发元素:当前图标或按钮 -->
+              <template #reference>
+                <div style="cursor: pointer; display: inline-block">
+                  <img
+                    v-if="formData.stepIcon"
+                    :src="formData.stepIcon"
+                    style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid #ccc"
+                  />
+                  <el-button v-else type="primary" size="small">选择图标</el-button>
+                </div>
+              </template>
+            </el-popover>
+          </el-form-item>
+
+          <el-form-item label="标题">
+            <el-input
+              :disabled="DisableCheckView"
+              v-model="formData.stepTitle"
+              placeholder="标题"
+              size="small"
+              @blur="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="标题缩写">
+            <el-input
+              :disabled="DisableCheckView"
+              v-model="formData.stepTitleShort"
+              placeholder="标题缩写"
+              size="small"
+              @blur="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="确认方式">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="formData.confirmType"
+              placeholder="请选择确认方式"
+              @change="handleFormChange"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STEP_CONFIRMTYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="确认角色" v-if="formData.confirmType == '2'">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="formData.confirmRoleCode"
+              placeholder="请选择确认角色"
+              @change="handleFormChange"
+              filterable
+              clearable
+              @clear="handleRoleClear(formData)"
+            >
+              <el-option
+                v-for="dict in RoleOptions"
+                :key="dict.code"
+                :label="dict.name"
+                :value="dict.code"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="确认人员" v-if="formData.confirmType == '2'">
+            <el-select
+              :disabled="DisableCheckView"
+              v-model="formData.confirmUser"
+              placeholder="请选择确认人员"
+              @change="handleFormChange"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="dict in UserOptions"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="取消作业">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableCancelJob"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="设置锁定人">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableSetLocker"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="设置共锁人">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableSetColocker"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="添加共锁人">
+            <el-checkbox
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.enableAddColocker"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="添加共锁人后跳转步骤">
+            <el-input-number
+              :disabled="DisableCheckView||props.isPreset"
+              v-model="formData.gotoStepAfterAddingColocker"
+              :min="1"
+              size="small"
+              placeholder="步骤号"
+              @change="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="减少共锁人">
+            <el-checkbox
+              v-model="formData.enableReduceColocker"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="上锁">
+            <el-checkbox
+              v-model="formData.enableLock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="共锁">
+            <el-checkbox
+              v-model="formData.enableColock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="解除共锁">
+            <el-checkbox
+              v-model="formData.enableReleaseColock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="解锁">
+            <el-checkbox
+              v-model="formData.enableUnlock"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+
+          <el-form-item label="结束作业">
+            <el-checkbox
+              v-model="formData.enableEndJob"
+              @change="handleFormChange"
+              :disabled="DisableCheckView||props.isPreset"
+            />
+          </el-form-item>
+        </el-form>
+
+        <!-- 右侧富文本 -->
+        <div style="flex: 1">
+          <label style="font-weight: bold; display: block; margin-bottom: 8px">步骤操作说明</label>
+          <TinyMCE
+            :disabled="DisableCheckView"
+            v-model:value="formData.stepDescription"
+            :height="700"
+            placeholder="请输入内容..."
+            @change="handleFormChange"
+            @update:value="handleContentChange"
+          />
+        </div>
+      </div>
+    </div>
+    <!--    从模板添加-->
+    <TemplateAddDialog
+      v-model:visible="templateDialogVisible"
+      @select-template="handleTemplateSelect"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch, onMounted, nextTick } from 'vue'
+import { VueFlow, useVueFlow, Position, addEdge, Handle } from '@vue-flow/core'
+import '@vue-flow/core/dist/style.css'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useRoute } from 'vue-router'
+import TinyMCE from '@/components/TinyMCE/index.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
+import {
+  getWorkflowStepPage,
+  insertWorkflowStep,
+  updateWorkflowStep,
+  deleteWorkflowStepList
+} from '@/api/custonWorkflow/step'
+
+import { getRolePage } from '@/api/system/role'
+import { getRoleUser } from '@/api/system/user'
+import { DocumentAdd } from '@element-plus/icons-vue'
+import TemplateAddDialog from './TemplateAdd.vue'
+
+const route = useRoute()
+const {
+  addNodes,
+  addEdges,
+  onNodeClick,
+  onNodeContextMenu,
+  removeNodes,
+  findNode,
+  zoomTo,
+  updateNode,
+  onConnect
+} = useVueFlow()
+const props = defineProps({
+  enableStepTable: {
+    type: Boolean,
+    required: true
+  },
+  modeId: {
+    type: [String, Number],
+    default: null
+  },
+  isPreset:{
+    type: Boolean,
+    required: true
+  }
+
+})
+const selectedNodeId = ref<string | null>(null)
+const showForm = ref(false)
+const nodeIdCounter = ref(1)
+const nodes = ref([])
+const edges = ref([]) // 存储连接线
+const iconOptions = ref([])
+const RoleOptions = ref([])
+const UserOptions = ref([])
+
+// 默认数据结构
+const formData = reactive({
+  modeId: route.query.id || props.modeId,
+  stepTemplateId: undefined,
+  stepIndex: 0,
+  stepName: undefined,
+  stepTitle: undefined,
+  stepTitleShort: undefined,
+  stepDescription: undefined,
+  confirmType: undefined,
+  confirmRoleCode: undefined,
+  confirmUser: undefined,
+  enableCancelJob: undefined,
+  enableSetLocker: undefined,
+  enableSetColocker: undefined,
+  enableAddColocker: undefined,
+  gotoStepAfterAddingColocker: undefined,
+  enableReduceColocker: undefined,
+  enableLock: undefined,
+  enableColock: undefined,
+  enableReleaseColock: undefined,
+  enableUnlock: undefined,
+  enableEndJob: undefined,
+  id: undefined,
+  stepIcon: undefined
+})
+
+// 从模板添加
+const templateDialogVisible = ref(false)
+const addFromTemplate = () => {
+  templateDialogVisible.value = true
+}
+// 接收模板选择结果
+const handleTemplateSelect = (templateNodeData) => {
+  const { template } = templateNodeData
+
+  const id = `node-${nodeIdCounter.value++}`
+  const stepIndex = getNextAvailableIndex()
+
+  const newNode = {
+    id,
+    position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
+    width: 100,
+    height: 150,
+    data: {
+      modeId: route.query.id || props.modeId,
+      stepIcon: template.stepIcon || '',
+      stepTitleShort: template.stepTitleShort || template.stepName || '',
+      stepTitle: template.stepTitle || template.stepName || '',
+      stepDescription: template.stepDescription || '',
+      stepIndex: stepIndex,
+      index: stepIndex,
+      // 其他模板字段
+      confirmType: template.confirmType || 1,
+      confirmRoleCode: template.confirmRoleCode || '',
+      confirmUser: template.confirmUser || null,
+      enableCancelJob: template.enableCancelJob || false,
+      enableSetLocker: template.enableSetLocker || false,
+      enableSetColocker: template.enableSetColocker || false,
+      enableAddColocker: template.enableAddColocker || false,
+      gotoStepAfterAddingColocker: template.gotoStepAfterAddingColocker || null,
+      enableReduceColocker: template.enableReduceColocker || false,
+      enableLock: template.enableLock || false,
+      enableColock: template.enableColock || false,
+      enableReleaseColock: template.enableReleaseColock || false,
+      enableUnlock: template.enableUnlock || false,
+      enableEndJob: template.enableEndJob || false
+    },
+    style: {
+      width: '130px',
+      height: '180px',
+      borderRadius: '12px',
+      border: '1px solid #999',
+      textAlign: 'center',
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+      padding: '10px',
+      backgroundColor: '#fff'
+    },
+    draggable: true
+  }
+
+  addNodes(newNode)
+  nodes.value.push(newNode)
+
+  console.log('从模板创建的新节点:', newNode)
+}
+// 防抖函数
+const debounce = (func, wait) => {
+  let timeout
+  return function executedFunction(...args) {
+    const later = () => {
+      clearTimeout(timeout)
+      func(...args)
+    }
+    clearTimeout(timeout)
+    timeout = setTimeout(later, wait)
+  }
+}
+// 表格图标切换
+const selectIcon = (formData, iconUrl) => {
+  formData.stepIcon = iconUrl
+  saveFormData(formData) // 你已有的接口保存方法
+}
+
+// 初始化步骤数据并渲染节点
+const initTableData = async () => {
+  try {
+    if (props.modeId || route.query.id) {
+      const data = await getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: route.query.id || props.modeId
+      })
+
+      if (Array.isArray(data.list) && data.list.length > 0) {
+        // 按 stepIndex 从小到大排序
+        const sortedData = data.list.sort((a, b) => {
+          const aIndex = a.stepIndex || 0
+          const bIndex = b.stepIndex || 0
+          return aIndex - bIndex
+        })
+
+        console.log(
+          '初始化数据完成,已按 stepIndex 排序:',
+          sortedData.map((row) => row.stepIndex)
+        )
+
+        // 渲染节点
+        renderNodesFromData(sortedData)
+
+        // 渲染连接线
+        renderEdgesFromData(sortedData)
+      } else {
+        console.log('没有找到步骤数据')
+      }
+    }
+  } catch (error) {
+    console.error('初始化表格数据失败:', error)
+    ElMessage.error('加载步骤数据失败')
+  }
+}
+
+// 初始化数据渲染节点 - 修正版本
+const renderNodesFromData = (data) => {
+  // 清空现有节点
+  nodes.value = []
+
+  data.forEach((item, index) => {
+    const nodeId = `node-${item.id || Date.now() + index}`
+
+    const newNode = {
+      id: nodeId,
+      position: {
+        x: 100 + index * 200,
+        y: 100
+      },
+      width: 100,
+      height: 150,
+      data: {
+        stepIcon: item.stepIcon,
+        stepTitleShort: item.stepTitleShort,
+        stepIndex: item.stepIndex || index + 1, // 使用 stepIndex
+        index: item.stepIndex || index + 1, // 保持兼容性
+        // 保存完整的数据用于表单编辑
+        stepData: item
+      },
+      style: {
+        width: '130px',
+        height: '180px',
+        borderRadius: '12px',
+        border: '1px solid #999',
+        textAlign: 'center',
+        display: 'flex',
+        flexDirection: 'column',
+        alignItems: 'center',
+        justifyContent: 'space-between',
+        padding: '10px',
+        backgroundColor: '#fff'
+      },
+      draggable: true
+    }
+
+    addNodes(newNode)
+    nodes.value.push(newNode)
+  })
+}
+
+// 渲染连接线 - 新增函数
+const renderEdgesFromData = (data) => {
+  // 清空现有连接线
+  edges.value = []
+
+  // 根据 stepIndex 顺序创建连接线
+  for (let i = 0; i < data.length - 1; i++) {
+    const currentStep = data[i]
+    const nextStep = data[i + 1]
+
+    const sourceNodeId = `node-${currentStep.id}`
+    const targetNodeId = `node-${nextStep.id}`
+
+    // 创建连接线,从右侧连接到左侧
+    const edge = {
+      id: `edge-${currentStep.id}-${nextStep.id}`,
+      source: sourceNodeId,
+      target: targetNodeId,
+      sourceHandle: `${sourceNodeId}-right`, // 从右侧连接点出发
+      targetHandle: `${targetNodeId}-left`, // 连接到左侧连接点
+      type: 'smoothstep',
+      style: { stroke: '#333', strokeWidth: 2 },
+      markerEnd: {
+        type: 'arrowclosed',
+        width: 20,
+        height: 20,
+        color: '#333'
+      }
+    }
+
+    addEdges(edge)
+    edges.value.push(edge)
+
+    console.log('创建连接线:', edge)
+  }
+}
+
+// 获取下一个可用的索引 - 修正版本
+function getNextAvailableIndex() {
+  if (nodes.value.length === 0) {
+    return 1
+  }
+
+  // 使用 stepIndex 而不是 index
+  const usedIndexes = nodes.value.map((node) => node.data.stepIndex || 1)
+  let nextIndex = 1
+  while (usedIndexes.includes(nextIndex)) {
+    nextIndex++
+  }
+
+  return nextIndex
+}
+
+// 新增节点
+function handleAddNode() {
+  const id = `node-${nodeIdCounter.value++}`
+  const stepIndex = getNextAvailableIndex() // 获取下一个 stepIndex
+
+  const newNode = {
+    id,
+    position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
+    width: 100,
+    height: 150,
+    data: {
+      stepIcon: '',
+      stepTitleShort: '',
+      stepIndex: stepIndex, // 使用 stepIndex
+      index: stepIndex // 保持兼容性
+    },
+    style: {
+      width: '130px',
+      height: '180px',
+      borderRadius: '12px',
+      border: '1px solid #999',
+      textAlign: 'center',
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+      padding: '10px',
+      backgroundColor: '#fff'
+    },
+    draggable: true
+  }
+
+  addNodes(newNode)
+  nodes.value.push(newNode)
+
+  selectedNodeId.value = id
+  showForm.value = true
+
+  // 重置表单数据,并设置 stepIndex
+  resetFormData()
+  formData.stepIndex = stepIndex
+}
+
+// 删除操作
+function handleDeleteNode() {
+  if (selectedNodeId.value) {
+    // 获取要删除的节点数据
+    const nodeToDelete = nodes.value.find((node) => node.id === selectedNodeId.value)
+
+    if (nodeToDelete && nodeToDelete.data.stepData && nodeToDelete.data.stepData.id) {
+      // 显示确认对话框
+      ElMessageBox.confirm(
+        `确定要删除步骤"${nodeToDelete.data.stepTitleShort || '未命名'}"吗?`,
+        '确认删除',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      )
+        .then(() => {
+          // 用户确认删除
+          deleteWorkflowStepList([nodeToDelete.data.stepData.id])
+            .then(() => {
+              // 删除相关的连接线
+              const relatedEdges = edges.value.filter(
+                (edge) =>
+                  edge.source === selectedNodeId.value || edge.target === selectedNodeId.value
+              )
+
+              relatedEdges.forEach((edge) => {
+                edges.value = edges.value.filter((e) => e.id !== edge.id)
+              })
+
+              // 删除成功,从界面移除节点
+              removeNodes([selectedNodeId.value])
+
+              const index = nodes.value.findIndex((node) => node.id === selectedNodeId.value)
+              if (index !== -1) {
+                nodes.value.splice(index, 1)
+              }
+
+              selectedNodeId.value = null
+              showForm.value = false
+
+              setTimeout(() => {
+                reorderNodeIndexes()
+              }, 100)
+
+              ElMessage.success('删除成功')
+            })
+            .catch((error) => {
+              console.error('删除失败:', error)
+              ElMessage.error('删除失败,请稍后重试')
+            })
+        })
+        .catch(() => {
+          // 用户取消删除
+          ElMessage.info('已取消删除')
+        })
+    } else {
+      // 如果节点没有对应的数据库ID,直接从界面删除
+      removeNodes([selectedNodeId.value])
+
+      const index = nodes.value.findIndex((node) => node.id === selectedNodeId.value)
+      if (index !== -1) {
+        nodes.value.splice(index, 1)
+      }
+
+      selectedNodeId.value = null
+      showForm.value = false
+
+      setTimeout(() => {
+        reorderNodeIndexes()
+      }, 100)
+    }
+  }
+}
+
+// 重置表单数据
+const resetFormData = () => {
+  Object.assign(formData, {
+    modeId: route.query.id,
+    stepTemplateId: undefined,
+    stepIndex: 0,
+    stepName: undefined,
+    stepTitle: undefined,
+    stepTitleShort: undefined,
+    stepDescription: undefined,
+    confirmType: undefined,
+    confirmRoleCode: undefined,
+    confirmUser: undefined,
+    enableCancelJob: undefined,
+    enableSetLocker: undefined,
+    enableSetColocker: undefined,
+    enableAddColocker: undefined,
+    gotoStepAfterAddingColocker: undefined,
+    enableReduceColocker: undefined,
+    enableLock: undefined,
+    enableColock: undefined,
+    enableReleaseColock: undefined,
+    enableUnlock: undefined,
+    enableEndJob: undefined,
+    id: undefined,
+    stepIcon: undefined
+  })
+}
+
+// 表单变化处理(防抖) - 修正版本
+const handleFormChange = debounce(async () => {
+  if (selectedNodeId.value) {
+    // 更新节点显示
+    updateNode(selectedNodeId.value, (node) => {
+      node.data.stepIcon = formData.stepIcon
+      node.data.stepTitleShort = formData.stepTitleShort
+      node.data.stepIndex = formData.stepIndex // 同步 stepIndex
+    })
+
+    // 更新本地节点数组
+    const localNode = nodes.value.find((n) => n.id === selectedNodeId.value)
+    if (localNode) {
+      localNode.data.stepIcon = formData.stepIcon
+      localNode.data.stepTitleShort = formData.stepTitleShort
+      localNode.data.stepIndex = formData.stepIndex // 同步 stepIndex
+    }
+
+    // 保存数据
+    await saveFormData(formData)
+  }
+}, 1000)
+
+// 自动保存单行数据
+const saveFormData = async (row) => {
+  try {
+    if (!row) return
+    // 如果没有 id,则执行新增
+    if (!row.id) {
+      const res = await insertWorkflowStep(row)
+      if (res && res) {
+        row.id = res
+        console.log('新增步骤成功:', res)
+        ElMessage.success(`新增步骤成功`)
+      }
+    } else {
+      // 有 id 则执行更新
+      console.log('准备更新步骤:', row)
+
+      await updateWorkflowStep(row)
+      ElMessage.success(`更新步骤成功`)
+    }
+  } catch (error) {
+    console.error('保存步骤失败', error)
+    ElMessage.error('保存失败,请稍后重试')
+  }
+}
+
+// 节点点击处理 - 修正版本
+onNodeClick(({ node }) => {
+  if (selectedNodeId.value === node.id) {
+    showForm.value = false
+    selectedNodeId.value = null
+  } else {
+    selectedNodeId.value = node.id
+    showForm.value = true
+
+    // 更新表单数据
+    const nodeData = node.data
+    if (nodeData.stepData) {
+      // 如果有完整数据,使用完整数据
+      Object.assign(formData, nodeData.stepData)
+    } else {
+      // 否则使用节点显示数据
+      formData.stepIcon = nodeData.stepIcon || ''
+      formData.stepTitleShort = nodeData.stepTitleShort || ''
+      formData.stepIndex = nodeData.stepIndex || 1 // 同步 stepIndex
+    }
+  }
+})
+
+onNodeContextMenu(({ node, event }) => {
+  event.preventDefault()
+  selectedNodeId.value = node.id
+  showForm.value = true
+})
+
+// 获取当前所有节点并重新排序索引 - 修正版本
+function reorderNodeIndexes() {
+  console.log('重新排序前的节点:', nodes.value)
+
+  const sortedNodes = [...nodes.value].sort((a, b) => {
+    if (Math.abs(a.position.y - b.position.y) < 50) {
+      return a.position.x - b.position.x
+    }
+    return a.position.y - b.position.y
+  })
+
+  sortedNodes.forEach((node, arrayIndex) => {
+    const newStepIndex = arrayIndex + 1
+    updateNode(node.id, (nodeData) => {
+      nodeData.data.stepIndex = newStepIndex
+      nodeData.data.index = newStepIndex // 保持兼容性
+    })
+
+    const localNode = nodes.value.find((n) => n.id === node.id)
+    if (localNode) {
+      localNode.data.stepIndex = newStepIndex
+      localNode.data.index = newStepIndex // 保持兼容性
+    }
+  })
+
+  console.log('重新排序后的节点:', nodes.value)
+}
+
+// 角色获取
+const InitRole = async () => {
+  try {
+    const data = await getRolePage({ pageNo: 1, pageSize: -1 })
+    RoleOptions.value = data.list
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 人员获取
+const InitUser = async () => {
+  try {
+    const data = await getRoleUser(formData.confirmRoleCode)
+    UserOptions.value = data.map((item) => {
+      return {
+        label: item.nickname,
+        value: item.id
+      }
+    })
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+// 获取图标选项
+const loadIconOptions = async () => {
+  try {
+    const icons = await getIcons()
+    if (icons && icons.length > 0) {
+      iconOptions.value = icons
+      console.log('图标选项已加载:', iconOptions.value)
+    }
+  } catch (error) {
+    console.error('加载图标选项失败:', error)
+  }
+}
+
+// 获取步骤基础图标
+const getIcons = async () => {
+  try {
+    const sysAttrKey1 = 'icon.step.all'
+    const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
+    console.log(iconRes, '获取到的图标配置')
+
+    if (iconRes && iconRes.sysAttrValue) {
+      const iconKeys = iconRes.sysAttrValue.split(',')
+      console.log('图标键值列表:', iconKeys)
+
+      const iconValues = await getIconValues(iconKeys)
+      console.log('所有图标值:', iconValues)
+
+      return iconValues
+    }
+  } catch (error) {
+    console.error('获取图标失败:', error)
+  }
+}
+
+// 批量获取图标值
+const getIconValues = async (iconKeys) => {
+  const iconValues = []
+
+  for (const key of iconKeys) {
+    try {
+      const iconData = await getIsSystemAttributeByKey(key.trim())
+      if (iconData && iconData.sysAttrValue) {
+        iconValues.push({
+          id: iconData.id,
+          key: key.trim(),
+          value: iconData.sysAttrValue,
+          name: iconData.sysAttrName || key.trim()
+        })
+      }
+    } catch (error) {
+      console.error(`获取图标 ${key} 失败:`, error)
+    }
+  }
+
+  return iconValues
+}
+
+// 内容变化
+const handleContentChange = (content: string) => {
+  console.log('内容变化:', content)
+  handleFormChange() // 触发保存
+}
+
+// 节点连接线 - 修正版本
+onConnect((params) => {
+  console.log('连接参数:', params)
+
+  // 创建新的连接线,确保使用正确的 sourceHandle 和 targetHandle
+  const newEdge = {
+    id: `e-${params.source}-${params.target}`,
+    source: params.source,
+    target: params.target,
+    sourceHandle: params.sourceHandle, // 使用实际的 sourceHandle
+    targetHandle: params.targetHandle, // 使用实际的 targetHandle
+    type: 'smoothstep',
+    style: { stroke: '#333', strokeWidth: 2 },
+    markerEnd: {
+      type: 'arrowclosed',
+      width: 20,
+      height: 20,
+      color: '#333'
+    }
+  }
+
+  console.log('创建连接线:', newEdge)
+  addEdges(newEdge)
+  edges.value.push(newEdge)
+})
+
+const DisableCheckView = ref()
+// 组件挂载时初始化
+onMounted(async () => {
+  if (route.query.type == 'view') {
+    DisableCheckView.value = true
+  } else {
+    DisableCheckView.value = false
+  }
+
+  zoomTo(1.1)
+
+  // 并行执行初始化
+  await Promise.all([loadIconOptions(), InitRole(), initTableData()])
+})
+// 确认角色清空操作
+const handleRoleClear = (formData) => {
+  formData.confirmRoleCode = ''
+  formData.confirmUser = ''
+}
+// 监听 confirmRoleCode 的变化
+watch(
+  () => formData.confirmRoleCode,
+  async (newRoleCode) => {
+    if (newRoleCode) {
+      await InitUser(newRoleCode)
+    }
+  },
+  { immediate: true } // immediate: true 表示在组件挂载时立即执行一次
+)
+</script>
+
+<style scoped lang="scss">
+.custom-node {
+  position: relative;
+  width: 125px;
+  height: 180px;
+  background-color: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px;
+  box-sizing: border-box;
+}
+
+.node-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+}
+
+// 连接点样式
+.handle {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+  position: absolute;
+  z-index: 10;
+}
+
+.handle:hover {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+.handle-top {
+  top: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-bottom {
+  bottom: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-left {
+  left: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.handle-right {
+  right: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+// 全局样式,确保连接点可见
+:deep(.vue-flow__handle) {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+}
+
+:deep(.vue-flow__handle:hover) {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+// 连接线样式
+:deep(.vue-flow__edge-path) {
+  stroke: #333;
+  stroke-width: 2;
+}
+
+:deep(.vue-flow__edge) {
+  z-index: 1;
+}
+
+// 箭头样式
+:deep(.vue-flow__edge-marker) {
+  fill: #333;
+}
+</style>

+ 79 - 0
src/views/sopm/sop/SetModeStep.vue

@@ -0,0 +1,79 @@
+<template>
+<div>
+  <ContentWrap>
+    <div class="custom-tabs-container">
+      <div class="tab-header">
+        <span class="tab-title">流程设置</span>
+        <div class="set-btn" @click="goBack">
+          <img src="../../../assets/images/返回.png" alt=""/>
+          返回</div>
+      </div>
+      <div class="tab-content">
+        <el-radio-group v-model="tabPosition" class="mb-15px">
+          <el-radio-button label="first">表格视图</el-radio-button>
+          <el-radio-button label="second">流程视图</el-radio-button>
+        </el-radio-group>
+        <ContentWrap>
+          <TableView
+            v-if="tabPosition == 'first'"
+          />
+          <WorkflowView v-else  />
+        </ContentWrap>
+      </div>
+    </div>
+  </ContentWrap>
+</div>
+</template>
+<script setup lang="ts">
+import TableView from './ModeView/TableView.vue'
+import WorkflowView from './ModeView/WorkFlowView.vue'
+const router=useRouter()
+const tabPosition = ref('first')
+const goBack=()=>{
+  router.back()
+}
+</script>
+
+
+
+<style scoped lang="scss">
+.custom-tabs-container {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  margin-top: 20px;
+}
+
+.tab-header {
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #dcdfe6;
+  padding: 12px 20px;
+  border-radius: 4px 4px 0 0;
+}
+
+.tab-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #303133;
+}
+
+.set-btn {
+  width: 60px;
+  height: 30px;
+  border: 1px solid black;
+  border-radius: 6px;
+  text-align: center;
+  line-height: 30px;
+  float: right;
+  cursor: pointer;
+  img{
+    width: 14px;
+    height: 14px;
+  }
+}
+
+.tab-content {
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 0 0 4px 4px;
+}
+</style>

+ 13 - 0
src/views/sopm/sop/SetPoint.vue

@@ -0,0 +1,13 @@
+<script setup lang="ts">
+
+</script>
+
+<template>
+  <div>
+
+  </div>
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 13 - 0
src/views/sopm/sop/SetUser.vue

@@ -0,0 +1,13 @@
+<script setup lang="ts">
+
+</script>
+
+<template>
+<div>
+
+</div>
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 805 - 0
src/views/sopm/sop/UpdateSop.vue

@@ -0,0 +1,805 @@
+<template>
+  <div>
+    <!--    sop表单-->
+    <ContentWrap>
+      <el-collapse v-model="activeName" accordion>
+        <el-collapse-item name="1">
+          <template #title>
+            <div style="display: flex; align-items: center; gap: 8px">
+              <el-icon size="20" style="margin-left: 10px">
+                <InfoFilled />
+              </el-icon>
+              <span style="font-size: 18px">SOP修改</span>
+            </div>
+          </template>
+
+          <div style="padding-left: 20px">
+            <div>1、设置SOP的基本信息</div>
+            <div>2、确定流程模式信息</div>
+            <div>3、确定点位及锁定分组</div>
+            <div>4、确定共锁人列表(有共锁)</div>
+          </div>
+        </el-collapse-item>
+      </el-collapse>
+
+      <!-- 自定义边框容器 与sop表单 -->
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">基本信息</span>
+        </div>
+        <div class="tab-content">
+          <el-form
+            class="-mb-15px"
+            :model="SopForm"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-row>
+              <el-col :span="4">
+                <el-form-item label="SOP名称" prop="sopName">
+                  <el-input
+                    v-model="SopForm.sopName"
+                    placeholder="请输入sop名称"
+                    clearable
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="2">
+                <el-checkbox v-model="SopAutoName">自动生成</el-checkbox>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="SOP区域" prop="workstationId">
+                  <el-tree-select
+                    v-model="SopForm.workstationId"
+                    :data="workstationOption"
+                    :props="{ label: 'workstationName', value: 'id', children: 'children' }"
+                    placeholder="选择岗位"
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="工艺设备" prop="machineryId">
+                  <el-tree-select
+                    v-model="SopForm.machineryId"
+                    :data="machineryOptions"
+                    :props="{ label: 'machineryName', value: 'id', children: 'children' }"
+                    placeholder="选择设备/工艺"
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="SOP类型" prop="sopType">
+                  <el-select
+                    v-model="SopForm.sopType"
+                    placeholder="请选择SOP类型"
+                    clearable
+                    class="!w-240px"
+                    @change="handleSopTypeChange"
+                  >
+                    <el-option
+                      v-for="dict in getStrDictOptions(DICT_TYPE.SOP_TYPE)"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="dict.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-form-item label="流程模式" prop="modeId">
+                <el-select
+                  v-model="SopForm.modeId"
+                  placeholder="请选择流程模式"
+                  clearable
+                  class="!w-240px"
+                  @change="handleModeChange"
+                >
+                  <el-option
+                    v-for="dict in ModeOption"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-row>
+          </el-form>
+        </div>
+      </div>
+    </ContentWrap>
+    <!--流程步骤画布-->
+    <ContentWrap>
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">流程设置</span>
+          <div class="set-btn" @click="goSetting('SetModeStep',SopForm.id)">设置</div>
+        </div>
+        <div class="tab-content">
+          <!-- VueFlow 主画布 -->
+          <VueFlow style="width: 100%; height: 300px">
+            <template #node-default="{id, data }">
+              <div class="custom-node">
+                <div class="node-content">
+                  <!-- 图标显示 -->
+                  <div style="font-size: 30px">
+                    <img
+                      v-if="data.stepIcon && data.stepIcon.startsWith('http')"
+                      :src="data.stepIcon"
+                      :alt="data.stepTitleShort"
+                      style="width: 40px; height: 40px; object-fit: contain"
+                    />
+                    <span v-else>{{ data.stepIcon || '📋' }}</span>
+                  </div>
+                  <div style="font-weight: bold; font-size: 14px">
+                    {{ data.stepTitleShort || '无标题' }}
+                  </div>
+                  <div style="font-size: 25px">
+                    {{ String.fromCharCode(9311 + (data.stepIndex || 1)) }}
+                  </div>
+                </div>
+                <!-- 四个连接点 -->
+                <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top" />
+                <Handle
+                  type="source"
+                  position="bottom"
+                  :id="`${id}-bottom`"
+                  class="handle handle-bottom"
+                />
+                <Handle
+                  type="target"
+                  position="left"
+                  :id="`${id}-left`"
+                  class="handle handle-left"
+                />
+                <Handle
+                  type="source"
+                  position="right"
+                  :id="`${id}-right`"
+                  class="handle handle-right"
+                />
+              </div>
+            </template>
+          </VueFlow>
+        </div>
+      </div>
+    </ContentWrap>
+    <!--    点位设置 -->
+    <ContentWrap>
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">点位设置</span>
+          <div class="set-btn"  @click="goSetting('SetPoint',SopForm.id)">设置</div>
+        </div>
+        <div class="tab-content" style="height: 300px">
+          <div class="point_center_box">
+            <img src="../../../assets/images/添加.png" alt="" @click="goSetting('SetPoint',SopForm.id)"/>
+            <span style="color: red">*请添加需要进行隔离的点位</span>
+          </div>
+        </div>
+      </div>
+    </ContentWrap>
+    <!--    人员设置 -->
+    <ContentWrap>
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">人员设置</span>
+          <div class="set-btn" @click="goSetting('SetUser',SopForm.id)">设置</div>
+        </div>
+        <div class="tab-content" style="display: flex; height: 300px">
+          <div class="left_box">
+            <div class="tab-header">
+              <span class="tab-title">锁定人</span>
+            </div>
+            <div class="point_center_box">
+              <img src="../../../assets/images/添加.png" alt="" @click="goSetting('SetUser',SopForm.id)"/>
+              <span>请添加参与锁定的人员</span>
+            </div>
+          </div>
+          <div class="right_box">
+            <div class="tab-header">
+              <span class="tab-title">共锁人</span>
+            </div>
+            <div class="point_center_box">
+              <img src="../../../assets/images/添加.png" alt="" @click="goSetting('SetUser',SopForm.id)"/>
+              <span>请添加参与共锁的人员</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </ContentWrap>
+    <div class="bottom-btn">
+      <el-button @click="submit">
+        <el-icon>
+          <Check />
+        </el-icon>
+        确 定
+      </el-button>
+
+      <el-button @click="cancel">
+        <el-icon>
+          <Close />
+        </el-icon>
+        取 消
+      </el-button>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { Check, Close } from '@element-plus/icons-vue'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { InfoFilled } from '@element-plus/icons-vue'
+import * as TechnologyApi from '@/api/dv/technology'
+import * as MarsDeptApi from '@/api/system/marsdept/index'
+import * as ModeApi from '@/api/custonWorkflow/index'
+import * as ModeStepApi from '@/api/custonWorkflow/step'
+import * as SopApi from '@/api/sop/index'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+import { handleTree } from '@/utils/tree'
+import { ref } from 'vue'
+import {Handle, useVueFlow, VueFlow} from '@vue-flow/core'
+
+const SopForm = reactive({
+  createTime: null,
+  id: null,
+  machineryId: null,
+  machineryName: null,
+  modeId: null,
+  pointCount: null,
+  remark: null,
+  sopCode: null,
+  sopContent: null,
+  sopGroupList: null,
+  sopIndex: null,
+  sopName: null,
+  sopPointsList: null,
+  sopStatus: null,
+  sopStepList: null,
+  sopType: null,
+  sopTypeName: null,
+  sopUserList: null,
+  workstationId: null,
+  workstationName: null
+})
+const SopAutoName = ref(false)
+const activeName = ref('0')
+const machineryOptions = ref()
+const workstationOption = ref()
+const ModeOption = ref()
+
+const nodes = ref([]) //储存节点
+const edges = ref([]) // 存储连接线
+// 创建查找映射
+const workstationMap = new Map()
+const machineryMap = new Map()
+const router = useRouter()
+const route = useRoute()
+// 添加数据修改标记
+const hasUnsavedChanges = ref(false)
+const { addNodes, addEdges, setEdges, setNodes } = useVueFlow()
+//跳转设置对应页面
+const goSetting = (type,sopId) => {
+  if(type=='SetModeStep'){
+    router.push({
+      name:'SetModeStep',
+      query:{
+        sopId:sopId,
+      }
+    })
+  }else if(type=='SetPoint'){
+    router.push({
+      name:'SetPoint',
+      query:{
+        sopId:sopId,
+      }
+    })
+  }else if(type=='SetUser'){
+    router.push({
+      name:'SetUser',
+      query:{
+        sopId:sopId,
+      }
+    })
+  }
+}
+// 获取基本信息
+const getOtherList = async () => {
+  try {
+    // 获取sop信息
+    const SopData = await SopApi.selectSopById(route.query.id)
+    console.log(SopData, 'aaa')
+    Object.assign(SopForm, SopData)
+    // 获取岗位数据
+    const deptRes = await MarsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 })
+    workstationOption.value = handleTree(deptRes.list, 'id', 'parentId')
+    buildWorkstationMap(deptRes.list)
+
+    // 获取设备/工艺数据
+    const techRes = await TechnologyApi.listTechnology({ pageNo: 1, pageSize: -1 })
+    const data = techRes.list.filter((item) => item.machineryType == '工艺')
+    machineryOptions.value = handleTree(data, 'id', 'parentId')
+    buildMachineryMap(data)
+
+    // 获取工作流模式数据
+    const modeRes = await ModeApi.getWorkflowModePage({ pageNo: 1, pageSize: -1 })
+    ModeOption.value = modeRes.list.map((item) => ({
+      label: item.modeName,
+      value: item.id
+    }))
+
+    console.log('数据加载完成')
+  } catch (error) {
+    console.error('获取数据失败:', error)
+    ElMessage.error('获取数据失败')
+  }
+}
+
+// 流程模式切换
+const handleModeChange = async (value) => {
+  console.log(value, 'value')
+
+  SopForm.modeId = value
+  // 清空画布
+  await clearCanvasProperly()
+  if (SopForm.modeId) {
+    const data = await ModeStepApi.getWorkflowStepPage({
+      pageNo: 1,
+      pageSize: -1,
+      modeId: SopForm.modeId
+    })
+
+    if (Array.isArray(data.list) && data.list.length > 0) {
+      // 按 stepIndex 从小到大排序
+      const sortedData = data.list.sort((a, b) => {
+        const aIndex = a.stepIndex || 0
+        const bIndex = b.stepIndex || 0
+        return aIndex - bIndex
+      })
+      // 渲染节点
+      renderNodesFromData(sortedData)
+      // 渲染连接线
+      renderEdgesFromData(sortedData)
+    }
+  }
+}
+// 独立的清空画布函数
+const clearCanvasProperly = async () => {
+  setNodes([])
+  setEdges([])
+  nodes.value = [] // 同步响应式数据(如果有自定义 nodes)
+  edges.value = []
+}
+// 初始化数据渲染节点 - 修正版本
+const renderNodesFromData = (data) => {
+  // 清空现有节点
+  nodes.value = []
+
+  data.forEach((item, index) => {
+    const nodeId = `node-${item.id || Date.now() + index}`
+
+    const newNode = {
+      id: nodeId,
+      position: {
+        x: 100 + index * 200,
+        y: 100
+      },
+      width: 100,
+      height: 150,
+      data: {
+        stepIcon: item.stepIcon,
+        stepTitleShort: item.stepTitleShort,
+        stepIndex: item.stepIndex || index + 1, // 使用 stepIndex
+        index: item.stepIndex || index + 1, // 保持兼容性
+        // 保存完整的数据用于表单编辑
+        stepData: item
+      },
+      style: {
+        width: '130px',
+        height: '180px',
+        borderRadius: '12px',
+        border: '1px solid #999',
+        textAlign: 'center',
+        display: 'flex',
+        flexDirection: 'column',
+        alignItems: 'center',
+        justifyContent: 'space-between',
+        padding: '10px',
+        backgroundColor: '#fff'
+      },
+      draggable: true
+    }
+
+    addNodes(newNode)
+    nodes.value.push(newNode)
+  })
+}
+
+// 渲染连接线 - 新增函数
+const renderEdgesFromData = (data) => {
+  // 清空现有连接线
+  edges.value = []
+
+  // 根据 stepIndex 顺序创建连接线
+  for (let i = 0; i < data.length - 1; i++) {
+    const currentStep = data[i]
+    const nextStep = data[i + 1]
+
+    const sourceNodeId = `node-${currentStep.id}`
+    const targetNodeId = `node-${nextStep.id}`
+
+    // 创建连接线,从右侧连接到左侧
+    const edge = {
+      id: `edge-${currentStep.id}-${nextStep.id}`,
+      source: sourceNodeId,
+      target: targetNodeId,
+      sourceHandle: `${sourceNodeId}-right`, // 从右侧连接点出发
+      targetHandle: `${targetNodeId}-left`, // 连接到左侧连接点
+      type: 'smoothstep',
+      style: { stroke: '#333', strokeWidth: 2 },
+      markerEnd: {
+        type: 'arrowclosed',
+        width: 20,
+        height: 20,
+        color: '#333'
+      }
+    }
+
+    addEdges(edge)
+    edges.value.push(edge)
+
+    console.log('创建连接线:', edge)
+  }
+}
+// 监听表单数据变化
+watch(
+  () => SopForm,
+  () => {
+    hasUnsavedChanges.value = true
+    handleModeChange(SopForm.modeId)
+
+  },
+  { deep: true }
+)
+// 保存成功后重置标记
+const submit = async () => {
+  try {
+    let data
+    let successMessage
+
+    if (SopForm.id) {
+      // 修改操作
+      data = await SopApi.updateSop(SopForm)
+      successMessage = t('common.updateSuccess')
+
+      if (data) {
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+      }
+    } else {
+      // 新增操作
+      data = await SopApi.insertSop(SopForm)
+      successMessage = t('common.createSuccess')
+
+      if (data) {
+        // 新增成功后,获取完整数据
+        try {
+          const selectData = await SopApi.selectSopById(data)
+          if (selectData) {
+            // 正确更新 ref 的值
+            SopForm = { ...SopForm, ...selectData }
+          }
+        } catch (selectError) {
+          console.warn('获取详情失败,但不影响保存:', selectError)
+          // 即使获取详情失败,也设置 id
+          SopForm.id = data
+        }
+
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+        Visible.value = true
+      }
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败')
+  }
+}
+
+// 取消操作
+const cancel = async () => {
+  if (hasUnsavedChanges.value) {
+    try {
+      await ElMessageBox.confirm('当前页面有未保存的更改,确定要离开吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+      // 用户确认离开
+      router.push('/sopm/sop')
+    } catch {
+      // 用户取消离开
+      console.log('用户取消离开')
+    }
+  } else {
+    // 没有未保存的更改,直接离开
+    router.push('/sopm/sop')
+  }
+}
+
+// 初始化
+onMounted(() => {
+  getOtherList()
+
+})
+
+// 构建岗位查找映射
+const buildWorkstationMap = (list) => {
+  const buildMap = (items) => {
+    for (const item of items) {
+      workstationMap.set(item.id, item.workstationName)
+      if (item.children && item.children.length > 0) {
+        buildMap(item.children)
+      }
+    }
+  }
+  buildMap(list)
+}
+
+// 构建工艺查找映射
+const buildMachineryMap = (list) => {
+  const buildMap = (items) => {
+    for (const item of items) {
+      machineryMap.set(item.id, item.machineryName || item.name)
+      if (item.children && item.children.length > 0) {
+        buildMap(item.children)
+      }
+    }
+  }
+  buildMap(list)
+}
+//SopType改变函数
+const handleSopTypeChange = (value) => {
+  SopForm.sopType = value
+}
+// 使用 watchEffect 更简洁
+watchEffect(() => {
+  const { workstationId, machineryId, sopType } = SopForm
+  const autoName = SopAutoName.value
+
+  if (autoName && workstationId && machineryId && sopType) {
+    generateSopName()
+  }
+})
+// 监听 workstationId 变化
+watchEffect(async () => {
+  const newWorkstationId = SopForm.workstationId
+  if (newWorkstationId) {
+    // console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
+    SopForm.machineryId = null
+    await getMachineryData(newWorkstationId)
+  }
+})
+
+// 获取工艺数据的函数
+const getMachineryData = async (workstationId) => {
+  try {
+    const techRes = await TechnologyApi.listTechnology({
+      pageNo: 1,
+      pageSize: -1,
+      workstationId: workstationId // 传递岗位ID参数
+    })
+
+    const data = techRes.list.filter((item) => item.machineryType == '工艺')
+    machineryOptions.value = handleTree(data, 'id', 'parentId')
+  } catch (error) {
+    console.error('获取工艺数据失败:', error)
+    ElMessage.error('获取工艺数据失败')
+  }
+}
+// 生成 SOP 名称
+const generateSopName = () => {
+  try {
+    const workstationName = workstationMap.get(SopForm.workstationId)
+    const machineryName = machineryMap.get(SopForm.machineryId)
+    const sopTypeName = getSopTypeName(SopForm.sopType)
+    console.log(workstationName, machineryName, sopTypeName, 'bbbbb')
+    SopForm.sopName = `${workstationName}-${machineryName}-${sopTypeName}`
+
+    console.log('自动生成的 SOP 名称:', SopForm.sopName)
+  } catch (error) {
+    console.error('生成 SOP 名称失败:', error)
+  }
+}
+
+// 获取 SOP 类型名称
+const getSopTypeName = (sopType) => {
+  const sopTypeOptions = getStrDictOptions(DICT_TYPE.SOP_TYPE)
+  const typeOption = sopTypeOptions.find((option) => option.value === sopType)
+  return typeOption ? typeOption.label : '未知类型'
+}
+</script>
+
+<style scoped lang="scss">
+.custom-tabs-container {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  margin-top: 20px;
+}
+
+.tab-header {
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #dcdfe6;
+  padding: 12px 20px;
+  border-radius: 4px 4px 0 0;
+}
+
+.tab-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #303133;
+}
+
+.set-btn {
+  width: 60px;
+  height: 30px;
+  border: 1px solid black;
+  border-radius: 6px;
+  text-align: center;
+  line-height: 30px;
+  float: right;
+  cursor: pointer;
+}
+
+.tab-content {
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 0 0 4px 4px;
+}
+
+.point_center_box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+
+  img {
+    width: 80px;
+    height: 80px;
+  }
+}
+
+.left_box {
+  width: 500px;
+  margin-right: 10px;
+  display: flex;
+  flex-direction: column;
+
+  img {
+    width: 80px;
+    height: 80px;
+  }
+}
+
+.right_box {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+
+  img {
+    width: 80px;
+    height: 80px;
+  }
+}
+
+.bottom-btn {
+  width: 100%;
+  height: 40px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.custom-node {
+  position: relative;
+  width: 125px;
+  height: 180px;
+  background-color: #fff;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px;
+  box-sizing: border-box;
+}
+
+.node-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+}
+
+//连接点样式
+
+.handle {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+  position: absolute;
+  z-index: 10;
+}
+
+.handle:hover {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+.handle-top {
+  top: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-bottom {
+  bottom: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.handle-left {
+  left: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.handle-right {
+  right: -8px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+//连接点全局样式
+:deep(.vue-flow__handle) {
+  width: 12px;
+  height: 12px;
+  background-color: #1a192b;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+}
+
+:deep(.vue-flow__handle:hover) {
+  background-color: #555;
+  transform: scale(1.2);
+}
+
+//连接线样式
+:deep(.vue-flow__edge-path) {
+  stroke: #333;
+  stroke-width: 2;
+}
+
+:deep(.vue-flow__edge) {
+  z-index: 1;
+}
+
+// 箭头样式
+:deep(.vue-flow__edge-marker) {
+  fill: #333;
+}
+</style>

+ 191 - 0
src/views/sopm/sop/index.vue

@@ -0,0 +1,191 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="sop名称" prop="sopName">
+        <el-input
+          v-model="queryParams.sopName"
+          placeholder="请输入sop名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="sop类型" prop="sopType">
+        <el-select
+          v-model="queryParams.sopType"
+          placeholder="请选择sop类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SOP_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['iscs:sop:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />
+          新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="sopList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" prop="id"  align="center" />
+      <el-table-column label="sop名称" prop="sopName"/>
+      <el-table-column label="sop类型" prop="sopTypeName" />
+      <el-table-column label="所属区域" prop="workstationName" :show-overflow-tooltip="true" />
+      <el-table-column label="设备/工艺" prop="machineryName"  align="center" />
+<!--      <el-table-column label="sop内容" prop="sopContent"  />-->
+<!--      <el-table-column label="sop状态" prop="sopStatus"  align="center" />-->
+<!--      <el-table-column label="sop权重序号" prop="sopIndex" />-->
+<!--      <el-table-column label="备注" prop="remark"  align="center" />-->
+      <el-table-column label="操作" align="center" >
+        <template #default="{ row }">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', row.id)"
+            v-hasPermi="['iscs:sop:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(row.id)"
+            v-hasPermi="['iscs:sop:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页组件 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import * as SopApi from '@/api/sop'
+import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
+
+const router = useRouter()
+
+defineOptions({ name: 'SopManagement' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const sopList = ref([]) // 列表的数据
+const total = ref(0) // 总条数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  sopName: undefined,
+  sopType: undefined,
+})
+const queryFormRef = ref() // 搜索的表单
+const ids = ref() // 选中的ID数组
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map((item) => item.id)
+}
+
+/** 查询模式列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SopApi.getSopPage(queryParams)
+    sopList.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.pageNo = 1
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改/查看操作 */
+const openForm = (type: string, id?: number) => {
+  if (type === 'create') {
+    router.push({
+      name: 'CreateSop',
+      query: { id: id, type: 'create' }
+    })
+  } else if (type === 'update') {
+    router.push({
+      name: 'UpdateSop',
+      query: { id: id, type: 'update' }
+    })
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await SopApi.deleteSopList(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>
+<style scoped>
+
+</style>

+ 2 - 7
src/views/statisticians/LockerOne/LockerChange.vue

@@ -5,11 +5,9 @@
 </template>
 
 <script lang="ts" setup>
-<<<<<<< HEAD
+
 import { getMaterialsChangeStatistics } from '@/api/material/statistics/index'
-=======
 import * as StatisticsApi from '@/api/material/statistics/index'
->>>>>>> f00047ed1956f2985366fa9347c83b2a9520744f
 import * as echarts from 'echarts'
 
 defineOptions({ name: 'LockerChange' })
@@ -49,12 +47,9 @@ watch(
 /** 获取数据 */
 const getList = async () => {
   try {
-<<<<<<< HEAD
+
     const response = await getMaterialsChangeStatistics(queryParams)
     console.log(response,'数据格式')
-=======
-    const response = await StatisticsApi.getMaterialsChangeStatistics(queryParams)
->>>>>>> f00047ed1956f2985366fa9347c83b2a9520744f
     cabinetData.value = response
     renderChart()
   } catch (error) {

+ 3 - 3
src/views/system/marsdept/index.vue

@@ -172,8 +172,8 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  if (route.query.userId) {
-    queryParams.userId = route.query.userId
+  if (route.query.id) {
+    queryParams.userId = route.query.id
   } else {
     queryParams.userId = undefined
   }
@@ -208,7 +208,7 @@ const handleLookWorkStation = (row: any) => {
 }
 
 // 监听路由参数变化 - 移到最后,确保所有函数都已定义
-watch(() => route.query.userId, (val) => {
+watch(() => route.query.id, (val) => {
   queryParams.userId = val
   getList()
 }, { immediate: true })

+ 275 - 0
src/views/system/user/FaceOrFingerForm.vue

@@ -0,0 +1,275 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px" style="height: 60vh">
+    <div class="form-header">
+      <el-button
+        v-no-more-click
+        type="danger"
+        plain
+        pageSize="small"
+        :disabled="!hasSelection"
+        @click="handleDeleteFaceOrFinger"
+        v-hasPermi="['system:user:delete']"
+      >
+        <Icon icon="ep:delete" />删除
+      </el-button>
+    </div>
+
+    <el-table
+      :data="tableData"
+      height="450"
+      @selection-change="handleSelectionChange"
+      v-loading="formLoading"
+      border
+      stripe
+    >
+      <el-table-column type="selection" width="50" align="center" />
+      <el-table-column label="序号" width="80" align="center" prop="orderNum"/>
+      <el-table-column
+        :label="dialogTitle === '人员指纹数据' ? '指纹' : '人脸'"
+        align="center"
+        prop="imageUrl"
+      >
+        <template #default="{ row }">
+          <div class="img-box">
+            <el-image
+              style="width: 60px; height: 60px;"
+              :preview-teleported="true"
+              class="images"
+              :hide-on-click-modal="true"
+              :src="row.imageUrl"
+              :zoom-rate="1.2"
+              :preview-src-list="[row.imageUrl]"
+              :initial-index="1"
+              fit="cover"
+            />
+            <el-icon class="eye-icon"><ZoomIn /></el-icon>
+          </div>
+        </template>
+      </el-table-column>
+
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+
+      <el-table-column
+        label="操作"
+        align="center"
+        width="120"
+        class-name="small-padding fixed-width"
+      >
+        <template #default="{ row }">
+          <el-button
+            v-no-more-click
+            pageSize="small"
+            type="text"
+            icon="Delete"
+            @click="handleDeleteFaceOrFinger(row)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+
+    </el-table>
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getFaceOrFingerList"
+    />
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { ZoomIn } from '@element-plus/icons-vue'
+import { ElMessageBox } from 'element-plus'
+import { dateFormatter } from '@/utils/formatTime'
+import { getUserType, deleteUserFaceOrFinger } from '@/api/system/user'
+
+defineOptions({ name: 'FaceOrFingerForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+// 定义接口类型
+interface CharacteristicData {
+  pageNo: number
+  pageSize: number
+  userId: number
+  type: string
+}
+
+interface CharacteristicResponse {
+  data: {
+    records: CharacteristicItem[]
+    total: number
+  }
+}
+
+interface CharacteristicItem {
+  id: number
+  imageUrl: string
+  userId: number
+  type: string
+  createTime: string
+  [key: string]: any
+}
+
+interface UserRow {
+  userId: number
+  [key: string]: any
+}
+
+// 定义事件
+const emit = defineEmits(['success'])
+
+// 响应式数据
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中
+const tableData = ref<CharacteristicItem[]>([])
+const selectedRows = ref<CharacteristicItem[]>([])
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+})
+const userId = ref<number>()
+const userType = ref<string>()
+
+// 计算属性
+const hasSelection = computed(() => selectedRows.value.length > 0)
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number,row?:any) => {
+  dialogVisible.value = true
+
+  if (type === 'finger') {
+    dialogTitle.value = '人员指纹数据'
+  } else if (type === 'face') {
+    dialogTitle.value = '人员面部数据'
+  }
+
+  userId.value = row.id
+  userType.value = type === 'finger' ? '1' : '2'
+
+  // 重置分页
+  queryParams.pageNo = 1
+  total.value = 0
+
+  await getFaceOrFingerList()
+}
+
+// 获取指纹或人脸列表
+const getFaceOrFingerList = async () => {
+  if (!userId.value || !userType.value) return
+
+  formLoading.value = true
+  try {
+    const data: CharacteristicData = {
+      pageNo: queryParams.pageNo,
+      pageSize: queryParams.pageSize,
+      userId: userId.value,
+      type: userType.value
+    }
+
+    const response: CharacteristicResponse = await getUserType(data)
+    tableData.value = response.list
+    total.value = response.total
+  } catch (error) {
+    console.error('获取数据失败:', error)
+    message.error('获取数据失败')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+// 选择变化处理
+const handleSelectionChange = (selection: CharacteristicItem[]) => {
+  selectedRows.value = selection
+}
+
+// 删除指纹或人脸
+const handleDeleteFaceOrFinger = async (row?: CharacteristicItem) => {
+  const itemsToDelete = row ? [row] : selectedRows.value
+
+  if (itemsToDelete.length === 0) {
+    message.warning('请选择要删除的数据')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除选中的 ${itemsToDelete.length} 条数据吗?`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+
+    // 执行删除操作
+    const deletePromises = itemsToDelete.map(item =>
+      deleteUserFaceOrFinger(item.id)
+    )
+
+    await Promise.all(deletePromises)
+
+    message.success(t('common.delSuccess'))
+    await getFaceOrFingerList() // 刷新列表
+    emit('success') // 通知父组件刷新
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除失败:', error)
+      message.error('删除失败')
+    }
+  }
+}
+
+
+// 暴露方法给父组件
+defineExpose({ open })
+</script>
+
+<style scoped lang="scss">
+.form-header {
+  margin-bottom: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.img-box {
+  position: relative;
+  display: inline-block;
+
+  .eye-icon {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    color: #fff;
+    font-pageSize: 16px;
+    opacity: 0;
+    transition: opacity 0.3s;
+    cursor: pointer;
+    background: rgba(0, 0, 0, 0.5);
+    border-radius: 50%;
+    padding: 4px;
+  }
+
+  &:hover .eye-icon {
+    opacity: 1;
+  }
+}
+
+.images {
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+}
+</style>

+ 28 - 6
src/views/system/user/UserForm.vue

@@ -40,8 +40,9 @@
       </el-row>
       <el-row>
         <el-col :span="12">
-          <el-form-item v-if="formData.id === undefined" label="用户名称" prop="username">
-            <el-input v-model="formData.username" placeholder="请输入用户名称" />
+          <!--          用户名称-->
+          <el-form-item v-if="formData.id === undefined" label="工号" prop="username">
+            <el-input v-model="formData.username" placeholder="请输入工号" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -69,7 +70,8 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="岗位">
+<!--          芋道原来的岗位-->
+          <el-form-item label="职位">
             <el-select v-model="formData.postIds" multiple placeholder="请选择">
               <el-option
                 v-for="item in postList"
@@ -82,7 +84,21 @@
         </el-col>
       </el-row>
       <el-row>
-        <el-col :span="24">
+        <el-col :span="12">
+          <el-form-item label="岗位">
+            <el-tree-select
+              v-model="formData.workstationIds"
+              :data="workstationList"
+              multiple
+              check-strictly
+              style="width: 290px"
+              :props="{ label: 'workstationName', value: 'id', children: 'children' }"
+              placeholder="选择岗位"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
           <el-form-item label="备注">
             <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
           </el-form-item>
@@ -101,6 +117,7 @@ import { CommonStatusEnum } from '@/utils/constants'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as PostApi from '@/api/system/post'
 import * as DeptApi from '@/api/system/dept'
+import * as workstationApi from '@/api/system/marsdept/index'
 import * as UserApi from '@/api/system/user'
 import { FormRules } from 'element-plus'
 
@@ -124,8 +141,10 @@ const formData = ref({
   sex: undefined,
   postIds: [],
   remark: '',
+  workstationIds:[],//mars岗位
   status: CommonStatusEnum.ENABLE,
-  roleIds: []
+  roleIds: [],
+  workstationids: [] //mars岗位
 })
 const formRules = reactive<FormRules>({
   username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
@@ -149,7 +168,7 @@ const formRules = reactive<FormRules>({
 const formRef = ref() // 表单 Ref
 const deptList = ref<Tree[]>([]) // 树形结构
 const postList = ref([] as PostApi.PostVO[]) // 岗位列表
-
+const workstationList = ref([]) // mars岗位列表
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
@@ -167,6 +186,9 @@ const open = async (type: string, id?: number) => {
   }
   // 加载部门树
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+  // 加载mars岗位
+  const data = await workstationApi.listMarsDept({ pageNo: 1, pageSize: -1 })
+  workstationList.value = handleTree(data.list, 'id', 'parentId')
   // 加载岗位列表
   postList.value = await PostApi.getSimplePostList()
 }

+ 55 - 5
src/views/system/user/index.vue

@@ -20,10 +20,10 @@
           :inline="true"
           label-width="68px"
         >
-          <el-form-item label="用户名称" prop="username">
+          <el-form-item label="工号" prop="username">
             <el-input
               v-model="queryParams.username"
-              placeholder="请输入用户名称"
+              placeholder="请输入工号"
               clearable
               @keyup.enter="handleQuery"
               class="!w-240px"
@@ -98,7 +98,7 @@
         <el-table v-loading="loading" :data="list">
           <el-table-column label="用户编号" align="center" key="id" prop="id" />
           <el-table-column
-            label="用户名称"
+            label="工号"
             align="center"
             prop="username"
             :show-overflow-tooltip="true"
@@ -117,6 +117,44 @@
             :show-overflow-tooltip="true"
           />
           <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
+          <el-table-column
+            label="岗位"
+            align="center"
+            key="workstationName"
+            prop="workstationName"
+            :show-overflow-tooltip="true"
+          >
+            <template #default="scope">
+              <el-button type="text" @click="HandleLookWorkStation(scope.row)"
+              >查看</el-button
+              >
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="指纹"
+            align="center"
+            prop=""
+            :show-overflow-tooltip="true"
+          >
+            <template #default="scope">
+              <el-button type="text" @click="openFaceOrFingerForm('finger', scope.row)">
+                查看
+              </el-button>
+            </template>
+          </el-table-column>
+
+          <el-table-column
+            label="人脸"
+            align="center"
+            prop=""
+            :show-overflow-tooltip="true"
+          >
+            <template #default="scope">
+              <el-button type="text" @click="openFaceOrFingerForm('face', scope.row)">
+                查看
+              </el-button>
+            </template>
+          </el-table-column>
           <el-table-column label="状态" key="status">
             <template #default="scope">
               <el-switch
@@ -198,6 +236,8 @@
   <UserImportForm ref="importFormRef" @success="getList" />
   <!-- 分配角色 -->
   <UserAssignRoleForm ref="assignRoleFormRef" @success="getList" />
+<!--  指纹和人脸弹框-->
+  <FaceOrFingerForm ref="faceOrFingerFormRef" @success="getList" />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -210,7 +250,7 @@ import UserForm from './UserForm.vue'
 import UserImportForm from './UserImportForm.vue'
 import UserAssignRoleForm from './UserAssignRoleForm.vue'
 import DeptTree from './DeptTree.vue'
-
+import FaceOrFingerForm from './FaceOrFingerForm.vue'
 defineOptions({ name: 'SystemUser' })
 
 const message = useMessage() // 消息弹窗
@@ -228,8 +268,18 @@ const queryParams = reactive({
   deptId: undefined,
   createTime: []
 })
+const router=useRouter()
 const queryFormRef = ref() // 搜索的表单
+// 打开指纹或人脸弹框
+// 弹窗引用
+const faceOrFingerFormRef = ref()
+const openFaceOrFingerForm = (type: string, row: any) => {
+  faceOrFingerFormRef.value?.open(type, row.id, row)
+}
 
+const HandleLookWorkStation=(row?:any)=> {
+  router.push("/system/marsdept?id=" + row.id);
+}
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -262,7 +312,7 @@ const handleDeptNodeClick = async (row) => {
 
 /** 添加/修改操作 */
 const formRef = ref()
-const openForm = (type: string, id?: number) => {
+const openForm = (type: string, id?: number,) => {
   formRef.value.open(type, id)
 }