JobMonitor.vue 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484
  1. <template>
  2. <!--基本信息-->
  3. <ContentWrap>
  4. <div class="basicInformation">
  5. <!-- <section class="topTitle">-->
  6. <div class="tab-header">
  7. <section class="topTitle_left">
  8. <img src="@/assets/images/information.png" alt="" class="titleimg" />
  9. <span class="tab-title"> 基本信息</span>
  10. </section>
  11. <div class="set-btn" @click="goBack">
  12. <img src="../../../assets/images/返回.png" alt="" />
  13. 返回
  14. </div>
  15. </div>
  16. <div class="basicContent">
  17. <p
  18. >作业名称: <span>{{ JobForm.ticketName }}</span></p
  19. >
  20. <p
  21. >作业区域: <span>{{ JobForm.workstationName }}</span></p
  22. >
  23. <p
  24. >工艺设备: <span>{{ JobForm.machineryName }}</span></p
  25. >
  26. <p
  27. >作业类型: <span>{{ ticketTypeLabel }}</span></p
  28. >
  29. <p
  30. >作业状态: <span>{{ ticketStatusLabel }}</span></p
  31. >
  32. <p
  33. >开始时间: <span>{{ JobForm.ticketStartTime || '-' }}</span></p
  34. >
  35. <p
  36. >结束时间: <span>{{ JobForm.ticketEndTime || '-' }} </span></p
  37. >
  38. <p
  39. >当前步骤: <span class="specialText">{{ currentStepTitle }}</span></p
  40. >
  41. <p
  42. >最新日志:
  43. <span
  44. v-if="latestLog"
  45. class="hh"
  46. v-html="renderLogContent(latestLog.operationContent)"
  47. ></span>
  48. </p>
  49. </div>
  50. </div>
  51. </ContentWrap>
  52. <!--作业流程-->
  53. <ContentWrap>
  54. <div class="jobProcess">
  55. <div class="tab-header">
  56. <img src="@/assets/images/jobProcess.png" alt="" class="titleimg" /><span class="tab-title"
  57. >作业流程</span
  58. >
  59. </div>
  60. <div class="processDetail" ref="scrollContainer">
  61. <!-- VueFlow 主画布 -->
  62. <VueFlow
  63. style="width: 100%; height: 300px"
  64. :min-zoom="1"
  65. :max-zoom="1"
  66. :default-zoom="1"
  67. :nodes="nodes"
  68. :edges="edges"
  69. >
  70. <template #node-default="{ id, data }">
  71. <div class="custom-node" :id="id">
  72. <div class="node-content">
  73. <!-- 图标显示 -->
  74. <div style="font-size: 30px">
  75. <img
  76. v-if="data.stepIcon && data.stepIcon.startsWith('http')"
  77. :src="data.stepIcon"
  78. :alt="data.stepTitleShort"
  79. style="width: 40px; height: 40px; object-fit: contain"
  80. />
  81. <span v-else>{{ data.stepIcon || '📋' }}</span>
  82. </div>
  83. <div style="font-weight: bold; font-size: 14px">
  84. {{ data.stepTitleShort || '无标题' }}
  85. </div>
  86. <div style="font-size: 25px">
  87. {{ String.fromCharCode(9311 + (data.stepIndex || 1)) }}
  88. </div>
  89. </div>
  90. <!-- 四个连接点 -->
  91. <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top" />
  92. <Handle
  93. type="source"
  94. position="bottom"
  95. :id="`${id}-bottom`"
  96. class="handle handle-bottom"
  97. />
  98. <Handle type="target" position="left" :id="`${id}-left`" class="handle handle-left" />
  99. <Handle
  100. type="source"
  101. position="right"
  102. :id="`${id}-right`"
  103. class="handle handle-right"
  104. />
  105. </div>
  106. </template>
  107. </VueFlow>
  108. </div>
  109. </div>
  110. </ContentWrap>
  111. <!--作业执行-->
  112. <ContentWrap style="overflow: auto">
  113. <div class="jobExecution">
  114. <el-tabs v-model="activeName" type="card" class="demo-tabs" @tab-click="handleClick">
  115. <!-- 作业人员-->
  116. <el-tab-pane name="first">
  117. <template #label>
  118. <div class="tab-label">
  119. <img src="@/assets/images/icon_job_members.png" alt="" class="titleimg" />
  120. <span class="tab-text">作业人员</span>
  121. </div>
  122. </template>
  123. <div class="jobMemberBox">
  124. <!-- 锁定人区域 -->
  125. <div class="left_box">
  126. <div class="tab-header">
  127. <span class="tab-title">锁定人</span>
  128. </div>
  129. <!-- 有锁定人数据时显示 -->
  130. <div v-if="groupedLockers.length" class="group-container-user">
  131. <div v-for="group in groupedLockers" :key="group.groupId" class="group-card-user">
  132. <div class="group-title">{{ group.groupName }}</div>
  133. <div class="user-list">
  134. <div v-for="user in group.users" :key="user.userId" class="user-card">
  135. <img src="@/assets/images/UserBlack.png" />
  136. <div class="user-name">{{ user.userName }}</div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. <p v-show="groupedLockers.length == 0" style="margin-top: 20px; text-align: center">
  142. 暂无数据</p
  143. >
  144. </div>
  145. <!-- 共锁人区域 -->
  146. <div class="right_box">
  147. <div class="tab-header">
  148. <span class="tab-title">共锁人</span>
  149. </div>
  150. <div v-if="coLockUsers.length" class="user-list-colocker">
  151. <div v-for="user in coLockUsers" :key="user.userId" class="user-card">
  152. <img src="@/assets/images/UserBlack.png" />
  153. <div class="user-name">{{ user.userName }}</div>
  154. </div>
  155. </div>
  156. <p v-show="coLockUsers.length == 0" style="margin-top: 20px; text-align: center">
  157. 暂无数据</p
  158. >
  159. </div>
  160. </div>
  161. </el-tab-pane>
  162. <!--点位锁定-->
  163. <el-tab-pane name="second">
  164. <template #label>
  165. <div class="tab-label">
  166. <img src="@/assets/images/icon_red_lock.png" alt="" class="titleimg" />
  167. <span class="tab-text">点位锁定</span>
  168. </div>
  169. </template>
  170. <div class="jobMemberBox">
  171. <div class="group-box" v-for="(group, index) in jobGroupList" :key="group.id">
  172. <div class="tab-header">
  173. <p class="tab-title"
  174. >{{ group.groupName }}(待锁定:{{ group.waitLock }} 已锁定:{{
  175. group.locked
  176. }}
  177. 已解锁:{{ group.unlocked }})</p
  178. >
  179. </div>
  180. <!-- 表格数据 -->
  181. <div class="tableCon">
  182. <el-radio-group v-model="tableLayouts[index]">
  183. <el-radio-button value="fixed">常规</el-radio-button>
  184. <el-radio-button value="auto">扩展</el-radio-button>
  185. </el-radio-group>
  186. <el-table :data="group.ticketPointsRespVOList" :table-layout="tableLayouts[index]">
  187. <el-table-column prop="pointName" label="隔离点" />
  188. <el-table-column prop="ability" label="作用" />
  189. <el-table-column prop="lockUserName" label="锁定人" />
  190. <el-table-column prop="pointStatus" label="锁定状态">
  191. <template #default="scope">
  192. <dict-tag :type="DICT_TYPE.POINT_STATUS" :value="scope.row.pointStatus" />
  193. </template>
  194. </el-table-column>
  195. </el-table>
  196. </div>
  197. </div>
  198. </div>
  199. </el-tab-pane>
  200. <!-- 人员共锁 -->
  201. <el-tab-pane name="third" v-if="isShowclocker">
  202. <template #label>
  203. <div class="tab-label">
  204. <img src="@/assets/images/icon_co-lock.png" alt="" class="titleimg" />
  205. <span class="tab-text">人员共锁</span>
  206. </div>
  207. </template>
  208. <div class="biglockBox">
  209. <!-- 待共锁-->
  210. <div class="mumberbox">
  211. <div class="tab-header">
  212. <span class="tab-title">待共锁({{ waitLockUsers.length }})</span>
  213. </div>
  214. <div class="user">
  215. <div class="userItem" v-for="user in waitLockUsers" :key="user.userId">
  216. <img :src="user.avatar" alt="" />
  217. <p>{{ user.userName }}</p>
  218. </div>
  219. </div>
  220. </div>
  221. <el-icon :size="50" :color="color" class="arrow">
  222. <CaretRight />
  223. </el-icon>
  224. <!-- 已共锁-->
  225. <div class="mumberbox">
  226. <div class="tab-header">
  227. <span class="tab-title">已共锁({{ lockedUsers.length }})</span>
  228. </div>
  229. <div class="user">
  230. <div class="userItem" v-for="user in lockedUsers" :key="user.userId">
  231. <img :src="user.avatar" alt="" />
  232. <p>{{ user.userName }}</p>
  233. </div>
  234. </div>
  235. </div>
  236. <el-icon :size="50" :color="color" class="arrow">
  237. <CaretRight />
  238. </el-icon>
  239. <!-- 已解除共锁-->
  240. <div class="mumberbox">
  241. <div class="tab-header">
  242. <span class="tab-title">已解除共锁({{ unlockPendingUsers.length }})</span>
  243. </div>
  244. <div class="user">
  245. <div class="userItem" v-for="user in unlockPendingUsers" :key="user.userId">
  246. <img :src="user.avatar" alt="" />
  247. <p>{{ user.userName }} </p>
  248. </div>
  249. </div>
  250. </div>
  251. </div>
  252. </el-tab-pane>
  253. <!--作业日志-->
  254. <el-tab-pane name="fourth">
  255. <template #label>
  256. <div class="tab-label">
  257. <img src="@/assets/images/icon_job_log.png" alt="" class="titleimg" />
  258. <span class="tab-text">作业日志</span>
  259. </div>
  260. </template>
  261. <p v-show="joblogList.length == 0" style="margin-top: 20px; text-align: center">
  262. 暂无数据</p
  263. >
  264. <div class="joblogCon" v-show="joblogList.length > 0">
  265. <!--顶部日志内容-->
  266. <div class="joblogTop">
  267. <p v-for="(item, index) in filteredJoblogList" :key="index">
  268. <span v-html="renderLogContent(item.operationContent)"></span>
  269. </p>
  270. </div>
  271. <!-- 底部过滤条件-->
  272. <div class="bottomCheck">
  273. <el-checkbox
  274. :indeterminate="isIndeterminate"
  275. :checked="checkAlljoblog"
  276. @change="handleCheckAllChange"
  277. style="margin: 0 25px; font-size: 16px"
  278. >
  279. 全部
  280. </el-checkbox>
  281. <!-- 多选项 -->
  282. <el-checkbox-group
  283. v-model="checkedJoblogs"
  284. @change="handleCheckedJoblogsChange"
  285. class="big-checkbox"
  286. >
  287. <el-checkbox
  288. v-for="item in jobLogtypes"
  289. :label="item.value"
  290. :key="item.value"
  291. class="big-checkbox"
  292. >
  293. {{ item.label }}
  294. </el-checkbox>
  295. </el-checkbox-group>
  296. </div>
  297. </div>
  298. </el-tab-pane>
  299. </el-tabs>
  300. </div>
  301. </ContentWrap>
  302. <!-- 作业日志弹框-->
  303. <el-dialog v-model="JoblogDialogVisible" title="新日志提醒" width="500" align-center>
  304. <p>
  305. <span v-html="renderLogContent(dialogLog.operationContent)"></span>
  306. </p>
  307. </el-dialog>
  308. </template>
  309. <script setup lang="ts">
  310. import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue'
  311. import { Handle, useVueFlow, VueFlow } from '@vue-flow/core'
  312. import * as JobPointGroup from '@/api/job/jobPointGroup'
  313. import * as PointApi from '@/api/dv/spm/index'
  314. import * as JobApi from '@/api/job/index'
  315. import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
  316. import type { TableInstance } from 'element-plus'
  317. import { CaretRight } from '@element-plus/icons-vue'
  318. import { getTicketOperLogPage } from '@/api/job/index'
  319. import { connectWebsocket, closeWebsocket } from '@/utils/webSocket'
  320. import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
  321. import type { TabsPaneContext } from 'element-plus'
  322. const router = useRouter()
  323. const JobForm = reactive({
  324. createTime: null,
  325. id: null,
  326. machineryId: null,
  327. machineryName: null,
  328. modeId: null,
  329. sopGroupList: null,
  330. sopIndex: null,
  331. sopName: null,
  332. workstationId: null,
  333. workstationName: null,
  334. ticketCode: null,
  335. ticketName: null,
  336. sopId: null,
  337. ticketType: null,
  338. ticketContent: null,
  339. ticketStatus: null,
  340. ticketStartTime: null,
  341. ticketEndTime: null,
  342. ticketGroupList: null,
  343. ticketPointsList: null,
  344. ticketStepList: null,
  345. ticketUserList: null,
  346. ticketOperLogList: null
  347. })
  348. const allGroups = ref<any[]>([]) //获取所有分组
  349. const groupList = ref([]) //获取当前sopId的分组
  350. const allPoints = ref<any[]>([]) //获取所有点位
  351. const activeName = ref('first')
  352. const route = useRoute()
  353. const jobTypeOptions = ref([])
  354. const jobStatusOptions = ref([])
  355. const currentStepTitle = ref('') // 当前步骤名称
  356. // 表格数据
  357. const jobGroupList = ref([]) //获取作业分组信息
  358. const ticketPointsList = ref([]) // 隔离点数据
  359. const tableLayouts = ref([]) // 每组表格的布局
  360. // 人员共锁
  361. const jobclockerList = ref([])
  362. const isShowclocker = ref(true) //是否展示人员共锁
  363. const waitLockUsers = ref([])
  364. const lockedUsers = ref([])
  365. const unlockPendingUsers = ref([])
  366. // 作业日志
  367. const joblogList = ref([]) //原始作业日志数据列表
  368. const filteredJoblogList = ref([]) // 过滤后日志
  369. // 作业日志底部查询
  370. const isIndeterminate = ref(false)
  371. const checkedJoblogs = ref([])
  372. const jobLogtypes = ref([])
  373. const joblogsOptions = ref([]) // 只存所有 dictValue,用于全选逻辑,
  374. const checkAlljoblog = ref(true)
  375. const dialogLog = ref('')
  376. const isJoblogVisible = ref(false)
  377. const JoblogDialogVisible = ref(false)
  378. // 初始化
  379. onMounted(async () => {
  380. await getDetail()
  381. //基础信息
  382. jobTypeOptions.value = await getIntDictOptions(DICT_TYPE.TICKET_TYPE)
  383. jobStatusOptions.value = await getIntDictOptions(DICT_TYPE.TICKET_STATUS)
  384. await fetchAllGroupsAndPoints() //获取所有分组和点位 来渲染SOP的首页
  385. await onModeContent() //作业流程
  386. window.addEventListener('resize', onResize)
  387. onResize()
  388. getWebSocket()
  389. const todoType = route.query.todoType as string | undefined
  390. if (!todoType) {
  391. // 没传值、undefined、null、空字符串都进这里
  392. activeName.value = 'first'
  393. } else if (todoType == 'CONFIRM' || todoType == 'END') {
  394. activeName.value = 'fourth'
  395. await handleClick({ paneName: 'fourth' } as TabsPaneContext, new Event('click'))
  396. } else {
  397. activeName.value = 'second'
  398. }
  399. })
  400. onBeforeUnmount(() => {
  401. window.removeEventListener('resize', onResize)
  402. closeWebsocket()
  403. })
  404. console.log(route.query.id, '是否传递成功')
  405. // 基础信息作业类型
  406. const ticketTypeLabel = computed(() => {
  407. const match = jobTypeOptions.value.find((item) => item.value == JobForm.ticketType)
  408. return match ? match.label : JobForm.ticketType
  409. })
  410. // 基础信息作业状态
  411. const ticketStatusLabel = computed(() => {
  412. const match = jobStatusOptions.value.find((item) => item.value == JobForm.ticketStatus)
  413. return match ? match.label : JobForm.ticketStatus
  414. })
  415. // 基础信息最新日志
  416. const latestLog = computed(() => {
  417. if (!joblogList.value || joblogList.value.length === 0) return null
  418. return joblogList.value.reduce((max, item) => (item.id > max.id ? item : max))
  419. })
  420. // 初始化详情数据
  421. const getDetail = async () => {
  422. const JobData = await JobApi.selectJobTicketById(route.query.id)
  423. // 将JobData的数据复制给JobForm
  424. Object.assign(JobForm, JobData)
  425. // 点位锁定
  426. jobGroupList.value = JobForm.ticketGroupList
  427. ticketPointsList.value = JobForm.ticketGroupList.ticketPointsRespVOList
  428. console.log(
  429. JobData,
  430. '数据有哪些',
  431. ticketPointsList.value,
  432. '隔离点数据',
  433. jobGroupList.value,
  434. '分组信息'
  435. )
  436. // 初始化每个表格的布局设置为 'fixed'
  437. tableLayouts.value = jobGroupList.value.map(() => 'fixed')
  438. // 人员共锁
  439. jobclockerList.value = JobForm.ticketUserList
  440. // 判断是否存在共锁人
  441. isShowclocker.value = jobclockerList.value.some((item) => item.userRole === 'jtcolocker')
  442. updateUserGroups() // 分类数据
  443. // 作业日志
  444. joblogList.value = JobForm.ticketOperLogList
  445. // ✨强制刷新筛选后的列表
  446. filterJoblogs()
  447. }
  448. // 作业流程绘制
  449. const scrollContainer = ref(null)
  450. const nodes = ref([]) //储存节点
  451. const edges = ref([]) // 存储连接线
  452. const allSteps = ref([]) // 缓存完整流程数据
  453. const { addNodes, addEdges, setEdges, setNodes } = useVueFlow()
  454. const scrollToCurrentNode = () => {
  455. nextTick(() => {
  456. const container = scrollContainer.value
  457. if (!container) return
  458. const currentIndex = nodes.value.findIndex((n) => n.data.stepStatus === 0)
  459. if (currentIndex === -1) return
  460. const currentNodeId = nodes.value[currentIndex]?.id
  461. const nextNodeId = nodes.value[currentIndex + 1]?.id
  462. const currentNodeEl = document.getElementById(currentNodeId)
  463. const nextNodeEl = nextNodeId ? document.getElementById(nextNodeId) : null
  464. if (!currentNodeEl) return
  465. // 获取节点相对于容器的偏移位置
  466. const currentLeft = currentNodeEl.offsetLeft
  467. const currentWidth = currentNodeEl.offsetWidth
  468. let centerPos = currentLeft + currentWidth / 2
  469. if (nextNodeEl) {
  470. const nextLeft = nextNodeEl.offsetLeft
  471. const nextWidth = nextNodeEl.offsetWidth
  472. centerPos = (centerPos + nextLeft + nextWidth / 2) / 2
  473. }
  474. // 目标是让 centerPos 居中容器
  475. const scrollTarget = centerPos - container.clientWidth / 2
  476. container.scrollTo({
  477. right: scrollTarget,
  478. behavior: 'smooth'
  479. })
  480. })
  481. }
  482. const onResize = () => {
  483. const isMobile = window.innerWidth <= 600
  484. const stepsData = [...allSteps.value] // 缓存的完整流程步骤数据
  485. const currentIndex = stepsData.findIndex((item) => item.stepStatus === 0)
  486. const filtered =
  487. isMobile && currentIndex !== -1
  488. ? stepsData.slice(currentIndex) // 小屏只显示当前及后续
  489. : stepsData // 大屏恢复全部
  490. renderNodesFromData(filtered)
  491. renderEdgesFromData(filtered)
  492. nextTick(() => {
  493. scrollToCurrentNode()
  494. })
  495. }
  496. // 模式初始化
  497. const onModeContent = async (value) => {
  498. JobForm.modeId = value
  499. await clearCanvasProperly()
  500. const Data = await JobApi.selectJobTicketById(route.query.id)
  501. const sortedData = Data.ticketStepList.sort((a, b) => (a.stepIndex || 0) - (b.stepIndex || 0))
  502. renderNodesFromData(sortedData)
  503. renderEdgesFromData(sortedData)
  504. allSteps.value = sortedData // 缓存原始数据
  505. renderResponsiveSteps()
  506. }
  507. // 屏幕缩小节点展示
  508. const renderResponsiveSteps = () => {
  509. const isMobile = window.innerWidth <= 600
  510. const stepsData = [...allSteps.value]
  511. const currentIndex = stepsData.findIndex((item) => item.stepStatus === 0)
  512. const filtered =
  513. isMobile && currentIndex !== -1
  514. ? stepsData.slice(currentIndex) // 从当前步骤开始
  515. : stepsData // 全部步骤
  516. renderNodesFromData(filtered)
  517. renderEdgesFromData(filtered)
  518. nextTick(() => {
  519. scrollToCurrentNode()
  520. })
  521. }
  522. // 独立的清空画布函数
  523. const clearCanvasProperly = async () => {
  524. setNodes([])
  525. setEdges([])
  526. nodes.value = [] // 同步响应式数据(如果有自定义 nodes)
  527. edges.value = []
  528. }
  529. // 初始化数据渲染节点 - 修正版本
  530. const renderNodesFromData = (data) => {
  531. nodes.value = []
  532. const firstZeroIndex = data.findIndex((item) => item.stepStatus === 0)
  533. if (firstZeroIndex !== -1) {
  534. currentStepTitle.value = data[firstZeroIndex].stepTitleShort || '无标题'
  535. } else {
  536. currentStepTitle.value = ''
  537. }
  538. data.forEach((item, index) => {
  539. let backgroundColor = '#fff'
  540. let isCurrent = false
  541. if (item.stepStatus === 1) {
  542. backgroundColor = '#a0e99b'
  543. } else if (item.stepStatus === 0 && index === firstZeroIndex) {
  544. backgroundColor = '#ffe58f'
  545. isCurrent = true
  546. }
  547. const nodeId = `node-${item.id || Date.now() + index}`
  548. nodes.value.push({
  549. id: nodeId,
  550. position: { x: 100 + index * 200, y: 100 },
  551. width: 100,
  552. height: 150,
  553. data: {
  554. ...item,
  555. stepIcon: item.stepIcon,
  556. stepTitleShort: item.stepTitleShort,
  557. stepIndex: item.stepIndex || index + 1,
  558. index: item.stepIndex || index + 1,
  559. stepData: item,
  560. bgColor: backgroundColor,
  561. isCurrent
  562. },
  563. style: {
  564. width: '130px',
  565. height: '180px',
  566. borderRadius: '12px',
  567. border: '1px solid #999',
  568. textAlign: 'center',
  569. display: 'flex',
  570. flexDirection: 'column',
  571. alignItems: 'center',
  572. justifyContent: 'space-between',
  573. padding: '10px',
  574. backgroundColor
  575. },
  576. draggable: false //设置是否可以拖动生成的图
  577. })
  578. })
  579. nextTick(() => {
  580. scrollToCurrentNode()
  581. })
  582. }
  583. // const renderNodesFromData = (data) => {
  584. // nodes.value = []
  585. //
  586. // const firstZeroIndex = data.findIndex(item => item.stepStatus === 0)
  587. //
  588. // // 设置当前步骤标题
  589. // if (firstZeroIndex !== -1) {
  590. // currentStepTitle.value = data[firstZeroIndex].stepTitleShort
  591. // } else {
  592. // currentStepTitle.value = '' // 或者显示“全部完成”之类的
  593. // }
  594. //
  595. // data.forEach((item, index) => {
  596. // const nodeId = `node-${item.id || Date.now() + index}`
  597. //
  598. // let backgroundColor = '#fff'
  599. // if (item.stepStatus === 1) {
  600. // backgroundColor = '#a0e99b'
  601. // } else if (item.stepStatus === 0 && index === firstZeroIndex) {
  602. // backgroundColor = '#ffe58f'
  603. // }
  604. //
  605. // const newNode = {
  606. // id: nodeId,
  607. // position: {
  608. // x: 100 + index * 200,
  609. // y: 100
  610. // },
  611. // width: 100,
  612. // height: 150,
  613. // data: {
  614. // stepIcon: item.stepIcon,
  615. // stepTitleShort: item.stepTitleShort,
  616. // stepIndex: item.stepIndex || index + 1,
  617. // index: item.stepIndex || index + 1,
  618. // stepData: item
  619. // },
  620. // style: {
  621. // width: '130px',
  622. // height: '180px',
  623. // borderRadius: '12px',
  624. // border: '1px solid #999',
  625. // textAlign: 'center',
  626. // display: 'flex',
  627. // flexDirection: 'column',
  628. // alignItems: 'center',
  629. // justifyContent: 'space-between',
  630. // padding: '10px',
  631. // backgroundColor
  632. // },
  633. // draggable: true
  634. // }
  635. //
  636. // addNodes(newNode)
  637. // nodes.value.push(newNode)
  638. // })
  639. // }
  640. // 渲染连接线 - 新增函数
  641. const renderEdgesFromData = (data) => {
  642. // 清空现有连接线
  643. edges.value = []
  644. // 根据 stepIndex 顺序创建连接线
  645. for (let i = 0; i < data.length - 1; i++) {
  646. const currentStep = data[i]
  647. const nextStep = data[i + 1]
  648. const sourceNodeId = `node-${currentStep.id}`
  649. const targetNodeId = `node-${nextStep.id}`
  650. // 创建连接线,从右侧连接到左侧
  651. const edge = {
  652. id: `edge-${currentStep.id}-${nextStep.id}`,
  653. source: sourceNodeId,
  654. target: targetNodeId,
  655. sourceHandle: `${sourceNodeId}-right`, // 从右侧连接点出发
  656. targetHandle: `${targetNodeId}-left`, // 连接到左侧连接点
  657. type: 'smoothstep',
  658. style: { stroke: '#333', strokeWidth: 2 },
  659. markerEnd: {
  660. type: 'arrowclosed',
  661. width: 20,
  662. height: 20,
  663. color: '#333'
  664. }
  665. }
  666. addEdges(edge)
  667. edges.value.push(edge)
  668. console.log('创建连接线:', edge)
  669. }
  670. }
  671. // 底部tabbar点击事件
  672. const handleClick = async (tab: TabsPaneContext, event: Event) => {
  673. // 当选中作业日志时传递isJoblogVisible给onNewSocketLog判断日志弹框的显示
  674. isJoblogVisible.value = tab.paneName === 'fourth'
  675. await getDetail()
  676. if (tab.paneName === 'fourth') {
  677. // console.log('作业日志')
  678. const dictRes = await getIntDictOptions(DICT_TYPE.JOB_LOG_TYPE)
  679. console.log(dictRes, '接口调用是否成功')
  680. const dicts = dictRes.map((item) => ({
  681. label: item.label,
  682. value: String(item.value) // 确保是字符串类型
  683. }))
  684. jobLogtypes.value = dicts
  685. joblogsOptions.value = dicts.map((item) => item.value)
  686. checkedJoblogs.value = [...joblogsOptions.value] // 初始化全选
  687. // console.log(checkAlljoblog.value, '选中状态')
  688. checkAlljoblog.value = true
  689. // console.log(checkAlljoblog.value, '是否选中')
  690. isIndeterminate.value = false
  691. filterJoblogs()
  692. }
  693. }
  694. //ticketType改变函数
  695. const handleTicketpTypeChange = (value) => {
  696. JobForm.ticketType = value
  697. }
  698. // 获取所有点位和分组的数据
  699. const fetchAllGroupsAndPoints = async () => {
  700. try {
  701. // 分组信息
  702. const groupRes = await JobPointGroup.getJobTicketGroupPage({ pageSize: -1, pageNo: 1 })
  703. allGroups.value = groupRes.list
  704. console.log('获取分组', groupRes.list)
  705. // 获取当前ticketId的分组
  706. if (route.query.id) {
  707. const groupData = await JobPointGroup.getJobTicketGroupPage({
  708. pageSize: -1,
  709. pageNo: 1,
  710. ticketId: route.query.id
  711. })
  712. groupList.value = groupData.list
  713. }
  714. // 点位信息
  715. const pointRes = await PointApi.getIsIsolationPointPage({ pageSize: -1, pageNo: 1 })
  716. allPoints.value = pointRes?.list
  717. console.log('获取点位', pointRes)
  718. } catch (e) {}
  719. }
  720. // 回显分组和点位数据的计算属性
  721. const resolvedGroupedPoints = computed(() => {
  722. const groupsMap = new Map<string, { groupId: string; groupName: string; points: any[] }>()
  723. console.log('JobForm.sopPointsList:', JobForm.ticketPointsList)
  724. console.log('allGroups.value:', allGroups.value)
  725. console.log('allPoints.value:', allPoints.value)
  726. JobForm.ticketPointsList.forEach((item) => {
  727. const groupId = String(item.groupId)
  728. const pointId = item.pointId
  729. console.log('处理项目:', { groupId, pointId, item })
  730. // 查分组名
  731. const groupInfo = allGroups.value.find((g) => String(g.id) === groupId)
  732. const groupName = groupInfo?.groupName
  733. console.log('找到的分组信息:', groupInfo, '分组名:', groupName)
  734. // 查点位详情
  735. const pointInfo = allPoints.value.find((p) => p.id == pointId)
  736. const pointName = pointInfo?.pointName
  737. const pointIcon = pointInfo?.pointIcon
  738. console.log('找到的点位信息:', pointInfo, '点位名:', pointName)
  739. if (!groupsMap.has(groupId)) {
  740. groupsMap.set(groupId, {
  741. groupId,
  742. groupName,
  743. points: []
  744. })
  745. }
  746. const group = groupsMap.get(groupId)!
  747. // 检查是否重复
  748. const isDuplicate = group.points.some((p) => p.pointId === pointId)
  749. console.log('是否重复:', isDuplicate, '当前组内点位:', group.points)
  750. // 防止重复添加
  751. if (!isDuplicate) {
  752. group.points.push({
  753. pointId,
  754. pointName,
  755. pointIcon
  756. })
  757. console.log('添加点位后:', group.points)
  758. }
  759. })
  760. const result = Array.from(groupsMap.values())
  761. console.log('最终结果:', result)
  762. return result
  763. })
  764. // 获取某个分组下的隔离点
  765. // const getPointsByGroupId = (groupId) => {
  766. // return ticketPointsList.value.filter((point) => point.groupId === groupId)
  767. // }
  768. // 从 JobForm.sopUserList 中提取锁定人并按 groupId 分组
  769. const groupedLockers = computed(() => {
  770. const lockerUsers =
  771. JobForm.ticketUserList?.filter((u) => u.userRole === 'jtlocker' && u.groupId != null) || []
  772. const groupMap = new Map()
  773. lockerUsers.forEach((user) => {
  774. if (!groupMap.has(user.groupId)) {
  775. const groupName =
  776. groupList.value.find((g) => g.id === user.groupId)?.groupName || '未命名分组'
  777. groupMap.set(user.groupId, { groupId: user.groupId, groupName, users: [] })
  778. }
  779. groupMap.get(user.groupId).users.push(user)
  780. })
  781. return Array.from(groupMap.values())
  782. })
  783. // 提取共锁人
  784. const coLockUsers = computed(() => {
  785. return JobForm.ticketUserList?.filter((u) => u.userRole === 'jtcolocker') || []
  786. })
  787. // 人员共锁
  788. const updateUserGroups = () => {
  789. const allUsers = jobclockerList.value
  790. console.log(allUsers, jobclockerList.value, '数据拿到了吗')
  791. waitLockUsers.value = jobclockerList.value.filter(
  792. (u) => u.userRole == 'jtcolocker' && u.jobStatus == 0
  793. )
  794. console.log(waitLockUsers.value, '待共锁')
  795. lockedUsers.value = allUsers.filter((u) => u.userRole === 'jtcolocker' && u.jobStatus == 1)
  796. unlockPendingUsers.value = allUsers.filter((u) => u.userRole === 'jtcolocker' && u.jobStatus == 2)
  797. }
  798. const getWebSocket = async () => {
  799. const code = route.query.id
  800. const address = 'sys.websocket.address'
  801. const addressData = await getIsSystemAttributeByKey(address)
  802. const url = addressData.sysAttrValue
  803. const isLocalDev = window.location.hostname === 'localhost'
  804. const baseAddress = isLocalDev ? 'ws://192.168.0.10:48080' : url
  805. connectWebsocket(
  806. `${baseAddress}/websocket/jobTicketLog/${code}`,
  807. { w: 'S' },
  808. async (msg) => {
  809. console.log('接收消息:', msg)
  810. // const parts = msg.split('operationType=');
  811. if (msg !== 'heartbeat') {
  812. // 判断是否需要弹窗显示
  813. onNewSocketLog(msg) // 👈传入最新的 WebSocket 消息
  814. await getDetail() // 抽出专用接口函数
  815. }
  816. // 你的处理逻辑
  817. },
  818. (err) => {
  819. console.error('WebSocket 错误:', err)
  820. }
  821. )
  822. }
  823. //获取作业日志数据
  824. // const getJobLogs = async () => {
  825. // const joblogData = {
  826. // titcketId: route.query.id,
  827. // current: 1,
  828. // size: -1
  829. // }
  830. // try {
  831. // const res = await JobApi.getTicketOperLogPage(joblogData)
  832. // joblogList.value = res.list
  833. // // ✨强制刷新筛选后的列表
  834. // filterJoblogs()
  835. // console.log(res.list, '日志数据', joblogList.value, '复制是否成功')
  836. // } catch (error) {
  837. // console.error('获取日志数据失败', error)
  838. // }
  839. //
  840. // }
  841. // 作业日志页面数据展示样式分割
  842. const renderLogContent = (content) => {
  843. if (!content) return ''
  844. // 第一步:先处理 <url>[label] 为 <img> + label
  845. content = content.replace(/<([^>]+)>\[([^\]]+)\]/g, (match, url, label) => {
  846. return `<img src="${url}" style="height: 25px; vertical-align: middle;"> <span style="color:#007BFF;margin: 0 3px">${label}</span>`
  847. })
  848. // 第二步:处理剩下的 [label],转成蓝色字体
  849. content = content.replace(/\[([^\]]+)\]/g, '<span style="color:#007BFF;margin: 0 5px">$1</span>')
  850. return content
  851. }
  852. // 作业日志底部筛选功能
  853. const handleCheckedJoblogsChange = (val) => {
  854. const checkedCount = val.length
  855. checkAlljoblog.value = checkedCount === joblogsOptions.value.length
  856. isIndeterminate.value = checkedCount > 0 && checkedCount < joblogsOptions.value.length
  857. filterJoblogs()
  858. }
  859. const handleCheckAllChange = (val) => {
  860. checkedJoblogs.value = val ? [...joblogsOptions.value] : []
  861. isIndeterminate.value = false
  862. filterJoblogs()
  863. }
  864. // 实际日志筛选逻辑
  865. const filterJoblogs = () => {
  866. if (checkedJoblogs.value.includes('ALL')) {
  867. filteredJoblogList.value = joblogList.value
  868. } else {
  869. filteredJoblogList.value = joblogList.value.filter((log) =>
  870. checkedJoblogs.value.includes(String(log.operationType))
  871. )
  872. }
  873. }
  874. // 作业日志弹框
  875. const parseLogString = (logStr) => {
  876. const obj = {}
  877. const match = logStr.match(/^[^(]+?\((.*)\)$/)
  878. if (!match) return obj
  879. const keyValuePairs = match[1].split(/,\s*(?=\w+=)/) // 处理 "key=value" 中的逗号
  880. keyValuePairs.forEach((pair) => {
  881. const [key, value] = pair.split('=')
  882. if (value === 'null') {
  883. obj[key] = null
  884. } else if (!isNaN(value)) {
  885. obj[key] = Number(value)
  886. } else {
  887. obj[key] = value
  888. }
  889. })
  890. return obj
  891. }
  892. // 判断某条日志是否在当前筛选之外(即是否未被选中)
  893. const onNewSocketLog = async (rawLogStr) => {
  894. // 1. 将字符串解析为对象
  895. const newLog = parseLogString(rawLogStr)
  896. // 2. 获取当前勾选的类型(都转成字符串)
  897. const selectedTypes = checkedJoblogs.value.map(String)
  898. const newLogType = String(newLog.operationType)
  899. // console.log('新日志类型:', newLogType, '勾选类型:', selectedTypes);
  900. // 3. 判断是否未被选中 → 弹窗
  901. if (!selectedTypes.includes(newLogType)) {
  902. dialogLog.value = newLog
  903. console.log(isJoblogVisible.value, 'this.isJoblogVisible')
  904. if (isJoblogVisible.value) {
  905. JoblogDialogVisible.value = true
  906. console.log('弹框内容:', newLog)
  907. }
  908. } else {
  909. JoblogDialogVisible.value = false
  910. // console.log('该类型已勾选,不弹框');
  911. }
  912. }
  913. // 返回
  914. const goBack = () => {
  915. router.push('/jobTicket/job')
  916. }
  917. </script>
  918. <style scoped lang="scss">
  919. .demo-tabs > .el-tabs__content {
  920. padding: 32px;
  921. color: #6b778c;
  922. font-size: 32px;
  923. font-weight: 600;
  924. }
  925. .basicInformation {
  926. border: 1px solid #dcdfe6;
  927. border-radius: 4px;
  928. margin-top: 20px;
  929. .basicContent {
  930. width: 100%;
  931. height: 100%;
  932. overflow-y: auto;
  933. display: flex;
  934. flex-wrap: wrap;
  935. padding: 10px 20px;
  936. box-sizing: border-box;
  937. //background: pink;
  938. p {
  939. display: block;
  940. min-width: 32%;
  941. min-height: 25px;
  942. margin: 8px 5px;
  943. //background: green;
  944. .specialText {
  945. color: #42bafa;
  946. }
  947. }
  948. }
  949. }
  950. .tab-header {
  951. background-color: #f5f7fa;
  952. border-bottom: 1px solid #dcdfe6;
  953. padding: 12px 20px;
  954. border-radius: 4px 4px 0 0;
  955. display: flex;
  956. align-items: center; /* 垂直居中 */
  957. .topTitle_left{
  958. width:95%;
  959. }
  960. .titleimg {
  961. width: 25px;
  962. height: 25px;
  963. margin-right: 8px;
  964. }
  965. .tab-title {
  966. font-size: 14px;
  967. font-weight: 500;
  968. color: #303133;
  969. }
  970. .set-btn {
  971. width: 60px;
  972. height: 30px;
  973. border: 1px solid black;
  974. border-radius: 6px;
  975. text-align: center;
  976. line-height: 30px;
  977. //float: right;
  978. cursor: pointer;
  979. margin: 10px 0;
  980. img {
  981. width: 14px;
  982. height: 14px;
  983. }
  984. }
  985. }
  986. .processDetail {
  987. width: 100%;
  988. height: 300px;
  989. overflow-x: auto;
  990. //background: green;
  991. }
  992. .jobExecution {
  993. width: 100%;
  994. height: 600px;
  995. //background: blue;
  996. .tab-label {
  997. padding: 12px 20px;
  998. border-radius: 4px 4px 0 0;
  999. display: flex;
  1000. align-items: center; /* 垂直居中 */
  1001. .titleimg {
  1002. width: 25px;
  1003. height: 25px;
  1004. margin-right: 8px;
  1005. }
  1006. .tab-text {
  1007. font-size: 14px;
  1008. font-weight: 500;
  1009. color: #303133;
  1010. }
  1011. }
  1012. .jobMemberBox {
  1013. width: 100%;
  1014. min-height: 300px;
  1015. display: flex;
  1016. flex-wrap: wrap;
  1017. //background: #000;
  1018. }
  1019. .left_box {
  1020. width: 500px;
  1021. margin-right: 10px;
  1022. display: flex;
  1023. flex-direction: column;
  1024. img {
  1025. width: 80px;
  1026. height: 80px;
  1027. }
  1028. }
  1029. .right_box {
  1030. flex: 1;
  1031. display: flex;
  1032. flex-direction: column;
  1033. img {
  1034. width: 80px;
  1035. height: 80px;
  1036. }
  1037. }
  1038. //作业流程
  1039. .custom-node {
  1040. position: relative;
  1041. width: 125px;
  1042. height: 180px;
  1043. //background-color: #fff;
  1044. border-radius: 12px;
  1045. display: flex;
  1046. flex-direction: column;
  1047. align-items: center;
  1048. justify-content: space-between;
  1049. padding: 10px;
  1050. box-sizing: border-box;
  1051. }
  1052. .node-content {
  1053. width: 100%;
  1054. height: 100%;
  1055. display: flex;
  1056. flex-direction: column;
  1057. align-items: center;
  1058. justify-content: space-between;
  1059. }
  1060. //连接点样式
  1061. .handle {
  1062. width: 12px;
  1063. height: 12px;
  1064. background-color: #1a192b;
  1065. border: 2px solid #fff;
  1066. border-radius: 50%;
  1067. cursor: crosshair;
  1068. position: absolute;
  1069. z-index: 10;
  1070. }
  1071. .handle:hover {
  1072. background-color: #555;
  1073. transform: scale(1.2);
  1074. }
  1075. .handle-top {
  1076. top: -8px;
  1077. left: 50%;
  1078. transform: translateX(-50%);
  1079. }
  1080. .handle-bottom {
  1081. bottom: -8px;
  1082. left: 50%;
  1083. transform: translateX(-50%);
  1084. }
  1085. .handle-left {
  1086. left: -8px;
  1087. top: 50%;
  1088. transform: translateY(-50%);
  1089. }
  1090. .handle-right {
  1091. right: -8px;
  1092. top: 50%;
  1093. transform: translateY(-50%);
  1094. }
  1095. //连接点全局样式
  1096. :deep(.vue-flow__handle) {
  1097. width: 12px;
  1098. height: 12px;
  1099. background-color: #1a192b;
  1100. border: 2px solid #fff;
  1101. border-radius: 50%;
  1102. cursor: crosshair;
  1103. }
  1104. :deep(.vue-flow__handle:hover) {
  1105. background-color: #555;
  1106. transform: scale(1.2);
  1107. }
  1108. //连接线样式
  1109. :deep(.vue-flow__edge-path) {
  1110. stroke: #333;
  1111. stroke-width: 2;
  1112. }
  1113. :deep(.vue-flow__edge) {
  1114. z-index: 1;
  1115. }
  1116. // 箭头样式
  1117. :deep(.vue-flow__edge-marker) {
  1118. fill: #333;
  1119. }
  1120. .group-container {
  1121. display: flex;
  1122. flex-wrap: wrap;
  1123. gap: 20px; /* 卡片之间的间距 */
  1124. }
  1125. .point-group {
  1126. border: 1px solid #ccc;
  1127. border-radius: 8px;
  1128. padding: 12px;
  1129. min-width: 250px;
  1130. background-color: #fafafa;
  1131. height: 250px;
  1132. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
  1133. }
  1134. .group-title {
  1135. font-weight: 600;
  1136. font-size: 16px;
  1137. margin-bottom: 12px;
  1138. padding-bottom: 6px;
  1139. border-bottom: 1px solid #e0e0e0;
  1140. color: #333;
  1141. }
  1142. .points-list {
  1143. display: flex;
  1144. flex-wrap: wrap;
  1145. gap: 12px;
  1146. }
  1147. .point-item {
  1148. display: flex;
  1149. flex-direction: column;
  1150. align-items: center;
  1151. width: 60px;
  1152. }
  1153. .point-icon {
  1154. width: 40px;
  1155. height: 40px;
  1156. object-fit: contain;
  1157. }
  1158. .point-name {
  1159. font-size: 12px;
  1160. text-align: center;
  1161. margin-top: 4px;
  1162. color: #555;
  1163. }
  1164. //用户的卡片
  1165. .group-container-user {
  1166. display: flex;
  1167. flex-direction: row;
  1168. flex-wrap: wrap;
  1169. gap: 16px;
  1170. overflow-x: auto;
  1171. padding-bottom: 10px;
  1172. }
  1173. .group-card-user {
  1174. width: 180px;
  1175. min-height: 150px;
  1176. background: #fff;
  1177. border-radius: 8px;
  1178. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
  1179. padding: 12px;
  1180. //flex-shrink: 0;
  1181. border: 1px solid #eee;
  1182. display: flex;
  1183. flex-direction: column;
  1184. margin-top: 10px;
  1185. }
  1186. .group-title {
  1187. font-weight: bold;
  1188. font-size: 16px;
  1189. margin-bottom: 10px;
  1190. color: #333;
  1191. text-align: center;
  1192. border-bottom: 1px solid #f0f0f0;
  1193. padding-bottom: 6px;
  1194. }
  1195. .user-list {
  1196. display: flex;
  1197. flex-wrap: wrap;
  1198. gap: 10px;
  1199. justify-content: center;
  1200. }
  1201. .user-list-colocker {
  1202. display: flex;
  1203. flex-wrap: wrap;
  1204. gap: 10px;
  1205. justify-content: flex-start;
  1206. margin-top: 10px;
  1207. }
  1208. .user-card {
  1209. width: 60px;
  1210. text-align: center;
  1211. img {
  1212. width: 40px;
  1213. height: 40px;
  1214. border-radius: 4px;
  1215. border: 1px solid #ccc;
  1216. }
  1217. .user-name {
  1218. font-size: 12px;
  1219. margin-top: 4px;
  1220. color: #555;
  1221. word-break: break-all;
  1222. }
  1223. }
  1224. //点位数据
  1225. .group-box {
  1226. margin: 0 20px;
  1227. }
  1228. //人员共锁
  1229. .biglockBox {
  1230. width: 100%;
  1231. height: 100%;
  1232. display: flex;
  1233. flex-wrap: wrap;
  1234. //background: green;
  1235. .mumberbox {
  1236. width: 28%;
  1237. height: 100%;
  1238. margin: 0 15px;
  1239. .user {
  1240. width: 100%;
  1241. max-height: 500px;
  1242. overflow-y: auto;
  1243. overflow-x: hidden;
  1244. display: flex;
  1245. flex-wrap: wrap;
  1246. padding: 2px 5px;
  1247. box-sizing: border-box;
  1248. //background: pink;
  1249. .userItem {
  1250. width: 20%;
  1251. height: 80px;
  1252. padding: 2px 3px;
  1253. margin: 10px;
  1254. text-align: center;
  1255. //background: #000;
  1256. box-sizing: border-box;
  1257. img {
  1258. width: 50px;
  1259. height: 50px;
  1260. }
  1261. p {
  1262. font-size: 16px;
  1263. display: -webkit-box;
  1264. -webkit-line-clamp: 1; /* 最多显示1行 */
  1265. -webkit-box-orient: vertical;
  1266. overflow: hidden;
  1267. text-overflow: ellipsis;
  1268. }
  1269. }
  1270. }
  1271. }
  1272. .arrow {
  1273. margin-top: 15%;
  1274. }
  1275. }
  1276. //作业日志
  1277. .joblogCon {
  1278. width: 98%;
  1279. height: 75%;
  1280. margin: auto;
  1281. //background: greenyellow;
  1282. .joblogTop {
  1283. width: 100%;
  1284. height: 490px;
  1285. margin: auto;
  1286. overflow-y: auto;
  1287. //background: cadetblue;
  1288. p {
  1289. font-size: 16px;
  1290. line-height: 20px;
  1291. margin: 10px 0;
  1292. }
  1293. }
  1294. .bottomCheck {
  1295. width: 100%;
  1296. min-height: 50px;
  1297. line-height: 50px;
  1298. font-size: 23px;
  1299. display: flex;
  1300. padding: 5px 0;
  1301. margin: auto;
  1302. //background: forestgreen;
  1303. .big-checkbox {
  1304. font-size: 23px; /* 字体大小 */
  1305. }
  1306. }
  1307. }
  1308. /* 小屏幕下隐藏文字,只显示图标 */
  1309. @media (max-width: 768px) {
  1310. .tab-label {
  1311. padding: 12px 2px;
  1312. border-radius: 4px 4px 0 0;
  1313. display: flex;
  1314. align-items: center; /* 垂直居中 */
  1315. .titleimg {
  1316. width: 25px;
  1317. height: 25px;
  1318. margin-right: 0;
  1319. }
  1320. .tab-text {
  1321. font-size: 14px;
  1322. font-weight: 500;
  1323. color: #303133;
  1324. display: none;
  1325. }
  1326. }
  1327. .group-card-user {
  1328. width: 160px;
  1329. min-height: 130px;
  1330. }
  1331. //点位数据
  1332. .group-box {
  1333. margin: 10px 5px;
  1334. }
  1335. // 人员共锁
  1336. .biglockBox {
  1337. overflow-y: auto;
  1338. flex-direction: column;
  1339. align-items: center;
  1340. //background: #bf2020;
  1341. .mumberbox {
  1342. width: 95%;
  1343. margin: 15px 0;
  1344. }
  1345. .arrow {
  1346. transform: rotate(90deg); // 向右 → 向下
  1347. margin-top: 10px;
  1348. margin-bottom: 10px;
  1349. }
  1350. }
  1351. }
  1352. }
  1353. </style>