|
|
@@ -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>
|