Эх сурвалжийг харах

自自定义sop和sop创建作业票完成

pm 4 сар өмнө
parent
commit
c09b6fd012
81 өөрчлөгдсөн 10734 нэмэгдсэн , 1158 устгасан
  1. 2 2
      .env
  2. 2 2
      .env.prod
  3. 1 1
      README.md
  4. 9 9
      index.html
  5. BIN
      public/logo.png
  6. 66 0
      src/api/job/index.ts
  7. 50 0
      src/api/job/jobPoint.ts
  8. 40 0
      src/api/job/jobPointGroup.ts
  9. 65 0
      src/api/job/jobStep.ts
  10. 49 0
      src/api/job/jobUser.ts
  11. 4 1
      src/api/sop/index.ts
  12. 1 1
      src/api/sop/sopPoint.ts
  13. 42 0
      src/api/sop/sopPointGroup.ts
  14. 3 3
      src/api/system/notify/message/index.ts
  15. 1 1
      src/api/system/user/index.ts
  16. BIN
      src/assets/images/UserBlack.png
  17. BIN
      src/assets/images/UserEmpty.png
  18. BIN
      src/assets/logo/logo.png
  19. 1 1
      src/components/DiyEditor/components/mobile/UserCard/index.vue
  20. 1 1
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  21. 1 1
      src/layout/components/Logo/src/Logo.vue
  22. 131 4
      src/router/modules/remaining.ts
  23. 1 1
      src/utils/constants.ts
  24. 2 1
      src/views/Basicdata/mappoint/index.vue
  25. 3 1
      src/views/CustomWorkflow/CW/CreateView.vue
  26. 3 3
      src/views/CustomWorkflow/CW/TableStepDetail.vue
  27. 5 3
      src/views/CustomWorkflow/CW/TableView.vue
  28. 56 4
      src/views/CustomWorkflow/CW/WorkFlowView.vue
  29. 1 0
      src/views/CustomWorkflow/CW/index.vue
  30. 77 77
      src/views/Home/Index.vue
  31. 2 2
      src/views/Login/Login.vue
  32. 1 1
      src/views/Login/SocialLogin.vue
  33. 58 58
      src/views/Login/components/LoginForm.vue
  34. 1 1
      src/views/Login/components/MobileForm.vue
  35. 1 1
      src/views/ai/chat/index/components/message/MessageListEmpty.vue
  36. 1 1
      src/views/ai/utils/constants.ts
  37. 1 1
      src/views/ai/utils/utils.ts
  38. 8 7
      src/views/dv/technology/technologyDetail/CraftDetail.vue
  39. 0 2
      src/views/dv/technology/technologyDetail/MapData.vue
  40. 1140 0
      src/views/jobTicket/job/CreateJob.vue
  41. 1247 0
      src/views/jobTicket/job/CreateSopJob.vue
  42. 149 0
      src/views/jobTicket/job/ModeView/StepFunction.vue
  43. 118 0
      src/views/jobTicket/job/ModeView/TableStepDetail.vue
  44. 439 0
      src/views/jobTicket/job/ModeView/TableView.vue
  45. 896 0
      src/views/jobTicket/job/ModeView/WorkFlowView.vue
  46. 675 0
      src/views/jobTicket/job/PointView/CardView.vue
  47. 13 0
      src/views/jobTicket/job/PointView/TableView.vue
  48. 100 0
      src/views/jobTicket/job/SetModeStep.vue
  49. 119 0
      src/views/jobTicket/job/SetPoint.vue
  50. 104 0
      src/views/jobTicket/job/SetUser.vue
  51. 1348 0
      src/views/jobTicket/job/UpdateJob.vue
  52. 492 0
      src/views/jobTicket/job/UserView/CardView.vue
  53. 6 0
      src/views/jobTicket/job/UserView/TableView.vue
  54. 239 0
      src/views/jobTicket/job/index.vue
  55. 1 1
      src/views/mp/components/wx-material-select/main.vue
  56. 1 1
      src/views/mp/components/wx-msg/main.vue
  57. 1 1
      src/views/mp/components/wx-news/main.vue
  58. 1 1
      src/views/mp/components/wx-reply/main.vue
  59. 1 1
      src/views/mp/components/wx-video-play/main.vue
  60. 1 1
      src/views/mp/components/wx-voice-play/main.vue
  61. 1 1
      src/views/mp/material/components/ImageTable.vue
  62. 420 72
      src/views/sopm/sop/CreateSop.vue
  63. 149 0
      src/views/sopm/sop/ModeView/StepFunction.vue
  64. 118 0
      src/views/sopm/sop/ModeView/TableStepDetail.vue
  65. 114 393
      src/views/sopm/sop/ModeView/TableView.vue
  66. 224 411
      src/views/sopm/sop/ModeView/WorkFlowView.vue
  67. 673 0
      src/views/sopm/sop/PointView/CardView.vue
  68. 13 0
      src/views/sopm/sop/PointView/TableView.vue
  69. 23 2
      src/views/sopm/sop/SetModeStep.vue
  70. 111 5
      src/views/sopm/sop/SetPoint.vue
  71. 96 5
      src/views/sopm/sop/SetUser.vue
  72. 493 56
      src/views/sopm/sop/UpdateSop.vue
  73. 438 0
      src/views/sopm/sop/UserView/CardView.vue
  74. 6 0
      src/views/sopm/sop/UserView/TableView.vue
  75. 64 6
      src/views/sopm/sop/index.vue
  76. 2 2
      src/views/system/menu/index.vue
  77. 2 2
      src/views/system/role/index.vue
  78. 1 1
      src/views/system/tenant/index.vue
  79. 1 1
      src/views/system/tenantPackage/index.vue
  80. 1 1
      src/views/system/user/UserForm.vue
  81. 3 3
      src/views/system/user/index.vue

+ 2 - 2
.env

@@ -1,5 +1,5 @@
 # 标题
-VITE_APP_TITLE=芋道管理系统
+VITE_APP_TITLE=锁控管理系统
 
 # 项目本地运行端口号
 VITE_PORT=80
@@ -21,6 +21,6 @@ VITE_APP_DOCALERT_ENABLE=true
 VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
 
 # 默认账户密码
-VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
+VITE_APP_DEFAULT_LOGIN_TENANT = 博士安全
 VITE_APP_DEFAULT_LOGIN_USERNAME = admin
 VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123

+ 2 - 2
.env.prod

@@ -4,7 +4,7 @@ NODE_ENV=production
 VITE_DEV=false
 
 # 请求路径
-VITE_BASE_URL='http://localhost:48080'
+VITE_BASE_URL='http://120.27.232.27:48080'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
 VITE_UPLOAD_TYPE=server
@@ -31,4 +31,4 @@ VITE_OUT_DIR=dist-prod
 VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
 
 # GoView域名
-VITE_GOVIEW_URL='http://127.0.0.1:3000'
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 1 - 1
README.md

@@ -18,7 +18,7 @@
 
 ## 🐯 平台简介
 
-**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
+**锁控**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
 
 * 采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 实现
 * 改换 saas,自动引入等功能

+ 9 - 9
index.html

@@ -5,14 +5,14 @@
     <link rel="icon" href="/favicon.ico" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <meta
-      name="keywords"
-      content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
-    />
-    <meta
-      name="description"
-      content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
-    />
+<!--    <meta-->
+<!--      name="keywords"-->
+<!--      content="锁控管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"-->
+<!--    />-->
+<!--    <meta-->
+<!--      name="description"-->
+<!--      content="锁控管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"-->
+<!--    />-->
     <title>%VITE_APP_TITLE%</title>
   </head>
   <body>
@@ -136,7 +136,7 @@
       <div class="app-loading">
         <div class="app-loading-wrap">
           <div class="app-loading-title">
-            <img src="/logo.gif" class="app-loading-logo" alt="Logo" />
+            <img src="/logo.png" class="app-loading-logo" alt="Logo" />
             <div class="app-loading-title">%VITE_APP_TITLE%</div>
           </div>
           <div class="app-loading-item">

BIN
public/logo.png


+ 66 - 0
src/api/job/index.ts

@@ -0,0 +1,66 @@
+
+import request from '@/config/axios'
+
+export interface jobVO {
+  id: number,
+  ticketCode: string,
+  ticketName: string,
+  workshopId: number,
+  workstationId: number,
+  machineryId: number,
+  sopId: number,
+  ticketType: number,
+  ticketContent: string,
+  ticketStatus: number,
+  ticketStartTime: string,
+  ticketEndTime: string,
+  remark: string,
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  ticketName?: string
+  ticketType?: string
+  machineryId?: string
+  workstationId: number,
+}
+
+// 查询Job列表
+export const getJobTicketPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/job-ticket/getJobTicketPage', params })
+}
+// 自动生成名称
+export const autoGenerateName = async (name) => {
+  return await request.get({
+    url: '/iscs/job-ticket/autoGenerateName',
+    params: { name }  // ✅ 这里 axios 会自动 encodeURIComponent
+  })
+}
+
+
+// 获取Job详细信息
+export const selectJobTicketById = async (id: number) => {
+  return await request.get({ url: '/iscs/job-ticket/selectJobTicketById', params: { id: id } })
+}
+
+
+// 新增Job 不通过sop创建
+export const insertJobTicket = async (data: jobVO) => {
+  return await request.post({ url: '/iscs/job-ticket/insertJobTicket', data })
+}
+// 新增Job 通过sop创建
+export const insertJobTicketBySop = async (data: jobVO) => {
+  return await request.post({ url: '/iscs/job-ticket/insertJobTicketBySop', data })
+}
+// 修改Job
+export const updateJobTicket = async (data: jobVO) => {
+  return await request.put({ url: '/iscs/job-ticket/updateJobTicket', data })
+}
+
+// 删除Job
+export const deleteJobTicketList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/job-ticket/deleteJobTicketList?ids='+ids,
+  })
+}

+ 50 - 0
src/api/job/jobPoint.ts

@@ -0,0 +1,50 @@
+
+import request from '@/config/axios'
+
+export interface jobVO {
+  id: number,
+  ticketId: number,
+  groupId: number,
+  pointId: number,
+  pointStatus: number,
+  lockId: number,
+  lockedByKeyId: number,
+  unlockedByKeyId: number,
+  lockTime: string,
+  unlockTime: string,
+  prePointId: number,
+  remark: string,
+  workstationId: number,
+  machineryId: number
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+}
+
+// 查询Job点位列表
+export const getJobTicketPointsPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/job-ticket-points/getJobTicketPointsPage', params })
+}
+
+// 获取Job点位详细信息
+export const selectJobTicketPointsById = async (id: number) => {
+  return await request.get({ url: '/iscs/job-ticket-points/selectJobTicketPointsById', params: { id: id } })
+}
+
+// 新增Job 点位
+export const insertJobTicketPoints = async (data: jobVO) => {
+  return await request.post({ url: '/iscs/job-ticket-points/insertJobTicketPoints', data })
+}
+// 修改Job点位
+export const updateJobTicketPoints = async (data: jobVO) => {
+  return await request.put({ url: '/iscs/job-ticket-points/updateJobTicketPoints', data })
+}
+
+// 删除Job点位
+export const deleteJobTicketPointsList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/job-ticket-points/deleteJobTicketPointsList?ids='+ids,
+  })
+}

+ 40 - 0
src/api/job/jobPointGroup.ts

@@ -0,0 +1,40 @@
+
+import request from '@/config/axios'
+
+export interface jobVO {
+  id: number,
+  ticketId: number,
+  groupName: string,
+  remark: string
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+}
+
+// 查询Job点位分组列表
+export const getJobTicketGroupPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/job-ticket-group/getJobTicketGroupPage', params })
+}
+
+// 获取Job点位分组详细信息
+export const selectJobTicketGroupById = async (id: number) => {
+  return await request.get({ url: '/iscs/job-ticket-group/selectJobTicketGroupById', params: { id: id } })
+}
+
+// 新增Job 点位分组
+export const insertJobTicketGroup = async (data: jobVO) => {
+  return await request.post({ url: '/iscs/job-ticket-group/insertJobTicketGroup', data })
+}
+// 修改Job 点位分组
+export const updateJobTicketGroup = async (data: jobVO) => {
+  return await request.put({ url: '/iscs/job-ticket-group/updateJobTicketGroup', data })
+}
+
+// 删除Job 点位分组
+export const deleteJobTicketGroupList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/job-ticket-group/deleteJobTicketGroupList?ids='+ids,
+  })
+}

+ 65 - 0
src/api/job/jobStep.ts

@@ -0,0 +1,65 @@
+
+import request from '@/config/axios'
+
+export interface jobVO {
+  id: number,
+  sopStepId: 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
+  sopStepId: number,
+  stepIndex: number,
+  stepName: string,
+  stepIcon: string,
+  stepTitle: string,
+  stepTitleShort: string,
+  stepDescription: string,
+}
+
+// 查询Job步骤列表
+export const getJobTicketStepPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/job-ticket-step/getJobTicketStepPage', params })
+}
+
+// 获取Job步骤详细信息
+export const selectJobTicketStepById = async (id: number) => {
+  return await request.get({ url: '/iscs/job-ticket-step/selectJobTicketStepById', params: { id: id } })
+}
+
+// 新增Job 步骤
+export const insertJobTicketStep = async (data: jobVO) => {
+  return await request.post({ url: '/iscs/job-ticket-step/insertJobTicketStep', data })
+}
+// 修改Job步骤
+export const updateJobTicketStep = async (data: jobVO) => {
+  return await request.put({ url: '/iscs/job-ticket-step/updateJobTicketStep', data })
+}
+
+// 删除Job步骤
+export const deleteJobTicketStepList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/job-ticket-step/deleteJobTicketStepList?ids='+ids,
+  })
+}

+ 49 - 0
src/api/job/jobUser.ts

@@ -0,0 +1,49 @@
+
+import request from '@/config/axios'
+
+export interface jobVO {
+  id: number,
+  ticketId: number,
+  groupId: number,
+  userId: number,
+  userName: string,
+  userType: number,
+  userRole: string,
+  jobStatus: number,
+  remark: string
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+  userName?: string
+  userType?: string
+  groupId?: string
+  ticketId: number,
+}
+
+// 查询Job用户列表
+export const getJobTicketUserPage = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/job-ticket-user/getJobTicketUserPage', params })
+}
+
+// 获取Job用户详细信息
+export const selectJobTicketUserById = async (id: number) => {
+  return await request.get({ url: '/iscs/job-ticket-user/selectJobTicketUserById', params: { id: id } })
+}
+
+// 新增Job 用户
+export const insertJobTicketUser = async (data: jobVO) => {
+  return await request.post({ url: '/iscs/job-ticket-user/insertJobTicketUser', data })
+}
+// 修改Job用户
+export const updateJobTicketUser = async (data: jobVO) => {
+  return await request.put({ url: '/iscs/job-ticket-user/updateJobTicketUser', data })
+}
+
+// 删除Job用户
+export const deleteJobTicketUserList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/job-ticket-user/deleteJobTicketUserList?ids='+ids,
+  })
+}

+ 4 - 1
src/api/sop/index.ts

@@ -43,7 +43,10 @@ export const insertSop = async (data: sopVO) => {
 export const updateSop = async (data: sopVO) => {
   return await request.put({ url: '/iscs/sop/updateSop', data })
 }
-
+// 更新SOP生效状态
+export const updateSopStatus = async (data: sopVO) => {
+  return await request.put({ url: '/iscs/sop/updateSopStatus', data })
+}
 // 删除SOP
 export const deleteSopList = async (ids: number) => {
   return await request.delete({

+ 1 - 1
src/api/sop/sopPoint.ts

@@ -16,7 +16,7 @@ export interface PageParam {
 }
 
 // 查询SopPoint列表
-export const getSopWorkflowStepPage = async (params: PageParam) => {
+export const getSopPointsPage = async (params: PageParam) => {
   return await request.get({ url: '/iscs/sop-points/getSopPointsPage', params })
 }
 // 查询SopPointlist

+ 42 - 0
src/api/sop/sopPointGroup.ts

@@ -0,0 +1,42 @@
+
+import request from '@/config/axios'
+
+export interface SOPVo {
+  id: number,
+  sopId: number,
+  groupName: number,
+  remark: string,
+}
+
+export interface PageParam {
+  pageNo: number
+  pageSize: number
+}
+
+// 查询SopPointGroup列表
+export const getSopGroupList = async (params: PageParam) => {
+  return await request.get({ url: '/iscs/sop-group/getSopGroupList', params })
+}
+
+// 获取SopPointGroup详细信息
+export const selectSopGroupById = async (id: number) => {
+  return await request.get({ url: '/iscs/sop-group/selectSopGroupById', params: { id: id } })
+}
+
+
+// 新增SopPointGroup
+export const insertSopGroup = async (data: SOPVo) => {
+  return await request.post({ url: '/iscs/sop-group/insertSopGroup', data })
+}
+
+// 修改SopPointGroup
+export const updateSopGroup= async (data: SOPVo) => {
+  return await request.put({ url: '/iscs/sop-group/updateSopGroup', data })
+}
+
+// 删除SopPointGroup
+export const deleteSopGroupList = async (ids: number) => {
+  return await request.delete({
+    url: '/iscs/sop-group/deleteSopGroupList?ids='+ids,
+  })
+}

+ 3 - 3
src/api/system/notify/message/index.ts

@@ -44,6 +44,6 @@ export const getUnreadNotifyMessageList = async () => {
 }
 
 // 获得当前用户的未读站内信数量
-// export const getUnreadNotifyMessageCount = async () => {
-//   return await request.get({ url: '/system/notify-message/get-unread-count' })
-// }
+export const getUnreadNotifyMessageCount = async () => {
+  return await request.get({ url: '/system/notify-message/get-unread-count' })
+}

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

@@ -34,7 +34,7 @@ export const getUser = (id: number) => {
   return request.get({ url: '/system/user/get?id=' + id })
 }
 // code查询 /system/user/getUserByRoleCode
-export const getRoleUser = (code: number) => {
+export const getRoleUser = (code: string) => {
   return request.get({ url: '/system/user/getUserByRoleCode?roleCode=' + code})
 }
 

BIN
src/assets/images/UserBlack.png


BIN
src/assets/images/UserEmpty.png


BIN
src/assets/logo/logo.png


+ 1 - 1
src/components/DiyEditor/components/mobile/UserCard/index.vue

@@ -5,7 +5,7 @@
         <el-avatar :size="60">
           <Icon icon="ep:avatar" :size="60" />
         </el-avatar>
-        <span class="text-18px font-bold">芋道源码</span>
+        <span class="text-18px font-bold">博士安全</span>
       </div>
       <Icon icon="tdesign:qrcode" :size="20" />
     </div>

+ 1 - 1
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -642,7 +642,7 @@ const previewProcessJson = () => {
   })
 }
 
-/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
+/* ------------------------------------------------ 博士安全 methods ------------------------------------------------------ */
 onMounted(() => {
   initBpmnModeler()
   createNewDiagram(props.value)

+ 1 - 1
src/layout/components/Logo/src/Logo.vue

@@ -68,7 +68,7 @@ watch(
     >
       <img
         class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
-        src="@/assets/imgs/logo.png"
+        src="@/assets/logo/logo.png"
       />
       <div
         v-if="show"

+ 131 - 4
src/router/modules/remaining.ts

@@ -118,7 +118,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         name: 'CreateView',
         meta: {
           title: '自定义作业流程新增',
-          noCache: true,
+          noCache: false,
           hidden: true,
           canTo: true,
           icon: 'ep:view',
@@ -155,7 +155,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       {
         path: 'CW/TableStepDetail',
         component: () => import('@/views/CustomWorkflow/CW/TableStepDetail.vue'),
-        name: 'TableStepDetail',
+        name: 'WorkflowTableStepDetail',
         meta: {
           title: '步骤操作说明',
           noCache: true,
@@ -205,10 +205,11 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: 'sop新增',
           noCache: true,
+          noTagsView: true,
           hidden: true,
           canTo: true,
           icon: 'ep:view',
-          activeMenu: '/sopm/sop/CreateSop'
+          activeMenu: '/sopm/sop'
         }
       },
       {
@@ -217,11 +218,12 @@ const remainingRouter: AppRouteRecordRaw[] = [
         name: 'UpdateSop',
         meta: {
           title: 'sop修改',
+          noTagsView: true,
           noCache: true,
           hidden: true,
           canTo: true,
           icon: 'ep:view',
-          activeMenu: '/sopm/sop/UpdateSop'
+          activeMenu: '/sopm/sop'
         }
       },
       {
@@ -231,6 +233,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: '设置模式步骤',
           noCache: true,
+          noTagsView: true,
           hidden: true,
           canTo: true,
           icon: 'ep:view',
@@ -244,6 +247,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: '设置点位',
           noCache: true,
+          noTagsView: true,
           hidden: true,
           canTo: true,
           icon: 'ep:view',
@@ -257,12 +261,135 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: '设置人员',
           noCache: true,
+          noTagsView: true,
           hidden: true,
           canTo: true,
           icon: 'ep:view',
           activeMenu: '/sopm/sop/SetUser'
         }
       },
+      {
+        path: 'sopm/sop/ModeView/TableStepDetail',
+        component: () => import('@/views/sopm/sop/ModeView/TableStepDetail.vue'),
+        name: 'TableStepDetail',
+        meta: {
+          title: '步骤操作说明',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/sopm/sop/ModeView/TableStepDetail'
+        }
+      },
+
+    ]
+  },
+  {
+    path: '/jobTicket',
+    component: Layout,
+    name: 'jobTicket',
+    meta: {
+      hidden: true,
+    },
+    children: [
+      {
+        path: 'jobTicket/job/CreateJob',
+        component: () => import('@/views/jobTicket/job/CreateJob.vue'),
+        name: 'CreateJob',
+        meta: {
+          title: '作业票新增',
+          noCache: true,
+          noTagsView: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/jobTicket/job'
+        }
+      },
+      {
+        path: 'jobTicket/job/CreateSopJob',
+        component: () => import('@/views/jobTicket/job/CreateSopJob.vue'),
+        name: 'CreateSopJob',
+        meta: {
+          title: '作业票新增',
+          noCache: true,
+          noTagsView: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/jobTicket/job'
+        }
+      },
+      {
+        path: 'jobTicket/job/UpdateJob',
+        component: () => import('@/views/jobTicket/job/UpdateJob.vue'),
+        name: 'UpdateJob',
+        meta: {
+          title: '作业票修改',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/jobTicket/job'
+        }
+      },
+      {
+        path: 'jobTicket/job/SetModeStep',
+        component: () => import('@/views/jobTicket/job/SetModeStep.vue'),
+        name: 'SetJobModeStep',
+        meta: {
+          title: '设置模式步骤',
+          noCache: true,
+          noTagsView: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/jobTicket/job/SetModeStep'
+        }
+      },
+      {
+        path: 'jobTicket/job/SetPoint',
+        component: () => import('@/views/jobTicket/job/SetPoint.vue'),
+        name: 'SetJobPoint',
+        meta: {
+          title: '设置点位',
+          noCache: true,
+          noTagsView: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/jobTicket/job/SetPoint'
+        }
+      },
+      {
+        path: 'jobTicket/job/SetUser',
+        component: () => import('@/views/jobTicket/job/SetUser.vue'),
+        name: 'SetJobUser',
+        meta: {
+          title: '设置人员',
+          noCache: true,
+          noTagsView: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/jobTicket/job/SetUser'
+        }
+      },
+      {
+        path: 'jobTicket/job/ModeView/TableStepDetail',
+        component: () => import('@/views/jobTicket/job/ModeView/TableStepDetail.vue'),
+        name: 'jobTableStepDetail',
+        meta: {
+          title: '步骤操作说明',
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          activeMenu: '/jobTicket/job/ModeView/TableStepDetail'
+        }
+      },
+
     ]
   },
   {

+ 1 - 1
src/utils/constants.ts

@@ -1,5 +1,5 @@
 /**
- * Created by 芋道源码
+ * Created by 博士安全
  *
  * 枚举类
  */

+ 2 - 1
src/views/Basicdata/mappoint/index.vue

@@ -190,8 +190,9 @@ const openForm = (type: string, id?: number) => {
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
+    const Pointsids=id||ids.value
     await message.delConfirm()
-    await MapPointApi.deleteIsMapPointByIds(id)
+    await MapPointApi.deleteIsMapPointByIds(Pointsids)
     message.success(t('common.delSuccess'))
     await getList()
   } catch {}

+ 3 - 1
src/views/CustomWorkflow/CW/CreateView.vue

@@ -61,7 +61,8 @@ const formData = ref({
   modeTitle: undefined,
   isColockSupport: false,
   modeDescription: undefined,
-  isPreset: undefined
+  isPreset: undefined,
+  id:undefined,
 })
 // 监听数据变化,设置未保存标记
 const hasUnsavedChanges = ref(false) // 标记是否有未保存的更改
@@ -135,6 +136,7 @@ onBeforeRouteLeave((to, from, next) => {
 const goBack = () => {
   router.push('/CustomWorkflow/CW')
 }
+
 </script>
 
 <style scoped lang="scss"></style>

+ 3 - 3
src/views/CustomWorkflow/CW/TableStepDetail.vue

@@ -23,7 +23,7 @@ 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'
+import { updateWorkflowStep,selectWorkflowStepById } from '@/api/custonWorkflow/step'
 
 const route = useRoute()
 const router = useRouter()
@@ -68,7 +68,7 @@ const handleCancel = () => {
   // 如果有内容变化,提示用户
   if (stepDescription.value !== stepData.value?.stepDescription) {
     ElMessageBox.confirm(
-      '内容已修改,确定要取消吗?',
+      '内容是否保存,确认离开吗?',
       '确认取消',
       {
         confirmButtonText: '确定',
@@ -76,7 +76,6 @@ const handleCancel = () => {
         type: 'warning'
       }
     ).then(() => {
-
       router.back()
     }).catch(() => {
       // 用户选择继续编辑,不做任何操作
@@ -89,6 +88,7 @@ const handleCancel = () => {
 // 页面初始化
 onMounted(() => {
   const tempData = localStorage.getItem('tempStepData')
+
   if (tempData) {
     stepData.value = JSON.parse(tempData)
     stepDescription.value = stepData.value.stepDescription || ''

+ 5 - 3
src/views/CustomWorkflow/CW/TableView.vue

@@ -96,6 +96,7 @@
 
         <el-table-column label="标题" width="180">
           <template #default="{ row }">
+
             <el-input
               :disabled="DisableCheckView"
               v-model="row.stepTitle"
@@ -374,7 +375,7 @@ const viewStepDetail = (row) => {
 
   // 跳转到详情页面
   router.push({
-    name: 'TableStepDetail',
+    name: 'WorkflowTableStepDetail',
     query: {
       stepId: row.id,
       modeId: route.query.id
@@ -384,7 +385,7 @@ const viewStepDetail = (row) => {
 // 生成新的表格行
 const createNewRow = () => {
   return {
-    modeId: route.query.id,
+    modeId: route.query.id||props.modeId,
     stepTemplateId: undefined,
     stepIndex: 0,
     stepName: undefined,
@@ -659,13 +660,13 @@ const getIconValues = async (iconKeys) => {
 // 初始化如果添加过步骤需要回显出来
 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) => {
@@ -723,6 +724,7 @@ watch(
   },
   { immediate: true, deep: true }
 )
+
 </script>
 
 <style scoped lang="scss">

+ 56 - 4
src/views/CustomWorkflow/CW/WorkFlowView.vue

@@ -23,7 +23,7 @@
     </div>
 
     <!-- VueFlow 主画布 -->
-    <VueFlow style="width: 100%; height: 600px">
+    <VueFlow style="width: 100%; height: 400px">
       <template #node-default="{ id, data }">
         <div class="custom-node">
           <div class="node-content">
@@ -64,7 +64,7 @@
       v-if="showForm && selectedNodeId"
       style="margin-top: 20px; padding: 20px; border: 1px solid #ccc"
     >
-      <h3>节点配置({{ selectedNodeId }})</h3>
+<!--      <h3>节点配置({{ selectedNodeId }})</h3>-->
       <div style="display: flex; gap: 20px">
         <el-form label-width="155px">
           <el-form-item label="图标">
@@ -482,6 +482,37 @@ const initTableData = async () => {
 
         // 渲染连接线
         renderEdgesFromData(sortedData)
+        // 默认选中第一个节点(高亮显示)
+        if (sortedData.length > 0) {
+          const firstNode = sortedData[0];
+          if(firstNode) {
+            // 设置选中节点ID
+            selectedNodeId.value = `node-${firstNode.id}`;
+
+            // 显示表单
+            showForm.value = true;
+
+            // 更新表单数据 - 直接使用 firstNode 的完整数据
+            Object.assign(formData, firstNode);
+
+            // 使用Vue Flow内置方法选中首个节点
+            const { addSelectedNodes } = useVueFlow();
+            addSelectedNodes([`node-${firstNode.id}`]);
+
+            // 更新节点样式为选中状态
+            updateNode(`node-${firstNode.id}`, (node) => {
+              node.style = {
+                ...node.style,
+                backgroundColor: '#ffec99', // 黄色背景
+                borderColor: '#f0c040', // 黄色边框
+                borderWidth: '2px'
+              };
+            });
+
+            console.log('默认选中第一个节点并高亮:', `node-${firstNode.id}`);
+            console.log('表单数据已更新:', formData);
+          }
+        }
       } else {
         console.log('没有找到步骤数据')
       }
@@ -788,6 +819,18 @@ const saveFormData = async (row) => {
 
 // 节点点击处理 - 修正版本
 onNodeClick(({ node }) => {
+  // 清除所有节点的选中样式
+  nodes.value.forEach(n => {
+    updateNode(n.id, (nodeItem) => {
+      nodeItem.style = {
+        ...nodeItem.style,
+        backgroundColor: '#fff',
+        borderColor: '#999',
+        borderWidth: '1px'
+      };
+    });
+  });
+
   if (selectedNodeId.value === node.id) {
     showForm.value = false
     selectedNodeId.value = null
@@ -795,6 +838,16 @@ onNodeClick(({ node }) => {
     selectedNodeId.value = node.id
     showForm.value = true
 
+    // 设置当前节点为选中样式
+    updateNode(node.id, (nodeItem) => {
+      nodeItem.style = {
+        ...nodeItem.style,
+        backgroundColor: '#ffec99', // 黄色背景
+        borderColor: '#f0c040', // 黄色边框
+        borderWidth: '2px'
+      };
+    });
+
     // 更新表单数据
     const nodeData = node.data
     if (nodeData.stepData) {
@@ -808,7 +861,6 @@ onNodeClick(({ node }) => {
     }
   }
 })
-
 onNodeContextMenu(({ node, event }) => {
   event.preventDefault()
   selectedNodeId.value = node.id
@@ -991,7 +1043,7 @@ watch(
   position: relative;
   width: 125px;
   height: 180px;
-  background-color: #fff;
+  background-color: transparent;
   border-radius: 12px;
   display: flex;
   flex-direction: column;

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

@@ -119,6 +119,7 @@
             修改
           </el-button>
           <el-button
+
             link
             type="danger"
             @click="handleDelete(row.id)"

+ 77 - 77
src/views/Home/Index.vue

@@ -58,55 +58,55 @@
 
   <el-row class="mt-8px" :gutter="8" justify="space-between">
     <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
-      <el-card shadow="never">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.project') }}</span>
-            <el-link
-              type="primary"
-              :underline="false"
-              href="https://github.com/yudaocode"
-              target="_blank"
-            >
-              {{ t('action.more') }}
-            </el-link>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <el-row>
-            <el-col
-              v-for="(item, index) in projects"
-              :key="`card-${index}`"
-              :xl="8"
-              :lg="8"
-              :md="8"
-              :sm="24"
-              :xs="24"
-            >
-              <el-card
-                shadow="hover"
-                class="mr-5px mt-5px cursor-pointer"
-                @click="handleProjectClick(item.message)"
-              >
-                <div class="flex items-center">
-                  <Icon
-                    :icon="item.icon"
-                    :size="25"
-                    class="mr-8px"
-                    :style="{ color: item.color }"
-                  />
-                  <span class="text-16px">{{ item.name }}</span>
-                </div>
-                <div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
-                <div class="mt-12px flex justify-between text-12px text-gray-400">
-                  <span>{{ item.personal }}</span>
-                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
-                </div>
-              </el-card>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
+<!--      <el-card shadow="never">-->
+<!--        <template #header>-->
+<!--          <div class="h-3 flex justify-between">-->
+<!--            <span>{{ t('workplace.project') }}</span>-->
+<!--            <el-link-->
+<!--              type="primary"-->
+<!--              :underline="false"-->
+<!--              href="https://github.com/yudaocode"-->
+<!--              target="_blank"-->
+<!--            >-->
+<!--              {{ t('action.more') }}-->
+<!--            </el-link>-->
+<!--          </div>-->
+<!--        </template>-->
+<!--        <el-skeleton :loading="loading" animated>-->
+<!--          <el-row>-->
+<!--            <el-col-->
+<!--              v-for="(item, index) in projects"-->
+<!--              :key="`card-${index}`"-->
+<!--              :xl="8"-->
+<!--              :lg="8"-->
+<!--              :md="8"-->
+<!--              :sm="24"-->
+<!--              :xs="24"-->
+<!--            >-->
+<!--              <el-card-->
+<!--                shadow="hover"-->
+<!--                class="mr-5px mt-5px cursor-pointer"-->
+<!--                @click="handleProjectClick(item.message)"-->
+<!--              >-->
+<!--                <div class="flex items-center">-->
+<!--                  <Icon-->
+<!--                    :icon="item.icon"-->
+<!--                    :size="25"-->
+<!--                    class="mr-8px"-->
+<!--                    :style="{ color: item.color }"-->
+<!--                  />-->
+<!--                  <span class="text-16px">{{ item.name }}</span>-->
+<!--                </div>-->
+<!--                <div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>-->
+<!--                <div class="mt-12px flex justify-between text-12px text-gray-400">-->
+<!--                  <span>{{ item.personal }}</span>-->
+<!--                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>-->
+<!--                </div>-->
+<!--              </el-card>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--        </el-skeleton>-->
+<!--      </el-card>-->
 
       <el-card shadow="never" class="mt-8px">
         <el-skeleton :loading="loading" animated>
@@ -149,34 +149,34 @@
           </el-row>
         </el-skeleton>
       </el-card>
-      <el-card shadow="never" class="mt-8px">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.notice') }}</span>
-            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
-            <div class="flex items-center">
-              <el-avatar :src="avatar" :size="35" class="mr-16px">
-                <img src="@/assets/imgs/avatar.gif" alt="" />
-              </el-avatar>
-              <div>
-                <div class="text-14px">
-                  <Highlight :keys="item.keys.map((v) => t(v))">
-                    {{ item.type }} : {{ item.title }}
-                  </Highlight>
-                </div>
-                <div class="mt-16px text-12px text-gray-400">
-                  {{ formatTime(item.date, 'yyyy-MM-dd') }}
-                </div>
-              </div>
-            </div>
-            <el-divider />
-          </div>
-        </el-skeleton>
-      </el-card>
+<!--      <el-card shadow="never" class="mt-8px">-->
+<!--        <template #header>-->
+<!--          <div class="h-3 flex justify-between">-->
+<!--            <span>{{ t('workplace.notice') }}</span>-->
+<!--            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>-->
+<!--          </div>-->
+<!--        </template>-->
+<!--        <el-skeleton :loading="loading" animated>-->
+<!--          <div v-for="(item, index) in notice" :key="`dynamics-${index}`">-->
+<!--            <div class="flex items-center">-->
+<!--              <el-avatar :src="avatar" :size="35" class="mr-16px">-->
+<!--                <img src="@/assets/imgs/avatar.gif" alt="" />-->
+<!--              </el-avatar>-->
+<!--              <div>-->
+<!--                <div class="text-14px">-->
+<!--                  <Highlight :keys="item.keys.map((v) => t(v))">-->
+<!--                    {{ item.type }} : {{ item.title }}-->
+<!--                  </Highlight>-->
+<!--                </div>-->
+<!--                <div class="mt-16px text-12px text-gray-400">-->
+<!--                  {{ formatTime(item.date, 'yyyy-MM-dd') }}-->
+<!--                </div>-->
+<!--              </div>-->
+<!--            </div>-->
+<!--            <el-divider />-->
+<!--          </div>-->
+<!--        </el-skeleton>-->
+<!--      </el-card>-->
     </el-col>
   </el-row>
 </template>

+ 2 - 2
src/views/Login/Login.vue

@@ -9,7 +9,7 @@
       >
         <!-- 左上角的 logo + 系统标题 -->
         <div class="relative flex items-center text-white">
-          <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+          <img alt="" class="mr-10px h-48px w-48px" src="@/assets/logo/logo.png" />
           <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
         </div>
         <!-- 左边的背景图 + 欢迎语 -->
@@ -118,4 +118,4 @@ $prefix-cls: #{$namespace}-login;
     background-color: var(--login-bg-color);
   }
 }
-</style>
+</style>

+ 1 - 1
src/views/Login/SocialLogin.vue

@@ -199,7 +199,7 @@ const loginData = reactive({
   captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
   tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
   loginForm: {
-    tenantName: '芋道源码',
+    tenantName: '博士安全',
     username: 'admin',
     password: 'admin123',
     captchaVerification: '',

+ 58 - 58
src/views/Login/components/LoginForm.vue

@@ -89,64 +89,64 @@
         mode="pop"
         @success="handleLogin"
       />
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <el-row :gutter="5" justify="space-between" style="width: 100%">
-            <el-col :span="8">
-              <XButton
-                :title="t('login.btnMobile')"
-                class="w-[100%]"
-                @click="setLoginState(LoginStateEnum.MOBILE)"
-              />
-            </el-col>
-            <el-col :span="8">
-              <XButton
-                :title="t('login.btnQRCode')"
-                class="w-[100%]"
-                @click="setLoginState(LoginStateEnum.QR_CODE)"
-              />
-            </el-col>
-            <el-col :span="8">
-              <XButton
-                :title="t('login.btnRegister')"
-                class="w-[100%]"
-                @click="setLoginState(LoginStateEnum.REGISTER)"
-              />
-            </el-col>
-          </el-row>
-        </el-form-item>
-      </el-col>
-      <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <div class="w-[100%] flex justify-between">
-            <Icon
-              v-for="(item, key) in socialList"
-              :key="key"
-              :icon="item.icon"
-              :size="30"
-              class="anticon cursor-pointer"
-              color="#999"
-              @click="doSocialLogin(item.type)"
-            />
-          </div>
-        </el-form-item>
-      </el-col>
-      <el-divider content-position="center">萌新必读</el-divider>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <div class="w-[100%] flex justify-between">
-            <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
-            <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
-            <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
-              ⚡面试手册
-            </el-link>
-            <el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
-              🤝外包咨询
-            </el-link>
-          </div>
-        </el-form-item>
-      </el-col>
+<!--      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">-->
+<!--        <el-form-item>-->
+<!--          <el-row :gutter="5" justify="space-between" style="width: 100%">-->
+<!--            <el-col :span="8">-->
+<!--              <XButton-->
+<!--                :title="t('login.btnMobile')"-->
+<!--                class="w-[100%]"-->
+<!--                @click="setLoginState(LoginStateEnum.MOBILE)"-->
+<!--              />-->
+<!--            </el-col>-->
+<!--            <el-col :span="8">-->
+<!--              <XButton-->
+<!--                :title="t('login.btnQRCode')"-->
+<!--                class="w-[100%]"-->
+<!--                @click="setLoginState(LoginStateEnum.QR_CODE)"-->
+<!--              />-->
+<!--            </el-col>-->
+<!--            <el-col :span="8">-->
+<!--              <XButton-->
+<!--                :title="t('login.btnRegister')"-->
+<!--                class="w-[100%]"-->
+<!--                @click="setLoginState(LoginStateEnum.REGISTER)"-->
+<!--              />-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--        </el-form-item>-->
+<!--      </el-col>-->
+<!--      <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>-->
+<!--      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">-->
+<!--        <el-form-item>-->
+<!--          <div class="w-[100%] flex justify-between">-->
+<!--            <Icon-->
+<!--              v-for="(item, key) in socialList"-->
+<!--              :key="key"-->
+<!--              :icon="item.icon"-->
+<!--              :size="30"-->
+<!--              class="anticon cursor-pointer"-->
+<!--              color="#999"-->
+<!--              @click="doSocialLogin(item.type)"-->
+<!--            />-->
+<!--          </div>-->
+<!--        </el-form-item>-->
+<!--      </el-col>-->
+<!--      <el-divider content-position="center">萌新必读</el-divider>-->
+<!--      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">-->
+<!--        <el-form-item>-->
+<!--          <div class="w-[100%] flex justify-between">-->
+<!--            <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>-->
+<!--            <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>-->
+<!--            <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">-->
+<!--              ⚡面试手册-->
+<!--            </el-link>-->
+<!--            <el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">-->
+<!--              🤝外包咨询-->
+<!--            </el-link>-->
+<!--          </div>-->
+<!--        </el-form-item>-->
+<!--      </el-col>-->
     </el-row>
   </el-form>
 </template>

+ 1 - 1
src/views/Login/components/MobileForm.vue

@@ -133,7 +133,7 @@ const loginData = reactive({
   },
   loginForm: {
     uuid: '',
-    tenantName: '芋道源码',
+    tenantName: '博士安全',
     mobileNumber: '',
     code: ''
   }

+ 1 - 1
src/views/ai/chat/index/components/message/MessageListEmpty.vue

@@ -3,7 +3,7 @@
   <div class="chat-empty">
     <!-- title -->
     <div class="center-container">
-      <div class="title">芋道 AI</div>
+      <div class="title">锁控 AI</div>
       <div class="role-list">
         <div
           class="role-item"

+ 1 - 1
src/views/ai/utils/constants.ts

@@ -1,5 +1,5 @@
 /**
- * Created by 芋道源码
+ * Created by 博士安全
  *
  * AI 枚举类
  *

+ 1 - 1
src/views/ai/utils/utils.ts

@@ -1,5 +1,5 @@
 /**
- * Created by 芋道源码
+ * Created by 博士安全
  *
  * AI 枚举类
  *

+ 8 - 7
src/views/dv/technology/technologyDetail/CraftDetail.vue

@@ -285,10 +285,11 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as TechnologyApi from '@/api/dv/technology'
 import * as MardDeptApi from "@/api/system/marsdept/index"
 import * as LotoApi from '@/api/dv/lotoStation'
-import * as SopApi from '@/api/dv/lotoStation'
+import * as SopApi from '@/api/sop/index'
 
 import MapData from './MapData.vue'
 import TinyMCE from '@/components/TinyMCE/index.vue'
+import {getSopPage} from "@/api/sop/index";
 
 defineOptions({ name: 'TechnologyDetail' })
 
@@ -397,9 +398,9 @@ const getSopList = async () => {
       ...queryParams,
       machineryId: route.query.machineryId
     }
-    // const res = await SopApi.getIsMarsSopPage(data)
-    // sopList.value = res.list
-    // sopTotal.value = res.total
+    const res = await SopApi.getSopPage(data)
+    sopList.value = res.list
+    sopTotal.value = res.total
   } finally {
     loading.value = false
   }
@@ -498,7 +499,7 @@ const handleSopDelete = async (row?: any) => {
   const ids = row?.sopId || selectedSopIds.value
   await message.confirm('确认删除数据项?')
   try {
-    await SopApi.deleteIsMarsSopByMarsSopIds(ids)
+    await SopApi.deleteSopList(ids)
     message.success(t('common.delSuccess'))
     getSopList()
   } catch {}
@@ -535,10 +536,10 @@ const submitSopForm = async () => {
   try {
     const data = sopFormData.value
     if (data.sopId) {
-      await SopApi.updateIsMarsSop(data)
+      await SopApi.updateSop(data)
       message.success(t('common.updateSuccess'))
     } else {
-      await SopApi.addinsertIsMarsSop(data)
+      await SopApi.insertSop(data)
       message.success(t('common.createSuccess'))
     }
     sopDialogVisible.value = false

+ 0 - 2
src/views/dv/technology/technologyDetail/MapData.vue

@@ -81,8 +81,6 @@ const selectPoints = ref<number[]>([])
 const pointList = computed(() => formData.value.pointList)
 
 onMounted(async () => {
-
-
   const techRes = await TechnologyApi.getTechnologyInfo(route.query.machineryId as string)
   const lotoId = techRes.lotoId
   const lotoRes = await LotoApi.selectIsLotoStationById(lotoId)

+ 1140 - 0
src/views/jobTicket/job/CreateJob.vue

@@ -0,0 +1,1140 @@
+<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">作业创建步骤</span>
+            </div>
+          </template>
+
+          <div style="padding-left: 20px" >
+            <div>1、选择作业区域、工艺信息、作业类型</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="JobForm"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="作业名称" prop="ticketName">
+                  <el-input
+                    v-model="JobForm.ticketName"
+                    placeholder="请输入作业票名称"
+                    clearable
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="2">
+                <el-checkbox v-model="JobAutoName">自动生成</el-checkbox>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="作业区域" prop="workstationId">
+                  <el-tree-select
+                    v-model="JobForm.workstationId"
+                    :data="workstationOption"
+                    :props="{ label: 'workstationName', value: 'id', children: 'children' }"
+                    placeholder="选择作业区域"
+                    class="!w-240px"
+                    @change="JobWorkstationChange"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="工艺设备" prop="machineryId">
+                  <el-tree-select
+                    v-model="JobForm.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="作业类型" prop="ticketType">
+                  <el-select
+                    v-model="JobForm.ticketType"
+                    placeholder="请选择作业类型"
+                    clearable
+                    class="!w-240px"
+                    @change="handleTicketpTypeChange"
+                  >
+                    <el-option
+                      v-for="dict in getStrDictOptions(DICT_TYPE.TICKET_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="JobForm.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('SetJobModeStep', JobForm,null)">设置</div>
+        </div>
+        <div class="tab-content">
+          <!-- VueFlow 主画布 -->
+          <VueFlow style="width: 100%; height: 300px" :min-zoom="1" :max-zoom="1" :default-zoom="1">
+            <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('SetJobPoint', JobForm,null)">设置</div>
+        </div>
+        <div class="tab-content" style="height: 300px">
+          <div class="point_center_box" v-if="!JobForm?.sopPointsList?.length">
+            <img
+              src="../../../assets/images/添加.png"
+              alt=""
+              @click="goSetting('SetJobPoint', JobForm,null)"
+            />
+            <span style="color: red">*请添加需要进行隔离的点位</span>
+          </div>
+          <div v-else>
+            <!-- 循环渲染分组 -->
+            <div class="group-container">
+              <div v-for="group in resolvedGroupedPoints" :key="group.groupId" class="point-group">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="points-list">
+                  <div
+                    v-for="point in group.points"
+                    :key="point.pointId"
+                    class="point-item"
+                  >
+                    <img :src="point.pointIcon" class="point-icon"/>
+                    <div class="point-name">{{ point.pointName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+          </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('SetJobUser', JobForm,null)">设置</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 v-if="groupedLockers.length" class="group-container-user">
+              <div v-for="group in groupedLockers" :key="group.groupId" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <div v-for="user in group.users" :key="user.userId" class="user-card">
+                    <img src="@/assets/images/UserBlack.png" />
+                    <div class="user-name">{{ user.userName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- 有分组点数据时显示 -->
+            <div v-else-if="JobForm.sopGroupList&&JobForm.sopGroupList[0].groupName!=='默认分组'" class="group-container">
+              <div v-for="group in JobForm.sopGroupList" :key="group.id" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <img src="@/assets/images/添加.png" class="user-card" @click="goSetting('SetJobUser', JobForm,group.id)"/>
+                </div>
+              </div>
+            </div>
+
+            <!-- 都没有数据时显示添加提示 -->
+            <div v-else class="point_center_box">
+              <img src="@/assets/images/添加.png" @click="goSetting('SetJobUser', JobForm,null)" />
+              <span>请添加参与锁定的人员</span>
+            </div>
+          </div>
+
+          <!-- 共锁人区域 -->
+          <div class="right_box">
+            <div class="tab-header">
+              <span class="tab-title">共锁人</span>
+            </div>
+            <div v-if="coLockUsers.length" class="user-list-colocker">
+              <div
+                v-for="user in coLockUsers"
+                :key="user.userId"
+                class="user-card"
+              >
+                <img src="@/assets/images/UserBlack.png"/>
+                <div class="user-name">{{ user.userName }}</div>
+              </div>
+            </div>
+            <div v-else class="point_center_box">
+              <img src="@/assets/images/添加.png" alt="" @click="goSetting('SetJobUser', JobForm,null)"/>
+              <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 JobApi from '@/api/job/index'
+import * as jobPointGroup from '@/api/job/jobPointGroup'
+import * as PointApi from '@/api/dv/spm/index'
+
+const {t} = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+import {handleTree} from '@/utils/tree'
+import {ref} from 'vue'
+import {Handle, useVueFlow, VueFlow} from '@vue-flow/core'
+import {deleteJobTicketStepList, insertJobTicketStep} from "@/api/job/jobStep";
+import {getJobTicketGroupPage} from "@/api/job/jobPointGroup";
+
+const JobForm = reactive({
+  id: null,
+  ticketCode: null,
+  ticketName: null,
+  modeId: null,
+  workstationId: null,
+  machineryId: null,
+  sopId: null,
+  ticketType: null,
+  ticketContent: null,
+  ticketStatus: null,
+  ticketStartTime: null,
+  ticketEndTime: null,
+  remark: null
+})
+const Visible=ref<boolean>(false)
+const JobAutoName = ref(false)
+const activeName = ref('1')
+const machineryOptions = ref()
+const workstationOption = ref()
+const ModeOption = ref()
+const allGroups = ref<any[]>([])//获取所有分组
+const groupList = ref([])//获取当前sopId的分组
+const allPoints = ref<any[]>([])//获取所有点位
+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()
+//job区域切换函数
+const JobWorkstationChange=async (value)=>{
+  JobForm.workstationId=value
+  // 获取设备/工艺数据
+  const techRes = await TechnologyApi.listTechnology({pageNo: 1, pageSize: -1,workstationId:JobForm.workstationId})
+  const data = techRes.list.filter((item) => item.machineryType == '工艺')
+  machineryOptions.value = handleTree(data, 'id', 'parentId')
+  buildMachineryMap(data)
+}
+//跳转设置对应页面
+const goSetting = (type, JobForm,groupId) => {
+  if (type == 'SetJobModeStep') {
+    router.push({
+      name: 'SetJobModeStep',
+      query: {
+        ticketId: JobForm.id,
+        modeId: JobForm.modeId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  } else if (type == 'SetJobPoint') {
+    router.push({
+      name: 'SetJobPoint',
+      query: {
+        ticketId: JobForm.id,
+        machineryId: JobForm.machineryId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  } else if (type == 'SetJobUser') {
+    router.push({
+      name: 'SetJobUser',
+      query: {
+        ticketId: JobForm.id,
+        groupId:groupId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  }
+}
+
+// 获取所有点位和分组的数据
+const fetchAllGroupsAndPoints = async () => {
+  try {
+    // 分组信息
+    const groupRes = await jobPointGroup.getJobTicketGroupPage({pageSize: -1, pageNo: 1})
+    allGroups.value = groupRes
+    console.log('获取分组', groupRes)
+    // 获取当前sopId的分组
+    if (JobForm.id) {
+      groupList.value = await jobPointGroup.getJobTicketGroupPage({
+        pageSize: -1,
+        pageNo: 1,
+        ticketId: JobForm.id,
+      })
+    }
+
+    // 点位信息
+    const pointRes = await PointApi.getIsIsolationPointPage({pageSize: -1, pageNo: 1})
+    allPoints.value = pointRes?.list
+    console.log('获取点位', pointRes)
+  } catch (e) {
+
+  }
+}
+// 回显分组和点位数据的计算属性
+const resolvedGroupedPoints = computed(() => {
+  const groupsMap = new Map<string, { groupId: string; groupName: string; points: any[] }>()
+
+  JobForm.sopPointsList.forEach(item => {
+    const groupId = String(item.groupId)
+    const pointId = item.pointId
+
+    // 查分组名
+    const groupInfo = allGroups.value.find(g => String(g.id) === groupId)
+    const groupName = groupInfo?.groupName || '未分组'
+
+    // 查点位详情
+    const pointInfo = allPoints.value.find(p => p.id === pointId)
+    const pointName = pointInfo?.pointName
+    const pointIcon = pointInfo?.pointIcon
+
+    if (!groupsMap.has(groupId)) {
+      groupsMap.set(groupId, {
+        groupId,
+        groupName,
+        points: []
+      })
+    }
+
+    const group = groupsMap.get(groupId)!
+    // 防止重复添加
+    if (!group.points.some(p => p.pointId === pointId)) {
+      group.points.push({
+        pointId,
+        pointName,
+        pointIcon
+      })
+    }
+  })
+
+  return Array.from(groupsMap.values())
+})
+
+// 从 JobForm.sopUserList 中提取锁定人并按 groupId 分组
+const groupedLockers = computed(() => {
+  const lockerUsers = JobForm.sopUserList?.filter(u => u.userRole === 'jtlocker' && u.groupId != null) || []
+  const groupMap = new Map()
+
+  lockerUsers.forEach(user => {
+    if (!groupMap.has(user.groupId)) {
+      const groupName = groupList.value.find(g => g.id === user.groupId)?.groupName || '未命名分组'
+      groupMap.set(user.groupId, {groupId: user.groupId, groupName, users: []})
+    }
+    groupMap.get(user.groupId).users.push(user)
+  })
+
+  return Array.from(groupMap.values())
+})
+
+// 提取共锁人
+const coLockUsers = computed(() => {
+  return JobForm.sopUserList?.filter(u => u.userRole === 'jtcolocker') || []
+})
+// 获取基本信息
+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')
+  JobForm.modeId = value
+  // 清空画布
+  await clearCanvasProperly()
+  if (JobForm.modeId) {
+    const data = await ModeStepApi.getWorkflowStepPage({
+      pageNo: 1,
+      pageSize: -1,
+      modeId: JobForm.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)
+  }
+}
+// 监听 modeId 变化
+watch(
+  () => JobForm.modeId,
+  async (newValue, oldValue) => {
+    if (newValue) {
+      hasUnsavedChanges.value = true
+      await handleModeChange(newValue)
+    }
+  },
+  {immediate: true}
+)
+
+// 监听其他表单字段变化(用于标记未保存)
+watch(
+  () => [JobForm.ticketName, JobForm.ticketType, JobForm.workstationId],
+  () => {
+    hasUnsavedChanges.value = true
+  },
+  {deep: true}
+)
+// 保存成功后重置标记
+const submit = async () => {
+  try {
+    let data
+    let successMessage
+
+    if (JobForm.id) {
+      // 修改操作
+      data = await JobApi.updateJobTicket(JobForm)
+      successMessage = t('common.updateSuccess')
+
+      if (data) {
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+      }
+    } else {
+      // 新增操作
+      data = await JobApi.insertJobTicket(JobForm)
+      // 新增之后直接给sopStep新增步骤
+      const dataNew = await ModeStepApi.getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: JobForm.modeId
+      })
+      const ticketStepData = dataNew.list.map((item) => ({
+        ...item,
+        ticketId: data,
+      }))
+      // 导入步骤数据
+      await insertJobTicketStep(ticketStepData)
+      successMessage = t('common.createSuccess')
+
+      if (data) {
+        Visible.value=true //显示下面的内容
+        // 新增成功后,获取完整数据
+        try {
+          const selectData = await JobApi.selectJobTicketById(data)
+          if (selectData) {
+            // 正确更新 ref 的值
+            JobForm = {...JobForm, ...selectData}
+
+          }
+        } catch (selectError) {
+          console.warn('获取详情失败,但不影响保存:', selectError)
+          // 即使获取详情失败,也设置 id
+          JobForm.id = data
+        }
+
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+      }
+      // 确定之后直接切换修改页面
+      router.push({
+        path:'/jobTicket/jobTicket/job/UpdateJob',
+        query:{
+          id:data,
+          type:'update',
+        }
+
+      })
+    }
+  } 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()
+  fetchAllGroupsAndPoints()//获取所有分组和点位 来渲染SOP的首页
+})
+
+// 构建岗位查找映射
+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)
+}
+//ticketType改变函数
+const handleTicketpTypeChange = (value) => {
+  JobForm.ticketType = value
+}
+// 使用 watchEffect 更简洁
+watch(
+  [
+    () => JobForm.workstationId,
+    () => JobForm.machineryId,
+    () => JobForm.ticketType
+  ],
+  async ([workstationId, machineryId, ticketType]) => {
+    if (JobAutoName.value && workstationId && machineryId && ticketType) {
+      const typeName = getJobTypeName(ticketType)
+      if (typeName) {
+        await generateJobName()
+      } else {
+        console.log('票据类型没转换出来,不调用生成接口')
+      }
+    }
+  }
+)
+// 添加一个标识,记录是否是首次加载
+const isFirstLoad = ref(true)
+// 使用 watch 替代 watchEffect
+watch(
+  () => JobForm.workstationId,
+  async (newWorkstationId, oldWorkstationId) => {
+    // 只有在值真正改变且不是首次加载时才执行
+    if (newWorkstationId && newWorkstationId !== oldWorkstationId && !isFirstLoad.value) {
+      console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
+      JobForm.machineryId = null
+      await getMachineryData(newWorkstationId)
+    }
+    // 标记首次加载完成
+    isFirstLoad.value = false
+  }
+)
+
+// 获取工艺数据的函数
+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('获取工艺数据失败')
+  }
+}
+// 生成 Job 名称
+const generateJobName = async () => {
+  const workstationName = workstationMap.get(JobForm.workstationId)
+  const machineryName = machineryMap.get(JobForm.machineryId)
+  const typeName = getJobTypeName(JobForm.ticketType)
+  const currentDate = new Date().toISOString().split('T')[0]
+
+  if (!workstationName || !machineryName || !typeName) {
+    console.warn('字段未准备好:', { workstationName, machineryName, typeName })
+    return
+  }
+
+  const name = `${workstationName}-${machineryName}-${typeName}-${currentDate}`
+  console.log('最终 name:', name)
+  const nameNew = await JobApi.autoGenerateName(name)
+  JobForm.ticketName = nameNew
+}
+
+// 获取 Job 类型名称
+const getJobTypeName = (ticketType) => {
+  const JobTypeOptions = getStrDictOptions(DICT_TYPE.TICKET_TYPE)
+  const typeOption = JobTypeOptions.find((option) => option.value === ticketType)
+  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;
+  padding-right: 70px;
+}
+
+.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;
+}
+
+.group-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px; /* 卡片之间的间距 */
+}
+
+.point-group {
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  padding: 12px;
+  min-width: 250px;
+  background-color: #fafafa;
+  height: 250px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.group-title {
+  font-weight: 600;
+  font-size: 16px;
+  margin-bottom: 12px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #e0e0e0;
+  color: #333;
+}
+
+.points-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.point-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 60px;
+}
+
+.point-icon {
+  width: 40px;
+  height: 40px;
+  object-fit: contain;
+}
+
+.point-name {
+  font-size: 12px;
+  text-align: center;
+  margin-top: 4px;
+  color: #555;
+}
+
+//用户的卡片
+.group-container-user {
+  display: flex;
+  flex-direction: row;
+  gap: 16px;
+  overflow-x: auto;
+  padding-bottom: 10px;
+}
+
+.group-card-user {
+  width: 180px;
+  min-height: 150px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+  padding: 12px;
+  flex-shrink: 0;
+  border: 1px solid #eee;
+
+  display: flex;
+  flex-direction: column;
+  margin-top: 10px;
+}
+
+.group-title {
+  font-weight: bold;
+  font-size: 16px;
+  margin-bottom: 10px;
+  color: #333;
+  text-align: center;
+  border-bottom: 1px solid #f0f0f0;
+  padding-bottom: 6px;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: center;
+
+}
+.user-list-colocker{
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: flex-start;
+  margin-top: 10px;
+}
+.user-card {
+  width: 60px;
+  text-align: center;
+
+  img {
+    width: 40px;
+    height: 40px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+  }
+
+  .user-name {
+    font-size: 12px;
+    margin-top: 4px;
+    color: #555;
+    word-break: break-all;
+  }
+}
+
+
+</style>

+ 1247 - 0
src/views/jobTicket/job/CreateSopJob.vue

@@ -0,0 +1,1247 @@
+<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">作业创建步骤</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="JobForm"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="SOP" prop="sopId">
+                  <el-select
+                    v-model="JobForm.sopId"
+                    placeholder="请选择SOP"
+                    clearable
+                    class="!w-240px"
+                    @change="SopChangeFunction"
+                    @clear="SopClearFunction"
+                  >
+                    <el-option
+                      v-for="dict in SopListOption"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="dict.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="作业名称" prop="ticketName">
+                  <el-input
+                    v-model="JobForm.ticketName"
+                    placeholder="请输入作业名称"
+                    clearable
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="2">
+                <el-checkbox v-model="JobAutoName">自动生成</el-checkbox>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="作业区域" prop="workstationId">
+                  <el-tree-select
+                    v-model="JobForm.workstationId"
+                    :data="workstationOption"
+                    :props="{ label: 'workstationName', value: 'id', children: 'children' }"
+                    placeholder="选择作业区域"
+                    class="!w-240px"
+                    clearable
+                    @change="JobWorkstationChange"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="工艺设备" prop="machineryId">
+                  <el-tree-select
+                    v-model="JobForm.machineryId"
+                    :data="machineryOptions"
+                    :props="{ label: 'machineryName', value: 'id', children: 'children' }"
+                    placeholder="选择设备/工艺"
+                    class="!w-240px"
+                    @change="JobMacharyChange"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="作业类型" prop="ticketType">
+                  <el-select
+                    v-model="JobForm.ticketType"
+                    placeholder="请选择作业类型"
+                    clearable
+                    class="!w-240px"
+                    @change="handleTicketpTypeChange"
+                  >
+                    <el-option
+                      v-for="dict in getStrDictOptions(DICT_TYPE.TICKET_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="JobForm.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('SetJobModeStep', JobForm, null)">设置</div>
+        </div>
+        <div class="tab-content">
+          <!-- VueFlow 主画布 -->
+          <VueFlow style="width: 100%; height: 300px" :min-zoom="1" :max-zoom="1" :default-zoom="1">
+            <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('SetJobPoint', JobForm, null)">设置</div>
+        </div>
+        <div class="tab-content" style="height: 300px">
+          <div class="point_center_box" v-if="!JobForm?.ticketStepList?.length">
+            <img
+              src="../../../assets/images/添加.png"
+              alt=""
+              @click="goSetting('SetJobPoint', JobForm, null)"
+            />
+            <span style="color: red">*请添加需要进行隔离的点位</span>
+          </div>
+          <div v-else>
+            <!-- 循环渲染分组 -->
+            <div class="group-container">
+              <div v-for="group in resolvedGroupedPoints" :key="group.groupId" class="point-group">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="points-list">
+                  <div v-for="point in group.points" :key="point.pointId" class="point-item">
+                    <img :src="point.pointIcon" class="point-icon" />
+                    <div class="point-name">{{ point.pointName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </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('SetJobUser', JobForm, null)">设置</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 v-if="groupedLockers.length" class="group-container-user">
+              <div v-for="group in groupedLockers" :key="group.groupId" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <div v-for="user in group.users" :key="user.userId" class="user-card">
+                    <img src="@/assets/images/UserBlack.png" />
+                    <div class="user-name">{{ user.userName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- 有分组点数据时显示 -->
+            <div
+              v-else-if="JobForm.ticketGroupList && JobForm.ticketGroupList[0].groupName !== '默认分组'"
+              class="group-container"
+            >
+              <div v-for="group in JobForm.ticketGroupList" :key="group.id" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <img
+                    src="@/assets/images/添加.png"
+                    class="user-card"
+                    @click="goSetting('SetJobUser', JobForm, group.id)"
+                  />
+                </div>
+              </div>
+            </div>
+
+            <!-- 都没有数据时显示添加提示 -->
+            <div v-else class="point_center_box">
+              <img src="@/assets/images/添加.png" @click="goSetting('SetJobUser', JobForm, null)" />
+              <span>请添加参与锁定的人员</span>
+            </div>
+          </div>
+
+          <!-- 共锁人区域 -->
+          <div class="right_box">
+            <div class="tab-header">
+              <span class="tab-title">共锁人</span>
+            </div>
+            <div v-if="coLockUsers.length" class="user-list-colocker">
+              <div v-for="user in coLockUsers" :key="user.userId" class="user-card">
+                <img src="@/assets/images/UserBlack.png" />
+                <div class="user-name">{{ user.userName }}</div>
+              </div>
+            </div>
+            <div v-else class="point_center_box">
+              <img
+                src="@/assets/images/添加.png"
+                alt=""
+                @click="goSetting('SetJobUser', JobForm, null)"
+              />
+              <span>请添加参与共锁的人员</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </ContentWrap>
+
+    <div class="bottom-btn">
+      <el-button @click="submit('1')">
+        <el-icon>
+          <Check />
+        </el-icon>
+        创建并启动
+      </el-button>
+      <el-button @click="submit('0')">
+        <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 JobApi from '@/api/job/index'
+import * as SopApi from '@/api/sop/index'
+import * as JobPointGroupApi from '@/api/job/jobPointGroup'
+import * as JobPointApi from '@/api/job/jobPoint'
+import * as JobUserApi from '@/api/job/jobUser'
+import * as JobStepApi from  '@/api/job/jobStep'
+import * as PointApi from '@/api/dv/spm/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 JobForm = reactive({
+  createTime: null,
+  id: null,
+  machineryId: null,
+  machineryName: null,
+  modeId: null,
+  sopGroupList: null,
+  sopIndex: null,
+  sopName: null,
+  workstationId: null,
+  workstationName: null,
+  ticketCode: null,
+  ticketName: null,
+  sopId: null,
+  ticketType: null,
+  ticketContent: null,
+  ticketStatus: null,
+  ticketStartTime: null,
+  ticketEndTime: null,
+  ticketGroupList:null,
+  ticketPointsList:null,
+  ticketStepList:null,
+  ticketUserList:null,
+})
+const Visible = ref<boolean>(false)
+const JobAutoName = ref(false)
+const activeName = ref('1')
+const machineryOptions = ref()
+const workstationOption = ref()
+const SopListOption = ref()
+const ModeOption = ref()
+const allGroups = ref<any[]>([]) //获取所有分组
+const groupList = ref([]) //获取当前sopId的分组
+const allPoints = ref<any[]>([]) //获取所有点位
+const nodes = ref([]) //储存节点
+const edges = ref([]) // 存储连接线
+// 创建查找映射
+const workstationMap = new Map()
+const machineryMap = new Map()
+const router = useRouter()
+const route = useRoute()
+const SopchangeInit = ref(false)
+// SOP表单切换函数
+const SopChangeFunction = async (value) => {
+  if (value) {
+    const data = await SopApi.selectSopById(value)
+    JobForm.workstationId = data.workstationId
+    JobForm.machineryId = data.machineryId
+    JobForm.modeId = data.modeId
+    JobForm.ticketType = data.sopType
+    console.log(JobForm, 'JobForm-SOp')
+    await getOtherList(data.workstationId, data.machineryId, data.modeId, data.sopType)
+  }
+}
+// SOP清空函数
+const SopClearFunction = async () => {
+  const data = await SopApi.getSopPage({ pageNo: 1, pageSize: 1 })
+  SopListOption.value = data.list.map((item) => {
+    return {
+      label: item.sopName,
+      value: item.id
+    }
+  })
+  SopchangeInit.value = true
+  JobForm.workstationId = null
+  JobForm.machineryId = null
+  JobForm.modeId = null
+  JobForm.ticketType = null
+  await getOtherList(JobForm.workstationId, JobForm.machineryId, JobForm.modeId, JobForm.ticketType)
+}
+// 添加数据修改标记
+const hasUnsavedChanges = ref(false)
+const { addNodes, addEdges, setEdges, setNodes } = useVueFlow()
+//job区域切换函数
+const JobWorkstationChange = async (value) => {
+  JobForm.workstationId = value
+  SopchangeInit.value = false //只要不是sop切换就修改状态方便更换区域清空工艺
+  // 获取设备/工艺数据
+  const techRes = await TechnologyApi.listTechnology({
+    pageNo: 1,
+    pageSize: -1,
+    workstationId: JobForm.workstationId
+  })
+  const data = techRes.list.filter((item) => item.machineryType == '工艺')
+  machineryOptions.value = handleTree(data, 'id', 'parentId')
+  buildMachineryMap(data)
+}
+// 工艺改变函数
+const JobMacharyChange = async (value) => {
+  JobForm.machineryId = value
+  SopchangeInit.value = false //只要不是sop切换就修改状态方便更换区域清空工艺
+}
+//跳转设置对应页面
+const goSetting = (type, JobForm, groupId) => {
+  if (type == 'SetJobModeStep') {
+    router.push({
+      name: 'SetJobModeStep',
+      query: {
+        sopId: JobForm.id,
+        modeId: JobForm.modeId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  } else if (type == 'SetJobPoint') {
+    router.push({
+      name: 'SetJobPoint',
+      query: {
+        sopId: JobForm.id,
+        machineryId: JobForm.machineryId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  } else if (type == 'SetJobUser') {
+    router.push({
+      name: 'SetJobUser',
+      query: {
+        sopId: JobForm.id,
+        groupId: groupId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  }
+}
+
+// 获取所有点位和分组的数据
+const fetchAllGroupsAndPoints = async () => {
+  try {
+    // 分组信息
+    const groupRes = await JobPointGroupApi.getJobTicketGroupPage({ pageSize: -1, pageNo: 1 })
+    allGroups.value = groupRes.list
+    console.log('获取分组', allGroups.value)
+    // 获取当前ticketId的分组
+    if (JobForm.id) {
+      const groupData=await JobPointGroupApi.getJobTicketGroupPage({
+        pageSize: -1,
+        pageNo: 1,
+        ticketId: JobForm.id
+      })
+      groupList.value = groupData.list
+    }
+
+    // 点位信息
+    const pointRes = await PointApi.getIsIsolationPointPage({ pageSize: -1, pageNo: 1 })
+    allPoints.value = pointRes?.list
+    console.log('获取点位', pointRes)
+  } catch (e) {}
+}
+// 回显分组和点位数据的计算属性
+const resolvedGroupedPoints = computed(() => {
+  const groupsMap = new Map<string, { groupId: string; groupName: string; points: any[] }>()
+console.log( JobForm.ticketStepList,' JobForm.ticketStepList')
+  JobForm.ticketStepList.forEach((item) => {
+    const groupId = String(item.groupId)
+    const pointId = item.pointId
+
+    // 查分组名
+    const groupInfo = allGroups.value.find((g) => String(g.id) === groupId)
+    const groupName = groupInfo?.groupName || '未分组'
+
+    // 查点位详情
+    const pointInfo = allPoints.value.find((p) => p.id === pointId)
+    const pointName = pointInfo?.pointName
+    const pointIcon = pointInfo?.pointIcon
+
+    if (!groupsMap.has(groupId)) {
+      groupsMap.set(groupId, {
+        groupId,
+        groupName,
+        points: []
+      })
+    }
+
+    const group = groupsMap.get(groupId)!
+    // 防止重复添加
+    if (!group.points.some((p) => p.pointId === pointId)) {
+      group.points.push({
+        pointId,
+        pointName,
+        pointIcon
+      })
+    }
+  })
+
+  return Array.from(groupsMap.values())
+})
+
+// 从 JobForm.sopUserList 中提取锁定人并按 groupId 分组
+const groupedLockers = computed(() => {
+  const lockerUsers =
+    JobForm.ticketUserList?.filter((u) => u.userRole === 'jtlocker' && u.groupId != null) || []
+  const groupMap = new Map()
+
+  lockerUsers.forEach((user) => {
+    if (!groupMap.has(user.groupId)) {
+      const groupName =
+        groupList.value.find((g) => g.id === user.groupId)?.groupName || '未命名分组'
+      groupMap.set(user.groupId, { groupId: user.groupId, groupName, users: [] })
+    }
+    groupMap.get(user.groupId).users.push(user)
+  })
+
+  return Array.from(groupMap.values())
+})
+
+// 提取共锁人
+const coLockUsers = computed(() => {
+  return JobForm.ticketUserList?.filter((u) => u.userRole === 'jtcolocker') || []
+})
+// 获取基本信息
+const getOtherList = async (workstationId, machineryId, modeId) => {
+  try {
+    // 获取SOP列表下拉
+    const sopData = await SopApi.getSopPage({ pageNo: 1, pageSize: -1, sopStatus: '1' })
+    SopListOption.value = sopData.list.map((item) => {
+      return {
+        label: item.sopName,
+        value: item.id
+      }
+    })
+    // 获取岗位数据
+    const deptRes = await MarsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1, id: workstationId })
+    workstationOption.value = handleTree(deptRes.list, 'id', 'parentId')
+    buildWorkstationMap(deptRes.list)
+
+    // 获取设备/工艺数据
+    const techRes = await TechnologyApi.listTechnology({ pageNo: 1, pageSize: -1, id: machineryId })
+    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, id: modeId })
+    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')
+  JobForm.modeId = value
+  // 清空画布
+  await clearCanvasProperly()
+  if (JobForm.modeId) {
+    const data = await ModeStepApi.getWorkflowStepPage({
+      pageNo: 1,
+      pageSize: -1,
+      modeId: JobForm.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)
+  }
+}
+// 监听 modeId 变化
+watch(
+  () => JobForm.modeId,
+  async (newValue, oldValue) => {
+    if (newValue) {
+      hasUnsavedChanges.value = true
+      await handleModeChange(newValue)
+    }
+  },
+  { immediate: true }
+)
+
+// 监听其他表单字段变化(用于标记未保存)
+watch(
+  () => [JobForm.ticketName, JobForm.machineryId, JobForm.workstationId, JobForm.ticketType],
+  async () => {
+    hasUnsavedChanges.value = true
+    const params = {
+      pageNo: 1,
+      pageSize: -1,
+      machineryId: JobForm.machineryId,
+      workstationId: JobForm.workstationId,
+      ticketType: JobForm.ticketType
+    }
+    // 根据选中的数据反向筛选sop
+    const data = await SopApi.getSopPage(params)
+    SopListOption.value = data.list.map((item) => {
+      return {
+        label: item.sopName,
+        value: item.id
+      }
+    })
+  },
+  { deep: true }
+)
+// 保存成功后重置标记
+const submit = async (ifStart) => {
+  try {
+    let data
+    let successMessage
+
+    if (JobForm.id) {
+      // 修改操作
+      data = await JobApi.updateJobTicket(JobForm)
+      successMessage = t('common.updateSuccess')
+
+      if (data) {
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+      }
+    } else {
+      // 新增操作
+      const JobAddData={
+        ...JobForm,
+        ifStart: ifStart,
+      }
+      data = await JobApi.insertJobTicketBySop(JobAddData)
+      console.log(data, '调用的地方')
+
+      try {
+        successMessage = t('common.createSuccess')
+
+        // 新增成功后,获取完整数据
+        try {
+          const selectData = await JobApi.selectJobTicketById(data)
+          if (selectData) {
+            // 正确更新 ref 的值
+            Object.assign(JobForm, selectData)
+          }
+        } catch (selectError) {
+          console.warn('获取详情失败,但不影响保存:', selectError)
+          // 即使获取详情失败,也设置 id
+          JobForm.id = data
+        }
+
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+        Visible.value = true // 显示下面的内容
+
+        // 延迟跳转,确保所有操作完成
+        setTimeout(() => {
+          router.push({
+            path: '/jobTicket/jobTicket/job/UpdateJob',
+            query: {
+              id: data,
+              type: 'update'
+            }
+          })
+        }, 100)
+
+      } catch (sopError) {
+        console.error('SOP相关操作失败:', sopError)
+        // 如果子表插入失败,可以考虑回滚主表
+        message.error('SOP数据关联失败,请检查数据完整性')
+        throw sopError
+      }
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败: ' + (error.message || '未知错误'))
+  }
+}
+// 取消操作
+const cancel = async () => {
+  if (hasUnsavedChanges.value) {
+    try {
+      await ElMessageBox.confirm('当前页面有未保存的更改,确定要离开吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+      // 用户确认离开
+      router.push('/jobTicket/job')
+    } catch {
+      // 用户取消离开
+      console.log('用户取消离开')
+    }
+  } else {
+    // 没有未保存的更改,直接离开
+    router.push('/jobTicket/job')
+  }
+}
+
+// 初始化
+onMounted(() => {
+  getOtherList(null, null, null)
+  fetchAllGroupsAndPoints() //获取所有分组和点位 来渲染SOP的首页
+})
+
+// 构建岗位查找映射
+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)
+}
+//ticketType改变函数
+const handleTicketpTypeChange = (value) => {
+  JobForm.ticketType = value
+}
+// 使用 watchEffect 更简洁
+watch(
+  [
+    () => JobForm.workstationId,
+    () => JobForm.machineryId,
+    () => JobForm.ticketType
+  ],
+  async ([workstationId, machineryId, ticketType]) => {
+    if (JobAutoName.value && workstationId && machineryId && ticketType) {
+      const typeName = getJobTypeName(ticketType)
+      if (typeName) {
+        await generateJobName()
+      } else {
+        console.log('票据类型没转换出来,不调用生成接口')
+      }
+    }
+  }
+)
+
+
+// 添加一个标识,记录是否是首次加载
+const isFirstLoad = ref(true)
+// 使用 watch 替代 watchEffect
+watch(
+  () => JobForm.workstationId,
+  async (newWorkstationId, oldWorkstationId) => {
+    // 只有在值真正改变且不是首次加载时才执行
+    if (
+      newWorkstationId &&
+      newWorkstationId !== oldWorkstationId &&
+      !isFirstLoad.value &&
+      !SopchangeInit.value
+    ) {
+      console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
+      JobForm.machineryId = null
+      await getMachineryData(newWorkstationId)
+    }
+    // 标记首次加载完成
+    isFirstLoad.value = false
+  }
+)
+
+// 获取工艺数据的函数
+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('获取工艺数据失败')
+  }
+}
+// 生成 Job 名称
+const generateJobName = async () => {
+  const workstationName = workstationMap.get(JobForm.workstationId)
+  const machineryName = machineryMap.get(JobForm.machineryId)
+  const typeName = getJobTypeName(JobForm.ticketType)
+  const currentDate = new Date().toISOString().split('T')[0]
+
+  if (!workstationName || !machineryName || !typeName) {
+    console.warn('字段未准备好:', { workstationName, machineryName, typeName })
+    return
+  }
+
+  const name = `${workstationName}-${machineryName}-${typeName}-${currentDate}`
+  console.log('最终 name:', name)
+  const nameNew = await JobApi.autoGenerateName(name)
+  JobForm.ticketName = nameNew
+}
+
+
+// 获取 Job 类型名称
+const getJobTypeName = (ticketType) => {
+  const JobTypeOptions = getStrDictOptions(DICT_TYPE.TICKET_TYPE)
+  const typeOption = JobTypeOptions.find((option) => option.value === ticketType)
+  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;
+  padding-right: 70px;
+}
+
+.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;
+}
+
+.group-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px; /* 卡片之间的间距 */
+}
+
+.point-group {
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  padding: 12px;
+  min-width: 250px;
+  background-color: #fafafa;
+  height: 250px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.group-title {
+  font-weight: 600;
+  font-size: 16px;
+  margin-bottom: 12px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #e0e0e0;
+  color: #333;
+}
+
+.points-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.point-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 60px;
+}
+
+.point-icon {
+  width: 40px;
+  height: 40px;
+  object-fit: contain;
+}
+
+.point-name {
+  font-size: 12px;
+  text-align: center;
+  margin-top: 4px;
+  color: #555;
+}
+
+//用户的卡片
+.group-container-user {
+  display: flex;
+  flex-direction: row;
+  gap: 16px;
+  overflow-x: auto;
+  padding-bottom: 10px;
+}
+
+.group-card-user {
+  width: 180px;
+  min-height: 150px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+  padding: 12px;
+  flex-shrink: 0;
+  border: 1px solid #eee;
+
+  display: flex;
+  flex-direction: column;
+  margin-top: 10px;
+}
+
+.group-title {
+  font-weight: bold;
+  font-size: 16px;
+  margin-bottom: 10px;
+  color: #333;
+  text-align: center;
+  border-bottom: 1px solid #f0f0f0;
+  padding-bottom: 6px;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: center;
+}
+
+.user-list-colocker {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: flex-start;
+  margin-top: 10px;
+}
+
+.user-card {
+  width: 60px;
+  text-align: center;
+
+  img {
+    width: 40px;
+    height: 40px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+  }
+
+  .user-name {
+    font-size: 12px;
+    margin-top: 4px;
+    color: #555;
+    word-break: break-all;
+  }
+}
+</style>

+ 149 - 0
src/views/jobTicket/job/ModeView/StepFunction.vue

@@ -0,0 +1,149 @@
+<template>
+  <div>
+    <Dialog v-model="dialogVisible" title="步骤功能" width="500px" :before-close="handleClose">
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-width="180px"
+        label-position="left"
+        class="step-function-form"
+      >
+        <el-form-item label="取消作业">
+          <el-checkbox v-model="formData.enableCancelJob" disabled />
+        </el-form-item>
+
+        <el-form-item label="设置锁定人">
+          <el-checkbox v-model="formData.enableSetLocker" disabled />
+        </el-form-item>
+
+        <el-form-item label="设置共锁人">
+          <el-checkbox v-model="formData.enableSetColocker" disabled />
+        </el-form-item>
+
+        <el-form-item label="添加共锁人">
+          <el-checkbox v-model="formData.enableAddColocker" disabled />
+        </el-form-item>
+        <el-form-item label="添加共锁人后跳转步骤">
+          <el-input-number
+            v-model="formData.gotoStepAfterAddingColocker"
+            :min="1"
+            size="default"
+            placeholder="步骤号"
+            disabled
+
+          />
+        </el-form-item>
+        <el-form-item label="减少共锁人">
+          <el-checkbox v-model="formData.enableReduceColocker" disabled />
+        </el-form-item>
+
+        <el-form-item label="上锁">
+          <el-checkbox v-model="formData.enableLock" disabled />
+        </el-form-item>
+
+        <el-form-item label="共锁">
+          <el-checkbox v-model="formData.enableColock" disabled />
+        </el-form-item>
+
+        <el-form-item label="解除共锁">
+          <el-checkbox v-model="formData.enableReleaseColock" disabled />
+        </el-form-item>
+
+        <el-form-item label="解锁">
+          <el-checkbox v-model="formData.enableUnlock" disabled />
+        </el-form-item>
+
+        <el-form-item label="结束作业">
+          <el-checkbox v-model="formData.enableEndJob" disabled />
+        </el-form-item>
+
+
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleClose">关闭</el-button>
+        </div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, watch, computed } from 'vue'
+
+interface Props {
+  visible: boolean
+  stepData: any
+}
+
+interface Emits {
+  (e: 'update:visible', value: boolean): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 弹框显示状态
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (value) => emit('update:visible', value)
+})
+
+// 表单数据
+const formData = ref({
+  enableCancelJob: false,
+  enableSetLocker: false,
+  enableSetColocker: false,
+  enableAddColocker: false,
+  enableReduceColocker: false,
+  enableLock: false,
+  enableColock: false,
+  enableReleaseColock: false,
+  enableUnlock: false,
+  enableEndJob: false,
+  gotoStepAfterAddingColocker: null
+})
+// 监听步骤数据变化,更新表单数据
+watch(
+  () => props.stepData,
+  (newData) => {
+    if (newData && Object.keys(newData).length > 0) {
+      formData.value = {
+        enableCancelJob: newData.enableCancelJob || false,
+        enableSetLocker: newData.enableSetLocker || false,
+        enableSetColocker: newData.enableSetColocker || false,
+        enableAddColocker: newData.enableAddColocker || false,
+        enableReduceColocker: newData.enableReduceColocker || false,
+        enableLock: newData.enableLock || false,
+        enableColock: newData.enableColock || false,
+        enableReleaseColock: newData.enableReleaseColock || false,
+        enableUnlock: newData.enableUnlock || false,
+        enableEndJob: newData.enableEndJob || false,
+        gotoStepAfterAddingColocker: newData.gotoStepAfterAddingColocker || null
+      }
+    }
+  },
+  { immediate: true, deep: true }
+)
+
+// 关闭弹框
+const handleClose = () => {
+  dialogVisible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.step-function-form {
+  .el-form-item {
+    margin-bottom: 20px;
+  }
+
+  .el-checkbox {
+    margin-right: 0;
+  }
+}
+
+.dialog-footer {
+  text-align: center;
+}
+</style>

+ 118 - 0
src/views/jobTicket/job/ModeView/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 { updateJobTicketStep } from '@/api/job/jobStep'
+
+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 updateJobTicketStep(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('jobTableDetail')
+  if (tempData) {
+    stepData.value = JSON.parse(tempData)
+    stepDescription.value = stepData.value.stepDescription || ''
+    // 清除临时数据
+    localStorage.removeItem('jobTableDetail')
+  }
+
+})
+</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>

+ 439 - 0
src/views/jobTicket/job/ModeView/TableView.vue

@@ -0,0 +1,439 @@
+<template>
+  <div class="workflow-page">
+    <!-- 表格区域 -->
+    <div class="table-container">
+      <el-table ref="tableRef" :data="tableData" border stripe row-key="id">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="{ row }">
+            {{ row.stepIndex }}
+          </template>
+        </el-table-column>
+
+        <el-table-column label="图标" 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="标题">
+          <template #default="{ row }">
+            <el-input v-model="row.stepTitle" placeholder="标题" size="small"  @blur="saveRowData(row)"/>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="标题缩写">
+          <template #default="{ row }">
+            <el-input v-model="row.stepTitleShort" placeholder="标题缩写" size="small"  @blur="saveRowData(row)"/>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="确认方式">
+          <template #default="{ row }">
+            <el-select v-model="row.confirmType" placeholder="请选择确认方式" filterable clearable @change="saveRowData(row)">
+              <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="确认角色">
+          <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="确认人员">
+          <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="步骤功能" align="center">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="StepFunction(row)">查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column label="步骤操作说明" align="center">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="viewStepDetail(row)">查看</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 步骤功能弹框 -->
+    <StepFunctionDialog v-model:visible="stepFunctionVisible" :step-data="currentStepData" />
+  </div>
+
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
+import {
+  getJobTicketStepPage,
+  insertJobTicketStep,
+  updateJobTicketStep
+} from '@/api/job/jobStep'
+import { getRolePage } from '@/api/system/role'
+import { getRoleUser } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import StepFunctionDialog from './StepFunction.vue'
+import { getWorkflowStepPage } from '@/api/custonWorkflow/step'
+
+const route = useRoute()
+const router = useRouter()
+// 响应式数据
+const tableData = ref([])
+
+const tableRef = ref()
+const stepFunctionVisible = ref(false)
+// 当前选中的步骤数据
+const currentStepData = ref<any>({})
+const props = defineProps({
+  sopId: {
+    type: [String, Number],
+    default: null
+  },
+  modeId: {
+    type: [String, Number],
+    default: null
+  }
+})
+
+// 表格图标切换
+const selectIcon = (row, iconUrl) => {
+  row.stepIcon = iconUrl
+  saveRowData(row) // 你已有的接口保存方法
+}
+// 查看步骤详情
+const viewStepDetail = (row) => {
+  // 临时存储步骤数据
+  localStorage.setItem('jobTableDetail', JSON.stringify(row))
+  // 跳转到详情页面
+  router.push({
+    name: 'jobTableStepDetail',
+    query: {
+      stepId: row.id,
+      ticketId: route.query.ticketId
+    }
+  })
+}
+//查看步骤功能
+const StepFunction = (row) => {
+  currentStepData.value = row
+  stepFunctionVisible.value = true
+}
+
+// 自动保存单行数据
+const saveRowData = async (row) => {
+  try {
+    if (!row) return
+    await updateJobTicketStep(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 {
+    // 1. 首先检查是否已经有SOP步骤数据
+    const existingData = await getJobTicketStepPage({
+      pageNo: 1,
+      pageSize: -1,
+      ticketId: route.query.ticketId || props.ticketId
+    })
+
+    // 2. 如果已经有数据,直接回显
+    if (existingData && existingData.list && existingData.list.length > 0) {
+      console.log('发现已存在的Ticket步骤数据,直接回显')
+      tableData.value = existingData.list.sort((a, b) => {
+        const aIndex = a.stepIndex || 0
+        const bIndex = b.stepIndex || 0
+        return aIndex - bIndex
+      })
+      // 初始化更新Sop步骤接口
+      if (!existingData.list || existingData.list.length === 0) {
+        // 如果没有现有数据,才调用新增接口
+        await insertJobTicketStep(existingData.list)
+        console.log('初始化Ticket步骤成功')
+      } else {
+        console.log('Ticket步骤已存在,跳过初始化')
+      }
+      console.log(
+        '初始化数据完成,已按 stepIndex 排序:',
+        tableData.value.map((row) => row.stepIndex)
+      )
+      return // 直接返回,不需要后续操作
+    }
+
+    // 3. 如果没有数据,且有modeId,则从工作流导入步骤
+    if (props.modeId) {
+      console.log('没有现有数据,开始从工作流导入步骤')
+      const workflowData = await getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: props.modeId
+      })
+
+      if (workflowData && workflowData.list && workflowData.list.length > 0) {
+        console.log('获取到工作流步骤数据:', workflowData.list.length, '个步骤')
+
+        // 为每个步骤添加sopId
+        const jobStepData = workflowData.list.map((item) => ({
+          ...item,
+          ticketId: route.query.ticketId || props.ticketId,
+          stepId: item.id
+        }))
+
+        // 导入步骤数据
+        await insertJobTicketStep(jobStepData)
+        console.log('步骤数据导入成功')
+
+        // 导入成功后,重新获取数据并回显
+        const newData = await getJobTicketStepPage({
+          pageNo: 1,
+          pageSize: -1,
+          ticketId: route.query.ticketId || props.ticketId
+        })
+
+        if (newData && newData.list && newData.list.length > 0) {
+          tableData.value = newData.list.sort((a, b) => {
+            const aIndex = a.stepIndex || 0
+            const bIndex = b.stepIndex || 0
+            return aIndex - bIndex
+          })
+          console.log('导入后数据回显完成:', tableData.value.length, '个步骤')
+          ElMessage.success(`成功导入 ${newData.list.length} 个步骤`)
+        }
+      } else {
+        console.log('工作流中没有步骤数据')
+        tableData.value = []
+      }
+    } else {
+      // 4. 没有modeId也没有现有数据,初始化为空
+      console.log('没有modeId和现有数据,初始化为空表格')
+      tableData.value = []
+    }
+  } catch (error) {
+    console.error('初始化表格数据失败:', error)
+    ElMessage.error('加载步骤数据失败')
+    tableData.value = []
+  }
+}
+// 确认角色清空操作
+const handleRoleClear = (row) => {
+  row.confirmRoleCode = ''
+  row.confirmUser = ''
+}
+
+// 组件挂载时初始化
+onMounted(() => {
+  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>

+ 896 - 0
src/views/jobTicket/job/ModeView/WorkFlowView.vue

@@ -0,0 +1,896 @@
+<template>
+  <div style="padding: 20px">
+
+    <!-- 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
+              v-model="formData.stepTitle"
+              placeholder="标题"
+              size="small"
+              @blur="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="标题缩写">
+            <el-input
+              v-model="formData.stepTitleShort"
+              placeholder="标题缩写"
+              size="small"
+              @blur="handleFormChange"
+            />
+          </el-form-item>
+
+          <el-form-item label="确认方式">
+            <el-select
+              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
+              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
+              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="步骤功能">
+            <div class="step-function-container">
+              <div v-if="formData.enableCancelJob" class="function-item">
+
+                <el-input
+                  v-model="formData.cancelJobValue"
+                  placeholder="取消作业"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+              <!-- 设置锁定人 -->
+              <div v-if="formData.enableSetLocker" class="function-item">
+                <el-input
+                  v-model="formData.setLockerValue"
+                  placeholder="设置锁定人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+              <!-- 设置共锁人 -->
+              <div v-if="formData.enableSetColocker" class="function-item">
+                <el-input
+                  v-model="formData.setColockerValue"
+                  placeholder="设置共锁人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+              <!-- 添加共锁人 -->
+              <div v-if="formData.enableAddColocker" class="function-item">
+
+                <el-input
+                  v-model="formData.addColockerValue"
+                  placeholder="添加共锁人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+              <!-- 添加共锁人后跳转步骤 -->
+              <div v-if="formData.enableAddColocker && formData.gotoStepAfterAddingColocker" class="function-item">
+
+                <el-input-number
+                  v-model="formData.gotoStepAfterAddingColocker"
+                  :min="1"
+                  size="small"
+                  placeholder="添加共锁人后跳转步骤"
+                  disabled
+                  style="width: 120px"
+                />
+              </div>
+
+              <!-- 减少共锁人 -->
+              <div v-if="formData.enableReduceColocker" class="function-item">
+                <el-input
+                  v-model="formData.reduceColockerValue"
+                  placeholder="减少共锁人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+              <!-- 上锁 -->
+              <div v-if="formData.enableLock" class="function-item">
+                <el-input
+                  v-model="formData.lockValue"
+                  placeholder="上锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+              <!-- 共锁 -->
+              <div v-if="formData.enableColock" class="function-item">
+                <el-input
+                  v-model="formData.colockValue"
+                  placeholder="共锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+              <!-- 解除共锁 -->
+              <div v-if="formData.enableReleaseColock" class="function-item">
+                <el-input
+                  v-model="formData.releaseColockValue"
+                  placeholder="解除共锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+              <!-- 解锁 -->
+              <div v-if="formData.enableUnlock" class="function-item">
+                <el-input
+                  v-model="formData.unlockValue"
+                  placeholder="解锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+              <!-- 结束作业 -->
+              <div v-if="formData.enableEndJob" class="function-item">
+                <el-input
+                  v-model="formData.endJobValue"
+                  placeholder="结束作业"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+
+            </div>
+          </el-form-item>
+
+        </el-form>
+
+        <!-- 右侧富文本 -->
+        <div style="flex: 1">
+          <label style="font-weight: bold; display: block; margin-bottom: 8px">步骤操作说明</label>
+          <TinyMCE
+            v-model:value="formData.stepDescription"
+            :height="700"
+            placeholder="请输入内容..."
+            @change="handleFormChange"
+            @update:value="handleContentChange"
+          />
+        </div>
+      </div>
+    </div>
+
+  </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 {
+  getJobTicketStepPage,
+  updateJobTicketStep,
+} from '@/api/job/jobStep'
+
+import { getRolePage } from '@/api/system/role'
+import { getRoleUser } from '@/api/system/user'
+
+const route = useRoute()
+const {
+  addNodes,
+  addEdges,
+  onNodeClick,
+  onNodeContextMenu,
+  zoomTo,
+  updateNode,
+  onConnect,
+} = useVueFlow()
+const props = defineProps({
+  sopId: {
+    type: [String, Number],
+    default: null
+  },
+
+
+})
+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({
+  sopId: route.query.sopId || props.sopId,
+  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 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.ticketId || route.query.ticketId) {
+      const data = await getJobTicketStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        ticketId: route.query.ticketId || props.ticketId
+      })
+
+      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)
+        // 默认选中第一个节点(高亮显示)
+        if (sortedData.length > 0) {
+          const firstNode = sortedData[0];
+          if(firstNode) {
+            // 设置选中节点ID
+            selectedNodeId.value = `node-${firstNode.id}`;
+            // 显示表单
+            showForm.value = true;
+
+            // 更新表单数据
+            if (firstNode) {
+              Object.assign(formData, firstNode);
+            } else {
+              formData.stepIcon = firstNode.stepIcon || '';
+              formData.stepTitleShort = firstNode.stepTitleShort || '';
+              formData.stepIndex = firstNode.stepIndex || 1;
+            }
+
+            // 使用Vue Flow内置方法选中首个节点
+            const { addSelectedNodes } = useVueFlow();
+            addSelectedNodes([`node-${firstNode.id}`]);
+
+            // 更新节点样式为选中状态
+            updateNode(`node-${firstNode.id}`, (node) => {
+              node.style = {
+                ...node.style,
+                backgroundColor: '#ffec99', // 黄色背景
+                borderColor: '#f0c040', // 黄色边框
+                borderWidth: '2px'
+              };
+            });
+
+            console.log('默认选中第一个节点并高亮:', `node-${firstNode.id}`);
+          }
+        }
+      }
+    }
+  } 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)
+  }
+}
+
+
+// 表单变化处理(防抖) - 修正版本
+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
+      await updateJobTicketStep(row)
+      ElMessage.success(`更新步骤成功`)
+
+  } catch (error) {
+    console.error('保存步骤失败', error)
+    ElMessage.error('保存失败,请稍后重试')
+  }
+}
+
+// 节点点击处理 - 修正版本
+onNodeClick(({ node }) => {
+  // 清除所有节点的选中样式
+  nodes.value.forEach(n => {
+    updateNode(n.id, (nodeItem) => {
+      nodeItem.style = {
+        ...nodeItem.style,
+        backgroundColor: '#fff',
+        borderColor: '#999',
+        borderWidth: '1px'
+      };
+    });
+  });
+
+  if (selectedNodeId.value === node.id) {
+    showForm.value = false
+    selectedNodeId.value = null
+  } else {
+    selectedNodeId.value = node.id
+    showForm.value = true
+
+    // 设置当前节点为选中样式
+    updateNode(node.id, (nodeItem) => {
+      nodeItem.style = {
+        ...nodeItem.style,
+        backgroundColor: '#ffec99', // 黄色背景
+        borderColor: '#f0c040', // 黄色边框
+        borderWidth: '2px'
+      };
+    });
+
+    // 更新表单数据
+    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
+})
+
+
+// 角色获取
+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)
+})
+
+
+// 组件挂载时初始化
+onMounted(async () => {
+  // 并行执行初始化
+  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;
+  background: transparent;
+  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;
+}
+.step-function-container {
+  //border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding-top: 6px;
+  //background-color: #fafafa;
+  min-height: 60px;
+
+  .function-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 12px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .function-label {
+      font-weight: 500;
+      color: #606266;
+      margin-right: 12px;
+      min-width: 120px;
+    }
+  }
+
+  .no-function-tip {
+    text-align: center;
+    padding: 20px 0;
+    color: #909399;
+  }
+}
+/* 在组件的style部分添加 */
+.vue-flow__node.selected {
+  background-color: yellow !important;
+  border-color: #ff9800 !important;
+  box-shadow: 0 0 10px rgba(255, 152, 0, 0.5) !important;
+}
+</style>

+ 675 - 0
src/views/jobTicket/job/PointView/CardView.vue

@@ -0,0 +1,675 @@
+<template>
+  <div class="page-container">
+    <!-- 左侧面板 -->
+    <div class="left-panel">
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">点位清单</span>
+        </div>
+        <div class="tab-content">
+          <v-stage
+            ref="stageRef"
+            :config="{ width: stageWidth, height: stageHeight }"
+            class="konva-container"
+          >
+            <!-- 点位图层 -->
+            <v-layer>
+              <template v-for="(point, index) in filterData" :key="point.id">
+                <v-group :config="getGroupConfig(point, index)" @click="handlePointClick(point)">
+                  <!-- 点位背景矩形 -->
+                  <v-rect :config="getRectConfig()" />
+                  <!-- 点位图片 -->
+                  <v-image :config="getImageConfig(point)" />
+                  <!-- 点位名称 -->
+                  <v-text :config="getTextConfig(point)" />
+                </v-group>
+              </template>
+            </v-layer>
+          </v-stage>
+        </div>
+      </div>
+    </div>
+    <!-- 右侧分组容器 -->
+    <div class="right-panel">
+      <div class="group-container">
+        <div v-for="group in groupList" :key="group.id" class="group-item">
+          <div
+            class="custom-tabs-container2"
+            :class="{ 'selected-group': selectedGroupId === group.id }"
+            @click="selectGroup(group.id)"
+          >
+            <div class="tab-header2">
+              <template v-if="group.isEditing">
+                <el-input
+                  v-model="group.groupName"
+                  size="small"
+                  autofocus
+                  @blur="handleTitleBlur(group)"
+                  @keyup.enter="handleTitleBlur(group)"
+                  class="title-input2"
+                />
+              </template>
+              <template v-else>
+                <span class="tab-title2" @click="group.isEditing = true">
+                  {{ group.groupName }}
+                </span>
+              </template>
+              <div class="del-group-btn" v-if="groupList.length > 1" @click="removeGroup(group.id)">
+                删除分组
+              </div>
+            </div>
+            <div class="tab-content2">
+              <!-- 分组内容 - 使用 vue-konva 渲染点位 -->
+              <v-stage :config="{ width: 380, height: 220 }" class="right-konva-container">
+                <v-layer>
+                  <template v-for="(pointId, index) in group.pointIds" :key="pointId">
+                    <v-group
+                      :config="getGroupPointConfig(pointId, index)"
+                      @click="handleGroupPointClick(group.id, pointId)"
+                    >
+                      <!-- 点位背景矩形 -->
+                      <v-rect :config="getGroupRectConfig()" />
+                      <!-- 点位图片 -->
+                      <v-image :config="getGroupImageConfig(pointId)" />
+                      <!-- 点位名称 -->
+                      <v-text :config="getGroupTextConfig(pointId)" />
+                    </v-group>
+                  </template>
+                </v-layer>
+              </v-stage>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <!-- 底部按钮区 -->
+  <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>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
+import * as MachineryApi from '@/api/dv/technology/index'
+import * as PointApi from '@/api/dv/spm/index'
+import VueKonva from 'vue-konva'
+import { Check, Close } from '@element-plus/icons-vue'
+import * as jobApi from '@/api/job/index'
+import * as jobPointGroupApi from '@/api/job/jobPointGroup'
+import * as jobPointApi from  '@/api/job/jobPoint'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const boundPointIds = ref<number[]>([])
+const allPoints = ref<any[]>([])
+const filterData = ref<any[]>([])
+const stageRef = ref()
+const stageWidth = 800
+const stageHeight = 700
+// 左侧点位配置
+const rectWidth = 60 // 增大宽度
+const rectHeight = 100 // 增大高度,为标题预留空间
+const rectGapX = 5 // 横向间距加大
+const rectGapY = 10 // 纵向间距加大
+
+// 右侧分组内点位的尺寸配置
+const groupPointWidth = 40
+const groupPointHeight = 60
+const groupPointGapX = 10
+const groupPointGapY = 10
+
+const route = useRoute()
+const router=useRouter()
+const props = defineProps({
+  groupAdded: Boolean
+})
+// 记录当前选中的分组ID(高亮分组)
+const selectedGroupId = ref<number | null>(null)
+const groupList = ref([])
+
+
+// 移除 watch 中的异步调用,改为同步添加本地分组
+watch(
+  () => props.groupAdded,
+  async (newVal) => {
+    if (newVal) {
+      try {
+        const defaultName = `目标点位${groupList.value.length + 1}`
+        const response = await jobPointGroupApi.insertJobTicketGroup({
+          ticketId: route.query.ticketId,
+          groupName: defaultName
+        })
+
+        if (response) {
+          groupList.value.push({
+            id: response,
+            ticketId: route.query.ticketId,
+            groupName: defaultName,
+            isEditing: false,
+            pointIds: [] as number[]
+          })
+          console.log('新增分组成功:', groupList.value)
+        } else {
+          console.warn('新增分组失败,接口未返回 ID')
+        }
+      } catch (error) {
+        console.error('新增分组失败:', error)
+      }
+    }
+  }
+)
+
+
+// 放置filterData数据移动之后丢失 映射一个齐全的隔离点数据方便渲染右侧
+const pointMap = computed(() => {
+  const map = new Map<number, any>()
+  allPoints.value.forEach((point) => {
+    map.set(point.id, point)
+  })
+  return map
+})
+
+// 右侧标题修改
+const handleTitleBlur = async (group: (typeof groupList.value)[0]) => {
+  if (!group.groupName.trim()) {
+    group.groupName = `目标点位${group.id}`
+  }
+
+  try {
+    // 直接走更新接口(因为新增时已经通过监听完成)
+    const updateData = {
+      id: group.id,
+      ticketId: route.query.ticketId,
+      groupName: group.groupName
+    }
+    await jobPointGroupApi.updateJobTicketGroup(updateData)
+    console.log('更新分组成功:', group.id)
+  } catch (error) {
+    console.error('更新分组失败:', error)
+  }
+
+  group.isEditing = false
+}
+
+// 移除分组
+const removeGroup = async (groupId: number) => {
+  try {
+    // 调用删除分组接口
+    await jobPointGroupApi.deleteJobTicketGroupList(groupId)
+
+    // 接口调用成功后,从本地列表中移除
+    groupList.value = groupList.value.filter((group) => group.id !== groupId)
+
+    // 如果删除的是当前选中分组,自动选中第一个分组
+    if (selectedGroupId.value === groupId) {
+      selectedGroupId.value = groupList.value.length > 0 ? groupList.value[0].id : null
+    }
+
+    // 更新过滤数据
+    updateFilterData()
+
+  } catch (error) {
+
+  }
+}
+// 选中分组
+const selectGroup = (groupId: number) => {
+  selectedGroupId.value = groupId
+  console.log(groupId,'groupId')
+}
+// 左侧点位点击:添加到当前高亮分组
+const handlePointClick = (point) => {
+  if (!selectedGroupId.value) {
+    console.warn('没有选中任何分组')
+    return
+  }
+  const group = groupList.value.find(g => g.id === selectedGroupId.value)
+  if (!group) return
+
+  if (!group.pointIds.includes(point.id)) {
+    group.pointIds.push(point.id)
+    updateFilterData(true)
+  } else {
+    console.log('点位已存在于分组中')
+  }
+}
+
+
+
+const imageCache = new Map<string, HTMLImageElement>()
+
+// 计算左侧每个点位的坐标(自动换行)
+const getGroupConfig = (point: any, index: number) => {
+  // 每行可容纳的点位数量(基于舞台宽度和间距计算)
+  const itemsPerRow = Math.floor(stageWidth / (rectWidth + rectGapX))
+  const row = Math.floor(index / itemsPerRow)
+  const col = index % itemsPerRow
+  return {
+    x: col * (rectWidth + rectGapX) + 5, // 左侧预留20px边距
+    y: row * (rectHeight + rectGapY) + 5 // 顶部预留20px边距
+  }
+}
+
+// 左侧背景矩形配置(与点位尺寸匹配)
+const getRectConfig = () => ({
+  width: rectWidth,
+  height: rectHeight
+})
+
+// 左侧图标配置(居中显示在矩形上半部分)
+const getImageConfig = (point: any) => {
+  let img = imageCache.get(point.pointIcon)
+  if (!img) {
+    img = new window.Image()
+    img.src = point.pointIcon || '' // 空值处理
+    img.onerror = () => {
+      console.warn(`图标加载失败: ${point.pointIcon}`)
+    }
+    imageCache.set(point.pointIcon, img)
+  }
+  return {
+    image: img,
+    x: (rectWidth - 50) / 2, // 图标水平居中(图标宽40)
+    y: 10, // 距离顶部10px
+    width: 50,
+    height: 50
+  }
+}
+
+// 左侧隔离点标题配置(显示在图标正下方,矩形底部)
+const getTextConfig = (point: any) => ({
+  x: rectWidth - 62, // 水平居中(基于矩形宽度)
+  y: rectHeight - 35, // 距离底部20px(确保在图标下方)
+  text: point.pointName || '未命名', // 空值处理
+  fontSize: 12, // 适配尺寸
+  fill: '#333',
+  align: 'center', // 文字居中
+  verticalAlign: 'middle',
+  width: rectWidth, // 限制文字宽度,避免溢出
+  wrap: 'word' // 自动换行
+})
+
+// 右侧分组内点位的 group 配置
+const getGroupPointConfig = (pointId: number, index: number) => {
+  const itemsPerRow = Math.floor(280 / (groupPointWidth + groupPointGapX))
+  const row = Math.floor(index / itemsPerRow)
+  const col = index % itemsPerRow
+  return {
+    x: col * (groupPointWidth + groupPointGapX) + 5,
+    y: row * (groupPointHeight + groupPointGapY) + 5
+  }
+}
+
+// 右侧分组内点位的矩形配置
+const getGroupRectConfig = () => ({
+  width: groupPointWidth,
+  height: groupPointHeight,
+  shadowBlur: 1
+})
+
+// 右侧分组内点位的图片配置
+const getGroupImageConfig = (pointId: number) => {
+  const point = pointMap.value.get(pointId)
+  if (!point) return {}
+
+  let img = imageCache.get(point.pointIcon)
+  if (!img) {
+    img = new window.Image()
+    img.src = point.pointIcon || ''
+    imageCache.set(point.pointIcon, img)
+  }
+
+  return {
+    image: img,
+    x: (groupPointWidth - 30) / 2,
+    y: 5,
+    width: 30,
+    height: 30
+  }
+}
+
+// 右侧分组内点位的文本配置
+const getGroupTextConfig = (pointId: number) => {
+  const point = pointMap.value.get(pointId)
+  return {
+    x: 0,
+    y: groupPointHeight - 20,
+    text: point ? point.pointName : '未知',
+    fontSize: 10,
+    // fill: '#333',
+    align: 'center',
+    verticalAlign: 'middle',
+    width: groupPointWidth,
+    wrap: 'word'
+  }
+}
+
+// 右侧分组内点位点击:从分组移除(回归左侧)
+const handleGroupPointClick = async (groupId: number, pointId: number) => {
+  removePointFromGroup(groupId, pointId)
+  await updateFilterData(true)
+}
+
+// 从分组中移除点位的通用方法
+const removePointFromGroup = (groupId: number, pointId: number) => {
+  const group = groupList.value.find((g) => g.id === groupId)
+  if (!group) return
+  group.pointIds = group.pointIds.filter((id) => id !== pointId)
+}
+// 更新filterData
+const updateFilterData = async (skipReload = false) => {
+  if (!skipReload) {
+    try {
+      const groupData = await jobApi.selectJobTicketById(route.query.ticketId)
+console.log(groupData,'groupData')
+      if (groupData?.ticketPointsList?.length > 0) {
+        groupList.value.forEach(group => group.pointIds = [])
+        groupData.ticketPointsList.forEach((jobPoint: any) => {
+          const group = groupList.value.find(g => g.id === jobPoint.groupId)
+          if (group && !group.pointIds.includes(jobPoint.pointId)) {
+            group.pointIds.push(jobPoint.pointId)
+          }
+          if (!group) {
+            console.warn('找不到对应的分组!点位ID:', jobPoint.pointId, '点位groupId:', jobPoint.groupId, '本地 groupList:', groupList.value)
+          }
+        })
+      }
+    } catch (error) {
+      console.error('接口失败,回退本地计算')
+    }
+  }
+
+  // 用当前 groupList 计算 filterData
+  filterData.value = allPoints.value.filter((point: any) => {
+    const isIncluded = boundPointIds.value.includes(point.id)
+    const inAnyGroup = groupList.value.some((group) => group.pointIds.includes(point.id))
+    return isIncluded && !inAnyGroup
+  })
+}
+
+// 初始化数据
+const getOtherList = async () => {
+  try {
+    // 获取工艺绑定过的隔离点
+    const data = await MachineryApi.getTechnologyInfo(route.query.machineryId)
+    boundPointIds.value = data.pointIdList || []
+
+    // 获取所有隔离点
+    const pointData = await PointApi.getIsIsolationPointPage({ pageNo: 1, pageSize: -1 })
+    allPoints.value = pointData.list || []
+
+    // 先尝试获取job点位分组
+    const jobPointGroupData=await jobPointGroupApi.getJobTicketGroupPage({
+      pageNo: 1,
+      pageSize: -1,
+      ticketId: route.query.ticketId // 传递ticketId参数
+    })
+    const jobGroupData = jobPointGroupData.list
+
+    console.log(jobGroupData, 'jobGroupData')
+
+    if (jobGroupData && jobGroupData.length > 0) {
+      // 如果能获取到分组数据,直接使用
+      groupList.value = jobGroupData.map((group: any) => ({
+        id: group.id,
+        ticketId: group.ticketId,
+        groupName: group.groupName,
+        isEditing: false,
+        pointIds: []
+      }))
+    } else {
+      // 如果没有分组数据,调用Job接口获取默认分组
+      const groupData = await jobApi.selectJobTicketById(route.query.ticketId)
+      console.log(groupData, 'groupData')
+      groupList.value = groupData.ticketGroupList.map((group: any, index: number) => ({
+        id: group.id,
+        ticketId: group.ticketId,
+        groupName: group.groupName,
+        isEditing: false,
+        pointIds: []
+      }))
+    }
+
+    // 筛选出真正绑定的点位
+    updateFilterData()
+  } catch (error) {
+    console.error('获取数据失败:', error)
+    filterData.value = []
+  }
+}
+
+watch(
+  filterData,
+  async () => {
+    await nextTick()
+  },
+  { deep: true }
+)
+
+onMounted(async () => {
+  await getOtherList()
+})
+// 保存更新的信息
+const submit = async () => {
+  try {
+    const detailSop = await jobApi.selectJobTicketById(route.query.ticketId)
+    const oldPoints = detailSop.ticketPointsList || []
+
+    const oldKeySet = new Set(
+      oldPoints.map(item => `${item.groupId}_${item.pointId}`)
+    )
+
+    // 1. 生成当前用户操作的点位集合(newPoints)
+    const allCurrentPoints = []
+    groupList.value.forEach(group => {
+      group.pointIds.forEach(pointId => {
+        allCurrentPoints.push({
+          ticketId: route.query.ticketId,
+          groupId: group.id,
+          pointId: pointId
+        })
+      })
+    })
+
+    const newKeySet = new Set(
+      allCurrentPoints.map(item => `${item.groupId}_${item.pointId}`)
+    )
+
+    // 2. 计算出旧数据中已被移除的点位(需要删除)
+    const toDeleteList = oldPoints.filter(item => {
+      const key = `${item.groupId}_${item.pointId}`
+      return !newKeySet.has(key)
+    })
+
+    // 3. 删除接口
+    if (toDeleteList.length > 0) {
+      const deleteIds = toDeleteList.map(item => item.id)
+      await jobPointApi.deleteJobTicketPointsList(deleteIds)
+      message.success('保存成功')
+      console.log('已删除点位:', deleteIds)
+    }
+
+    // ✅ 4. 从 allCurrentPoints 中剔除掉旧数据中已存在的内容,仅新增
+    const oldKeySetAfterDelete = new Set(
+      oldPoints.map(item => `${item.groupId}_${item.pointId}`)
+    )
+    const newOnlyInsertPoints = allCurrentPoints.filter(item => {
+      const key = `${item.groupId}_${item.pointId}`
+      return !oldKeySetAfterDelete.has(key)
+    })
+
+    if (newOnlyInsertPoints.length > 0) {
+      await jobPointApi.insertJobTicketPoints(newOnlyInsertPoints)
+      message.success(t('common.createSuccess'))
+      console.log('新增成功:', newOnlyInsertPoints)
+    } else {
+      console.log('无新增点位')
+    }
+  } catch (error) {
+    console.error('提交失败:', error)
+  }
+}
+
+const cancel = () => {
+  router.push({
+    path:'/jobTicket/jobTicket/job/UpdateJob',
+    query:{
+      id:route.query.ticketId,
+      type:route.query.type,
+    }
+  })
+}
+</script>
+<style scoped lang="scss">
+// 外层页面容器:占满整个可视区域
+.page-container {
+  height: 100vh; // 占满屏幕高度
+  display: flex;
+}
+
+// 左侧面板
+.left-panel {
+  flex: 20;
+}
+
+// 右侧面板
+.right-panel {
+  flex: 4;
+  margin-left: 10px;
+  min-width: 600px;
+}
+
+// 右侧分组容器
+.group-container {
+  display: flex;
+  flex-direction: column; // 纵向堆叠
+  gap: 20px;
+  height: 100%; // 高度占满右侧面板
+  overflow-y: auto; // 超出高度时滚动
+  padding-right: 10px; // 预留滚动条空间
+  box-sizing: border-box;
+}
+
+// 单个分组
+.group-item {
+  width: 100%;
+  // 强制分组高度一致(可选)
+  min-height: 300px;
+}
+
+// 左侧容器样式
+.custom-tabs-container {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  height: 100%; // 占满左侧面板高度
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box; // 包含 border 计算高度
+}
+
+.tab-header {
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #dcdfe6;
+  padding: 12px 20px;
+  border-radius: 4px 4px 0 0;
+}
+
+.tab-content {
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 0 0 4px 4px;
+  flex: 1; // 占满剩余高度
+  overflow-y: auto; // 内容超出时滚动
+}
+
+// 右侧分组样式
+.custom-tabs-container2 {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  height: 300px; // 固定高度
+  min-width: 400px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.tab-header2 {
+  padding: 8px 16px;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #e4e7ed;
+  display: flex;
+  justify-content: space-between;
+  align-items: center; // 垂直居中
+  gap: 10px;
+}
+
+// 底部按钮区
+.bottom-btn {
+  display: flex;
+  gap: 10px; // 按钮间距
+  justify-content: flex-end;
+  padding-top: 10px;
+  border-top: 1px solid #eee; // 分隔线
+}
+
+// 其他样式保持不变(标题、输入框、删除按钮等)
+.tab-title2 {
+  cursor: pointer;
+  transition: color 0.2s;
+}
+
+.tab-title2:hover {
+  color: #409eff;
+}
+
+.title-input2 {
+  width: 120px;
+}
+
+.del-group-btn {
+  width: 70px;
+  height: 30px;
+  border: 1px solid #ccc;
+  border-radius: 6px;
+  text-align: center;
+  line-height: 30px;
+  cursor: pointer;
+  font-size: 12px; // 适配按钮大小
+}
+
+.konva-container {
+  width: 100%;
+  height: 700px;
+  background-color: #fff;
+  border: 1px dashed red; // 可视化调试
+}
+
+//选中右侧的分组高亮
+.selected-group {
+  border: 2px solid #409eff !important;
+  box-shadow: 0 0 8px #409eff33;
+}
+
+//右侧隔离点渲染
+.right-konva-container {
+  width: 100%;
+  height: 220px;
+  //background-color: #fafafa;
+  //border-radius: 4px;
+  margin: 10px;
+}
+</style>

+ 13 - 0
src/views/jobTicket/job/PointView/TableView.vue

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

+ 100 - 0
src/views/jobTicket/job/SetModeStep.vue

@@ -0,0 +1,100 @@
+<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'"
+            :ticketId="route.query.ticketId"
+            :modeId="route.query.modeId"
+          />
+          <WorkflowView v-else  :ticketId="route.query.ticketId" :modeId="route.query.modeId"/>
+        </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 route=useRoute()
+const tabPosition = ref('first')
+const goBack=()=>{
+  if(route.query.type=='update'){
+    router.push({
+      path:'/jobTicket/jobTicket/job/UpdateJob',
+      query:{
+        id:route.query.ticketId,
+        type:route.query.type,
+      }
+
+    })
+  }else {
+    router.push({
+      path:'/jobTicket/jobTicket/job/UpdateJob',
+      query:{
+        id:route.query.ticketId,
+        type:route.query.type,
+      }
+    })
+  }
+
+}
+</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>

+ 119 - 0
src/views/jobTicket/job/SetPoint.vue

@@ -0,0 +1,119 @@
+<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>
+          <div class="add-group-btn" @click="addPointGroup">添加分组</div>
+          <CardView
+            v-if="tabPosition == 'first'"
+            :ticketId="route.query.ticketId"
+            :group-added="groupAdded"
+          />
+          <TableView v-else :ticketId="route.query.ticketId" />
+        </div>
+      </div>
+    </ContentWrap>
+  </div>
+</template>
+<script setup lang="ts">
+import TableView from './PointView/TableView.vue'
+import CardView from './PointView/CardView.vue'
+
+const router = useRouter()
+const route = useRoute()
+const tabPosition = ref('first')
+// 定义信号
+const groupAdded = ref(false)
+const goBack=()=>{
+  if(route.query.type=='update'){
+    router.push({
+      path:'/jobTicket/jobTicket/job/UpdateJob',
+      query:{
+        id:route.query.ticketId,
+        type:route.query.type,
+      }
+
+    })
+  }else {
+    router.push({
+      path:'/jobTicket/jobTicket/job/UpdateJob',
+      query:{
+        id:route.query.ticketId,
+        type:route.query.type,
+      }
+    })
+  }
+
+}
+const addPointGroup = () => {
+  groupAdded.value = true // 触发信号
+  setTimeout(() => {
+    groupAdded.value = false // 重置信号(确保下次点击能再次触发)
+  }, 100)
+}
+</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;
+}
+
+.add-group-btn {
+  width: 70px;
+  height: 30px;
+  border: 1px solid black;
+  border-radius: 6px;
+  text-align: center;
+  line-height: 30px;
+  float: right;
+  cursor: pointer;
+}
+</style>

+ 104 - 0
src/views/jobTicket/job/SetUser.vue

@@ -0,0 +1,104 @@
+<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>
+
+          <CardView
+            v-if="tabPosition == 'first'"
+            :ticketId="route.query.ticketId"
+            :group-added="groupAdded"
+          />
+          <TableView v-else :ticketId="route.query.ticketId" />
+        </div>
+      </div>
+    </ContentWrap>
+  </div>
+</template>
+<script setup lang="ts">
+import TableView from './UserView/TableView.vue'
+import CardView from './UserView/CardView.vue'
+
+const router = useRouter()
+const route = useRoute()
+const tabPosition = ref('first')
+// 定义信号
+const groupAdded = ref(false)
+const goBack=()=>{
+  if(route.query.type=='update'){
+    router.push({
+      path:'/jobTicket/jobTicket/job/UpdateJob',
+      query:{
+        id:route.query.ticketId,
+        type:route.query.type,
+      }
+
+    })
+  }else {
+    router.push({
+      path:'/jobTicket/jobTicket/job/UpdateJob',
+      query:{
+        id:route.query.ticketId,
+        type:route.query.type,
+      }
+    })
+  }
+
+}
+
+</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>

+ 1348 - 0
src/views/jobTicket/job/UpdateJob.vue

@@ -0,0 +1,1348 @@
+<template>
+  <div>
+    <!--    job表单-->
+    <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">作业修改步骤</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="JobForm"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="SOP" prop="sopId">
+                  <el-select
+                    v-model="JobForm.sopId"
+                    placeholder="请选择SOP"
+                    clearable
+                    class="!w-240px"
+                    @change="SopChangeFunction"
+                    @clear="SopClearFunction"
+                  >
+                    <el-option
+                      v-for="dict in SopListOption"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="dict.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="作业名称" prop="ticketName">
+                  <el-input
+                    v-model="JobForm.ticketName"
+                    placeholder="请输入作业名称"
+                    clearable
+                    class="!w-240px"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="2">
+                <el-checkbox v-model="JobAutoName">自动生成</el-checkbox>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="5">
+                <el-form-item label="作业区域" prop="workstationId">
+                  <el-tree-select
+                    v-model="JobForm.workstationId"
+                    :data="workstationOption"
+                    :props="{ label: 'workstationName', value: 'id', children: 'children' }"
+                    placeholder="选择岗位"
+                    class="!w-240px"
+                    clearable
+                    @change="JobWorkstationChange"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="工艺设备" prop="machineryId">
+                  <el-tree-select
+                    v-model="JobForm.machineryId"
+                    :data="machineryOptions"
+                    :props="{ label: 'machineryName', value: 'id', children: 'children' }"
+                    placeholder="选择设备/工艺"
+                    class="!w-240px"
+                    @change="machineryChangeFunction"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="5">
+                <el-form-item label="作业类型" prop="ticketType">
+                  <el-select
+                    v-model="JobForm.ticketType"
+                    placeholder="请选择作业类型"
+                    clearable
+                    class="!w-240px"
+                    @change="handleTicketpTypeChange"
+                  >
+                    <el-option
+                      v-for="dict in getStrDictOptions(DICT_TYPE.TICKET_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="JobForm.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('SetJobModeStep', JobForm, null)">设置</div>
+        </div>
+        <div class="tab-content">
+          <!-- VueFlow 主画布 -->
+          <VueFlow style="width: 100%; height: 300px" :min-zoom="1" :max-zoom="1" :default-zoom="1">
+            <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('SetJobPoint', JobForm, null)">设置</div>
+        </div>
+        <div class="tab-content" style="height: 300px">
+          <div class="point_center_box" v-if="!JobForm?.ticketPointsList?.length">
+            <img
+              src="../../../assets/images/添加.png"
+              alt=""
+              @click="goSetting('SetJobPoint', JobForm, null)"
+            />
+            <span style="color: red">*请添加需要进行隔离的点位</span>
+          </div>
+          <div v-else>
+            <!-- 循环渲染分组 -->
+            <div class="group-container">
+              <div v-for="group in resolvedGroupedPoints" :key="group.groupId" class="point-group">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="points-list">
+                  <div v-for="point in group.points" :key="point.pointId" class="point-item">
+                    <img :src="point.pointIcon" class="point-icon" />
+                    <div class="point-name">{{ point.pointName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </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('SetJobUser', JobForm, 0)">设置</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 v-if="groupedLockers.length" class="group-container-user">
+              <div v-for="group in groupedLockers" :key="group.groupId" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <div v-for="user in group.users" :key="user.userId" class="user-card">
+                    <img src="@/assets/images/UserBlack.png" />
+                    <div class="user-name">{{ user.userName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- 有分组点数据时显示 -->
+            <div
+              v-else-if="JobForm.ticketGroupList && JobForm.ticketGroupList[0].groupName !== '默认分组'"
+              class="group-container"
+            >
+              <div v-for="group in JobForm.ticketGroupList" :key="group.id" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <img
+                    src="@/assets/images/添加.png"
+                    class="user-card"
+                    @click="goSetting('SetJobUser', JobForm, group.id)"
+                  />
+                </div>
+              </div>
+            </div>
+
+            <!-- 都没有数据时显示添加提示 -->
+            <div v-else class="point_center_box">
+              <img src="@/assets/images/添加.png" @click="goSetting('SetJobUser', JobForm, null)" />
+              <span>请添加参与锁定的人员</span>
+            </div>
+          </div>
+          <!-- 共锁人区域 -->
+          <div class="right_box">
+            <div class="tab-header">
+              <span class="tab-title">共锁人</span>
+            </div>
+            <div v-if="coLockUsers.length" class="user-list-colocker">
+              <div v-for="user in coLockUsers" :key="user.userId" class="user-card">
+                <img src="@/assets/images/UserBlack.png" />
+                <div class="user-name">{{ user.userName }}</div>
+              </div>
+            </div>
+            <div v-else class="point_center_box">
+              <img
+                src="@/assets/images/添加.png"
+                alt=""
+                @click="goSetting('SetJobUser', JobForm, null)"
+              />
+              <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'
+import * as JobApi from '@/api/job/index'
+import * as JobPointGroup from '@/api/job/jobPointGroup'
+import * as PointApi from '@/api/dv/spm/index'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+import { handleTree } from '@/utils/tree'
+import { ref } from 'vue'
+import { Handle, useVueFlow, VueFlow } from '@vue-flow/core'
+import { ElMessageBox } from 'element-plus'
+
+import {insertJobTicketStep} from "@/api/job/jobStep";
+
+const JobForm = reactive({
+  createTime: null,
+  id: null,
+  machineryId: null,
+  machineryName: null,
+  modeId: null,
+  sopGroupList: null,
+  sopIndex: null,
+  sopName: null,
+  workstationId: null,
+  workstationName: null,
+  ticketCode: null,
+  ticketName: null,
+  sopId: null,
+  ticketType: null,
+  ticketContent: null,
+  ticketStatus: null,
+  ticketStartTime: null,
+  ticketEndTime: null,
+  ticketGroupList:null,
+  ticketPointsList:null,
+  ticketStepList:null,
+  ticketUserList:null,
+})
+const JobAutoName = ref(false)
+const activeName = ref('0')
+const machineryOptions = ref()
+const workstationOption = ref()
+const SopListOption = ref()
+const ModeOption = ref()
+const allGroups = ref<any[]>([]) //获取所有分组
+const groupList = ref([]) //获取当前sopId的分组
+const allPoints = ref<any[]>([]) //获取所有点位
+const nodes = ref([]) //储存节点
+const edges = ref([]) // 存储连接线
+// 创建查找映射
+const workstationMap = new Map()
+const machineryMap = new Map()
+const router = useRouter()
+const route = useRoute()
+const SopchangeInit = ref(false)
+// SOP表单切换函数
+const SopChangeFunction = async (value) => {
+  if (value) {
+    const data = await SopApi.selectSopById(value)
+    JobForm.workstationId = data.workstationId
+    JobForm.machineryId = data.machineryId
+    JobForm.modeId = data.modeId
+    JobForm.ticketType = data.sopType
+    console.log(JobForm, 'JobForm-SOp')
+    await getOtherList(data.workstationId, data.machineryId, data.modeId, data.sopType)
+  }
+}
+// SOP清空函数
+const SopClearFunction = async () => {
+  const data = await SopApi.getSopPage({ pageNo: 1, pageSize: 1 })
+  SopListOption.value = data.list.map((item) => {
+    return {
+      label: item.sopName,
+      value: item.id
+    }
+  })
+  SopchangeInit.value = true
+  JobForm.workstationId = null
+  JobForm.machineryId = null
+  JobForm.modeId = null
+  JobForm.ticketType = null
+  await getOtherList(JobForm.workstationId, JobForm.machineryId, JobForm.modeId, JobForm.ticketType)
+}
+// 添加数据修改标记
+const hasUnsavedChanges = ref(false)
+const { addNodes, addEdges, setEdges, setNodes } = useVueFlow()
+//job区域切换函数
+const JobWorkstationChange = async (value) => {
+  JobForm.workstationId = value
+  SopchangeInit.value = false //只要不是sop切换就修改状态方便更换区域清空工艺
+  // 获取设备/工艺数据
+  const techRes = await TechnologyApi.listTechnology({
+    pageNo: 1,
+    pageSize: -1,
+    workstationId: JobForm.workstationId
+  })
+  const data = techRes.list.filter((item) => item.machineryType == '工艺')
+  machineryOptions.value = handleTree(data, 'id', 'parentId')
+  buildMachineryMap(data)
+}
+
+//跳转设置对应页面
+const goSetting = (type, JobForm, groupId) => {
+  if (type == 'SetJobModeStep') {
+    router.push({
+      name: 'SetJobModeStep',
+      query: {
+        ticketId: JobForm.id,
+        modeId: JobForm.modeId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  } else if (type == 'SetJobPoint') {
+    router.push({
+      name: 'SetJobPoint',
+      query: {
+        ticketId: JobForm.id,
+        machineryId: JobForm.machineryId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  } else if (type == 'SetJobUser') {
+    router.push({
+      name: 'SetJobUser',
+      query: {
+        ticketId: JobForm.id,
+        groupId: groupId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
+      }
+    })
+  }
+}
+
+// 获取所有点位和分组的数据
+const fetchAllGroupsAndPoints = async () => {
+  try {
+    // 分组信息
+    const groupRes = await JobPointGroup.getJobTicketGroupPage({ pageSize: -1, pageNo: 1 })
+    allGroups.value = groupRes.list
+    console.log('获取分组', groupRes.list)
+    // 获取当前ticketId的分组
+    if (route.query.id) {
+      const groupData=await JobPointGroup.getJobTicketGroupPage({
+        pageSize: -1,
+        pageNo: 1,
+        ticketId: route.query.id
+      })
+      groupList.value = groupData.list
+    }
+
+    // 点位信息
+    const pointRes = await PointApi.getIsIsolationPointPage({ pageSize: -1, pageNo: 1 })
+    allPoints.value = pointRes?.list
+    console.log('获取点位', pointRes)
+  } catch (e) {}
+}
+// 回显分组和点位数据的计算属性
+const resolvedGroupedPoints = computed(() => {
+  const groupsMap = new Map<string, { groupId: string; groupName: string; points: any[] }>()
+
+  console.log('JobForm.sopPointsList:', JobForm.ticketPointsList)
+  console.log('allGroups.value:', allGroups.value)
+  console.log('allPoints.value:', allPoints.value)
+
+  JobForm.ticketPointsList.forEach((item) => {
+    const groupId = String(item.groupId)
+    const pointId = item.pointId
+
+    console.log('处理项目:', { groupId, pointId, item })
+
+    // 查分组名
+    const groupInfo = allGroups.value.find((g) => String(g.id) === groupId)
+    const groupName = groupInfo?.groupName
+
+    console.log('找到的分组信息:', groupInfo, '分组名:', groupName)
+
+    // 查点位详情
+    const pointInfo = allPoints.value.find((p) => p.id == pointId)
+    const pointName = pointInfo?.pointName
+    const pointIcon = pointInfo?.pointIcon
+
+    console.log('找到的点位信息:', pointInfo, '点位名:', pointName)
+
+    if (!groupsMap.has(groupId)) {
+      groupsMap.set(groupId, {
+        groupId,
+        groupName,
+        points: []
+      })
+    }
+
+    const group = groupsMap.get(groupId)!
+
+    // 检查是否重复
+    const isDuplicate = group.points.some((p) => p.pointId === pointId)
+    console.log('是否重复:', isDuplicate, '当前组内点位:', group.points)
+
+    // 防止重复添加
+    if (!isDuplicate) {
+      group.points.push({
+        pointId,
+        pointName,
+        pointIcon
+      })
+      console.log('添加点位后:', group.points)
+    }
+  })
+
+  const result = Array.from(groupsMap.values())
+  console.log('最终结果:', result)
+  return result
+})
+// 从 JobForm.sopUserList 中提取锁定人并按 groupId 分组
+const groupedLockers = computed(() => {
+  const lockerUsers =
+    JobForm.ticketUserList?.filter((u) => u.userRole === 'jtlocker' && u.groupId != null) || []
+  const groupMap = new Map()
+
+  lockerUsers.forEach((user) => {
+    if (!groupMap.has(user.groupId)) {
+      const groupName =
+        groupList.value.find((g) => g.id === user.groupId)?.groupName || '未命名分组'
+      groupMap.set(user.groupId, { groupId: user.groupId, groupName, users: [] })
+    }
+    groupMap.get(user.groupId).users.push(user)
+  })
+
+  return Array.from(groupMap.values())
+})
+
+// 提取共锁人
+const coLockUsers = computed(() => {
+  return JobForm.ticketUserList?.filter((u) => u.userRole === 'jtcolocker') || []
+})
+// 获取基本信息
+const getOtherList = async (workstationId, machineryId, modeId) => {
+  try {
+    // 获取SOP列表下拉
+    const sopData = await SopApi.getSopPage({ pageNo: 1, pageSize: -1, sopStatus: '1' })
+    SopListOption.value = sopData.list.map((item) => {
+      return {
+        label: item.sopName,
+        value: item.id
+      }
+    })
+    // 获取sop信息
+    const JobData = await JobApi.selectJobTicketById(route.query.id)
+
+    Object.assign(JobForm, JobData)
+    console.log(JobData, 'aaa')
+    // 获取岗位数据
+    const deptRes = await MarsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1, id: workstationId })
+    workstationOption.value = handleTree(deptRes.list, 'id', 'parentId')
+    buildWorkstationMap(deptRes.list)
+
+    // 获取设备/工艺数据
+    const techRes = await TechnologyApi.listTechnology({ pageNo: 1, pageSize: -1, id: machineryId })
+    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, id: modeId })
+    ModeOption.value = modeRes.list.map((item) => ({
+      label: item.modeName,
+      value: item.id
+    }))
+
+    console.log('数据加载完成')
+  } catch (error) {
+    // console.error('获取数据失败:', error)
+    // ElMessage.error('获取数据失败')
+  }
+}
+// 工艺/设备手动切换函数
+const machineryChangeFunction = async (value) => {
+  const isUpdateMachinery = route.query.type === 'update'
+  const data = await JobApi.selectJobTicketById(route.query.id)
+  const oldMachineryId = data.machineryId
+  const oldWorkstationId = data.workstationId
+  const isMachineryChanged = oldMachineryId !== value
+
+  if (isUpdateMachinery && isMachineryChanged) {
+    try {
+      await ElMessageBox.confirm(
+        '警告:切换设备工艺将清空对应所有数据,是否继续?',
+        '工艺/设备切换确认',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      )
+
+      // 用户点击了“确定”
+      JobForm.machineryId = value
+
+    } catch (error) {
+      // 用户取消
+      isReverting.value = true // 避免触发 watch 的副作用
+      JobForm.machineryId = oldMachineryId
+      JobForm.workstationId = oldWorkstationId
+      await getMachineryData(oldWorkstationId)
+      console.log('用户取消了切换,回退旧值')
+    }
+  } else {
+    JobForm.machineryId = value
+  }
+}
+
+// 模式初始化
+const onModeChange = async (value) => {
+  JobForm.modeId = value
+  await clearCanvasProperly()
+
+  if (JobForm.modeId && route.query.type !== 'update') {
+    const data = await ModeStepApi.getWorkflowStepPage({
+      pageNo: 1,
+      pageSize: -1,
+      modeId: JobForm.modeId
+    })
+
+    const sortedData = data.list.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
+    renderNodesFromData(sortedData)
+    renderEdgesFromData(sortedData)
+  } else if (JobForm.modeId && route.query.type == 'update') {
+    const Data = await JobApi.selectJobTicketById(route.query.id)
+    const sortedData = Data.ticketStepList.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
+    renderNodesFromData(sortedData)
+    renderEdgesFromData(sortedData)
+  }
+}
+
+// 流程模式手动切换
+const isModeChangedBoolean = ref(false)//给确认按钮是否更改了模式做判断
+const handleModeChange = async (value) => {
+  const isUpdateMode = route.query.type === 'update'
+  const data = await JobApi.selectJobTicketById(route.query.id)
+  const oldModeId = data.modeId
+  const isModeChanged = oldModeId !== value
+
+  // 修改模式且模式发生变化,需要确认
+  if (isUpdateMode && isModeChanged) {
+    try {
+      await ElMessageBox.confirm('警告:切换流程模式将清空对应所有数据,是否继续?', '模式切换确认', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+
+    } catch (error) {
+      // 用户取消操作
+      JobForm.modeId = oldModeId
+      isModeChangedBoolean.value = true
+      console.log('用户取消了模式切换')
+      return
+    }
+  }
+
+  // 更新模式ID
+  JobForm.modeId = value
+
+  // 清空画布
+  await clearCanvasProperly()
+
+  // 根据情况加载和渲染数据
+  if (isUpdateMode && !isModeChanged) {
+    // 修改模式但模式未变化:从SOP数据渲染
+    const Data = await SopApi.selectSopById(route.query.id)
+    if (Array.isArray(Data.sopStepList) && Data.sopStepList.length > 0) {
+      const sortedData = Data.sopStepList.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
+      renderNodesFromData(sortedData)
+      renderEdgesFromData(sortedData)
+    }
+  } else {
+    // 新增模式或模式发生变化:从模式模板获取数据
+    if (JobForm.modeId) {
+      const data = await ModeStepApi.getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: JobForm.modeId
+      })
+
+      if (Array.isArray(data.list) && data.list.length > 0) {
+        const sortedData = data.list.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
+
+        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)
+  }
+}
+// 监听 modeId 变化
+watch(
+  () => JobForm.modeId,
+  async (newValue) => {
+    hasUnsavedChanges.value = true
+
+    await onModeChange(newValue) // 标记是初始化
+  },
+  { immediate: true }
+)
+
+// 监听其他表单字段变化(用于标记未保存)
+// 监听其他表单字段变化(用于标记未保存)
+watch(
+  () => [JobForm.ticketName, JobForm.machineryId, JobForm.workstationId, JobForm.ticketType],
+  async () => {
+    hasUnsavedChanges.value = true
+    const params = {
+      pageNo: 1,
+      pageSize: -1,
+      machineryId: JobForm.machineryId,
+      workstationId: JobForm.workstationId,
+      ticketType: JobForm.ticketType
+    }
+    // 根据选中的数据反向筛选sop
+    const data = await SopApi.getSopPage(params)
+    SopListOption.value = data.list.map((item) => {
+      return {
+        label: item.sopName,
+        value: item.id
+      }
+    })
+  },
+  { deep: true }
+)
+
+// 保存成功后重置标记
+const submit = async () => {
+  try {
+    let data
+    let successMessage
+
+    if (JobForm.id) {
+      // 修改操作
+      data = await JobApi.updateJobTicket(JobForm)
+
+      successMessage = t('common.updateSuccess')
+
+      if (data) {
+        message.success(successMessage)
+        // 如果修改了模式步骤 重新插入最新的模式步骤
+        if(isModeChangedBoolean.value){
+          const dataStep = await ModeStepApi.getWorkflowStepPage({
+            pageNo: 1,
+            pageSize: -1,
+            modeId: JobForm.modeId
+          })
+          const jobStepData = dataStep.list.map((item) => ({
+            ...item,
+            ticketId: data
+          }))
+          // 导入步骤数据
+          await insertJobTicketStep(jobStepData)
+          isModeChangedBoolean.value = false
+        }
+
+        hasUnsavedChanges.value = false
+      }
+    } else {
+      // 新增操作
+      data = await JobApi.insertJobTicketBySop(JobForm)
+      successMessage = t('common.createSuccess')
+
+      if (data) {
+
+        // 新增成功后,获取完整数据
+        try {
+          const selectData = await JobApi.selectJobTicketById(data)
+          if (selectData) {
+            // 正确更新 ref 的值
+            JobForm = { ...JobForm, ...selectData }
+          }
+        } catch (selectError) {
+          console.warn('获取详情失败,但不影响保存:', selectError)
+          // 即使获取详情失败,也设置 id
+          JobForm.id = data
+        }
+
+        message.success(successMessage)
+        hasUnsavedChanges.value = false
+      }
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败')
+  }
+}
+
+// 取消操作
+const cancel = async () => {
+  if (hasUnsavedChanges.value) {
+    try {
+      await ElMessageBox.confirm('当前页面有未保存的更改,确定要离开吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+      // 用户确认离开
+      router.push('/jobTicket/job')
+    } catch {
+      // 用户取消离开
+      console.log('用户取消离开')
+    }
+  } else {
+    // 没有未保存的更改,直接离开
+    router.push('/jobTicket/job')
+  }
+}
+
+// 初始化
+onMounted(() => {
+  getOtherList()
+  fetchAllGroupsAndPoints() //获取所有分组和点位 来渲染SOP的首页
+})
+
+// 构建岗位查找映射
+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)
+}
+//ticketType改变函数
+const handleTicketpTypeChange = (value) => {
+  JobForm.ticketType = value
+}
+// 使用 watchEffect 更简洁
+watch(
+  [
+    () => JobForm.workstationId,
+    () => JobForm.machineryId,
+    () => JobForm.ticketType
+  ],
+  async ([workstationId, machineryId, ticketType]) => {
+    if (JobAutoName.value && workstationId && machineryId && ticketType) {
+      const typeName = getJobTypeName(ticketType)
+      if (typeName) {
+        await generateJobName()
+      } else {
+        console.log('票据类型没转换出来,不调用生成接口')
+      }
+    }
+  }
+)
+// 添加一个标识,记录是否是首次加载
+const isFirstLoad = ref(true)
+// 修改设备工艺回退操作
+const isReverting = ref(false) // 标记是否为取消操作回退
+
+watch(
+  () => JobForm.workstationId,
+  async (newWorkstationId, oldWorkstationId) => {
+    if (isFirstLoad.value) {
+      isFirstLoad.value = false
+      return
+    }
+
+    // 取消操作导致的回退,跳过 watch
+    if (isReverting.value) {
+      isReverting.value = false
+      return
+    }
+    if(!SopchangeInit.value){
+      return
+    }
+
+    if (newWorkstationId && newWorkstationId !== oldWorkstationId) {
+      console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
+      JobForm.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('获取工艺数据失败')
+  }
+}
+// 生成 Job 名称
+const generateJobName = async () => {
+  const workstationName = workstationMap.get(JobForm.workstationId)
+  const machineryName = machineryMap.get(JobForm.machineryId)
+  const typeName = getJobTypeName(JobForm.ticketType)
+  const currentDate = new Date().toISOString().split('T')[0]
+
+  if (!workstationName || !machineryName || !typeName) {
+    console.warn('字段未准备好:', { workstationName, machineryName, typeName })
+    return
+  }
+
+  const name = `${workstationName}-${machineryName}-${typeName}-${currentDate}`
+  console.log('最终 name:', name)
+  const nameNew = await JobApi.autoGenerateName(name)
+  JobForm.ticketName = nameNew
+}
+
+// 获取 Job 类型名称
+const getJobTypeName = (ticketType) => {
+  const JobTypeOptions = getStrDictOptions(DICT_TYPE.TICKET_TYPE)
+  const typeOption = JobTypeOptions.find((option) => option.value === ticketType)
+  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;
+  padding-right: 70px;
+}
+
+.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;
+}
+
+.group-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px; /* 卡片之间的间距 */
+}
+
+.point-group {
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  padding: 12px;
+  min-width: 250px;
+  background-color: #fafafa;
+  height: 250px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.group-title {
+  font-weight: 600;
+  font-size: 16px;
+  margin-bottom: 12px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #e0e0e0;
+  color: #333;
+}
+
+.points-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.point-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 60px;
+}
+
+.point-icon {
+  width: 40px;
+  height: 40px;
+  object-fit: contain;
+}
+
+.point-name {
+  font-size: 12px;
+  text-align: center;
+  margin-top: 4px;
+  color: #555;
+}
+
+//用户的卡片
+.group-container-user {
+  display: flex;
+  flex-direction: row;
+  gap: 16px;
+  overflow-x: auto;
+  padding-bottom: 10px;
+}
+
+.group-card-user {
+  width: 180px;
+  min-height: 150px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+  padding: 12px;
+  flex-shrink: 0;
+  border: 1px solid #eee;
+
+  display: flex;
+  flex-direction: column;
+  margin-top: 10px;
+}
+
+.group-title {
+  font-weight: bold;
+  font-size: 16px;
+  margin-bottom: 10px;
+  color: #333;
+  text-align: center;
+  border-bottom: 1px solid #f0f0f0;
+  padding-bottom: 6px;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: center;
+}
+
+.user-list-colocker {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: flex-start;
+  margin-top: 10px;
+}
+
+.user-card {
+  width: 60px;
+  text-align: center;
+
+  img {
+    width: 40px;
+    height: 40px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+  }
+
+  .user-name {
+    font-size: 12px;
+    margin-top: 4px;
+    color: #555;
+    word-break: break-all;
+  }
+}
+</style>

+ 492 - 0
src/views/jobTicket/job/UserView/CardView.vue

@@ -0,0 +1,492 @@
+<template>
+  <div class="page-container">
+    <div class="main-layout">
+      <!-- 左侧 -->
+      <div class="left-side">
+        <!-- 可选择的锁定人 -->
+        <el-card class="card-block" style="height: 300px">
+          <template #header>可选择的锁定人</template>
+          <div class="user-list">
+            <div
+              v-for="user in jtlocker"
+              :key="user.id"
+              class="user-card"
+              :class="{ selected: isUserInSelectedGroup(user) }"
+              @click="selectUser(user, 'lock')"
+            >
+              <img src="@/assets/images/UserEmpty.png" />
+              <div class="user-name">{{ user.nickname }}</div>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 可选择的共锁人 -->
+        <el-card class="card-block" style="height: 350px">
+          <template #header>可选择的共锁人</template>
+          <div class="user-list">
+            <div
+              v-for="user in jtcolocker"
+              :key="user.id"
+              class="user-card"
+              @click="selectUser(user, 'coLock')"
+            >
+              <img src="@/assets/images/UserEmpty.png" />
+              <div class="user-name">{{ user.nickname }}</div>
+            </div>
+          </div>
+        </el-card>
+      </div>
+
+      <!-- 右侧 -->
+      <div class="right-side">
+        <!-- 已添加的锁定人 -->
+        <el-card class="card-block red-border" style="height: 300px">
+          <template #header>已添加的锁定人</template>
+          <div class="group-container">
+            <div
+              v-for="group in allGroup"
+              :key="group.id"
+              class="group-card"
+              :class="{ active: selectedGroupId == group.id }"
+              @click="selectGroup(group.id)"
+            >
+              <el-card class="inner-card">
+                <template #header>{{ group.groupName }}</template>
+                <div class="user-list">
+                  <div
+                    class="user-card"
+                    v-for="user in addedLockUsers[group.id] || []"
+                    :key="user.id"
+                    @click.stop="removeLockUser(user, group.id)"
+                  >
+                    <img src="@/assets/images/UserBlack.png" />
+                    <div class="user-name">{{ user.nickname }}</div>
+                  </div>
+                </div>
+              </el-card>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 已添加的共锁人 -->
+        <el-card class="card-block orange-border" style="height: 350px">
+          <template #header>已添加的共锁人</template>
+          <div class="user-list">
+            <div
+              v-for="user in addedCoLockUsers"
+              :key="user.id"
+              class="user-card"
+              @click="removeCoLockUser(user)"
+            >
+              <img src="@/assets/images/UserBlack.png" />
+              <div class="user-name">{{ user.nickname }}</div>
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="bottom-btn">
+      <el-button type="primary" @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 { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import * as RoleUser from '@/api/system/user'
+import * as JobUserApi from '@/api/job/jobUser'
+import * as JobApi from '@/api/job/index'
+import * as JobPointGroupApi from '@/api/job/jobPointGroup'
+
+import { Check, Close } from '@element-plus/icons-vue'
+
+const route = useRoute()
+const router = useRouter()
+const allGroup = ref<any[]>([])
+const jtlocker = ref<any[]>([])
+const jtcolocker = ref<any[]>([])
+
+const addedLockUsers = ref<Record<string, any[]>>({})
+const addedCoLockUsers = ref<any[]>([])
+const selectedGroupId = ref<string>('')
+const toDeleteUserIds = ref<number[]>([]) //删除的人员
+// 新增:保存当前票已存在的用户
+const existingUserList = ref<any[]>([])
+// 加载数据
+onMounted(async () => {
+  const groupData = await JobPointGroupApi.getJobTicketGroupPage({
+    pageNo: 1,
+    pageSize: -1,
+    ticketId: route.query.ticketId
+  })
+
+  allGroup.value = groupData.list
+  jtlocker.value = await RoleUser.getRoleUser('jtlocker')
+  if(route.query.groupId&&route.query.groupId!=='0'){
+    selectedGroupId.value=route.query.groupId
+  }else if(route.query.groupId=='0'){
+    // 作业通过sop创建之后从设置点进来此时默认选中第一个分组
+    selectedGroupId.value=groupData.list[0].id
+  }else{
+    selectedGroupId.value = ''
+  }
+
+  const JobUserData = await JobApi.selectJobTicketById(route.query.ticketId)
+  console.log(JobUserData,'JobUserData')
+
+  if (Array.isArray(JobUserData.ticketUserList) && JobUserData.ticketUserList.length > 0) {
+    const userList = JobUserData.ticketUserList
+    existingUserList.value = JobUserData.ticketUserList//给新增用来判断后端已有数据
+    // 1. 渲染锁定人
+    userList
+      .filter((user) => user.userRole == 'jtlocker')
+      .forEach((user) => {
+        const groupId = user.groupId
+        if (!groupId) return // 没分组就跳过
+
+        if (!addedLockUsers.value[groupId]) {
+          addedLockUsers.value[groupId] = []
+
+        }
+
+        addedLockUsers.value[groupId].push({
+          id: user.id,
+          userId: user.userId,
+          nickname: user.userName
+        })
+      })
+
+
+    // 2. 渲染共锁人(groupId 为 null)
+    userList
+      .filter((user) => user.userRole === 'jtcolocker')
+      .forEach((user) => {
+        addedCoLockUsers.value.push({
+          id: user.id,
+          userId: user.userId,
+          nickname: user.userName
+        })
+
+        // 从左侧移除已分配的共锁人
+        jtcolocker.value = jtcolocker.value.filter((u) => u.id !== user.userId)
+      })
+  } else {
+
+    // 渲染初始化的
+
+    jtcolocker.value = await RoleUser.getRoleUser('jtcolocker')
+  }
+})
+
+// 切换分组
+const selectGroup = (groupId: string) => {
+  console.log('选中分组:', groupId)
+  selectedGroupId.value = groupId
+}
+
+// 是否在选中的分组中(只检查当前选中的分组)
+const isUserInSelectedGroup = (user) => {
+
+  if (!selectedGroupId.value) return false
+  const groupList = addedLockUsers.value[selectedGroupId.value] || []
+  return groupList.some((u) => u.id == user.id)
+}
+
+// 添加 or 移除
+const selectUser = (user, type: 'lock' | 'coLock') => {
+  if (type === 'lock') {
+    if (!selectedGroupId.value) {
+      ElMessage.warning('请先选择一个分组')
+      return
+    }
+
+    const groupList = addedLockUsers.value[selectedGroupId.value] || []
+    const existingIndex = groupList.findIndex((u) => u.id === user.id)
+
+    if (existingIndex !== -1) {
+      groupList.splice(existingIndex, 1)
+    } else {
+      if (!addedLockUsers.value[selectedGroupId.value]) {
+        addedLockUsers.value[selectedGroupId.value] = []
+      }
+      addedLockUsers.value[selectedGroupId.value].push(user)
+    }
+  } else {
+    const index = jtcolocker.value.findIndex((u) => u.id === user.id)
+    if (index !== -1) {
+      addedCoLockUsers.value.push(user)
+      jtcolocker.value.splice(index, 1)
+    }
+  }
+}
+
+// 移除锁定人
+const removeLockUser = (user, groupId) => {
+  const list = addedLockUsers.value[groupId]
+  if (!list) return
+  addedLockUsers.value[groupId] = list.filter((u) => u.id !== user.id)
+
+  // 记录待删除
+  toDeleteUserIds.value.push(user.id)
+}
+
+// 移除共锁人(同时回到左侧)
+const removeCoLockUser = (user) => {
+  addedCoLockUsers.value = addedCoLockUsers.value.filter((u) => u.id !== user.id)
+  jtcolocker.value.push(user)
+
+  // 记录待删除
+  toDeleteUserIds.value.push(user.id)
+  console.log(user,'user')
+}
+
+// 确认按钮事件
+// 确认按钮事件
+const submit = async () => {
+  try {
+    const ticketId = route.query.ticketId
+    const userList: any[] = []
+
+    // 【1】锁定人:同组去重
+    Object.keys(addedLockUsers.value).forEach((groupId) => {
+      const users = addedLockUsers.value[groupId] || []
+      const uniqueIds = new Set()
+      const uniqueUsers = []
+
+      users.forEach((user) => {
+        if (!uniqueIds.has(user.id)) {
+          uniqueIds.add(user.id)
+          uniqueUsers.push(user)
+        }
+      })
+
+      uniqueUsers.forEach((user) => {
+        userList.push({
+          ticketId,
+          groupId,
+          userId: user.id,
+          userName: user.nickname,
+          userType: 0,
+          userRole: 'jtlocker'
+        })
+      })
+    })
+
+    // 【2】共锁人:全局去重
+    const coLockIds = new Set()
+    addedCoLockUsers.value.forEach((user) => {
+      if (!coLockIds.has(user.id)) {
+        coLockIds.add(user.id)
+        userList.push({
+          ticketId,
+          groupId: null,
+          userId: user.id,
+          userName: user.nickname,
+          userType: 0,
+          userRole: 'jtcolocker'
+        })
+      }
+    })
+
+    // 【3】过滤现有用户数据
+    const finalUserList = existingUserList.value.filter((existingUser) => {
+      // 检查是否在删除列表中
+      if (toDeleteUserIds.value.includes(existingUser.id)) {
+        return false // 被删除的用户,过滤掉
+      }
+
+      // 检查是否在新增列表中(说明有变化)
+      const isInAddedList = userList.some((addedUser) =>
+        addedUser.userId === existingUser.userId &&
+        addedUser.userRole === existingUser.userRole &&
+        addedUser.groupId === existingUser.groupId
+      )
+
+      // 如果不在新增列表中,说明没有变化,过滤掉
+      if (!isInAddedList) {
+        return false
+      }
+
+      return true // 保留有变化的用户
+    })
+
+    console.log('最终要新增的:', finalUserList)
+
+    // 【4】提示
+    if (finalUserList.length === 0 && toDeleteUserIds.value.length === 0) {
+      ElMessage.warning('请至少选择一个人员或移除一个人员')
+      return
+    }
+
+    // 【5】先删
+    if (toDeleteUserIds.value.length > 0) {
+      const deleteIds = toDeleteUserIds.value.join(',')
+      await JobUserApi.deleteJobTicketUserList(deleteIds)
+      console.log('删除成功:', toDeleteUserIds.value)
+    }
+
+    // 【6】再新增 - 只有当有新增数据时才调用接口
+    if (finalUserList.length > 0) {
+      console.log(finalUserList,'finalUserList')
+      const result = await JobUserApi.insertJobTicketUser(finalUserList)
+      if (result) {
+        ElMessage.success('保存成功')
+        toDeleteUserIds.value = []
+      } else {
+        ElMessage.error(result?.msg || '保存失败')
+      }
+    } else {
+      // 如果只有删除操作,也提示成功
+      ElMessage.success('保存成功')
+      toDeleteUserIds.value = []
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+  }
+}
+const cancel = () => {
+  if (route.query.type == 'update') {
+    router.push({
+      path: '/jobTicket/jobTicket/job/UpdateJob',
+      query: {
+        id: route.query.ticketId,
+        type: route.query.type
+      }
+    })
+  } else {
+    router.push({
+      path: '/jobTicket/jobTicket/job/UpdateJob',
+      query: {
+        id: route.query.ticketId,
+        type: route.query.type
+      }
+    })
+  }
+}
+</script>
+<style lang="scss" scoped>
+.page-container {
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+}
+
+.main-layout {
+  display: flex;
+  gap: 20px;
+}
+
+.left-side,
+.right-side {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.card-block {
+  border-radius: 8px;
+}
+
+.red-border {
+  border-top: 4px solid #d3342e;
+}
+
+.orange-border {
+  border-top: 4px solid #f7941d;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.user-card {
+  width: 80px;
+  text-align: center;
+  cursor: pointer;
+  border: 2px solid transparent;
+  padding: 4px;
+
+  img {
+    width: 50px;
+    height: 50px;
+    object-fit: cover;
+    border: 1px solid #ccc;
+  }
+
+  .user-name {
+    margin-top: 4px;
+    font-size: 14px;
+  }
+
+  &.selected {
+    border: 2px solid red;
+
+    .user-name {
+      background: red;
+      color: #fff;
+      padding: 2px;
+    }
+  }
+}
+
+.group-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+}
+
+.group-card {
+  min-width: 200px;
+  min-height: 120px;
+  cursor: pointer;
+  border: 2px solid transparent;
+  border-radius: 6px;
+  transition: border-color 0.3s;
+
+  &.active {
+    border: 2px solid red;
+  }
+
+  // 防止内部卡片影响外部点击
+  .inner-card {
+    height: 100%;
+
+    :deep(.el-card__header) {
+      padding: 8px 12px;
+      font-size: 14px;
+      font-weight: bold;
+    }
+
+    :deep(.el-card__body) {
+      padding: 8px;
+    }
+  }
+}
+
+// 底部按钮
+.bottom-btn {
+  display: flex;
+  gap: 10px;
+  justify-content: flex-end;
+  padding-top: 10px;
+  border-top: 1px solid #eee;
+}
+</style>

+ 6 - 0
src/views/jobTicket/job/UserView/TableView.vue

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

+ 239 - 0
src/views/jobTicket/job/index.vue

@@ -0,0 +1,239 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="作业名称" prop="ticketName">
+        <el-input
+          v-model="queryParams.ticketName"
+          placeholder="请输入作业名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="作业类型" prop="ticketType">
+        <el-select
+          v-model="queryParams.ticketType"
+          placeholder="请选择作业类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TICKET_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="作业状态" prop="ticketStatus">
+        <el-select
+          v-model="queryParams.ticketStatus"
+          placeholder="请选择作业状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TICKET_STATUS)"
+            :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', 0, 'noSop')"
+          v-hasPermi="['iscs:job:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />
+          新增普通作业
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create', 0, 'Sop')"
+          v-hasPermi="['iscs:job:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />
+          新增SOP作业
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="multiple"
+          @click="handleDelete()"
+          v-hasPermi="['iscs:job:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" />
+          批量删除
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="jobList" @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="作业名称" prop="ticketName" />
+      <el-table-column label="作业类型" prop="ticketType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.TICKET_TYPE" :value="scope.row.ticketType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="作业状态" prop="ticketStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.TICKET_STATUS" :value="scope.row.ticketStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column label="所属区域" prop="workstationName" :show-overflow-tooltip="true" />
+      <el-table-column label="设备/工艺" prop="machineryName" align="center" />
+      <el-table-column label="操作" align="center">
+        <template #default="{ row }">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', row.id, '')"
+            v-hasPermi="['iscs:job:update']"
+          >
+            修改
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(row.id)"
+            v-hasPermi="['iscs:job: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 JobApi from '@/api/job/index'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+const router = useRouter()
+
+defineOptions({ name: 'SopManagement' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const jobList = ref([]) // 列表的数据
+const total = ref(0) // 总条数
+const ids = ref<number[]>([]) // 选中的数据
+const multiple = ref(true) // 非多个禁用
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  ticketName: undefined,
+  ticketType: undefined,
+  ticketStatus: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 多选框选中数据 */
+
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map((item) => item.id)
+
+  multiple.value = !selection.length
+}
+
+/** 查询模式列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await JobApi.getJobTicketPage(queryParams)
+    jobList.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, info?: string) => {
+  if (type === 'create' && info == 'noSop') {
+    router.push({
+      name: 'CreateJob',
+      query: { id: id, type: 'create', info: info }
+    })
+  } else if (type === 'create'&& info == 'Sop') {
+    router.push({
+      name: 'CreateSopJob',
+      query: { id: id, type: 'create',info: info }
+    })
+  } else if (type === 'update') {
+    router.push({
+      name: 'UpdateJob',
+      query: { id: id, type: 'update' }
+    })
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  const jobId = id || ids.value
+
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await JobApi.deleteJobTicketList(jobId)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
+</script>
+<style scoped></style>

+ 1 - 1
src/views/mp/components/wx-material-select/main.vue

@@ -1,7 +1,7 @@
 <!--
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
-  芋道源码
+  博士安全
   ① 移除 avue 组件,使用 ElementUI 原生组件
 -->
 <template>

+ 1 - 1
src/views/mp/components/wx-msg/main.vue

@@ -1,7 +1,7 @@
 <!--
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
-  芋道源码
+  博士安全
   ① 移除暂时用不到的 websocket
   ② 代码优化,补充注释,提升阅读性
 -->

+ 1 - 1
src/views/mp/components/wx-news/main.vue

@@ -2,7 +2,7 @@
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
   【微信消息 - 图文】
-  芋道源码
+  博士安全
   ① 代码优化,补充注释,提升阅读性
 -->
 <template>

+ 1 - 1
src/views/mp/components/wx-reply/main.vue

@@ -1,7 +1,7 @@
 <!--
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
-  芋道源码
+  博士安全
   ① 移除多余的 rep 为前缀的变量,让 message 消息更简单
   ② 代码优化,补充注释,提升阅读性
   ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入

+ 1 - 1
src/views/mp/components/wx-video-play/main.vue

@@ -2,7 +2,7 @@
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
   【微信消息 - 视频】
-  芋道源码
+  博士安全
   ① bug 修复:
     1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
       存在的问题:mediaId 有效期是 3 天,超过时间后无法播放

+ 1 - 1
src/views/mp/components/wx-voice-play/main.vue

@@ -2,7 +2,7 @@
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
   【微信消息 - 语音】
-   芋道源码
+   博士安全
   ① bug 修复:
     1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
       存在的问题:mediaId 有效期是 3 天,超过时间后无法播放

+ 1 - 1
src/views/mp/material/components/ImageTable.vue

@@ -63,7 +63,7 @@ const emit = defineEmits<{
   column-count: 5;
   margin-top: 10px;
 
-  /* 芋道源码:增加 10px,避免顶着上面 */
+  /* 博士安全:增加 10px,避免顶着上面 */
 }
 
 .waterfall-item {

+ 420 - 72
src/views/sopm/sop/CreateSop.vue

@@ -7,7 +7,7 @@
           <template #title>
             <div style="display: flex; align-items: center; gap: 8px">
               <el-icon size="20" style="margin-left: 10px">
-                <InfoFilled />
+                <InfoFilled/>
               </el-icon>
               <span style="font-size: 18px">SOP创建步骤</span>
             </div>
@@ -36,7 +36,7 @@
             label-width="68px"
           >
             <el-row>
-              <el-col :span="4">
+              <el-col :span="5">
                 <el-form-item label="SOP名称" prop="sopName">
                   <el-input
                     v-model="SopForm.sopName"
@@ -59,6 +59,7 @@
                     :props="{ label: 'workstationName', value: 'id', children: 'children' }"
                     placeholder="选择岗位"
                     class="!w-240px"
+                    @change="SopWorkstationChange"
                   />
                 </el-form-item>
               </el-col>
@@ -83,7 +84,7 @@
                     @change="handleSopTypeChange"
                   >
                     <el-option
-                      v-for="dict in getIntDictOptions(DICT_TYPE.SOP_TYPE)"
+                      v-for="dict in getStrDictOptions(DICT_TYPE.SOP_TYPE)"
                       :key="dict.value"
                       :label="dict.label"
                       :value="dict.value"
@@ -119,11 +120,11 @@
       <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 class="set-btn" @click="goSetting('SetModeStep', SopForm,null)">设置</div>
         </div>
         <div class="tab-content">
           <!-- VueFlow 主画布 -->
-          <VueFlow style="width: 100%; height: 300px">
+          <VueFlow style="width: 100%; height: 300px" :min-zoom="1" :max-zoom="1" :default-zoom="1">
             <template #node-default="{ id, data }">
               <div class="custom-node">
                 <div class="node-content">
@@ -144,9 +145,8 @@
                     {{ String.fromCharCode(9311 + (data.stepIndex || 1)) }}
                   </div>
                 </div>
-
                 <!-- 四个连接点 -->
-                <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top" />
+                <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top"/>
                 <Handle
                   type="source"
                   position="bottom"
@@ -176,13 +176,36 @@
       <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 class="set-btn" @click="goSetting('SetPoint', SopForm,null)">设置</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)"/>
+          <div class="point_center_box" v-if="!SopForm?.sopPointsList?.length">
+            <img
+              src="../../../assets/images/添加.png"
+              alt=""
+              @click="goSetting('SetPoint', SopForm,null)"
+            />
             <span style="color: red">*请添加需要进行隔离的点位</span>
           </div>
+          <div v-else>
+            <!-- 循环渲染分组 -->
+            <div class="group-container">
+              <div v-for="group in resolvedGroupedPoints" :key="group.groupId" class="point-group">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="points-list">
+                  <div
+                    v-for="point in group.points"
+                    :key="point.pointId"
+                    class="point-item"
+                  >
+                    <img :src="point.pointIcon" class="point-icon"/>
+                    <div class="point-name">{{ point.pointName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+          </div>
         </div>
       </div>
     </ContentWrap>
@@ -191,43 +214,82 @@
       <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 class="set-btn" @click="goSetting('SetUser', SopForm,null)">设置</div>
         </div>
-        <div class="tab-content" style="display: flex;height: 300px">
+
+        <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 v-if="groupedLockers.length" class="group-container-user">
+              <div v-for="group in groupedLockers" :key="group.groupId" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <div v-for="user in group.users" :key="user.userId" class="user-card">
+                    <img src="@/assets/images/UserBlack.png" />
+                    <div class="user-name">{{ user.userName }}</div>
+                  </div>
+                </div>
+              </div>
             </div>
 
+            <!-- 有分组点数据时显示 -->
+            <div v-else-if="SopForm.sopGroupList&&SopForm.sopGroupList[0].groupName!=='默认分组'" class="group-container">
+              <div v-for="group in SopForm.sopGroupList" :key="group.id" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <img src="@/assets/images/添加.png" class="user-card" @click="goSetting('SetUser', SopForm,group.id)"/>
+                </div>
+              </div>
+            </div>
+
+            <!-- 都没有数据时显示添加提示 -->
+            <div v-else class="point_center_box">
+              <img src="@/assets/images/添加.png" @click="goSetting('SetUser', SopForm,null)" />
+              <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)"/>
+            <div v-if="coLockUsers.length" class="user-list-colocker">
+              <div
+                v-for="user in coLockUsers"
+                :key="user.userId"
+                class="user-card"
+              >
+                <img src="@/assets/images/UserBlack.png"/>
+                <div class="user-name">{{ user.userName }}</div>
+              </div>
+            </div>
+            <div v-else class="point_center_box">
+              <img src="@/assets/images/添加.png" alt="" @click="goSetting('SetUser', SopForm,null)"/>
               <span>请添加参与共锁的人员</span>
             </div>
-
           </div>
         </div>
       </div>
-    </ContentWrap >
+    </ContentWrap>
+
+
     <div class="bottom-btn">
       <el-button @click="submit">
         <el-icon>
-          <Check />
+          <Check/>
         </el-icon>
         确 定
       </el-button>
 
       <el-button @click="cancel">
         <el-icon>
-          <Close />
+          <Close/>
         </el-icon>
         取 消
       </el-button>
@@ -235,35 +297,55 @@
   </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 {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() // 国际化
+import * as SopPointGroup from '@/api/sop/sopPointGroup'
+import * as PointApi from '@/api/dv/spm/index'
+import * as SopStepApi from '@/api/sop/sopStep'
+const {t} = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-import { handleTree } from '@/utils/tree'
-import { ref } from 'vue'
-import { Handle, useVueFlow, VueFlow } from '@vue-flow/core'
+import {handleTree} from '@/utils/tree'
+import {ref} from 'vue'
+import {Handle, useVueFlow, VueFlow} from '@vue-flow/core'
+import {deleteSopWorkflowStepList, insertSopWorkflowStep} from "@/api/sop/sopStep";
 
 const SopForm = reactive({
-  sopName: '',
-  sopType: '',
-  machineryId: '',
-  modeId: '',
-  workstationId: '',
-  id: undefined
+  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 Visible=ref<boolean>(false)
 const SopAutoName = ref(false)
 const activeName = ref('1')
 const machineryOptions = ref()
 const workstationOption = ref()
 const ModeOption = ref()
-const Visible = ref(false) //控制底部流程 点位 人员模块的显示
+const allGroups = ref<any[]>([])//获取所有分组
+const groupList = ref([])//获取当前sopId的分组
+const allPoints = ref<any[]>([])//获取所有点位
 const nodes = ref([]) //储存节点
 const edges = ref([]) // 存储连接线
 // 创建查找映射
@@ -271,50 +353,151 @@ 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 {addNodes, addEdges, setEdges, setNodes} = useVueFlow()
+//sop区域切换函数
+const SopWorkstationChange=async (value)=>{
+  SopForm.workstationId=value
+  // 获取设备/工艺数据
+  const techRes = await TechnologyApi.listTechnology({pageNo: 1, pageSize: -1,workstationId:SopForm.workstationId})
+  const data = techRes.list.filter((item) => item.machineryType == '工艺')
+  machineryOptions.value = handleTree(data, 'id', 'parentId')
+  buildMachineryMap(data)
+}
 //跳转设置对应页面
-const goSetting = (type,sopId) => {
-  if(type=='SetModeStep'){
+const goSetting = (type, SopForm,groupId) => {
+  if (type == 'SetModeStep') {
     router.push({
-      name:'SetModeStep',
-      query:{
-        sopId:sopId,
+      name: 'SetModeStep',
+      query: {
+        sopId: SopForm.id,
+        modeId: SopForm.modeId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
       }
     })
-  }else if(type=='SetPoint'){
+  } else if (type == 'SetPoint') {
     router.push({
-      name:'SetPoint',
-      query:{
-        sopId:sopId,
+      name: 'SetPoint',
+      query: {
+        sopId: SopForm.id,
+        machineryId: SopForm.machineryId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
       }
     })
-  }else if(type=='SetUser'){
+  } else if (type == 'SetUser') {
     router.push({
-      name:'SetUser',
-      query:{
-        sopId:sopId,
+      name: 'SetUser',
+      query: {
+        sopId: SopForm.id,
+        groupId:groupId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
       }
     })
   }
 }
+
+// 获取所有点位和分组的数据
+const fetchAllGroupsAndPoints = async () => {
+  try {
+    // 分组信息
+    const groupRes = await SopPointGroup.getSopGroupList({pageSize: -1, pageNo: 1})
+    allGroups.value = groupRes
+    console.log('获取分组', groupRes)
+    // 获取当前sopId的分组
+    if (SopForm.id) {
+      groupList.value = await SopPointGroup.getSopGroupList({
+        pageSize: -1,
+        pageNo: 1,
+        sopId: SopForm.id,
+      })
+    }
+
+    // 点位信息
+    const pointRes = await PointApi.getIsIsolationPointPage({pageSize: -1, pageNo: 1})
+    allPoints.value = pointRes?.list
+    console.log('获取点位', pointRes)
+  } catch (e) {
+
+  }
+}
+// 回显分组和点位数据的计算属性
+const resolvedGroupedPoints = computed(() => {
+  const groupsMap = new Map<string, { groupId: string; groupName: string; points: any[] }>()
+
+  SopForm.sopPointsList.forEach(item => {
+    const groupId = String(item.groupId)
+    const pointId = item.pointId
+
+    // 查分组名
+    const groupInfo = allGroups.value.find(g => String(g.id) === groupId)
+    const groupName = groupInfo?.groupName || '未分组'
+
+    // 查点位详情
+    const pointInfo = allPoints.value.find(p => p.id === pointId)
+    const pointName = pointInfo?.pointName
+    const pointIcon = pointInfo?.pointIcon
+
+    if (!groupsMap.has(groupId)) {
+      groupsMap.set(groupId, {
+        groupId,
+        groupName,
+        points: []
+      })
+    }
+
+    const group = groupsMap.get(groupId)!
+    // 防止重复添加
+    if (!group.points.some(p => p.pointId === pointId)) {
+      group.points.push({
+        pointId,
+        pointName,
+        pointIcon
+      })
+    }
+  })
+
+  return Array.from(groupsMap.values())
+})
+
+// 从 SopForm.sopUserList 中提取锁定人并按 groupId 分组
+const groupedLockers = computed(() => {
+  const lockerUsers = SopForm.sopUserList?.filter(u => u.userRole === 'jtlocker' && u.groupId != null) || []
+  const groupMap = new Map()
+
+  lockerUsers.forEach(user => {
+    if (!groupMap.has(user.groupId)) {
+      const groupName = groupList.value.find(g => g.id === user.groupId)?.groupName || '未命名分组'
+      groupMap.set(user.groupId, {groupId: user.groupId, groupName, users: []})
+    }
+    groupMap.get(user.groupId).users.push(user)
+  })
+
+  return Array.from(groupMap.values())
+})
+
+// 提取共锁人
+const coLockUsers = computed(() => {
+  return SopForm.sopUserList?.filter(u => u.userRole === 'jtcolocker') || []
+})
 // 获取基本信息
 const getOtherList = async () => {
   try {
+
     // 获取岗位数据
-    const deptRes = await MarsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 })
+    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 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 })
+    const modeRes = await ModeApi.getWorkflowModePage({pageNo: 1, pageSize: -1})
     ModeOption.value = modeRes.list.map((item) => ({
       label: item.modeName,
       value: item.id
@@ -330,7 +513,6 @@ const getOtherList = async () => {
 // 流程模式切换
 const handleModeChange = async (value) => {
   console.log(value, 'value')
-
   SopForm.modeId = value
   // 清空画布
   await clearCanvasProperly()
@@ -348,10 +530,12 @@ const handleModeChange = async (value) => {
         const bIndex = b.stepIndex || 0
         return aIndex - bIndex
       })
+
       // 渲染节点
       renderNodesFromData(sortedData)
       // 渲染连接线
       renderEdgesFromData(sortedData)
+
     }
   }
 }
@@ -428,7 +612,7 @@ const renderEdgesFromData = (data) => {
       sourceHandle: `${sourceNodeId}-right`, // 从右侧连接点出发
       targetHandle: `${targetNodeId}-left`, // 连接到左侧连接点
       type: 'smoothstep',
-      style: { stroke: '#333', strokeWidth: 2 },
+      style: {stroke: '#333', strokeWidth: 2},
       markerEnd: {
         type: 'arrowclosed',
         width: 20,
@@ -443,13 +627,25 @@ const renderEdgesFromData = (data) => {
     console.log('创建连接线:', edge)
   }
 }
-// 监听表单数据变化
+// 监听 modeId 变化
+watch(
+  () => SopForm.modeId,
+  async (newValue, oldValue) => {
+    if (newValue) {
+      hasUnsavedChanges.value = true
+      await handleModeChange(newValue)
+    }
+  },
+  {immediate: true}
+)
+
+// 监听其他表单字段变化(用于标记未保存)
 watch(
-  () => SopForm,
+  () => [SopForm.sopName, SopForm.sopContent, SopForm.workstationId],
   () => {
     hasUnsavedChanges.value = true
   },
-  { deep: true }
+  {deep: true}
 )
 // 保存成功后重置标记
 const submit = async () => {
@@ -465,20 +661,34 @@ const submit = async () => {
       if (data) {
         message.success(successMessage)
         hasUnsavedChanges.value = false
-        Visible.value = true
       }
     } else {
       // 新增操作
       data = await SopApi.insertSop(SopForm)
+      // 新增之后直接给sopStep新增改出局
+      const dataNew = await ModeStepApi.getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: SopForm.modeId
+      })
+      const sopStepData = dataNew.list.map((item) => ({
+        ...item,
+        sopId: data,
+        stepId: item.id
+      }))
+      // 导入步骤数据
+      await insertSopWorkflowStep(sopStepData)
       successMessage = t('common.createSuccess')
 
       if (data) {
+        Visible.value=true //显示下面的内容
         // 新增成功后,获取完整数据
         try {
           const selectData = await SopApi.selectSopById(data)
           if (selectData) {
             // 正确更新 ref 的值
-            SopForm = { ...SopForm, ...selectData }
+            SopForm = {...SopForm, ...selectData}
+
           }
         } catch (selectError) {
           console.warn('获取详情失败,但不影响保存:', selectError)
@@ -488,8 +698,16 @@ const submit = async () => {
 
         message.success(successMessage)
         hasUnsavedChanges.value = false
-        Visible.value = true
       }
+      // 确定之后直接切换修改页面
+      router.push({
+        path:'/sopm/sopm/sop/UpdateSop',
+        query:{
+          id:data,
+          type:'update',
+        }
+
+      })
     }
   } catch (error) {
     console.error('保存失败:', error)
@@ -521,6 +739,7 @@ const cancel = async () => {
 // 初始化
 onMounted(() => {
   getOtherList()
+  fetchAllGroupsAndPoints()//获取所有分组和点位 来渲染SOP的首页
 })
 
 // 构建岗位查找映射
@@ -554,22 +773,29 @@ const handleSopTypeChange = (value) => {
 }
 // 使用 watchEffect 更简洁
 watchEffect(() => {
-  const { workstationId, machineryId, sopType } = SopForm
+  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 isFirstLoad = ref(true)
+// 使用 watch 替代 watchEffect
+watch(
+  () => SopForm.workstationId,
+  async (newWorkstationId, oldWorkstationId) => {
+    // 只有在值真正改变且不是首次加载时才执行
+    if (newWorkstationId && newWorkstationId !== oldWorkstationId && !isFirstLoad.value) {
+      console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
+      SopForm.machineryId = null
+      await getMachineryData(newWorkstationId)
+    }
+    // 标记首次加载完成
+    isFirstLoad.value = false
   }
-})
+)
 
 // 获取工艺数据的函数
 const getMachineryData = async (workstationId) => {
@@ -604,7 +830,7 @@ const generateSopName = () => {
 
 // 获取 SOP 类型名称
 const getSopTypeName = (sopType) => {
-  const sopTypeOptions = getIntDictOptions(DICT_TYPE.SOP_TYPE)
+  const sopTypeOptions = getStrDictOptions(DICT_TYPE.SOP_TYPE)
   const typeOption = sopTypeOptions.find((option) => option.value === sopType)
   return typeOption ? typeOption.label : '未知类型'
 }
@@ -638,6 +864,7 @@ const getSopTypeName = (sopType) => {
   text-align: center;
   line-height: 30px;
   float: right;
+  cursor: pointer;
 }
 
 .tab-content {
@@ -665,6 +892,7 @@ const getSopTypeName = (sopType) => {
   margin-right: 10px;
   display: flex;
   flex-direction: column;
+
   img {
     width: 80px;
     height: 80px;
@@ -679,7 +907,6 @@ const getSopTypeName = (sopType) => {
   img {
     width: 80px;
     height: 80px;
-
   }
 }
 
@@ -688,6 +915,7 @@ const getSopTypeName = (sopType) => {
   height: 40px;
   display: flex;
   justify-content: flex-end;
+  padding-right: 70px;
 }
 
 .custom-node {
@@ -784,4 +1012,124 @@ const getSopTypeName = (sopType) => {
 :deep(.vue-flow__edge-marker) {
   fill: #333;
 }
+
+.group-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px; /* 卡片之间的间距 */
+}
+
+.point-group {
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  padding: 12px;
+  min-width: 250px;
+  background-color: #fafafa;
+  height: 250px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.group-title {
+  font-weight: 600;
+  font-size: 16px;
+  margin-bottom: 12px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #e0e0e0;
+  color: #333;
+}
+
+.points-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.point-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 60px;
+}
+
+.point-icon {
+  width: 40px;
+  height: 40px;
+  object-fit: contain;
+}
+
+.point-name {
+  font-size: 12px;
+  text-align: center;
+  margin-top: 4px;
+  color: #555;
+}
+
+//用户的卡片
+.group-container-user {
+  display: flex;
+  flex-direction: row;
+  gap: 16px;
+  overflow-x: auto;
+  padding-bottom: 10px;
+}
+
+.group-card-user {
+  width: 180px;
+  min-height: 150px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+  padding: 12px;
+  flex-shrink: 0;
+  border: 1px solid #eee;
+
+  display: flex;
+  flex-direction: column;
+  margin-top: 10px;
+}
+
+.group-title {
+  font-weight: bold;
+  font-size: 16px;
+  margin-bottom: 10px;
+  color: #333;
+  text-align: center;
+  border-bottom: 1px solid #f0f0f0;
+  padding-bottom: 6px;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: center;
+
+}
+.user-list-colocker{
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: flex-start;
+  margin-top: 10px;
+}
+.user-card {
+  width: 60px;
+  text-align: center;
+
+  img {
+    width: 40px;
+    height: 40px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+  }
+
+  .user-name {
+    font-size: 12px;
+    margin-top: 4px;
+    color: #555;
+    word-break: break-all;
+  }
+}
+
+
 </style>

+ 149 - 0
src/views/sopm/sop/ModeView/StepFunction.vue

@@ -0,0 +1,149 @@
+<template>
+  <div>
+    <Dialog v-model="dialogVisible" title="步骤功能" width="500px" :before-close="handleClose">
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-width="180px"
+        label-position="left"
+        class="step-function-form"
+      >
+        <el-form-item label="取消作业">
+          <el-checkbox v-model="formData.enableCancelJob" disabled />
+        </el-form-item>
+
+        <el-form-item label="设置锁定人">
+          <el-checkbox v-model="formData.enableSetLocker" disabled />
+        </el-form-item>
+
+        <el-form-item label="设置共锁人">
+          <el-checkbox v-model="formData.enableSetColocker" disabled />
+        </el-form-item>
+
+        <el-form-item label="添加共锁人">
+          <el-checkbox v-model="formData.enableAddColocker" disabled />
+        </el-form-item>
+        <el-form-item label="添加共锁人后跳转步骤">
+          <el-input-number
+            v-model="formData.gotoStepAfterAddingColocker"
+            :min="1"
+            size="default"
+            placeholder="步骤号"
+            disabled
+
+          />
+        </el-form-item>
+        <el-form-item label="减少共锁人">
+          <el-checkbox v-model="formData.enableReduceColocker" disabled />
+        </el-form-item>
+
+        <el-form-item label="上锁">
+          <el-checkbox v-model="formData.enableLock" disabled />
+        </el-form-item>
+
+        <el-form-item label="共锁">
+          <el-checkbox v-model="formData.enableColock" disabled />
+        </el-form-item>
+
+        <el-form-item label="解除共锁">
+          <el-checkbox v-model="formData.enableReleaseColock" disabled />
+        </el-form-item>
+
+        <el-form-item label="解锁">
+          <el-checkbox v-model="formData.enableUnlock" disabled />
+        </el-form-item>
+
+        <el-form-item label="结束作业">
+          <el-checkbox v-model="formData.enableEndJob" disabled />
+        </el-form-item>
+
+
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleClose">关闭</el-button>
+        </div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, watch, computed } from 'vue'
+
+interface Props {
+  visible: boolean
+  stepData: any
+}
+
+interface Emits {
+  (e: 'update:visible', value: boolean): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 弹框显示状态
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (value) => emit('update:visible', value)
+})
+
+// 表单数据
+const formData = ref({
+  enableCancelJob: false,
+  enableSetLocker: false,
+  enableSetColocker: false,
+  enableAddColocker: false,
+  enableReduceColocker: false,
+  enableLock: false,
+  enableColock: false,
+  enableReleaseColock: false,
+  enableUnlock: false,
+  enableEndJob: false,
+  gotoStepAfterAddingColocker: null
+})
+// 监听步骤数据变化,更新表单数据
+watch(
+  () => props.stepData,
+  (newData) => {
+    if (newData && Object.keys(newData).length > 0) {
+      formData.value = {
+        enableCancelJob: newData.enableCancelJob || false,
+        enableSetLocker: newData.enableSetLocker || false,
+        enableSetColocker: newData.enableSetColocker || false,
+        enableAddColocker: newData.enableAddColocker || false,
+        enableReduceColocker: newData.enableReduceColocker || false,
+        enableLock: newData.enableLock || false,
+        enableColock: newData.enableColock || false,
+        enableReleaseColock: newData.enableReleaseColock || false,
+        enableUnlock: newData.enableUnlock || false,
+        enableEndJob: newData.enableEndJob || false,
+        gotoStepAfterAddingColocker: newData.gotoStepAfterAddingColocker || null
+      }
+    }
+  },
+  { immediate: true, deep: true }
+)
+
+// 关闭弹框
+const handleClose = () => {
+  dialogVisible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.step-function-form {
+  .el-form-item {
+    margin-bottom: 20px;
+  }
+
+  .el-checkbox {
+    margin-right: 0;
+  }
+}
+
+.dialog-footer {
+  text-align: center;
+}
+</style>

+ 118 - 0
src/views/sopm/sop/ModeView/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 { updateSopWorkflowStep } from '@/api/sop/sopStep'
+
+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 updateSopWorkflowStep(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('sopTableDetail')
+  if (tempData) {
+    stepData.value = JSON.parse(tempData)
+    stepDescription.value = stepData.value.stepDescription || ''
+    // 清除临时数据
+    localStorage.removeItem('sopTableDetail')
+  }
+
+})
+</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>

+ 114 - 393
src/views/sopm/sop/ModeView/TableView.vue

@@ -2,22 +2,14 @@
   <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 ref="tableRef" :data="tableData" border stripe row-key="id">
         <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">
+        <el-table-column label="图标" align="center">
           <template #default="{ row }">
             <el-popover placement="bottom" trigger="click" width="230">
               <!-- 图标选择面板 -->
@@ -56,39 +48,21 @@
           </template>
         </el-table-column>
 
-        <el-table-column label="标题" width="180">
+        <el-table-column label="标题">
           <template #default="{ row }">
-            <el-input
-
-              v-model="row.stepTitle"
-              placeholder="标题"
-              size="small"
-              @blur="saveRowData(row)"
-            />
+            <el-input v-model="row.stepTitle" placeholder="标题" size="small"  @blur="saveRowData(row)"/>
           </template>
         </el-table-column>
 
-        <el-table-column label="标题缩写" width="120">
+        <el-table-column label="标题缩写">
           <template #default="{ row }">
-            <el-input
-
-              v-model="row.stepTitleShort"
-              placeholder="标题缩写"
-              size="small"
-              @blur="saveRowData(row)"
-            />
+            <el-input v-model="row.stepTitleShort" placeholder="标题缩写" size="small"  @blur="saveRowData(row)"/>
           </template>
         </el-table-column>
 
-        <el-table-column label="确认方式" width="130">
+        <el-table-column label="确认方式">
           <template #default="{ row }">
-            <el-select
-              v-model="row.confirmType"
-              placeholder="请选择确认方式"
-              @change="saveRowData(row)"
-              filterable
-              clearable
-            >
+            <el-select v-model="row.confirmType" placeholder="请选择确认方式" filterable clearable @change="saveRowData(row)">
               <el-option
                 v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STEP_CONFIRMTYPE)"
                 :key="dict.value"
@@ -99,7 +73,7 @@
           </template>
         </el-table-column>
 
-        <el-table-column label="确认角色" width="130">
+        <el-table-column label="确认角色">
           <template #default="{ row }">
             <el-select
               v-model="row.confirmRoleCode"
@@ -118,7 +92,7 @@
             </el-select>
           </template>
         </el-table-column>
-        <el-table-column label="确认人员" width="130">
+        <el-table-column label="确认人员">
           <template #default="{ row }">
             <el-select
               v-model="row.confirmUser"
@@ -136,190 +110,61 @@
             </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">
+        <el-table-column label="步骤功能" align="center">
           <template #default="{ row }">
-            <el-checkbox
-              :disabled="DisableCheckView||props.isPreset"
-              v-model="row.enableUnlock"
-              @change="saveRowData(row)"
-            />
+            <el-button type="primary" link @click="StepFunction(row)">查看</el-button>
           </template>
         </el-table-column>
-
-        <el-table-column label="结束作业" width="100" align="center">
+        <el-table-column label="步骤操作说明" align="center">
           <template #default="{ row }">
-            <el-checkbox v-model="row.enableEndJob" @change="saveRowData(row)" :disabled="DisableCheckView||props.isPreset"/>
+            <el-button type="primary" link @click="viewStepDetail(row)">查看</el-button>
           </template>
         </el-table-column>
       </el-table>
     </div>
+
+    <!-- 步骤功能弹框 -->
+    <StepFunctionDialog v-model:visible="stepFunctionVisible" :step-data="currentStepData" />
   </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'
+  getSopWorkflowStepPage,
+  insertSopWorkflowStep,
+  updateSopWorkflowStep
+} from '@/api/sop/sopStep'
 import { getRolePage } from '@/api/system/role'
 import { getRoleUser } from '@/api/system/user'
 import * as UserApi from '@/api/system/user'
-import TemplateAddDialog from './TemplateAdd.vue'
-
+import StepFunctionDialog from './StepFunction.vue'
+import { getWorkflowStepPage } from '@/api/custonWorkflow/step'
+import * as SopStep from '@/api/sop/sopStep'
 const route = useRoute()
 const router = useRouter()
 // 响应式数据
 const tableData = ref([])
-const selectedRows = ref([])
+
 const tableRef = ref()
-const templateDialogVisible = ref(false)
+const stepFunctionVisible = ref(false)
+// 当前选中的步骤数据
+const currentStepData = ref<any>({})
 const props = defineProps({
-  enableStepTable: {
-    type: Boolean,
-    required: true
+  sopId: {
+    type: [String, Number],
+    default: null
   },
   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
@@ -328,207 +173,32 @@ const selectIcon = (row, iconUrl) => {
 // 查看步骤详情
 const viewStepDetail = (row) => {
   // 临时存储步骤数据
-  localStorage.setItem('tempStepData', JSON.stringify(row))
-
+  localStorage.setItem('sopTableDetail', JSON.stringify(row))
   // 跳转到详情页面
   router.push({
     name: 'TableStepDetail',
     query: {
       stepId: row.id,
-      modeId: route.query.id
+      sopId: route.query.sopId
     }
   })
 }
-// 生成新的表格行
-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 StepFunction = (row) => {
+  currentStepData.value = row
+  stepFunctionVisible.value = true
 }
 
 // 自动保存单行数据
 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('更新步骤成功')
+    await updateSopWorkflowStep(row)
+    // 为了给确认人员传递查询条件
+    if (row.confirmRoleCode) {
+      await InitUser(row)
     }
+    // ElMessage.success('更新步骤成功')
   } catch (error) {
     console.error('保存步骤失败', error)
     ElMessage.error('保存失败,请稍后重试')
@@ -617,28 +287,83 @@ const getIconValues = async (iconKeys) => {
 // 初始化如果添加过步骤需要回显出来
 const initTableData = async () => {
   try {
-    if (props.modeId || route.query.id) {
-      const data = await getWorkflowStepPage({
+    // 1. 首先检查是否已经有SOP步骤数据
+    const existingData = await getSopWorkflowStepPage({
+      pageNo: 1,
+      pageSize: -1,
+      sopId: route.query.sopId || props.sopId
+    })
+
+    // 2. 如果已经有数据,直接回显
+    if (existingData && existingData.list && existingData.list.length > 0) {
+      console.log('发现已存在的SOP步骤数据,直接回显')
+      tableData.value = existingData.list.sort((a, b) => {
+        const aIndex = a.stepIndex || 0
+        const bIndex = b.stepIndex || 0
+        return aIndex - bIndex
+      })
+      // 初始化更新Sop步骤接口
+      if (!existingData.list || existingData.list.length === 0) {
+        // 如果没有现有数据,才调用新增接口
+        await SopStep.insertSopWorkflowStep(existingData.list)
+        console.log('初始化SOP步骤成功')
+      } else {
+        console.log('SOP步骤已存在,跳过初始化')
+      }
+      console.log(
+        '初始化数据完成,已按 stepIndex 排序:',
+        tableData.value.map((row) => row.stepIndex)
+      )
+      return // 直接返回,不需要后续操作
+    }
+
+    // 3. 如果没有数据,且有modeId,则从工作流导入步骤
+    if (props.modeId) {
+      console.log('没有现有数据,开始从工作流导入步骤')
+      const workflowData = await getWorkflowStepPage({
         pageNo: 1,
         pageSize: -1,
-        modeId: route.query.id || props.modeId
+        modeId: 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
+      if (workflowData && workflowData.list && workflowData.list.length > 0) {
+        console.log('获取到工作流步骤数据:', workflowData.list.length, '个步骤')
+
+        // 为每个步骤添加sopId
+        const sopStepData = workflowData.list.map((item) => ({
+          ...item,
+          sopId: route.query.sopId || props.sopId,
+          stepId: item.id
+        }))
+
+        // 导入步骤数据
+        await insertSopWorkflowStep(sopStepData)
+        console.log('步骤数据导入成功')
+
+        // 导入成功后,重新获取数据并回显
+        const newData = await getSopWorkflowStepPage({
+          pageNo: 1,
+          pageSize: -1,
+          sopId: route.query.sopId || props.sopId
         })
 
-        console.log(
-          '初始化数据完成,已按 stepIndex 排序:',
-          tableData.value.map((row) => row.stepIndex)
-        )
+        if (newData && newData.list && newData.list.length > 0) {
+          tableData.value = newData.list.sort((a, b) => {
+            const aIndex = a.stepIndex || 0
+            const bIndex = b.stepIndex || 0
+            return aIndex - bIndex
+          })
+          console.log('导入后数据回显完成:', tableData.value.length, '个步骤')
+          ElMessage.success(`成功导入 ${newData.list.length} 个步骤`)
+        }
       } else {
+        console.log('工作流中没有步骤数据')
         tableData.value = []
       }
+    } else {
+      // 4. 没有modeId也没有现有数据,初始化为空
+      console.log('没有modeId和现有数据,初始化为空表格')
+      tableData.value = []
     }
   } catch (error) {
     console.error('初始化表格数据失败:', error)
@@ -651,15 +376,9 @@ 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() //初始化步骤表格里的数据
@@ -715,4 +434,6 @@ watch(
     }
   }
 }
+
+
 </style>

+ 224 - 411
src/views/sopm/sop/ModeView/WorkFlowView.vue

@@ -1,26 +1,5 @@
 <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">
@@ -107,7 +86,6 @@
 
           <el-form-item label="标题">
             <el-input
-              :disabled="DisableCheckView"
               v-model="formData.stepTitle"
               placeholder="标题"
               size="small"
@@ -117,7 +95,6 @@
 
           <el-form-item label="标题缩写">
             <el-input
-              :disabled="DisableCheckView"
               v-model="formData.stepTitleShort"
               placeholder="标题缩写"
               size="small"
@@ -127,7 +104,6 @@
 
           <el-form-item label="确认方式">
             <el-select
-              :disabled="DisableCheckView"
               v-model="formData.confirmType"
               placeholder="请选择确认方式"
               @change="handleFormChange"
@@ -143,7 +119,6 @@
 
           <el-form-item label="确认角色" v-if="formData.confirmType == '2'">
             <el-select
-              :disabled="DisableCheckView"
               v-model="formData.confirmRoleCode"
               placeholder="请选择确认角色"
               @change="handleFormChange"
@@ -161,7 +136,6 @@
           </el-form-item>
           <el-form-item label="确认人员" v-if="formData.confirmType == '2'">
             <el-select
-              :disabled="DisableCheckView"
               v-model="formData.confirmUser"
               placeholder="请选择确认人员"
               @change="handleFormChange"
@@ -176,103 +150,139 @@
               />
             </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="步骤功能">
+            <div class="step-function-container">
+              <div v-if="formData.enableCancelJob" class="function-item">
+
+                <el-input
+                  v-model="formData.cancelJobValue"
+                  placeholder="取消作业"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+              <!-- 设置锁定人 -->
+              <div v-if="formData.enableSetLocker" class="function-item">
+                <el-input
+                  v-model="formData.setLockerValue"
+                  placeholder="设置锁定人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <el-form-item label="设置锁定人">
-            <el-checkbox
-              :disabled="DisableCheckView||props.isPreset"
-              v-model="formData.enableSetLocker"
-              @change="handleFormChange"
-            />
-          </el-form-item>
+              <!-- 设置共锁人 -->
+              <div v-if="formData.enableSetColocker" class="function-item">
+                <el-input
+                  v-model="formData.setColockerValue"
+                  placeholder="设置共锁人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
+              <!-- 添加共锁人 -->
+              <div v-if="formData.enableAddColocker" class="function-item">
+
+                <el-input
+                  v-model="formData.addColockerValue"
+                  placeholder="添加共锁人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <el-form-item label="设置共锁人">
-            <el-checkbox
-              :disabled="DisableCheckView||props.isPreset"
-              v-model="formData.enableSetColocker"
-              @change="handleFormChange"
-            />
-          </el-form-item>
+              <!-- 添加共锁人后跳转步骤 -->
+              <div v-if="formData.enableAddColocker && formData.gotoStepAfterAddingColocker" class="function-item">
+
+                <el-input-number
+                  v-model="formData.gotoStepAfterAddingColocker"
+                  :min="1"
+                  size="small"
+                  placeholder="添加共锁人后跳转步骤"
+                  disabled
+                  style="width: 120px"
+                />
+              </div>
 
-          <el-form-item label="添加共锁人">
-            <el-checkbox
-              :disabled="DisableCheckView||props.isPreset"
-              v-model="formData.enableAddColocker"
-              @change="handleFormChange"
-            />
-          </el-form-item>
+              <!-- 减少共锁人 -->
+              <div v-if="formData.enableReduceColocker" class="function-item">
+                <el-input
+                  v-model="formData.reduceColockerValue"
+                  placeholder="减少共锁人"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <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>
+              <!-- 上锁 -->
+              <div v-if="formData.enableLock" class="function-item">
+                <el-input
+                  v-model="formData.lockValue"
+                  placeholder="上锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <el-form-item label="减少共锁人">
-            <el-checkbox
-              v-model="formData.enableReduceColocker"
-              @change="handleFormChange"
-              :disabled="DisableCheckView||props.isPreset"
-            />
-          </el-form-item>
+              <!-- 共锁 -->
+              <div v-if="formData.enableColock" class="function-item">
+                <el-input
+                  v-model="formData.colockValue"
+                  placeholder="共锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <el-form-item label="上锁">
-            <el-checkbox
-              v-model="formData.enableLock"
-              @change="handleFormChange"
-              :disabled="DisableCheckView||props.isPreset"
-            />
-          </el-form-item>
+              <!-- 解除共锁 -->
+              <div v-if="formData.enableReleaseColock" class="function-item">
+                <el-input
+                  v-model="formData.releaseColockValue"
+                  placeholder="解除共锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <el-form-item label="共锁">
-            <el-checkbox
-              v-model="formData.enableColock"
-              @change="handleFormChange"
-              :disabled="DisableCheckView||props.isPreset"
-            />
-          </el-form-item>
+              <!-- 解锁 -->
+              <div v-if="formData.enableUnlock" class="function-item">
+                <el-input
+                  v-model="formData.unlockValue"
+                  placeholder="解锁"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <el-form-item label="解除共锁">
-            <el-checkbox
-              v-model="formData.enableReleaseColock"
-              @change="handleFormChange"
-              :disabled="DisableCheckView||props.isPreset"
-            />
-          </el-form-item>
+              <!-- 结束作业 -->
+              <div v-if="formData.enableEndJob" class="function-item">
+                <el-input
+                  v-model="formData.endJobValue"
+                  placeholder="结束作业"
+                  disabled
+                  size="small"
+                  style="width: 200px"
+                />
+              </div>
 
-          <el-form-item label="解锁">
-            <el-checkbox
-              v-model="formData.enableUnlock"
-              @change="handleFormChange"
-              :disabled="DisableCheckView||props.isPreset"
-            />
+            </div>
           </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="请输入内容..."
@@ -282,11 +292,7 @@
         </div>
       </div>
     </div>
-    <!--    从模板添加-->
-    <TemplateAddDialog
-      v-model:visible="templateDialogVisible"
-      @select-template="handleTemplateSelect"
-    />
+
   </div>
 </template>
 
@@ -300,16 +306,12 @@ 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'
+  getSopWorkflowStepPage,
+  updateSopWorkflowStep,
+} from '@/api/sop/sopStep'
 
 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 {
@@ -317,25 +319,16 @@ const {
   addEdges,
   onNodeClick,
   onNodeContextMenu,
-  removeNodes,
-  findNode,
   zoomTo,
   updateNode,
-  onConnect
+  onConnect,
 } = useVueFlow()
 const props = defineProps({
-  enableStepTable: {
-    type: Boolean,
-    required: true
-  },
-  modeId: {
+  sopId: {
     type: [String, Number],
     default: null
   },
-  isPreset:{
-    type: Boolean,
-    required: true
-  }
+
 
 })
 const selectedNodeId = ref<string | null>(null)
@@ -349,7 +342,7 @@ const UserOptions = ref([])
 
 // 默认数据结构
 const formData = reactive({
-  modeId: route.query.id || props.modeId,
+  sopId: route.query.sopId || props.sopId,
   stepTemplateId: undefined,
   stepIndex: 0,
   stepName: undefined,
@@ -374,68 +367,6 @@ const formData = reactive({
   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
@@ -457,11 +388,11 @@ const selectIcon = (formData, iconUrl) => {
 // 初始化步骤数据并渲染节点
 const initTableData = async () => {
   try {
-    if (props.modeId || route.query.id) {
-      const data = await getWorkflowStepPage({
+    if (props.sopId || route.query.sopId) {
+      const data = await getSopWorkflowStepPage({
         pageNo: 1,
         pageSize: -1,
-        modeId: route.query.id || props.modeId
+        sopId: route.query.sopId || props.sopId
       })
 
       if (Array.isArray(data.list) && data.list.length > 0) {
@@ -482,8 +413,41 @@ const initTableData = async () => {
 
         // 渲染连接线
         renderEdgesFromData(sortedData)
-      } else {
-        console.log('没有找到步骤数据')
+        // 默认选中第一个节点(高亮显示)
+        if (sortedData.length > 0) {
+          const firstNode = sortedData[0];
+          if(firstNode) {
+            // 设置选中节点ID
+            selectedNodeId.value = `node-${firstNode.id}`;
+            // 显示表单
+            showForm.value = true;
+
+            // 更新表单数据
+            if (firstNode) {
+              Object.assign(formData, firstNode);
+            } else {
+              formData.stepIcon = firstNode.stepIcon || '';
+              formData.stepTitleShort = firstNode.stepTitleShort || '';
+              formData.stepIndex = firstNode.stepIndex || 1;
+            }
+
+            // 使用Vue Flow内置方法选中首个节点
+            const { addSelectedNodes } = useVueFlow();
+            addSelectedNodes([`node-${firstNode.id}`]);
+
+            // 更新节点样式为选中状态
+            updateNode(`node-${firstNode.id}`, (node) => {
+              node.style = {
+                ...node.style,
+                backgroundColor: '#ffec99', // 黄色背景
+                borderColor: '#f0c040', // 黄色边框
+                borderWidth: '2px'
+              };
+            });
+
+            console.log('默认选中第一个节点并高亮:', `node-${firstNode.id}`);
+          }
+        }
       }
     }
   } catch (error) {
@@ -574,169 +538,6 @@ const renderEdgesFromData = (data) => {
   }
 }
 
-// 获取下一个可用的索引 - 修正版本
-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 () => {
@@ -765,21 +566,9 @@ const handleFormChange = debounce(async () => {
 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)
+      await updateSopWorkflowStep(row)
       ElMessage.success(`更新步骤成功`)
-    }
+
   } catch (error) {
     console.error('保存步骤失败', error)
     ElMessage.error('保存失败,请稍后重试')
@@ -788,6 +577,18 @@ const saveFormData = async (row) => {
 
 // 节点点击处理 - 修正版本
 onNodeClick(({ node }) => {
+  // 清除所有节点的选中样式
+  nodes.value.forEach(n => {
+    updateNode(n.id, (nodeItem) => {
+      nodeItem.style = {
+        ...nodeItem.style,
+        backgroundColor: '#fff',
+        borderColor: '#999',
+        borderWidth: '1px'
+      };
+    });
+  });
+
   if (selectedNodeId.value === node.id) {
     showForm.value = false
     selectedNodeId.value = null
@@ -795,6 +596,16 @@ onNodeClick(({ node }) => {
     selectedNodeId.value = node.id
     showForm.value = true
 
+    // 设置当前节点为选中样式
+    updateNode(node.id, (nodeItem) => {
+      nodeItem.style = {
+        ...nodeItem.style,
+        backgroundColor: '#ffec99', // 黄色背景
+        borderColor: '#f0c040', // 黄色边框
+        borderWidth: '2px'
+      };
+    });
+
     // 更新表单数据
     const nodeData = node.data
     if (nodeData.stepData) {
@@ -815,33 +626,6 @@ onNodeContextMenu(({ node, event }) => {
   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 () => {
@@ -955,17 +739,9 @@ onConnect((params) => {
   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()])
 })
@@ -992,6 +768,7 @@ watch(
   width: 125px;
   height: 180px;
   background-color: #fff;
+  background: transparent;
   border-radius: 12px;
   display: flex;
   flex-direction: column;
@@ -1080,4 +857,40 @@ watch(
 :deep(.vue-flow__edge-marker) {
   fill: #333;
 }
+.step-function-container {
+  //border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding-top: 6px;
+  //background-color: #fafafa;
+  min-height: 60px;
+
+  .function-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 12px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .function-label {
+      font-weight: 500;
+      color: #606266;
+      margin-right: 12px;
+      min-width: 120px;
+    }
+  }
+
+  .no-function-tip {
+    text-align: center;
+    padding: 20px 0;
+    color: #909399;
+  }
+}
+/* 在组件的style部分添加 */
+.vue-flow__node.selected {
+  background-color: yellow !important;
+  border-color: #ff9800 !important;
+  box-shadow: 0 0 10px rgba(255, 152, 0, 0.5) !important;
+}
 </style>

+ 673 - 0
src/views/sopm/sop/PointView/CardView.vue

@@ -0,0 +1,673 @@
+<template>
+  <div class="page-container">
+    <!-- 左侧面板 -->
+    <div class="left-panel">
+      <div class="custom-tabs-container">
+        <div class="tab-header">
+          <span class="tab-title">点位清单</span>
+        </div>
+        <div class="tab-content">
+          <v-stage
+            ref="stageRef"
+            :config="{ width: stageWidth, height: stageHeight }"
+            class="konva-container"
+          >
+            <!-- 点位图层 -->
+            <v-layer>
+              <template v-for="(point, index) in filterData" :key="point.id">
+                <v-group :config="getGroupConfig(point, index)" @click="handlePointClick(point)">
+                  <!-- 点位背景矩形 -->
+                  <v-rect :config="getRectConfig()" />
+                  <!-- 点位图片 -->
+                  <v-image :config="getImageConfig(point)" />
+                  <!-- 点位名称 -->
+                  <v-text :config="getTextConfig(point)" />
+                </v-group>
+              </template>
+            </v-layer>
+          </v-stage>
+        </div>
+      </div>
+    </div>
+    <!-- 右侧分组容器 -->
+    <div class="right-panel">
+      <div class="group-container">
+        <div v-for="group in groupList" :key="group.id" class="group-item">
+          <div
+            class="custom-tabs-container2"
+            :class="{ 'selected-group': selectedGroupId === group.id }"
+            @click="selectGroup(group.id)"
+          >
+            <div class="tab-header2">
+              <template v-if="group.isEditing">
+                <el-input
+                  v-model="group.groupName"
+                  size="small"
+                  autofocus
+                  @blur="handleTitleBlur(group)"
+                  @change="handleTitleBlur(group)"
+                  @keyup.enter="handleTitleBlur(group)"
+                  class="title-input2"
+                />
+              </template>
+              <template v-else>
+                <span class="tab-title2" @click="group.isEditing = true">
+                  {{ group.groupName }}
+                </span>
+              </template>
+              <div class="del-group-btn" v-if="groupList.length > 1" @click="removeGroup(group.id)">
+                删除分组
+              </div>
+            </div>
+            <div class="tab-content2">
+              <!-- 分组内容 - 使用 vue-konva 渲染点位 -->
+              <v-stage :config="{ width: 380, height: 220 }" class="right-konva-container">
+                <v-layer>
+                  <template v-for="(pointId, index) in group.pointIds" :key="pointId">
+                    <v-group
+                      :config="getGroupPointConfig(pointId, index)"
+                      @click="handleGroupPointClick(group.id, pointId)"
+                    >
+                      <!-- 点位背景矩形 -->
+                      <v-rect :config="getGroupRectConfig()" />
+                      <!-- 点位图片 -->
+                      <v-image :config="getGroupImageConfig(pointId)" />
+                      <!-- 点位名称 -->
+                      <v-text :config="getGroupTextConfig(pointId)" />
+                    </v-group>
+                  </template>
+                </v-layer>
+              </v-stage>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <!-- 底部按钮区 -->
+  <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>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
+import * as MachineryApi from '@/api/dv/technology/index'
+import * as PointApi from '@/api/dv/spm/index'
+import VueKonva from 'vue-konva'
+import { Check, Close } from '@element-plus/icons-vue'
+import * as SopApi from '@/api/sop/index'
+import * as SopPointGroup from '@/api/sop/sopPointGroup'
+import * as SopPoint from  '@/api/sop/sopPoint'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const boundPointIds = ref<number[]>([])
+const allPoints = ref<any[]>([])
+const filterData = ref<any[]>([])
+const stageRef = ref()
+const stageWidth = 800
+const stageHeight = 700
+// 左侧点位配置
+const rectWidth = 60 // 增大宽度
+const rectHeight = 100 // 增大高度,为标题预留空间
+const rectGapX = 5 // 横向间距加大
+const rectGapY = 10 // 纵向间距加大
+
+// 右侧分组内点位的尺寸配置
+const groupPointWidth = 40
+const groupPointHeight = 60
+const groupPointGapX = 10
+const groupPointGapY = 10
+
+const route = useRoute()
+const router=useRouter()
+const props = defineProps({
+  groupAdded: Boolean
+})
+// 记录当前选中的分组ID(高亮分组)
+const selectedGroupId = ref<number | null>(null)
+const groupList = ref([])
+
+
+// 移除 watch 中的异步调用,改为同步添加本地分组
+watch(
+  () => props.groupAdded,
+  async (newVal) => {
+    if (newVal) {
+      try {
+        const defaultName = `目标点位${groupList.value.length + 1}`
+        const response = await SopPointGroup.insertSopGroup({
+          sopId: route.query.sopId,
+          groupName: defaultName
+        })
+
+        if (response) {
+          groupList.value.push({
+            id: response,
+            sopId: route.query.sopId,
+            groupName: defaultName,
+            isEditing: false,
+            pointIds: [] as number[]
+          })
+          console.log('新增分组成功:', groupList.value)
+        } else {
+          console.warn('新增分组失败,接口未返回 ID')
+        }
+      } catch (error) {
+        console.error('新增分组失败:', error)
+      }
+    }
+  }
+)
+
+
+// 放置filterData数据移动之后丢失 映射一个齐全的隔离点数据方便渲染右侧
+const pointMap = computed(() => {
+  const map = new Map<number, any>()
+  allPoints.value.forEach((point) => {
+    map.set(point.id, point)
+  })
+  return map
+})
+
+// 右侧标题修改
+const handleTitleBlur = async (group: (typeof groupList.value)[0]) => {
+  if (!group.groupName.trim()) {
+    group.groupName = `目标点位${group.id}`
+  }
+
+  try {
+    // 直接走更新接口(因为新增时已经通过监听完成)
+    const updateData = {
+      id: group.id,
+      sopId: route.query.sopId,
+      groupName: group.groupName
+    }
+    await SopPointGroup.updateSopGroup(updateData)
+    console.log('更新分组成功:', group.id)
+  } catch (error) {
+    console.error('更新分组失败:', error)
+  }
+
+  group.isEditing = false
+}
+
+// 移除分组
+const removeGroup = async (groupId: number) => {
+  try {
+    // 调用删除分组接口
+    await SopPointGroup.deleteSopGroupList(groupId)
+
+    // 接口调用成功后,从本地列表中移除
+    groupList.value = groupList.value.filter((group) => group.id !== groupId)
+
+    // 如果删除的是当前选中分组,自动选中第一个分组
+    if (selectedGroupId.value === groupId) {
+      selectedGroupId.value = groupList.value.length > 0 ? groupList.value[0].id : null
+    }
+
+    // 更新过滤数据
+    updateFilterData()
+
+  } catch (error) {
+
+  }
+}
+// 选中分组
+const selectGroup = (groupId: number) => {
+  selectedGroupId.value = groupId
+  console.log(groupId,'groupId')
+}
+// 左侧点位点击:添加到当前高亮分组
+const handlePointClick = (point) => {
+  if (!selectedGroupId.value) {
+    console.warn('没有选中任何分组')
+    return
+  }
+  const group = groupList.value.find(g => g.id === selectedGroupId.value)
+  if (!group) return
+
+  if (!group.pointIds.includes(point.id)) {
+    group.pointIds.push(point.id)
+    updateFilterData(true)
+  } else {
+    console.log('点位已存在于分组中')
+  }
+}
+
+
+
+const imageCache = new Map<string, HTMLImageElement>()
+
+// 计算左侧每个点位的坐标(自动换行)
+const getGroupConfig = (point: any, index: number) => {
+  // 每行可容纳的点位数量(基于舞台宽度和间距计算)
+  const itemsPerRow = Math.floor(stageWidth / (rectWidth + rectGapX))
+  const row = Math.floor(index / itemsPerRow)
+  const col = index % itemsPerRow
+  return {
+    x: col * (rectWidth + rectGapX) + 5, // 左侧预留20px边距
+    y: row * (rectHeight + rectGapY) + 5 // 顶部预留20px边距
+  }
+}
+
+// 左侧背景矩形配置(与点位尺寸匹配)
+const getRectConfig = () => ({
+  width: rectWidth,
+  height: rectHeight
+})
+
+// 左侧图标配置(居中显示在矩形上半部分)
+const getImageConfig = (point: any) => {
+  let img = imageCache.get(point.pointIcon)
+  if (!img) {
+    img = new window.Image()
+    img.src = point.pointIcon || '' // 空值处理
+    img.onerror = () => {
+      console.warn(`图标加载失败: ${point.pointIcon}`)
+    }
+    imageCache.set(point.pointIcon, img)
+  }
+  return {
+    image: img,
+    x: (rectWidth - 50) / 2, // 图标水平居中(图标宽40)
+    y: 10, // 距离顶部10px
+    width: 50,
+    height: 50
+  }
+}
+
+// 左侧隔离点标题配置(显示在图标正下方,矩形底部)
+const getTextConfig = (point: any) => ({
+  x: rectWidth - 62, // 水平居中(基于矩形宽度)
+  y: rectHeight - 35, // 距离底部20px(确保在图标下方)
+  text: point.pointName || '未命名', // 空值处理
+  fontSize: 12, // 适配尺寸
+  fill: '#333',
+  align: 'center', // 文字居中
+  verticalAlign: 'middle',
+  width: rectWidth, // 限制文字宽度,避免溢出
+  wrap: 'word' // 自动换行
+})
+
+// 右侧分组内点位的 group 配置
+const getGroupPointConfig = (pointId: number, index: number) => {
+  const itemsPerRow = Math.floor(280 / (groupPointWidth + groupPointGapX))
+  const row = Math.floor(index / itemsPerRow)
+  const col = index % itemsPerRow
+  return {
+    x: col * (groupPointWidth + groupPointGapX) + 5,
+    y: row * (groupPointHeight + groupPointGapY) + 5
+  }
+}
+
+// 右侧分组内点位的矩形配置
+const getGroupRectConfig = () => ({
+  width: groupPointWidth,
+  height: groupPointHeight,
+  shadowBlur: 1
+})
+
+// 右侧分组内点位的图片配置
+const getGroupImageConfig = (pointId: number) => {
+  const point = pointMap.value.get(pointId)
+  if (!point) return {}
+
+  let img = imageCache.get(point.pointIcon)
+  if (!img) {
+    img = new window.Image()
+    img.src = point.pointIcon || ''
+    imageCache.set(point.pointIcon, img)
+  }
+
+  return {
+    image: img,
+    x: (groupPointWidth - 30) / 2,
+    y: 5,
+    width: 30,
+    height: 30
+  }
+}
+
+// 右侧分组内点位的文本配置
+const getGroupTextConfig = (pointId: number) => {
+  const point = pointMap.value.get(pointId)
+  return {
+    x: 0,
+    y: groupPointHeight - 20,
+    text: point ? point.pointName : '未知',
+    fontSize: 10,
+    // fill: '#333',
+    align: 'center',
+    verticalAlign: 'middle',
+    width: groupPointWidth,
+    wrap: 'word'
+  }
+}
+
+// 右侧分组内点位点击:从分组移除(回归左侧)
+const handleGroupPointClick = async (groupId: number, pointId: number) => {
+  removePointFromGroup(groupId, pointId)
+  await updateFilterData(true)
+}
+
+// 从分组中移除点位的通用方法
+const removePointFromGroup = (groupId: number, pointId: number) => {
+  const group = groupList.value.find((g) => g.id === groupId)
+  if (!group) return
+  group.pointIds = group.pointIds.filter((id) => id !== pointId)
+}
+// 更新filterData
+const updateFilterData = async (skipReload = false) => {
+  if (!skipReload) {
+    try {
+      const groupData = await SopApi.selectSopById(route.query.sopId)
+
+      if (groupData?.sopPointsList?.length > 0) {
+        groupList.value.forEach(group => group.pointIds = [])
+
+        groupData.sopPointsList.forEach((sopPoint: any) => {
+          const group = groupList.value.find(g => g.id === sopPoint.groupId)
+          if (group && !group.pointIds.includes(sopPoint.pointId)) {
+            group.pointIds.push(sopPoint.pointId)
+          }
+        })
+      }
+    } catch (error) {
+      console.error('接口失败,回退本地计算')
+    }
+  }
+
+  // 用当前 groupList 计算 filterData
+  filterData.value = allPoints.value.filter((point: any) => {
+    const isIncluded = boundPointIds.value.includes(point.id)
+    const inAnyGroup = groupList.value.some((group) => group.pointIds.includes(point.id))
+    return isIncluded && !inAnyGroup
+  })
+}
+
+// 初始化数据
+const getOtherList = async () => {
+  try {
+    // 获取工艺绑定过的隔离点
+    const data = await MachineryApi.getTechnologyInfo(route.query.machineryId)
+    boundPointIds.value = data.pointIdList || []
+
+    // 获取所有隔离点
+    const pointData = await PointApi.getIsIsolationPointPage({ pageNo: 1, pageSize: -1 })
+    allPoints.value = pointData.list || []
+
+    // 先尝试获取sop点位分组
+    const sopGroupData = await SopPointGroup.getSopGroupList({
+      pageNo: 1,
+      pageSize: -1,
+      sopId: route.query.sopId // 传递sopId参数
+    })
+
+    console.log(sopGroupData, 'sopGroupData')
+
+    if (sopGroupData && sopGroupData.length > 0) {
+      // 如果能获取到分组数据,直接使用
+      groupList.value = sopGroupData.map((group: any) => ({
+        id: group.id,
+        sopId: group.sopId,
+        groupName: group.groupName,
+        isEditing: false,
+        pointIds: []
+      }))
+    } else {
+      // 如果没有分组数据,调用SOP接口获取默认分组
+      const groupData = await SopApi.selectSopById(route.query.sopId)
+      console.log(groupData, 'groupData')
+      groupList.value = groupData.sopGroupList.map((group: any, index: number) => ({
+        id: group.id,
+        sopId: group.sopId,
+        groupName: group.groupName,
+        isEditing: false,
+        pointIds: []
+      }))
+    }
+
+    // 筛选出真正绑定的点位
+    updateFilterData()
+  } catch (error) {
+    console.error('获取数据失败:', error)
+    filterData.value = []
+  }
+}
+
+watch(
+  filterData,
+  async () => {
+    await nextTick()
+  },
+  { deep: true }
+)
+
+onMounted(async () => {
+  await getOtherList()
+})
+// 保存更新的信息
+const submit = async () => {
+  try {
+    const detailSop = await SopApi.selectSopById(route.query.sopId)
+    const oldPoints = detailSop.sopPointsList || []
+
+    const oldKeySet = new Set(
+      oldPoints.map(item => `${item.groupId}_${item.pointId}`)
+    )
+
+    // 1. 生成当前用户操作的点位集合(newPoints)
+    const allCurrentPoints = []
+    groupList.value.forEach(group => {
+      group.pointIds.forEach(pointId => {
+        allCurrentPoints.push({
+          sopId: route.query.sopId,
+          groupId: group.id,
+          pointId: pointId
+        })
+      })
+    })
+
+    const newKeySet = new Set(
+      allCurrentPoints.map(item => `${item.groupId}_${item.pointId}`)
+    )
+
+    // 2. 计算出旧数据中已被移除的点位(需要删除)
+    const toDeleteList = oldPoints.filter(item => {
+      const key = `${item.groupId}_${item.pointId}`
+      return !newKeySet.has(key)
+    })
+
+    // 3. 删除接口
+    if (toDeleteList.length > 0) {
+      const deleteIds = toDeleteList.map(item => item.id)
+      await SopPoint.deleteSopPointsList(deleteIds)
+      message.success('保存成功')
+      console.log('已删除点位:', deleteIds)
+    }
+
+    // ✅ 4. 从 allCurrentPoints 中剔除掉旧数据中已存在的内容,仅新增
+    const oldKeySetAfterDelete = new Set(
+      oldPoints.map(item => `${item.groupId}_${item.pointId}`)
+    )
+    const newOnlyInsertPoints = allCurrentPoints.filter(item => {
+      const key = `${item.groupId}_${item.pointId}`
+      return !oldKeySetAfterDelete.has(key)
+    })
+
+    if (newOnlyInsertPoints.length > 0) {
+      await SopPoint.insertSopPoints(newOnlyInsertPoints)
+      message.success(t('common.createSuccess'))
+      console.log('新增成功:', newOnlyInsertPoints)
+    } else {
+      console.log('无新增点位')
+    }
+  } catch (error) {
+    console.error('提交失败:', error)
+  }
+}
+
+const cancel = () => {
+  router.push({
+    path:'/sopm/sopm/sop/UpdateSop',
+    query:{
+      id:route.query.sopId,
+      type:route.query.type,
+    }
+  })
+}
+</script>
+<style scoped lang="scss">
+// 外层页面容器:占满整个可视区域
+.page-container {
+  height: 100vh; // 占满屏幕高度
+  display: flex;
+}
+
+// 左侧面板
+.left-panel {
+  flex: 20;
+}
+
+// 右侧面板
+.right-panel {
+  flex: 4;
+  margin-left: 10px;
+  min-width: 600px;
+}
+
+// 右侧分组容器
+.group-container {
+  display: flex;
+  flex-direction: column; // 纵向堆叠
+  gap: 20px;
+  height: 100%; // 高度占满右侧面板
+  overflow-y: auto; // 超出高度时滚动
+  padding-right: 10px; // 预留滚动条空间
+  box-sizing: border-box;
+}
+
+// 单个分组
+.group-item {
+  width: 100%;
+  // 强制分组高度一致(可选)
+  min-height: 300px;
+}
+
+// 左侧容器样式
+.custom-tabs-container {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  height: 100%; // 占满左侧面板高度
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box; // 包含 border 计算高度
+}
+
+.tab-header {
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #dcdfe6;
+  padding: 12px 20px;
+  border-radius: 4px 4px 0 0;
+}
+
+.tab-content {
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 0 0 4px 4px;
+  flex: 1; // 占满剩余高度
+  overflow-y: auto; // 内容超出时滚动
+}
+
+// 右侧分组样式
+.custom-tabs-container2 {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  height: 300px; // 固定高度
+  min-width: 400px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.tab-header2 {
+  padding: 8px 16px;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #e4e7ed;
+  display: flex;
+  justify-content: space-between;
+  align-items: center; // 垂直居中
+  gap: 10px;
+}
+
+// 底部按钮区
+.bottom-btn {
+  display: flex;
+  gap: 10px; // 按钮间距
+  justify-content: flex-end;
+  padding-top: 10px;
+  border-top: 1px solid #eee; // 分隔线
+}
+
+// 其他样式保持不变(标题、输入框、删除按钮等)
+.tab-title2 {
+  cursor: pointer;
+  transition: color 0.2s;
+}
+
+.tab-title2:hover {
+  color: #409eff;
+}
+
+.title-input2 {
+  width: 120px;
+}
+
+.del-group-btn {
+  width: 70px;
+  height: 30px;
+  border: 1px solid #ccc;
+  border-radius: 6px;
+  text-align: center;
+  line-height: 30px;
+  cursor: pointer;
+  font-size: 12px; // 适配按钮大小
+}
+
+.konva-container {
+  width: 100%;
+  height: 700px;
+  background-color: #fff;
+  border: 1px dashed red; // 可视化调试
+}
+
+//选中右侧的分组高亮
+.selected-group {
+  border: 2px solid #409eff !important;
+  box-shadow: 0 0 8px #409eff33;
+}
+
+//右侧隔离点渲染
+.right-konva-container {
+  width: 100%;
+  height: 220px;
+  //background-color: #fafafa;
+  //border-radius: 4px;
+  margin: 10px;
+}
+</style>

+ 13 - 0
src/views/sopm/sop/PointView/TableView.vue

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

+ 23 - 2
src/views/sopm/sop/SetModeStep.vue

@@ -16,8 +16,10 @@
         <ContentWrap>
           <TableView
             v-if="tabPosition == 'first'"
+            :sopId="route.query.sopId"
+            :modeId="route.query.modeId"
           />
-          <WorkflowView v-else  />
+          <WorkflowView v-else  :sopId="route.query.sopId" :modeId="route.query.modeId"/>
         </ContentWrap>
       </div>
     </div>
@@ -28,9 +30,28 @@
 import TableView from './ModeView/TableView.vue'
 import WorkflowView from './ModeView/WorkFlowView.vue'
 const router=useRouter()
+const route=useRoute()
 const tabPosition = ref('first')
 const goBack=()=>{
-  router.back()
+  if(route.query.type=='update'){
+    router.push({
+      path:'/sopm/sopm/sop/UpdateSop',
+      query:{
+        id:route.query.sopId,
+        type:route.query.type,
+      }
+
+    })
+  }else {
+    router.push({
+      path:'/sopm/sopm/sop/UpdateSop',
+      query:{
+        id:route.query.sopId,
+        type:route.query.type,
+      }
+    })
+  }
+
 }
 </script>
 

+ 111 - 5
src/views/sopm/sop/SetPoint.vue

@@ -1,13 +1,119 @@
-<script setup lang="ts">
-
-</script>
-
 <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>
+          <div class="add-group-btn" @click="addPointGroup">添加分组</div>
+          <CardView
+            v-if="tabPosition == 'first'"
+            :sopId="route.query.sopId"
+            :group-added="groupAdded"
+          />
+          <TableView v-else :sopId="route.query.sopId" />
+        </div>
+      </div>
+    </ContentWrap>
   </div>
 </template>
+<script setup lang="ts">
+import TableView from './PointView/TableView.vue'
+import CardView from './PointView/CardView.vue'
+
+const router = useRouter()
+const route = useRoute()
+const tabPosition = ref('first')
+// 定义信号
+const groupAdded = ref(false)
+const goBack=()=>{
+  if(route.query.type=='update'){
+    router.push({
+      path:'/sopm/sopm/sop/UpdateSop',
+      query:{
+        id:route.query.sopId,
+        type:route.query.type,
+      }
+
+    })
+  }else {
+    router.push({
+      path:'/sopm/sopm/sop/UpdateSop',
+      query:{
+        id:route.query.sopId,
+        type:route.query.type,
+      }
+    })
+  }
+
+}
+const addPointGroup = () => {
+  groupAdded.value = true // 触发信号
+  setTimeout(() => {
+    groupAdded.value = false // 重置信号(确保下次点击能再次触发)
+  }, 100)
+}
+</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;
+}
 
+.add-group-btn {
+  width: 70px;
+  height: 30px;
+  border: 1px solid black;
+  border-radius: 6px;
+  text-align: center;
+  line-height: 30px;
+  float: right;
+  cursor: pointer;
+}
 </style>

+ 96 - 5
src/views/sopm/sop/SetUser.vue

@@ -1,13 +1,104 @@
+<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>
+
+          <CardView
+            v-if="tabPosition == 'first'"
+            :sopId="route.query.sopId"
+            :group-added="groupAdded"
+          />
+          <TableView v-else :sopId="route.query.sopId" />
+        </div>
+      </div>
+    </ContentWrap>
+  </div>
+</template>
 <script setup lang="ts">
+import TableView from './UserView/TableView.vue'
+import CardView from './UserView/CardView.vue'
 
-</script>
+const router = useRouter()
+const route = useRoute()
+const tabPosition = ref('first')
+// 定义信号
+const groupAdded = ref(false)
+const goBack=()=>{
+  if(route.query.type=='update'){
+    router.push({
+      path:'/sopm/sopm/sop/UpdateSop',
+      query:{
+        id:route.query.sopId,
+        type:route.query.type,
+      }
 
-<template>
-<div>
+    })
+  }else {
+    router.push({
+      path:'/sopm/sopm/sop/UpdateSop',
+      query:{
+        id:route.query.sopId,
+        type:route.query.type,
+      }
+    })
+  }
 
-</div>
-</template>
+}
+
+</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>

+ 493 - 56
src/views/sopm/sop/UpdateSop.vue

@@ -9,7 +9,7 @@
               <el-icon size="20" style="margin-left: 10px">
                 <InfoFilled />
               </el-icon>
-              <span style="font-size: 18px">SOP修改</span>
+              <span style="font-size: 18px">SOP创建步骤</span>
             </div>
           </template>
 
@@ -36,7 +36,7 @@
             label-width="68px"
           >
             <el-row>
-              <el-col :span="4">
+              <el-col :span="5">
                 <el-form-item label="SOP名称" prop="sopName">
                   <el-input
                     v-model="SopForm.sopName"
@@ -59,6 +59,7 @@
                     :props="{ label: 'workstationName', value: 'id', children: 'children' }"
                     placeholder="选择岗位"
                     class="!w-240px"
+
                   />
                 </el-form-item>
               </el-col>
@@ -69,6 +70,7 @@
                     :data="machineryOptions"
                     :props="{ label: 'machineryName', value: 'id', children: 'children' }"
                     placeholder="选择设备/工艺"
+                    @change="machineryChangeFunction"
                     class="!w-240px"
                   />
                 </el-form-item>
@@ -119,12 +121,12 @@
       <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 class="set-btn" @click="goSetting('SetModeStep', SopForm, null)">设置</div>
         </div>
         <div class="tab-content">
           <!-- VueFlow 主画布 -->
-          <VueFlow style="width: 100%; height: 300px">
-            <template #node-default="{id, data }">
+          <VueFlow style="width: 100%; height: 300px" :min-zoom="1" :max-zoom="1" :default-zoom="1">
+            <template #node-default="{ id, data }">
               <div class="custom-node">
                 <div class="node-content">
                   <!-- 图标显示 -->
@@ -175,13 +177,31 @@
       <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 class="set-btn" @click="goSetting('SetPoint', SopForm, null)">设置</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)"/>
+          <div class="point_center_box" v-if="!SopForm?.sopPointsList?.length">
+            <img
+              src="../../../assets/images/添加.png"
+              alt=""
+              @click="goSetting('SetPoint', SopForm, null)"
+            />
             <span style="color: red">*请添加需要进行隔离的点位</span>
           </div>
+          <div v-else>
+            <!-- 循环渲染分组 -->
+            <div class="group-container">
+              <div v-for="group in resolvedGroupedPoints" :key="group.groupId" class="point-group">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="points-list">
+                  <div v-for="point in group.points" :key="point.pointId" class="point-item">
+                    <img :src="point.pointIcon" class="point-icon" />
+                    <div class="point-name">{{ point.pointName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </ContentWrap>
@@ -190,30 +210,76 @@
       <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 class="set-btn" @click="goSetting('SetUser', SopForm, null)">设置</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)"/>
+
+            <!-- 有锁定人数据时显示 -->
+            <div v-if="groupedLockers.length" class="group-container-user">
+              <div v-for="group in groupedLockers" :key="group.groupId" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <div v-for="user in group.users" :key="user.userId" class="user-card">
+                    <img src="@/assets/images/UserBlack.png" />
+                    <div class="user-name">{{ user.userName }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- 有分组点数据时显示 -->
+            <div
+              v-else-if="SopForm.sopGroupList && SopForm.sopGroupList[0].groupName !== '默认分组'"
+              class="group-container"
+            >
+              <div v-for="group in SopForm.sopGroupList" :key="group.id" class="group-card-user">
+                <div class="group-title">{{ group.groupName }}</div>
+                <div class="user-list">
+                  <img
+                    src="@/assets/images/添加.png"
+                    class="user-card"
+                    @click="goSetting('SetUser', SopForm, group.id)"
+                  />
+                </div>
+              </div>
+            </div>
+
+            <!-- 都没有数据时显示添加提示 -->
+            <div v-else class="point_center_box">
+              <img src="@/assets/images/添加.png" @click="goSetting('SetUser', SopForm, null)" />
               <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)"/>
+            <div v-if="coLockUsers.length" class="user-list-colocker">
+              <div v-for="user in coLockUsers" :key="user.userId" class="user-card">
+                <img src="@/assets/images/UserBlack.png" />
+                <div class="user-name">{{ user.userName }}</div>
+              </div>
+            </div>
+            <div v-else class="point_center_box">
+              <img
+                src="@/assets/images/添加.png"
+                alt=""
+                @click="goSetting('SetUser', SopForm, null)"
+              />
               <span>请添加参与共锁的人员</span>
             </div>
           </div>
         </div>
       </div>
     </ContentWrap>
+
     <div class="bottom-btn">
       <el-button @click="submit">
         <el-icon>
@@ -240,12 +306,16 @@ 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'
+import * as SopPointGroup from '@/api/sop/sopPointGroup'
+import * as PointApi from '@/api/dv/spm/index'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 import { handleTree } from '@/utils/tree'
 import { ref } from 'vue'
-import {Handle, useVueFlow, VueFlow} from '@vue-flow/core'
+import { Handle, useVueFlow, VueFlow } from '@vue-flow/core'
+import { ElMessageBox } from 'element-plus'
+import { insertSopWorkflowStep } from '@/api/sop/sopStep'
 
 const SopForm = reactive({
   createTime: null,
@@ -274,7 +344,9 @@ const activeName = ref('0')
 const machineryOptions = ref()
 const workstationOption = ref()
 const ModeOption = ref()
-
+const allGroups = ref<any[]>([]) //获取所有分组
+const groupList = ref([]) //获取当前sopId的分组
+const allPoints = ref<any[]>([]) //获取所有点位
 const nodes = ref([]) //储存节点
 const edges = ref([]) // 存储连接线
 // 创建查找映射
@@ -282,41 +354,151 @@ 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'){
+const goSetting = (type, SopForm, groupId) => {
+  if (type == 'SetModeStep') {
     router.push({
-      name:'SetModeStep',
-      query:{
-        sopId:sopId,
+      name: 'SetModeStep',
+      query: {
+        sopId: SopForm.id,
+        modeId: SopForm.modeId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
       }
     })
-  }else if(type=='SetPoint'){
+  } else if (type == 'SetPoint') {
     router.push({
-      name:'SetPoint',
-      query:{
-        sopId:sopId,
+      name: 'SetPoint',
+      query: {
+        sopId: SopForm.id,
+        machineryId: SopForm.machineryId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
       }
     })
-  }else if(type=='SetUser'){
+  } else if (type == 'SetUser') {
     router.push({
-      name:'SetUser',
-      query:{
-        sopId:sopId,
+      name: 'SetUser',
+      query: {
+        sopId: SopForm.id,
+        groupId: groupId,
+        type: route.query.type //获取的是新增或者修改 不是设置的type
       }
     })
   }
 }
+
+// 获取所有点位和分组的数据
+const fetchAllGroupsAndPoints = async () => {
+  try {
+    // 分组信息
+    const groupRes = await SopPointGroup.getSopGroupList({ pageSize: -1, pageNo: 1 })
+    allGroups.value = groupRes
+    console.log('获取分组', groupRes)
+    // 获取当前sopId的分组
+    if (route.query.id) {
+      groupList.value = await SopPointGroup.getSopGroupList({
+        pageSize: -1,
+        pageNo: 1,
+        sopId: route.query.id
+      })
+    }
+
+    // 点位信息
+    const pointRes = await PointApi.getIsIsolationPointPage({ pageSize: -1, pageNo: 1 })
+    allPoints.value = pointRes?.list
+    console.log('获取点位', pointRes)
+  } catch (e) {}
+}
+// 回显分组和点位数据的计算属性
+// 回显分组和点位数据的计算属性
+const resolvedGroupedPoints = computed(() => {
+  const groupsMap = new Map<string, { groupId: string; groupName: string; points: any[] }>()
+
+  console.log('SopForm.sopPointsList:', SopForm.sopPointsList)
+  console.log('allGroups.value:', allGroups.value)
+  console.log('allPoints.value:', allPoints.value)
+
+  SopForm.sopPointsList.forEach((item) => {
+    const groupId = String(item.groupId)
+    const pointId = item.pointId
+
+    console.log('处理项目:', { groupId, pointId, item })
+
+    // 查分组名
+    const groupInfo = allGroups.value.find((g) => String(g.id) === groupId)
+    const groupName = groupInfo?.groupName
+
+    console.log('找到的分组信息:', groupInfo, '分组名:', groupName)
+
+    // 查点位详情
+    const pointInfo = allPoints.value.find((p) => p.id == pointId)
+    const pointName = pointInfo?.pointName
+    const pointIcon = pointInfo?.pointIcon
+
+    console.log('找到的点位信息:', pointInfo, '点位名:', pointName)
+
+    if (!groupsMap.has(groupId)) {
+      groupsMap.set(groupId, {
+        groupId,
+        groupName,
+        points: []
+      })
+    }
+
+    const group = groupsMap.get(groupId)!
+
+    // 检查是否重复
+    const isDuplicate = group.points.some((p) => p.pointId === pointId)
+    console.log('是否重复:', isDuplicate, '当前组内点位:', group.points)
+
+    // 防止重复添加
+    if (!isDuplicate) {
+      group.points.push({
+        pointId,
+        pointName,
+        pointIcon
+      })
+      console.log('添加点位后:', group.points)
+    }
+  })
+
+  const result = Array.from(groupsMap.values())
+  console.log('最终结果:', result)
+  return result
+})
+// 从 SopForm.sopUserList 中提取锁定人并按 groupId 分组
+const groupedLockers = computed(() => {
+  const lockerUsers =
+    SopForm.sopUserList?.filter((u) => u.userRole === 'jtlocker' && u.groupId != null) || []
+  const groupMap = new Map()
+
+  lockerUsers.forEach((user) => {
+    if (!groupMap.has(user.groupId)) {
+      const groupName =
+        groupList.value.find((g) => g.id === user.groupId)?.groupName || '未命名分组'
+      groupMap.set(user.groupId, { groupId: user.groupId, groupName, users: [] })
+    }
+    groupMap.get(user.groupId).users.push(user)
+  })
+
+  return Array.from(groupMap.values())
+})
+
+// 提取共锁人
+const coLockUsers = computed(() => {
+  return SopForm.sopUserList?.filter((u) => u.userRole === 'jtcolocker') || []
+})
 // 获取基本信息
 const getOtherList = async () => {
   try {
     // 获取sop信息
     const SopData = await SopApi.selectSopById(route.query.id)
-    console.log(SopData, 'aaa')
+
     Object.assign(SopForm, SopData)
+    console.log(SopData, 'aaa')
     // 获取岗位数据
     const deptRes = await MarsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 })
     workstationOption.value = handleTree(deptRes.list, 'id', 'parentId')
@@ -341,33 +523,122 @@ const getOtherList = async () => {
     ElMessage.error('获取数据失败')
   }
 }
+// 工艺/设备手动切换函数
+const machineryChangeFunction = async (value) => {
+  const isUpdateMachinery = route.query.type === 'update'
+  const data = await SopApi.selectSopById(route.query.id)
+  const oldMachineryId = data.machineryId
+  const oldWorkstationId = data.workstationId
+  const isMachineryChanged = oldMachineryId !== value
 
-// 流程模式切换
-const handleModeChange = async (value) => {
-  console.log(value, 'value')
+  if (isUpdateMachinery && isMachineryChanged) {
+    try {
+      await ElMessageBox.confirm(
+        '警告:切换设备工艺将清空对应所有数据,是否继续?',
+        '工艺/设备切换确认',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      )
 
+      // 用户点击了“确定”
+      SopForm.machineryId = value
+
+    } catch (error) {
+      // 用户取消
+      isReverting.value = true // 避免触发 watch 的副作用
+      SopForm.machineryId = oldMachineryId
+      SopForm.workstationId = oldWorkstationId
+      await getMachineryData(oldWorkstationId)
+      console.log('用户取消了切换,回退旧值')
+    }
+  } else {
+    SopForm.machineryId = value
+  }
+}
+
+// 模式初始化
+const onModeChange = async (value) => {
   SopForm.modeId = value
-  // 清空画布
   await clearCanvasProperly()
-  if (SopForm.modeId) {
+
+  if (SopForm.modeId && route.query.type !== 'update') {
     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
+    const sortedData = data.list.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
+    renderNodesFromData(sortedData)
+    renderEdgesFromData(sortedData)
+  } else if (SopForm.modeId && route.query.type == 'update') {
+    const Data = await SopApi.selectSopById(route.query.id)
+    const sortedData = Data.sopStepList.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
+    renderNodesFromData(sortedData)
+    renderEdgesFromData(sortedData)
+  }
+}
+
+// 流程模式手动切换
+const isModeChangedBoolean = ref(false)//给确认按钮是否更改了模式做判断
+const handleModeChange = async (value) => {
+  const isUpdateMode = route.query.type === 'update'
+  const data = await SopApi.selectSopById(route.query.id)
+  const oldModeId = data.modeId
+  const isModeChanged = oldModeId !== value
+
+  // 修改模式且模式发生变化,需要确认
+  if (isUpdateMode && isModeChanged) {
+    try {
+      await ElMessageBox.confirm('警告:切换流程模式将清空对应所有数据,是否继续?', '模式切换确认', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
       })
-      // 渲染节点
+
+    } catch (error) {
+      // 用户取消操作
+      SopForm.modeId = oldModeId
+      isModeChangedBoolean.value = true
+      console.log('用户取消了模式切换')
+      return
+    }
+  }
+
+  // 更新模式ID
+  SopForm.modeId = value
+
+  // 清空画布
+  await clearCanvasProperly()
+
+  // 根据情况加载和渲染数据
+  if (isUpdateMode && !isModeChanged) {
+    // 修改模式但模式未变化:从SOP数据渲染
+    const Data = await SopApi.selectSopById(route.query.id)
+    if (Array.isArray(Data.sopStepList) && Data.sopStepList.length > 0) {
+      const sortedData = Data.sopStepList.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
       renderNodesFromData(sortedData)
-      // 渲染连接线
       renderEdgesFromData(sortedData)
     }
+  } else {
+    // 新增模式或模式发生变化:从模式模板获取数据
+    if (SopForm.modeId) {
+      const data = await ModeStepApi.getWorkflowStepPage({
+        pageNo: 1,
+        pageSize: -1,
+        modeId: SopForm.modeId
+      })
+
+      if (Array.isArray(data.list) && data.list.length > 0) {
+        const sortedData = data.list.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
+
+        renderNodesFromData(sortedData)
+        renderEdgesFromData(sortedData)
+      }
+    }
   }
 }
 // 独立的清空画布函数
@@ -458,16 +729,26 @@ const renderEdgesFromData = (data) => {
     console.log('创建连接线:', edge)
   }
 }
-// 监听表单数据变化
+// 监听 modeId 变化
 watch(
-  () => SopForm,
-  () => {
+  () => SopForm.modeId,
+  async (newValue) => {
     hasUnsavedChanges.value = true
-    handleModeChange(SopForm.modeId)
 
+    await onModeChange(newValue) // 标记是初始化
+  },
+  { immediate: true }
+)
+
+// 监听其他表单字段变化(用于标记未保存)
+watch(
+  () => [SopForm.sopName, SopForm.sopContent, SopForm.workstationId],
+  () => {
+    hasUnsavedChanges.value = true
   },
   { deep: true }
 )
+
 // 保存成功后重置标记
 const submit = async () => {
   try {
@@ -477,10 +758,28 @@ const submit = async () => {
     if (SopForm.id) {
       // 修改操作
       data = await SopApi.updateSop(SopForm)
+
       successMessage = t('common.updateSuccess')
 
       if (data) {
         message.success(successMessage)
+        // 如果修改了模式步骤 重新插入最新的模式步骤
+        if(isModeChangedBoolean.value){
+          const dataStep = await ModeStepApi.getWorkflowStepPage({
+            pageNo: 1,
+            pageSize: -1,
+            modeId: SopForm.modeId
+          })
+          const sopStepList = dataStep.list.map((item) => ({
+            ...item,
+            sopId: SopForm.id,
+            stepId: item.id
+          }))
+          // 导入步骤数据
+          await insertSopWorkflowStep(sopStepList)
+          isModeChangedBoolean.value = false
+        }
+
         hasUnsavedChanges.value = false
       }
     } else {
@@ -489,6 +788,7 @@ const submit = async () => {
       successMessage = t('common.createSuccess')
 
       if (data) {
+
         // 新增成功后,获取完整数据
         try {
           const selectData = await SopApi.selectSopById(data)
@@ -504,7 +804,6 @@ const submit = async () => {
 
         message.success(successMessage)
         hasUnsavedChanges.value = false
-        Visible.value = true
       }
     }
   } catch (error) {
@@ -537,7 +836,7 @@ const cancel = async () => {
 // 初始化
 onMounted(() => {
   getOtherList()
-
+  fetchAllGroupsAndPoints() //获取所有分组和点位 来渲染SOP的首页
 })
 
 // 构建岗位查找映射
@@ -578,15 +877,33 @@ watchEffect(() => {
     generateSopName()
   }
 })
-// 监听 workstationId 变化
-watchEffect(async () => {
-  const newWorkstationId = SopForm.workstationId
-  if (newWorkstationId) {
-    // console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
-    SopForm.machineryId = null
-    await getMachineryData(newWorkstationId)
+// 添加一个标识,记录是否是首次加载
+const isFirstLoad = ref(true)
+// 修改设备工艺回退操作
+const isReverting = ref(false) // 标记是否为取消操作回退
+
+watch(
+  () => SopForm.workstationId,
+  async (newWorkstationId, oldWorkstationId) => {
+    if (isFirstLoad.value) {
+      isFirstLoad.value = false
+      return
+    }
+
+    // 取消操作导致的回退,跳过 watch
+    if (isReverting.value) {
+      isReverting.value = false
+      return
+    }
+
+    if (newWorkstationId && newWorkstationId !== oldWorkstationId) {
+      console.log('岗位ID变化,重新获取工艺数据:', newWorkstationId)
+      SopForm.machineryId = null
+      await getMachineryData(newWorkstationId)
+    }
   }
-})
+)
+
 
 // 获取工艺数据的函数
 const getMachineryData = async (workstationId) => {
@@ -706,6 +1023,7 @@ const getSopTypeName = (sopType) => {
   height: 40px;
   display: flex;
   justify-content: flex-end;
+  padding-right: 70px;
 }
 
 .custom-node {
@@ -802,4 +1120,123 @@ const getSopTypeName = (sopType) => {
 :deep(.vue-flow__edge-marker) {
   fill: #333;
 }
+
+.group-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px; /* 卡片之间的间距 */
+}
+
+.point-group {
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  padding: 12px;
+  min-width: 250px;
+  background-color: #fafafa;
+  height: 250px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.group-title {
+  font-weight: 600;
+  font-size: 16px;
+  margin-bottom: 12px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #e0e0e0;
+  color: #333;
+}
+
+.points-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.point-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 60px;
+}
+
+.point-icon {
+  width: 40px;
+  height: 40px;
+  object-fit: contain;
+}
+
+.point-name {
+  font-size: 12px;
+  text-align: center;
+  margin-top: 4px;
+  color: #555;
+}
+
+//用户的卡片
+.group-container-user {
+  display: flex;
+  flex-direction: row;
+  gap: 16px;
+  overflow-x: auto;
+  padding-bottom: 10px;
+}
+
+.group-card-user {
+  width: 180px;
+  min-height: 150px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+  padding: 12px;
+  flex-shrink: 0;
+  border: 1px solid #eee;
+
+  display: flex;
+  flex-direction: column;
+  margin-top: 10px;
+}
+
+.group-title {
+  font-weight: bold;
+  font-size: 16px;
+  margin-bottom: 10px;
+  color: #333;
+  text-align: center;
+  border-bottom: 1px solid #f0f0f0;
+  padding-bottom: 6px;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: center;
+}
+
+.user-list-colocker {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: flex-start;
+  margin-top: 10px;
+}
+
+.user-card {
+  width: 60px;
+  text-align: center;
+
+  img {
+    width: 40px;
+    height: 40px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+  }
+
+  .user-name {
+    font-size: 12px;
+    margin-top: 4px;
+    color: #555;
+    word-break: break-all;
+  }
+}
 </style>

+ 438 - 0
src/views/sopm/sop/UserView/CardView.vue

@@ -0,0 +1,438 @@
+<template>
+  <div class="page-container">
+    <div class="main-layout">
+      <!-- 左侧 -->
+      <div class="left-side">
+        <!-- 可选择的锁定人 -->
+        <el-card class="card-block" style="height: 300px">
+          <template #header>可选择的锁定人</template>
+          <div class="user-list">
+            <div
+              v-for="user in jtlocker"
+              :key="user.id"
+              class="user-card"
+              :class="{ selected: isUserInSelectedGroup(user) }"
+              @click="selectUser(user, 'lock')"
+            >
+              <img src="@/assets/images/UserEmpty.png" />
+              <div class="user-name">{{ user.nickname }}</div>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 可选择的共锁人 -->
+        <el-card class="card-block" style="height: 350px">
+          <template #header>可选择的共锁人</template>
+          <div class="user-list">
+            <div
+              v-for="user in jtcolocker"
+              :key="user.id"
+              class="user-card"
+              @click="selectUser(user, 'coLock')"
+            >
+              <img src="@/assets/images/UserEmpty.png" />
+              <div class="user-name">{{ user.nickname }}</div>
+            </div>
+          </div>
+        </el-card>
+      </div>
+
+      <!-- 右侧 -->
+      <div class="right-side">
+        <!-- 已添加的锁定人 -->
+        <el-card class="card-block red-border" style="height: 300px">
+          <template #header>已添加的锁定人</template>
+          <div class="group-container">
+            <div
+              v-for="group in allGroup"
+              :key="group.id"
+              class="group-card"
+              :class="{ active: selectedGroupId == group.id }"
+              @click="selectGroup(group.id)"
+            >
+              <el-card class="inner-card">
+                <template #header>{{ group.groupName }}</template>
+                <div class="user-list">
+                  <div
+                    class="user-card"
+                    v-for="user in addedLockUsers[group.id] || []"
+                    :key="user.id"
+                    @click.stop="removeLockUser(user, group.id)"
+                  >
+                    <img src="@/assets/images/UserBlack.png" />
+                    <div class="user-name">{{ user.nickname }}</div>
+                  </div>
+                </div>
+              </el-card>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 已添加的共锁人 -->
+        <el-card class="card-block orange-border" style="height: 350px">
+          <template #header>已添加的共锁人</template>
+          <div class="user-list">
+            <div
+              v-for="user in addedCoLockUsers"
+              :key="user.id"
+              class="user-card"
+              @click="removeCoLockUser(user)"
+            >
+              <img src="@/assets/images/UserBlack.png" />
+              <div class="user-name">{{ user.nickname }}</div>
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="bottom-btn">
+      <el-button type="primary" @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 { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import * as SopPointGroup from '@/api/sop/sopPointGroup'
+import * as RoleUser from '@/api/system/user'
+import * as SopUserApi from '@/api/sop/sopUser'
+import * as SopApi from '@/api/sop/index'
+import { Check, Close } from '@element-plus/icons-vue'
+
+const route = useRoute()
+const router = useRouter()
+const allGroup = ref<any[]>([])
+const jtlocker = ref<any[]>([])
+const jtcolocker = ref<any[]>([])
+
+const addedLockUsers = ref<Record<string, any[]>>({})
+const addedCoLockUsers = ref<any[]>([])
+const selectedGroupId = ref<string>('')
+const toDeleteUserIds = ref<number[]>([]) //删除的人员
+
+// 加载数据
+onMounted(async () => {
+  const groupData = await SopPointGroup.getSopGroupList({
+    pageNo: 1,
+    pageSize: -1,
+    sopId: route.query.sopId
+  })
+
+  allGroup.value = groupData
+  jtlocker.value = await RoleUser.getRoleUser('jtlocker')
+  if(route.query.groupId){
+    selectedGroupId.value=route.query.groupId
+  }else {
+    selectedGroupId.value = ''
+  }
+
+
+  const SopUserData = await SopApi.selectSopById(route.query.sopId)
+  console.log(SopUserData,'SopUserData')
+  if (Array.isArray(SopUserData.sopUserList) && SopUserData.sopUserList.length > 0) {
+    const userList = SopUserData.sopUserList
+    // 1. 渲染锁定人
+    userList
+      .filter((user) => user.userRole === 'jtlocker')
+      .forEach((user) => {
+        const groupId = user.groupId
+        if (!groupId) return // 没分组就跳过
+
+        if (!addedLockUsers.value[groupId]) {
+          addedLockUsers.value[groupId] = []
+        }
+
+        addedLockUsers.value[groupId].push({
+          id: user.id,
+          userId: user.userId,
+          nickname: user.userName
+        })
+      })
+
+    // 2. 渲染共锁人(groupId 为 null)
+    userList
+      .filter((user) => user.userRole === 'jtcolocker')
+      .forEach((user) => {
+        addedCoLockUsers.value.push({
+          id: user.id,
+          userId: user.userId,
+          nickname: user.userName
+        })
+
+        // 从左侧移除已分配的共锁人
+        jtcolocker.value = jtcolocker.value.filter((u) => u.id !== user.userId)
+      })
+  } else {
+
+    // 渲染初始化的
+
+    jtcolocker.value = await RoleUser.getRoleUser('jtcolocker')
+  }
+})
+
+// 切换分组
+const selectGroup = (groupId: string) => {
+  console.log('选中分组:', groupId)
+  selectedGroupId.value = groupId
+}
+
+// 是否在选中的分组中(只检查当前选中的分组)
+const isUserInSelectedGroup = (user) => {
+  if (!selectedGroupId.value) return false
+  const groupList = addedLockUsers.value[selectedGroupId.value] || []
+  return groupList.some((u) => u.id === user.id)
+}
+
+// 添加 or 移除
+const selectUser = (user, type: 'lock' | 'coLock') => {
+  if (type === 'lock') {
+    if (!selectedGroupId.value) {
+      ElMessage.warning('请先选择一个分组')
+      return
+    }
+
+    const groupList = addedLockUsers.value[selectedGroupId.value] || []
+    const existingIndex = groupList.findIndex((u) => u.id === user.id)
+
+    if (existingIndex !== -1) {
+      groupList.splice(existingIndex, 1)
+    } else {
+      if (!addedLockUsers.value[selectedGroupId.value]) {
+        addedLockUsers.value[selectedGroupId.value] = []
+      }
+      addedLockUsers.value[selectedGroupId.value].push(user)
+    }
+  } else {
+    const index = jtcolocker.value.findIndex((u) => u.id === user.id)
+    if (index !== -1) {
+      addedCoLockUsers.value.push(user)
+      jtcolocker.value.splice(index, 1)
+    }
+  }
+}
+
+// 移除锁定人
+const removeLockUser = (user, groupId) => {
+  const list = addedLockUsers.value[groupId]
+  if (!list) return
+  addedLockUsers.value[groupId] = list.filter((u) => u.id !== user.id)
+
+  // 记录待删除
+  toDeleteUserIds.value.push(user.id)
+}
+
+// 移除共锁人(同时回到左侧)
+const removeCoLockUser = (user) => {
+  addedCoLockUsers.value = addedCoLockUsers.value.filter((u) => u.id !== user.id)
+  jtcolocker.value.push(user)
+
+  // 记录待删除
+  toDeleteUserIds.value.push(user.id)
+  console.log(user,'user')
+}
+
+// 确认按钮事件
+const submit = async () => {
+  try {
+    const sopId = route.query.sopId
+    const userList: any[] = []
+
+    // === 构造新增人员数据 ===
+    Object.keys(addedLockUsers.value).forEach((groupId) => {
+      const users = addedLockUsers.value[groupId] || []
+      users.forEach((user) => {
+        userList.push({
+          sopId,
+          groupId,
+          userId: user.id,
+          userName: user.nickname,
+          userType: 0,
+          userRole: 'jtlocker'
+        })
+      })
+    })
+
+    addedCoLockUsers.value.forEach((user) => {
+      userList.push({
+        sopId,
+        groupId: null,
+        userId: user.id,
+        userName: user.nickname,
+        userType: 0,
+        userRole: 'jtcolocker'
+      })
+    })
+
+    // === 没有任何操作则提示 ===
+    if (userList.length === 0 && toDeleteUserIds.value.length === 0) {
+      ElMessage.warning('请至少选择一个人员或移除一个人员')
+      return
+    }
+
+    // === 删除操作 ===
+    if (toDeleteUserIds.value.length > 0) {
+      const deleteIds = toDeleteUserIds.value.join(',')
+      await SopUserApi.deleteSopUserList(deleteIds)
+      console.log('删除成功:', toDeleteUserIds.value)
+    }
+
+    // === 新增操作 ===
+    if (userList.length > 0) {
+      const result = await SopUserApi.insertSopUser(userList)
+      if (result) {
+        ElMessage.success('保存成功')
+        // 成功后清空删除列表
+        toDeleteUserIds.value = []
+      } else {
+        ElMessage.error(result?.msg || '保存失败')
+      }
+    }
+  } catch (error) {}
+}
+
+const cancel = () => {
+  if (route.query.type == 'update') {
+    router.push({
+      path: '/sopm/sopm/sop/UpdateSop',
+      query: {
+        id: route.query.sopId,
+        type: route.query.type
+      }
+    })
+  } else {
+    router.push({
+      path: '/sopm/sopm/sop/UpdateSop',
+      query: {
+        id: route.query.sopId,
+        type: route.query.type
+      }
+    })
+  }
+}
+</script>
+<style lang="scss" scoped>
+.page-container {
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+}
+
+.main-layout {
+  display: flex;
+  gap: 20px;
+}
+
+.left-side,
+.right-side {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.card-block {
+  border-radius: 8px;
+}
+
+.red-border {
+  border-top: 4px solid #d3342e;
+}
+
+.orange-border {
+  border-top: 4px solid #f7941d;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.user-card {
+  width: 80px;
+  text-align: center;
+  cursor: pointer;
+  border: 2px solid transparent;
+  padding: 4px;
+
+  img {
+    width: 50px;
+    height: 50px;
+    object-fit: cover;
+    border: 1px solid #ccc;
+  }
+
+  .user-name {
+    margin-top: 4px;
+    font-size: 14px;
+  }
+
+  &.selected {
+    border: 2px solid red;
+
+    .user-name {
+      background: red;
+      color: #fff;
+      padding: 2px;
+    }
+  }
+}
+
+.group-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+}
+
+.group-card {
+  min-width: 200px;
+  min-height: 120px;
+  cursor: pointer;
+  border: 2px solid transparent;
+  border-radius: 6px;
+  transition: border-color 0.3s;
+
+  &.active {
+    border: 2px solid red;
+  }
+
+  // 防止内部卡片影响外部点击
+  .inner-card {
+    height: 100%;
+
+    :deep(.el-card__header) {
+      padding: 8px 12px;
+      font-size: 14px;
+      font-weight: bold;
+    }
+
+    :deep(.el-card__body) {
+      padding: 8px;
+    }
+  }
+}
+
+// 底部按钮
+.bottom-btn {
+  display: flex;
+  gap: 10px;
+  justify-content: flex-end;
+  padding-top: 10px;
+  border-top: 1px solid #eee;
+}
+</style>

+ 6 - 0
src/views/sopm/sop/UserView/TableView.vue

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

+ 64 - 6
src/views/sopm/sop/index.vue

@@ -51,6 +51,15 @@
           <Icon icon="ep:plus" class="mr-5px" />
           新增
         </el-button>
+        <el-button
+          type="danger"
+          plain
+          :disabled="multiple"
+          @click="handleDelete()"
+          v-hasPermi="['iscs:sop:delete']"
+        >
+          <Icon icon="ep:delete" class="mr-5px" /> 批量删除
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -64,6 +73,16 @@
       <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="是否生效" align="center" width="120">
+        <template #default="scope">
+          <el-switch
+            :model-value="scope.row.sopStatus"
+            active-value="1"
+            inactive-value="0"
+            @update:modelValue="val => handleStatusChange(scope.row, val)"
+          />
+        </template>
+      </el-table-column>
 <!--      <el-table-column label="sop内容" prop="sopContent"  />-->
 <!--      <el-table-column label="sop状态" prop="sopStatus"  align="center" />-->
 <!--      <el-table-column label="sop权重序号" prop="sopIndex" />-->
@@ -103,7 +122,8 @@
 <script lang="ts" setup>
 import * as SopApi from '@/api/sop'
 import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
-
+// 添加初始化标志位
+const isInitialized = ref(false);
 const router = useRouter()
 
 defineOptions({ name: 'SopManagement' })
@@ -114,6 +134,8 @@ const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const sopList = ref([]) // 列表的数据
 const total = ref(0) // 总条数
+const ids = ref<number[]>([]) // 选中的数据
+const multiple = ref(true) // 非多个禁用
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -121,13 +143,46 @@ const queryParams = reactive({
   sopType: undefined,
 })
 const queryFormRef = ref() // 搜索的表单
-const ids = ref() // 选中的ID数组
+// 是否生效
+const handleStatusChange = async (row, val) => {
+  if (!isInitialized.value) return
+
+  const originalValue = row.sopStatus
+
+  try {
+    const data = {
+      ...row,
+      sopStatus: val
+    }
+
+    await SopApi.updateSopStatus(data)
+    ElMessage.success(val == 1 ? '已生效' : '已取消生效')
+
+    // 成功后更新值(防止 UI 不更新)
+    row.sopStatus = val
+
+  } catch (err) {
+    // 接口失败,回滚原始值
+    row.sopStatus = originalValue
+
+    // 强制刷新 UI
+    await nextTick()
+
+    // 提示错误
+    // ElMessage.error('状态更新失败,请重试')
+  }
+}
+
+
 
 /** 多选框选中数据 */
+
 const handleSelectionChange = (selection: any[]) => {
-  ids.value = selection.map((item) => item.id)
+  ids.value = selection.map(item => item.id)
+  multiple.value = !selection.length
 }
 
+
 /** 查询模式列表 */
 const getList = async () => {
   loading.value = true
@@ -155,12 +210,12 @@ const resetQuery = () => {
 
 /** 添加/修改/查看操作 */
 const openForm = (type: string, id?: number) => {
-  if (type === 'create') {
+  if (type == 'create') {
     router.push({
       name: 'CreateSop',
       query: { id: id, type: 'create' }
     })
-  } else if (type === 'update') {
+  } else if (type == 'update') {
     router.push({
       name: 'UpdateSop',
       query: { id: id, type: 'update' }
@@ -170,11 +225,12 @@ const openForm = (type: string, id?: number) => {
 
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
+  const sopId = id || ids.value
   try {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await SopApi.deleteSopList(id)
+    await SopApi.deleteSopList(sopId)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -184,6 +240,8 @@ const handleDelete = async (id: number) => {
 /** 初始化 **/
 onMounted(async () => {
   await getList()
+  // 数据加载完成后
+  isInitialized.value = true
 })
 </script>
 <style scoped>

+ 2 - 2
src/views/system/menu/index.vue

@@ -1,6 +1,6 @@
 <template>
-  <doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />
-  <doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" />
+<!--  <doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />-->
+<!--  <doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" />-->
 
   <!-- 搜索工作栏 -->
   <ContentWrap>

+ 2 - 2
src/views/system/role/index.vue

@@ -1,6 +1,6 @@
 <template>
-  <doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />
-  <doc-alert title="数据权限" url="https://doc.iocoder.cn/data-permission" />
+<!--  <doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />-->
+<!--  <doc-alert title="数据权限" url="https://doc.iocoder.cn/data-permission" />-->
 
   <ContentWrap>
     <!-- 搜索工作栏 -->

+ 1 - 1
src/views/system/tenant/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <doc-alert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />
+<!--  <doc-alert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />-->
 
   <!-- 搜索 -->
   <ContentWrap>

+ 1 - 1
src/views/system/tenantPackage/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <doc-alert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />
+<!--  <doc-alert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />-->
 
   <!-- 搜索 -->
   <ContentWrap>

+ 1 - 1
src/views/system/user/UserForm.vue

@@ -70,7 +70,7 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-<!--          芋道原来的岗位-->
+<!--          锁控原来的岗位-->
           <el-form-item label="职位">
             <el-select v-model="formData.postIds" multiple placeholder="请选择">
               <el-option

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

@@ -1,7 +1,7 @@
 <template>
-  <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
-  <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
-  <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
+<!--  <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />-->
+<!--  <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />-->
+<!--  <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />-->
 
   <el-row :gutter="20">
     <!-- 左侧部门树 -->