| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484 |
- <template>
- <!--基本信息-->
- <ContentWrap>
- <div class="basicInformation">
- <!-- <section class="topTitle">-->
- <div class="tab-header">
- <section class="topTitle_left">
- <img src="@/assets/images/information.png" alt="" class="titleimg" />
- <span class="tab-title"> 基本信息</span>
- </section>
- <div class="set-btn" @click="goBack">
- <img src="../../../assets/images/返回.png" alt="" />
- 返回
- </div>
- </div>
- <div class="basicContent">
- <p
- >作业名称: <span>{{ JobForm.ticketName }}</span></p
- >
- <p
- >作业区域: <span>{{ JobForm.workstationName }}</span></p
- >
- <p
- >工艺设备: <span>{{ JobForm.machineryName }}</span></p
- >
- <p
- >作业类型: <span>{{ ticketTypeLabel }}</span></p
- >
- <p
- >作业状态: <span>{{ ticketStatusLabel }}</span></p
- >
- <p
- >开始时间: <span>{{ JobForm.ticketStartTime || '-' }}</span></p
- >
- <p
- >结束时间: <span>{{ JobForm.ticketEndTime || '-' }} </span></p
- >
- <p
- >当前步骤: <span class="specialText">{{ currentStepTitle }}</span></p
- >
- <p
- >最新日志:
- <span
- v-if="latestLog"
- class="hh"
- v-html="renderLogContent(latestLog.operationContent)"
- ></span>
- </p>
- </div>
- </div>
- </ContentWrap>
- <!--作业流程-->
- <ContentWrap>
- <div class="jobProcess">
- <div class="tab-header">
- <img src="@/assets/images/jobProcess.png" alt="" class="titleimg" /><span class="tab-title"
- >作业流程</span
- >
- </div>
- <div class="processDetail" ref="scrollContainer">
- <!-- VueFlow 主画布 -->
- <VueFlow
- style="width: 100%; height: 300px"
- :min-zoom="1"
- :max-zoom="1"
- :default-zoom="1"
- :nodes="nodes"
- :edges="edges"
- >
- <template #node-default="{ id, data }">
- <div class="custom-node" :id="id">
- <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 style="overflow: auto">
- <div class="jobExecution">
- <el-tabs v-model="activeName" type="card" class="demo-tabs" @tab-click="handleClick">
- <!-- 作业人员-->
- <el-tab-pane name="first">
- <template #label>
- <div class="tab-label">
- <img src="@/assets/images/icon_job_members.png" alt="" class="titleimg" />
- <span class="tab-text">作业人员</span>
- </div>
- </template>
- <div class="jobMemberBox">
- <!-- 锁定人区域 -->
- <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>
- <p v-show="groupedLockers.length == 0" style="margin-top: 20px; text-align: center">
- 暂无数据</p
- >
- </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>
- <p v-show="coLockUsers.length == 0" style="margin-top: 20px; text-align: center">
- 暂无数据</p
- >
- </div>
- </div>
- </el-tab-pane>
- <!--点位锁定-->
- <el-tab-pane name="second">
- <template #label>
- <div class="tab-label">
- <img src="@/assets/images/icon_red_lock.png" alt="" class="titleimg" />
- <span class="tab-text">点位锁定</span>
- </div>
- </template>
- <div class="jobMemberBox">
- <div class="group-box" v-for="(group, index) in jobGroupList" :key="group.id">
- <div class="tab-header">
- <p class="tab-title"
- >{{ group.groupName }}(待锁定:{{ group.waitLock }} 已锁定:{{
- group.locked
- }}
- 已解锁:{{ group.unlocked }})</p
- >
- </div>
- <!-- 表格数据 -->
- <div class="tableCon">
- <el-radio-group v-model="tableLayouts[index]">
- <el-radio-button value="fixed">常规</el-radio-button>
- <el-radio-button value="auto">扩展</el-radio-button>
- </el-radio-group>
- <el-table :data="group.ticketPointsRespVOList" :table-layout="tableLayouts[index]">
- <el-table-column prop="pointName" label="隔离点" />
- <el-table-column prop="ability" label="作用" />
- <el-table-column prop="lockUserName" label="锁定人" />
- <el-table-column prop="pointStatus" label="锁定状态">
- <template #default="scope">
- <dict-tag :type="DICT_TYPE.POINT_STATUS" :value="scope.row.pointStatus" />
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </el-tab-pane>
- <!-- 人员共锁 -->
- <el-tab-pane name="third" v-if="isShowclocker">
- <template #label>
- <div class="tab-label">
- <img src="@/assets/images/icon_co-lock.png" alt="" class="titleimg" />
- <span class="tab-text">人员共锁</span>
- </div>
- </template>
- <div class="biglockBox">
- <!-- 待共锁-->
- <div class="mumberbox">
- <div class="tab-header">
- <span class="tab-title">待共锁({{ waitLockUsers.length }})</span>
- </div>
- <div class="user">
- <div class="userItem" v-for="user in waitLockUsers" :key="user.userId">
- <img :src="user.avatar" alt="" />
- <p>{{ user.userName }}</p>
- </div>
- </div>
- </div>
- <el-icon :size="50" :color="color" class="arrow">
- <CaretRight />
- </el-icon>
- <!-- 已共锁-->
- <div class="mumberbox">
- <div class="tab-header">
- <span class="tab-title">已共锁({{ lockedUsers.length }})</span>
- </div>
- <div class="user">
- <div class="userItem" v-for="user in lockedUsers" :key="user.userId">
- <img :src="user.avatar" alt="" />
- <p>{{ user.userName }}</p>
- </div>
- </div>
- </div>
- <el-icon :size="50" :color="color" class="arrow">
- <CaretRight />
- </el-icon>
- <!-- 已解除共锁-->
- <div class="mumberbox">
- <div class="tab-header">
- <span class="tab-title">已解除共锁({{ unlockPendingUsers.length }})</span>
- </div>
- <div class="user">
- <div class="userItem" v-for="user in unlockPendingUsers" :key="user.userId">
- <img :src="user.avatar" alt="" />
- <p>{{ user.userName }} </p>
- </div>
- </div>
- </div>
- </div>
- </el-tab-pane>
- <!--作业日志-->
- <el-tab-pane name="fourth">
- <template #label>
- <div class="tab-label">
- <img src="@/assets/images/icon_job_log.png" alt="" class="titleimg" />
- <span class="tab-text">作业日志</span>
- </div>
- </template>
- <p v-show="joblogList.length == 0" style="margin-top: 20px; text-align: center">
- 暂无数据</p
- >
- <div class="joblogCon" v-show="joblogList.length > 0">
- <!--顶部日志内容-->
- <div class="joblogTop">
- <p v-for="(item, index) in filteredJoblogList" :key="index">
- <span v-html="renderLogContent(item.operationContent)"></span>
- </p>
- </div>
- <!-- 底部过滤条件-->
- <div class="bottomCheck">
- <el-checkbox
- :indeterminate="isIndeterminate"
- :checked="checkAlljoblog"
- @change="handleCheckAllChange"
- style="margin: 0 25px; font-size: 16px"
- >
- 全部
- </el-checkbox>
- <!-- 多选项 -->
- <el-checkbox-group
- v-model="checkedJoblogs"
- @change="handleCheckedJoblogsChange"
- class="big-checkbox"
- >
- <el-checkbox
- v-for="item in jobLogtypes"
- :label="item.value"
- :key="item.value"
- class="big-checkbox"
- >
- {{ item.label }}
- </el-checkbox>
- </el-checkbox-group>
- </div>
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- </ContentWrap>
- <!-- 作业日志弹框-->
- <el-dialog v-model="JoblogDialogVisible" title="新日志提醒" width="500" align-center>
- <p>
- <span v-html="renderLogContent(dialogLog.operationContent)"></span>
- </p>
- </el-dialog>
- </template>
- <script setup lang="ts">
- import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue'
- import { Handle, useVueFlow, VueFlow } from '@vue-flow/core'
- import * as JobPointGroup from '@/api/job/jobPointGroup'
- import * as PointApi from '@/api/dv/spm/index'
- import * as JobApi from '@/api/job/index'
- import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
- import type { TableInstance } from 'element-plus'
- import { CaretRight } from '@element-plus/icons-vue'
- import { getTicketOperLogPage } from '@/api/job/index'
- import { connectWebsocket, closeWebsocket } from '@/utils/webSocket'
- import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
- import type { TabsPaneContext } from 'element-plus'
- const router = useRouter()
- 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,
- ticketOperLogList: null
- })
- const allGroups = ref<any[]>([]) //获取所有分组
- const groupList = ref([]) //获取当前sopId的分组
- const allPoints = ref<any[]>([]) //获取所有点位
- const activeName = ref('first')
- const route = useRoute()
- const jobTypeOptions = ref([])
- const jobStatusOptions = ref([])
- const currentStepTitle = ref('') // 当前步骤名称
- // 表格数据
- const jobGroupList = ref([]) //获取作业分组信息
- const ticketPointsList = ref([]) // 隔离点数据
- const tableLayouts = ref([]) // 每组表格的布局
- // 人员共锁
- const jobclockerList = ref([])
- const isShowclocker = ref(true) //是否展示人员共锁
- const waitLockUsers = ref([])
- const lockedUsers = ref([])
- const unlockPendingUsers = ref([])
- // 作业日志
- const joblogList = ref([]) //原始作业日志数据列表
- const filteredJoblogList = ref([]) // 过滤后日志
- // 作业日志底部查询
- const isIndeterminate = ref(false)
- const checkedJoblogs = ref([])
- const jobLogtypes = ref([])
- const joblogsOptions = ref([]) // 只存所有 dictValue,用于全选逻辑,
- const checkAlljoblog = ref(true)
- const dialogLog = ref('')
- const isJoblogVisible = ref(false)
- const JoblogDialogVisible = ref(false)
- // 初始化
- onMounted(async () => {
- await getDetail()
- //基础信息
- jobTypeOptions.value = await getIntDictOptions(DICT_TYPE.TICKET_TYPE)
- jobStatusOptions.value = await getIntDictOptions(DICT_TYPE.TICKET_STATUS)
- await fetchAllGroupsAndPoints() //获取所有分组和点位 来渲染SOP的首页
- await onModeContent() //作业流程
- window.addEventListener('resize', onResize)
- onResize()
- getWebSocket()
- const todoType = route.query.todoType as string | undefined
- if (!todoType) {
- // 没传值、undefined、null、空字符串都进这里
- activeName.value = 'first'
- } else if (todoType == 'CONFIRM' || todoType == 'END') {
- activeName.value = 'fourth'
- await handleClick({ paneName: 'fourth' } as TabsPaneContext, new Event('click'))
- } else {
- activeName.value = 'second'
- }
- })
- onBeforeUnmount(() => {
- window.removeEventListener('resize', onResize)
- closeWebsocket()
- })
- console.log(route.query.id, '是否传递成功')
- // 基础信息作业类型
- const ticketTypeLabel = computed(() => {
- const match = jobTypeOptions.value.find((item) => item.value == JobForm.ticketType)
- return match ? match.label : JobForm.ticketType
- })
- // 基础信息作业状态
- const ticketStatusLabel = computed(() => {
- const match = jobStatusOptions.value.find((item) => item.value == JobForm.ticketStatus)
- return match ? match.label : JobForm.ticketStatus
- })
- // 基础信息最新日志
- const latestLog = computed(() => {
- if (!joblogList.value || joblogList.value.length === 0) return null
- return joblogList.value.reduce((max, item) => (item.id > max.id ? item : max))
- })
- // 初始化详情数据
- const getDetail = async () => {
- const JobData = await JobApi.selectJobTicketById(route.query.id)
- // 将JobData的数据复制给JobForm
- Object.assign(JobForm, JobData)
- // 点位锁定
- jobGroupList.value = JobForm.ticketGroupList
- ticketPointsList.value = JobForm.ticketGroupList.ticketPointsRespVOList
- console.log(
- JobData,
- '数据有哪些',
- ticketPointsList.value,
- '隔离点数据',
- jobGroupList.value,
- '分组信息'
- )
- // 初始化每个表格的布局设置为 'fixed'
- tableLayouts.value = jobGroupList.value.map(() => 'fixed')
- // 人员共锁
- jobclockerList.value = JobForm.ticketUserList
- // 判断是否存在共锁人
- isShowclocker.value = jobclockerList.value.some((item) => item.userRole === 'jtcolocker')
- updateUserGroups() // 分类数据
- // 作业日志
- joblogList.value = JobForm.ticketOperLogList
- // ✨强制刷新筛选后的列表
- filterJoblogs()
- }
- // 作业流程绘制
- const scrollContainer = ref(null)
- const nodes = ref([]) //储存节点
- const edges = ref([]) // 存储连接线
- const allSteps = ref([]) // 缓存完整流程数据
- const { addNodes, addEdges, setEdges, setNodes } = useVueFlow()
- const scrollToCurrentNode = () => {
- nextTick(() => {
- const container = scrollContainer.value
- if (!container) return
- const currentIndex = nodes.value.findIndex((n) => n.data.stepStatus === 0)
- if (currentIndex === -1) return
- const currentNodeId = nodes.value[currentIndex]?.id
- const nextNodeId = nodes.value[currentIndex + 1]?.id
- const currentNodeEl = document.getElementById(currentNodeId)
- const nextNodeEl = nextNodeId ? document.getElementById(nextNodeId) : null
- if (!currentNodeEl) return
- // 获取节点相对于容器的偏移位置
- const currentLeft = currentNodeEl.offsetLeft
- const currentWidth = currentNodeEl.offsetWidth
- let centerPos = currentLeft + currentWidth / 2
- if (nextNodeEl) {
- const nextLeft = nextNodeEl.offsetLeft
- const nextWidth = nextNodeEl.offsetWidth
- centerPos = (centerPos + nextLeft + nextWidth / 2) / 2
- }
- // 目标是让 centerPos 居中容器
- const scrollTarget = centerPos - container.clientWidth / 2
- container.scrollTo({
- right: scrollTarget,
- behavior: 'smooth'
- })
- })
- }
- const onResize = () => {
- const isMobile = window.innerWidth <= 600
- const stepsData = [...allSteps.value] // 缓存的完整流程步骤数据
- const currentIndex = stepsData.findIndex((item) => item.stepStatus === 0)
- const filtered =
- isMobile && currentIndex !== -1
- ? stepsData.slice(currentIndex) // 小屏只显示当前及后续
- : stepsData // 大屏恢复全部
- renderNodesFromData(filtered)
- renderEdgesFromData(filtered)
- nextTick(() => {
- scrollToCurrentNode()
- })
- }
- // 模式初始化
- const onModeContent = async (value) => {
- JobForm.modeId = value
- await clearCanvasProperly()
- 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)
- allSteps.value = sortedData // 缓存原始数据
- renderResponsiveSteps()
- }
- // 屏幕缩小节点展示
- const renderResponsiveSteps = () => {
- const isMobile = window.innerWidth <= 600
- const stepsData = [...allSteps.value]
- const currentIndex = stepsData.findIndex((item) => item.stepStatus === 0)
- const filtered =
- isMobile && currentIndex !== -1
- ? stepsData.slice(currentIndex) // 从当前步骤开始
- : stepsData // 全部步骤
- renderNodesFromData(filtered)
- renderEdgesFromData(filtered)
- nextTick(() => {
- scrollToCurrentNode()
- })
- }
- // 独立的清空画布函数
- const clearCanvasProperly = async () => {
- setNodes([])
- setEdges([])
- nodes.value = [] // 同步响应式数据(如果有自定义 nodes)
- edges.value = []
- }
- // 初始化数据渲染节点 - 修正版本
- const renderNodesFromData = (data) => {
- nodes.value = []
- const firstZeroIndex = data.findIndex((item) => item.stepStatus === 0)
- if (firstZeroIndex !== -1) {
- currentStepTitle.value = data[firstZeroIndex].stepTitleShort || '无标题'
- } else {
- currentStepTitle.value = ''
- }
- data.forEach((item, index) => {
- let backgroundColor = '#fff'
- let isCurrent = false
- if (item.stepStatus === 1) {
- backgroundColor = '#a0e99b'
- } else if (item.stepStatus === 0 && index === firstZeroIndex) {
- backgroundColor = '#ffe58f'
- isCurrent = true
- }
- const nodeId = `node-${item.id || Date.now() + index}`
- nodes.value.push({
- id: nodeId,
- position: { x: 100 + index * 200, y: 100 },
- width: 100,
- height: 150,
- data: {
- ...item,
- stepIcon: item.stepIcon,
- stepTitleShort: item.stepTitleShort,
- stepIndex: item.stepIndex || index + 1,
- index: item.stepIndex || index + 1,
- stepData: item,
- bgColor: backgroundColor,
- isCurrent
- },
- 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
- },
- draggable: false //设置是否可以拖动生成的图
- })
- })
- nextTick(() => {
- scrollToCurrentNode()
- })
- }
- // const renderNodesFromData = (data) => {
- // nodes.value = []
- //
- // const firstZeroIndex = data.findIndex(item => item.stepStatus === 0)
- //
- // // 设置当前步骤标题
- // if (firstZeroIndex !== -1) {
- // currentStepTitle.value = data[firstZeroIndex].stepTitleShort
- // } else {
- // currentStepTitle.value = '' // 或者显示“全部完成”之类的
- // }
- //
- // data.forEach((item, index) => {
- // const nodeId = `node-${item.id || Date.now() + index}`
- //
- // let backgroundColor = '#fff'
- // if (item.stepStatus === 1) {
- // backgroundColor = '#a0e99b'
- // } else if (item.stepStatus === 0 && index === firstZeroIndex) {
- // backgroundColor = '#ffe58f'
- // }
- //
- // 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,
- // 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
- // },
- // 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)
- }
- }
- // 底部tabbar点击事件
- const handleClick = async (tab: TabsPaneContext, event: Event) => {
- // 当选中作业日志时传递isJoblogVisible给onNewSocketLog判断日志弹框的显示
- isJoblogVisible.value = tab.paneName === 'fourth'
- await getDetail()
- if (tab.paneName === 'fourth') {
- // console.log('作业日志')
- const dictRes = await getIntDictOptions(DICT_TYPE.JOB_LOG_TYPE)
- console.log(dictRes, '接口调用是否成功')
- const dicts = dictRes.map((item) => ({
- label: item.label,
- value: String(item.value) // 确保是字符串类型
- }))
- jobLogtypes.value = dicts
- joblogsOptions.value = dicts.map((item) => item.value)
- checkedJoblogs.value = [...joblogsOptions.value] // 初始化全选
- // console.log(checkAlljoblog.value, '选中状态')
- checkAlljoblog.value = true
- // console.log(checkAlljoblog.value, '是否选中')
- isIndeterminate.value = false
- filterJoblogs()
- }
- }
- //ticketType改变函数
- const handleTicketpTypeChange = (value) => {
- JobForm.ticketType = value
- }
- // 获取所有点位和分组的数据
- 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
- })
- // 获取某个分组下的隔离点
- // const getPointsByGroupId = (groupId) => {
- // return ticketPointsList.value.filter((point) => point.groupId === groupId)
- // }
- // 从 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 updateUserGroups = () => {
- const allUsers = jobclockerList.value
- console.log(allUsers, jobclockerList.value, '数据拿到了吗')
- waitLockUsers.value = jobclockerList.value.filter(
- (u) => u.userRole == 'jtcolocker' && u.jobStatus == 0
- )
- console.log(waitLockUsers.value, '待共锁')
- lockedUsers.value = allUsers.filter((u) => u.userRole === 'jtcolocker' && u.jobStatus == 1)
- unlockPendingUsers.value = allUsers.filter((u) => u.userRole === 'jtcolocker' && u.jobStatus == 2)
- }
- const getWebSocket = async () => {
- const code = route.query.id
- const address = 'sys.websocket.address'
- const addressData = await getIsSystemAttributeByKey(address)
- const url = addressData.sysAttrValue
- const isLocalDev = window.location.hostname === 'localhost'
- const baseAddress = isLocalDev ? 'ws://192.168.0.10:48080' : url
- connectWebsocket(
- `${baseAddress}/websocket/jobTicketLog/${code}`,
- { w: 'S' },
- async (msg) => {
- console.log('接收消息:', msg)
- // const parts = msg.split('operationType=');
- if (msg !== 'heartbeat') {
- // 判断是否需要弹窗显示
- onNewSocketLog(msg) // 👈传入最新的 WebSocket 消息
- await getDetail() // 抽出专用接口函数
- }
- // 你的处理逻辑
- },
- (err) => {
- console.error('WebSocket 错误:', err)
- }
- )
- }
- //获取作业日志数据
- // const getJobLogs = async () => {
- // const joblogData = {
- // titcketId: route.query.id,
- // current: 1,
- // size: -1
- // }
- // try {
- // const res = await JobApi.getTicketOperLogPage(joblogData)
- // joblogList.value = res.list
- // // ✨强制刷新筛选后的列表
- // filterJoblogs()
- // console.log(res.list, '日志数据', joblogList.value, '复制是否成功')
- // } catch (error) {
- // console.error('获取日志数据失败', error)
- // }
- //
- // }
- // 作业日志页面数据展示样式分割
- const renderLogContent = (content) => {
- if (!content) return ''
- // 第一步:先处理 <url>[label] 为 <img> + label
- content = content.replace(/<([^>]+)>\[([^\]]+)\]/g, (match, url, label) => {
- return `<img src="${url}" style="height: 25px; vertical-align: middle;"> <span style="color:#007BFF;margin: 0 3px">${label}</span>`
- })
- // 第二步:处理剩下的 [label],转成蓝色字体
- content = content.replace(/\[([^\]]+)\]/g, '<span style="color:#007BFF;margin: 0 5px">$1</span>')
- return content
- }
- // 作业日志底部筛选功能
- const handleCheckedJoblogsChange = (val) => {
- const checkedCount = val.length
- checkAlljoblog.value = checkedCount === joblogsOptions.value.length
- isIndeterminate.value = checkedCount > 0 && checkedCount < joblogsOptions.value.length
- filterJoblogs()
- }
- const handleCheckAllChange = (val) => {
- checkedJoblogs.value = val ? [...joblogsOptions.value] : []
- isIndeterminate.value = false
- filterJoblogs()
- }
- // 实际日志筛选逻辑
- const filterJoblogs = () => {
- if (checkedJoblogs.value.includes('ALL')) {
- filteredJoblogList.value = joblogList.value
- } else {
- filteredJoblogList.value = joblogList.value.filter((log) =>
- checkedJoblogs.value.includes(String(log.operationType))
- )
- }
- }
- // 作业日志弹框
- const parseLogString = (logStr) => {
- const obj = {}
- const match = logStr.match(/^[^(]+?\((.*)\)$/)
- if (!match) return obj
- const keyValuePairs = match[1].split(/,\s*(?=\w+=)/) // 处理 "key=value" 中的逗号
- keyValuePairs.forEach((pair) => {
- const [key, value] = pair.split('=')
- if (value === 'null') {
- obj[key] = null
- } else if (!isNaN(value)) {
- obj[key] = Number(value)
- } else {
- obj[key] = value
- }
- })
- return obj
- }
- // 判断某条日志是否在当前筛选之外(即是否未被选中)
- const onNewSocketLog = async (rawLogStr) => {
- // 1. 将字符串解析为对象
- const newLog = parseLogString(rawLogStr)
- // 2. 获取当前勾选的类型(都转成字符串)
- const selectedTypes = checkedJoblogs.value.map(String)
- const newLogType = String(newLog.operationType)
- // console.log('新日志类型:', newLogType, '勾选类型:', selectedTypes);
- // 3. 判断是否未被选中 → 弹窗
- if (!selectedTypes.includes(newLogType)) {
- dialogLog.value = newLog
- console.log(isJoblogVisible.value, 'this.isJoblogVisible')
- if (isJoblogVisible.value) {
- JoblogDialogVisible.value = true
- console.log('弹框内容:', newLog)
- }
- } else {
- JoblogDialogVisible.value = false
- // console.log('该类型已勾选,不弹框');
- }
- }
- // 返回
- const goBack = () => {
- router.push('/jobTicket/job')
- }
- </script>
- <style scoped lang="scss">
- .demo-tabs > .el-tabs__content {
- padding: 32px;
- color: #6b778c;
- font-size: 32px;
- font-weight: 600;
- }
- .basicInformation {
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- margin-top: 20px;
- .basicContent {
- width: 100%;
- height: 100%;
- overflow-y: auto;
- display: flex;
- flex-wrap: wrap;
- padding: 10px 20px;
- box-sizing: border-box;
- //background: pink;
- p {
- display: block;
- min-width: 32%;
- min-height: 25px;
- margin: 8px 5px;
- //background: green;
- .specialText {
- color: #42bafa;
- }
- }
- }
- }
- .tab-header {
- background-color: #f5f7fa;
- border-bottom: 1px solid #dcdfe6;
- padding: 12px 20px;
- border-radius: 4px 4px 0 0;
- display: flex;
- align-items: center; /* 垂直居中 */
- .topTitle_left{
- width:95%;
- }
- .titleimg {
- width: 25px;
- height: 25px;
- margin-right: 8px;
- }
- .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;
- margin: 10px 0;
- img {
- width: 14px;
- height: 14px;
- }
- }
- }
- .processDetail {
- width: 100%;
- height: 300px;
- overflow-x: auto;
- //background: green;
- }
- .jobExecution {
- width: 100%;
- height: 600px;
- //background: blue;
- .tab-label {
- padding: 12px 20px;
- border-radius: 4px 4px 0 0;
- display: flex;
- align-items: center; /* 垂直居中 */
- .titleimg {
- width: 25px;
- height: 25px;
- margin-right: 8px;
- }
- .tab-text {
- font-size: 14px;
- font-weight: 500;
- color: #303133;
- }
- }
- .jobMemberBox {
- width: 100%;
- min-height: 300px;
- display: flex;
- flex-wrap: wrap;
- //background: #000;
- }
- .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;
- }
- }
- //作业流程
- .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;
- flex-wrap: wrap;
- 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;
- }
- }
- //点位数据
- .group-box {
- margin: 0 20px;
- }
- //人员共锁
- .biglockBox {
- width: 100%;
- height: 100%;
- display: flex;
- flex-wrap: wrap;
- //background: green;
- .mumberbox {
- width: 28%;
- height: 100%;
- margin: 0 15px;
- .user {
- width: 100%;
- max-height: 500px;
- overflow-y: auto;
- overflow-x: hidden;
- display: flex;
- flex-wrap: wrap;
- padding: 2px 5px;
- box-sizing: border-box;
- //background: pink;
- .userItem {
- width: 20%;
- height: 80px;
- padding: 2px 3px;
- margin: 10px;
- text-align: center;
- //background: #000;
- box-sizing: border-box;
- img {
- width: 50px;
- height: 50px;
- }
- p {
- font-size: 16px;
- display: -webkit-box;
- -webkit-line-clamp: 1; /* 最多显示1行 */
- -webkit-box-orient: vertical;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
- }
- }
- .arrow {
- margin-top: 15%;
- }
- }
- //作业日志
- .joblogCon {
- width: 98%;
- height: 75%;
- margin: auto;
- //background: greenyellow;
- .joblogTop {
- width: 100%;
- height: 490px;
- margin: auto;
- overflow-y: auto;
- //background: cadetblue;
- p {
- font-size: 16px;
- line-height: 20px;
- margin: 10px 0;
- }
- }
- .bottomCheck {
- width: 100%;
- min-height: 50px;
- line-height: 50px;
- font-size: 23px;
- display: flex;
- padding: 5px 0;
- margin: auto;
- //background: forestgreen;
- .big-checkbox {
- font-size: 23px; /* 字体大小 */
- }
- }
- }
- /* 小屏幕下隐藏文字,只显示图标 */
- @media (max-width: 768px) {
- .tab-label {
- padding: 12px 2px;
- border-radius: 4px 4px 0 0;
- display: flex;
- align-items: center; /* 垂直居中 */
- .titleimg {
- width: 25px;
- height: 25px;
- margin-right: 0;
- }
- .tab-text {
- font-size: 14px;
- font-weight: 500;
- color: #303133;
- display: none;
- }
- }
- .group-card-user {
- width: 160px;
- min-height: 130px;
- }
- //点位数据
- .group-box {
- margin: 10px 5px;
- }
- // 人员共锁
- .biglockBox {
- overflow-y: auto;
- flex-direction: column;
- align-items: center;
- //background: #bf2020;
- .mumberbox {
- width: 95%;
- margin: 15px 0;
- }
- .arrow {
- transform: rotate(90deg); // 向右 → 向下
- margin-top: 10px;
- margin-bottom: 10px;
- }
- }
- }
- }
- </style>
|