index.vue 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151
  1. <!--
  2. * @Author: your name
  3. * @Date: 2022-01-17 10:39:22
  4. * @LastEditTime: 2022-08-12 19:14:09
  5. * @LastEditors: your name
  6. * @Description: 行李视图
  7. -->
  8. <template>
  9. <div class="baggage-view">
  10. <div class="part1" :style="{ height: basicInfoHeight }">
  11. <div class="title">
  12. <span>行李基本信息</span>
  13. <el-radio-group v-model="infoBtn" class="radioBtn" size="mini" fill="#FFFFFF" text-color="#28344D">
  14. <el-radio-button v-for="item in infoRadios" :key="item" :label="item" />
  15. </el-radio-group>
  16. <!-- <BackButton /> -->
  17. </div>
  18. <div v-show="basicInfoOpen" class="part1_info">
  19. <el-row :gutter="12">
  20. <el-col v-for="(item, index) in baggageBasicInfoCols" :key="index" :xl="[0, 2, 4].includes(index % 7) ? 4 : 3" :sm="6" :xs="6">
  21. <span class="label">{{ item.label }}:</span>
  22. <span class="content" :class="{
  23. click: item.prop === 'compensationSign' && baggageBasicInfo[item.prop]
  24. }" :title="formattedBaggageInfo(item.prop)" @click="baggageBasicInfoClickHandler(item.prop)">{{ formattedBaggageInfo(item.prop) }}</span>
  25. </el-col>
  26. </el-row>
  27. </div>
  28. <div class="button-toggle" @click="basicInfoToggle">
  29. <i :class="basicInfoOpen ? 'el-icon-caret-top' : 'el-icon-caret-bottom'" />
  30. </div>
  31. </div>
  32. <div v-show="infoBtn === infoRadios[0]" class="part2" :style="{ height: trackListHeight }">
  33. <div class="part2_info">
  34. <div class="title">行李跟踪信息</div>
  35. <div class="type normal">
  36. {{ baggageBasicInfo.BagStatus }}
  37. </div>
  38. <div class="airline">
  39. <el-select v-model="selectedAirline" size="mini" class="airline-select">
  40. <el-option v-for="airline in airlineList" :key="airline.value" :value="airline.value" :label="airline.label" />
  41. </el-select>
  42. </div>
  43. <div class="baggage-track-chart">
  44. <div class="step-line">
  45. <div v-for="(line, index) in 6" :key="index" :class="['step-line-segment', { 'step-line-active': activeStepLine(index) }]" />
  46. </div>
  47. <div v-for="(item, index) in stepNodes" :key="index" :class="{ 'step-item': true, 'active-item': item.status }">
  48. <div class="step-circle">
  49. <span class="step-name">{{ item.nodeName }}</span>
  50. </div>
  51. <div class="step-info">
  52. <div :class="statusClasses(item.status)">{{ item.status }}</div>
  53. <span class="step-time">{{ item.processingTime }}</span>
  54. <div class="step-location">{{ item.locationId }}</div>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. <div class="btns">
  60. <img class="btn-square btn-shadow" src="@/assets/baggage/ic_export.png" title="导出" @click="exportHandler('table', '行李节点列表')" />
  61. <img class="btn-square btn-shadow" src="@/assets/baggage/ic_setting.png" title="列设置" @click="show" />
  62. </div>
  63. <div class="button-toggle" @click="trackListToggle">
  64. <i :class="trackListOpen ? 'el-icon-caret-top' : 'el-icon-caret-bottom'" />
  65. </div>
  66. </div>
  67. <div v-show="infoBtn == infoRadios[0]" class="part3" :style="{
  68. height: `calc(100vh - 80px - ${basicInfoHeight} - ${trackListHeight} - 3 * 8px - 20px)`
  69. }">
  70. <el-table ref="table" :data="baggageTableData" height="100%" size="mini" border fit :header-cell-class-name="headerCellClass" :header-cell-style="{ color: '#101116' }" :cell-class-name="cellClass" :span-method="tableSpanMethod" @cell-click="cellClickHandler">
  71. <el-table-column v-for="item in tableColsCopy" :key="item.index" :prop="item.prop" :label="item.name" :align="item.align || 'center'" :width="item.width" show-overflow-tooltip :fixed="item.fixed">
  72. <template slot="header">
  73. <div class="cell-content">{{ item.name }}</div>
  74. </template>
  75. <template slot-scope="scope">
  76. <div class="cell-content">{{ scope.row[item.prop] }}</div>
  77. </template>
  78. </el-table-column>
  79. </el-table>
  80. </div>
  81. <div v-show="infoBtn === infoRadios[1]" class="part4" :style="{
  82. height: `calc(100vh - 80px - ${basicInfoHeight} - 2 * 8px - 20px)`
  83. }">
  84. <header class="head">
  85. <div class="title">行李跟踪信息</div>
  86. <div class="btns">
  87. <img class="btn-square btn-shadow" src="@/assets/baggage/ic_export.png" title="导出" @click="exportMessageToExcel" />
  88. </div>
  89. </header>
  90. <main class="main">
  91. <template v-if="messageList.length">
  92. <el-row :gutter="24" type="flex">
  93. <el-col v-for="(message, index) in messageList" :key="index" :span="6">
  94. <div class="card">
  95. <div class="message-date">{{ message.date }}</div>
  96. <div class="message-content">
  97. <!-- {{ message.dataContent.replaceAll(/[\r\n]{2,}/g, '\n').replaceAll('\\', '') }} -->
  98. {{ message.dataContent.replaceAll(/[\\r\\n]{2,}/g, '\n').replaceAll('\\', '') }}
  99. </div>
  100. </div>
  101. </el-col>
  102. </el-row>
  103. </template>
  104. <template v-else>
  105. <el-empty :image-size="1" description="暂无数据" />
  106. </template>
  107. </main>
  108. </div>
  109. <!--列设置-->
  110. <Dialog :flag="dialogFlag" class="dialog-check-group">
  111. <div class="dialog-wrapper">
  112. <div class="title">列设置</div>
  113. <div class="content">
  114. <el-tree ref="columnSetTree" :data="tableCols" :class="colsCheckClass" show-checkbox node-key="index" :default-expand-all="true" :props="{
  115. label: 'name',
  116. children: 'children'
  117. }" :default-checked-keys="checkedKeysTemp" @check="handleCheck" />
  118. </div>
  119. <div class="foot right t30">
  120. <el-button size="medium" class="r24" type="primary" @click="onCheck('baggageTableData')">确定</el-button>
  121. <el-button size="medium" @click="hide">取消</el-button>
  122. </div>
  123. </div>
  124. </Dialog>
  125. </div>
  126. </template>
  127. <script>
  128. // import BackButton from '@/components/BackButton'
  129. import Dialog from '@/layout/components/Dialog/index.vue'
  130. import { myQuery } from '@/api/temp'
  131. import { BaggageMessageQuery } from '@/api/temp'
  132. import tableColsMixin from '../../mixins/tableCols'
  133. import { throttledExportToExcel } from '@/utils/table'
  134. import * as XLSX from 'xlsx'
  135. import XLSX_STYLE from 'xlsx-style'
  136. import FileSaver from 'file-saver'
  137. import { mapGetters } from 'vuex'
  138. export default {
  139. name: 'BaggageView',
  140. components: {
  141. // BackButton,
  142. Dialog
  143. },
  144. mixins: [tableColsMixin],
  145. data () {
  146. return {
  147. queryData: {},
  148. airlineList: [],
  149. selectedAirline: '',
  150. debounceTime: 300,
  151. baggageBasicInfoCols: [
  152. {
  153. label: '行李牌号',
  154. prop: 'bagNo'
  155. },
  156. {
  157. label: '航班号',
  158. prop: 'flightNO'
  159. },
  160. {
  161. label: '特殊行李类型',
  162. prop: 'specialType'
  163. },
  164. {
  165. label: 'PNR编号',
  166. prop: 'PNR'
  167. },
  168. {
  169. label: '旅客',
  170. prop: 'name'
  171. },
  172. {
  173. label: '装载序列号',
  174. prop: 'loadSequenceIndex'
  175. },
  176. {
  177. label: '总件数',
  178. prop: 'totalNumber'
  179. },
  180. {
  181. label: '总重量',
  182. prop: 'totalWeight'
  183. },
  184. {
  185. label: '尺寸',
  186. prop: 'size'
  187. },
  188. {
  189. label: '常旅客号',
  190. prop: 'frequentFlyerNumber'
  191. },
  192. {
  193. label: '常旅客级别',
  194. prop: 'frequentFlyerClass'
  195. },
  196. {
  197. label: '取消值机',
  198. prop: 'whetherToCancelTheCheckIn'
  199. },
  200. {
  201. label: '是否可装载',
  202. prop: 'isItLoadable'
  203. },
  204. {
  205. label: '是否可运输',
  206. prop: 'isItTransportable'
  207. },
  208. {
  209. label: '行李激活状态',
  210. prop: 'activeState'
  211. },
  212. {
  213. label: '无BSM状态',
  214. prop: 'noBSM'
  215. },
  216. {
  217. label: '中转进航班',
  218. prop: 'inFlightNo'
  219. },
  220. {
  221. label: '中转出航班',
  222. prop: 'transferFlightNo'
  223. },
  224. {
  225. label: '速运标记',
  226. prop: 'expressSign'
  227. },
  228. {
  229. label: '破损标记',
  230. prop: 'brokenSign'
  231. },
  232. {
  233. label: '投诉标记',
  234. prop: 'complaintSign'
  235. },
  236. {
  237. label: '赔偿标记',
  238. prop: 'compensationSign'
  239. },
  240. {
  241. label: '异常状态',
  242. prop: 'bagExcType'
  243. },
  244. {
  245. label: '企业或团队名称',
  246. prop: 'teamOrGroup'
  247. }
  248. ],
  249. baggageBasicInfo: {},
  250. infoBtn: '',
  251. infoRadios: ['跟踪信息', '跟踪报文'],
  252. messageList: [],
  253. stepNodes: [],
  254. tableCols: [
  255. { name: '航班号', prop: 'flightNO', fixed: true },
  256. { name: '航班日期', prop: 'flightDate', width: 95, fixed: true },
  257. { name: '起飞航站\n起飞时间', prop: 'departureInfo', width: 100 },
  258. { name: '目的航站\n降落时间', prop: 'landingInfo', width: 100 },
  259. { name: '旅客舱位', prop: 'passengerCompartment', width: 70 },
  260. { name: '旅客座位号', prop: 'passengerSeatNumber' },
  261. { name: '值机序号', prop: 'passengerCheckInNumber', width: 70 },
  262. { name: '节点标识', prop: 'nodeCode', width: 100 },
  263. { name: '节点名称', prop: 'nodeName', width: 70 },
  264. { name: '位置标识', prop: 'locationCode' },
  265. { name: '位置描述', prop: 'locationRemark' },
  266. { name: '读取时间', prop: 'dealTime', width: 158 },
  267. { name: '结果', prop: 'status', width: 60 },
  268. { name: '次级代码', prop: 'secondaryCode', width: 70 },
  269. { name: '操作人', prop: 'AgentCode', width: 90 },
  270. { name: '设备ID', prop: 'DeviceCode' },
  271. { name: '发往位置', prop: 'toLocation', width: 70 },
  272. { name: '发往位置描述', prop: 'toLocationMark', width: 100 },
  273. { name: '装载序号', prop: 'LoadSN', width: 70 },
  274. { name: '容器编号', prop: 'U_Device_ID', width: 110 },
  275. { name: '数据来源', prop: 'dataSource' }
  276. ],
  277. baggageTableData: [],
  278. spanArr: [],
  279. pos: 0,
  280. queryLoop: null,
  281. queryTrackLoop: null,
  282. basicInfoOpen: true,
  283. trackListOpen: false
  284. }
  285. },
  286. computed: {
  287. ...mapGetters(['sidebar']),
  288. activeStepLine () {
  289. return function (index) {
  290. return this.stepNodes[index].status && this.stepNodes[index + 1].status
  291. }
  292. },
  293. statusClasses () {
  294. return function (status) {
  295. const classes = ['step-status']
  296. if (typeof status === 'string') {
  297. if (status.includes('正常') || status.includes('通过')) {
  298. classes.push('step-status-normal')
  299. } else {
  300. classes.push('step-status-abnormal')
  301. }
  302. }
  303. return classes
  304. }
  305. },
  306. formattedBaggageInfo () {
  307. return function (prop) {
  308. const value = this.baggageBasicInfo[prop]
  309. if ((value ?? '') === '') {
  310. return ''
  311. } else {
  312. switch (prop) {
  313. case 'transitSign':
  314. return Number(value) === 0 ? '非中转' : '中转'
  315. case 'activeState':
  316. return Number(value) === 0 ? '未激活' : '激活'
  317. case 'whetherToCancelTheCheckIn':
  318. return Number(value) === 1 ? '取消' : ''
  319. case 'isItLoadable':
  320. case 'isItTransportable':
  321. case 'noBSM':
  322. case 'expressSign':
  323. case 'brokenSign':
  324. case 'complaintSign':
  325. // case 'compensationSign':
  326. return Number(value) === 1 || value === 'Y' ? '是' : '否'
  327. default:
  328. return value
  329. }
  330. }
  331. }
  332. },
  333. basicInfoHeight () {
  334. return this.basicInfoOpen ? '220px' : '68px'
  335. },
  336. trackListHeight () {
  337. return this.trackListOpen ? '176px' : '81px'
  338. }
  339. },
  340. watch: {
  341. infoBtn: {
  342. handler (val) {
  343. this.stopLoopAll()
  344. if (val === this.infoRadios[0]) {
  345. if (this.selectedAirline) {
  346. this.startQueryTrack()
  347. }
  348. this.startQueryDetails()
  349. } else if (val === this.infoRadios[1]) {
  350. this.startQueryMessage()
  351. }
  352. },
  353. immediate: true
  354. },
  355. selectedAirline () {
  356. this.stopQueryTrack()
  357. this.startQueryTrack()
  358. }
  359. },
  360. created () {
  361. this.resetStepNodes()
  362. const { flightNO, flightDate, bagSN } = this.$route.query
  363. if (flightNO && flightDate && bagSN) {
  364. this.queryData = { flightNO, flightDate, bagSN }
  365. this.queryBasicInfo()
  366. this.queryAirline()
  367. this.infoBtn = this.infoRadios[0]
  368. } else {
  369. this.$router.push('/')
  370. }
  371. },
  372. activated () {
  373. this.resizeHandler()
  374. this.debouncedResizeHandler = this._.debounce(this.resizeHandler, this.debounceTime)
  375. window.addEventListener('resize', this.debouncedResizeHandler)
  376. },
  377. updated () {
  378. this.resizeHandler()
  379. },
  380. deactivated () {
  381. this.stopLoopAll()
  382. window.removeEventListener('resize', this.debouncedResizeHandler)
  383. },
  384. methods: {
  385. basicInfoToggle () {
  386. this.basicInfoOpen = !this.basicInfoOpen
  387. },
  388. trackListToggle () {
  389. this.trackListOpen = !this.trackListOpen
  390. },
  391. resizeHandler () {
  392. this.$refs['table']?.doLayout()
  393. },
  394. baggageBasicInfoClickHandler (prop) {
  395. if (prop !== 'compensationSign' || !this.baggageBasicInfo[prop]) {
  396. return
  397. }
  398. this.$store.dispatch('app/setAbnormalBaggageQueryParams', {
  399. flightNO: this.queryData.flightNO,
  400. flightDate: this.queryData.flightDate,
  401. bagSN: this.queryData.bagSN,
  402. fileNumber: this.baggageBasicInfo[prop]
  403. })
  404. this.$store.dispatch('app/toggleAbnormalBaggageDialogFlag', true)
  405. },
  406. startQueryDetails () {
  407. this.queryDetails()
  408. this.queryLoop = setInterval(this.queryDetails.bind(this), LOOP_INTERVAL.baggageDetails)
  409. },
  410. startQueryTrack () {
  411. this.queryTrack()
  412. this.queryTrackLoop = setInterval(this.queryTrack.bind(this), LOOP_INTERVAL.baggageTrack)
  413. },
  414. startQueryMessage () {
  415. this.queryMessage()
  416. this.queryLoop = setInterval(this.queryMessage.bind(this), LOOP_INTERVAL.baggageMessage)
  417. },
  418. stopQueryTrack () {
  419. this.queryTrackLoop && clearInterval(this.queryTrackLoop)
  420. this.queryTrackLoop = null
  421. },
  422. stopLoopAll () {
  423. this.queryLoop && clearInterval(this.queryLoop)
  424. this.queryLoop = null
  425. this.stopQueryTrack()
  426. },
  427. resetStepNodes () {
  428. this.stepNodes = [
  429. {
  430. nodeCode: 'CHECKIN',
  431. nodeName: '值机'
  432. },
  433. {
  434. nodeCode: 'SECURITY',
  435. nodeName: '安检'
  436. },
  437. {
  438. nodeCode: 'SORT',
  439. nodeName: '分拣'
  440. },
  441. {
  442. nodeCode: 'LOAD',
  443. nodeName: '装车'
  444. },
  445. {
  446. nodeCode: 'INFL',
  447. nodeName: '装机'
  448. },
  449. {
  450. nodeCode: 'UNLOAD',
  451. nodeName: '卸机'
  452. },
  453. {
  454. nodeCode: 'ARRIVED',
  455. nodeName: '到达'
  456. }
  457. ]
  458. },
  459. initTableData (tableData) {
  460. const spanArr = []
  461. let pos = 0
  462. for (let i = 0; i < tableData.length; i++) {
  463. if (i === 0) {
  464. spanArr.push(1)
  465. } else {
  466. if (
  467. tableData[i]['flightNO'] === tableData[i - 1]['flightNO'] &&
  468. tableData[i]['flightDate'] === tableData[i - 1]['flightDate'] &&
  469. tableData[i]['departureAirport'] === tableData[i - 1]['departureAirport'] &&
  470. tableData[i]['landingAirport'] === tableData[i - 1]['landingAirport']
  471. ) {
  472. spanArr[pos] += 1
  473. spanArr.push(0)
  474. } else {
  475. spanArr.push(1)
  476. pos = i
  477. }
  478. }
  479. }
  480. this.spanArr = spanArr
  481. this.pos = pos
  482. },
  483. headerCellClass ({ row, column, rowIndex, columnIndex }) {
  484. if (['departureInfo', 'landingInfo'].includes(column.property)) {
  485. return 'pre-line'
  486. }
  487. },
  488. cellClass ({ row, column, rowIndex, columnIndex }) {
  489. const classes = []
  490. if (
  491. ['flightNO', 'U_Device_ID'].includes(column.property) &&
  492. row[column.property] &&
  493. row[column.property] !== 'FBULK'
  494. ) {
  495. classes.push('cell-click')
  496. }
  497. if (['departureInfo', 'landingInfo'].includes(column.property)) {
  498. classes.push('pre-line')
  499. }
  500. return classes.join(' ')
  501. },
  502. cellClickHandler (row, column, cell, event) {
  503. if (row[column.property] && row[column.property] !== 'FBULK') {
  504. switch (column.property) {
  505. case 'flightNO':
  506. this.$router.push({
  507. path: `${this.$route.path.split('/').slice(0, -1).join('/')}/flightView`,
  508. query: {
  509. flightNO: row.flightNO,
  510. flightDate: row.flightDate
  511. }
  512. })
  513. break
  514. case 'U_Device_ID':
  515. this.$router.push({
  516. path: `${this.$route.path.split('/').slice(0, -1).join('/')}/containerView`,
  517. query: {
  518. flightNO: row.flightNO,
  519. flightDate: row.flightDate,
  520. departureAirport: row.departureAirport,
  521. landingAirport: row.landingAirport,
  522. containerID: row.U_Device_ID
  523. }
  524. })
  525. break
  526. default:
  527. break
  528. }
  529. }
  530. },
  531. tableSpanMethod ({ row, column, rowIndex, columnIndex }) {
  532. if (['flightNO', 'flightDate', 'departureInfo', 'landingInfo'].includes(column['property'])) {
  533. const _row = this.spanArr[rowIndex]
  534. const _col = _row > 0 ? 1 : 0
  535. return {
  536. rowspan: _row,
  537. colspan: _col
  538. }
  539. }
  540. },
  541. exportHandler (refName, tableName) {
  542. const table = this.$refs[refName].$el.cloneNode(true)
  543. const { bagSN, flightNO, flightDate } = this.queryData
  544. const fileName = `${tableName}-${bagSN}-${flightNO}-${flightDate}.xlsx`
  545. throttledExportToExcel(table, tableName, fileName)
  546. },
  547. exportMessageToExcel () {
  548. const xlsxDatas = [['Date & Time', 'Message']]
  549. xlsxDatas.push(
  550. ...this.messageList.map(message => [
  551. message.date,
  552. message.dataContent.replaceAll(/[\\r\\n]{2,}/g, '\n').replaceAll('\\', '')
  553. ])
  554. )
  555. const columnWidths = []
  556. xlsxDatas.forEach(row => {
  557. // 计算每一列宽度,考虑换行
  558. row.forEach((cell, columnIndex) => {
  559. const cellWidth = Math.max(
  560. ...cell
  561. .toString()
  562. .split('\n')
  563. .map(cellRow =>
  564. cellRow.split('').reduce((pre, curr) => {
  565. const letterSize = curr.charCodeAt(0) > 255 ? 2 : 1
  566. return pre + letterSize
  567. }, 0)
  568. )
  569. )
  570. if ((!columnWidths[columnIndex] && cellWidth > 0) || cellWidth > columnWidths[columnIndex]) {
  571. columnWidths[columnIndex] = cellWidth
  572. }
  573. })
  574. })
  575. // 生成表格数据
  576. const sheet = XLSX.utils.aoa_to_sheet(xlsxDatas)
  577. // 添加列宽度
  578. sheet['!cols'] = columnWidths.map(width => ({
  579. wch: width + 2
  580. }))
  581. // 表格对齐、添加边框
  582. const borderStyle = {
  583. style: 'medium',
  584. color: {
  585. rgb: 'FFFFFF'
  586. }
  587. }
  588. const reg = /^[A-Z]+([\d]+$)/
  589. for (const key in sheet) {
  590. const match = reg.test(key)
  591. if (match) {
  592. const rowIndex = reg.exec(key)[1]
  593. let cellStyle = {
  594. alignment: {
  595. horizontal: 'center',
  596. vertical: 'center',
  597. wrapText: true
  598. }
  599. }
  600. if (Number(rowIndex) === 1) {
  601. cellStyle = {
  602. ...cellStyle,
  603. border: {
  604. top: borderStyle,
  605. right: borderStyle,
  606. bottom: borderStyle,
  607. left: borderStyle
  608. },
  609. font: {
  610. color: {
  611. rgb: 'FFFFFF'
  612. }
  613. },
  614. fill: {
  615. fgColor: {
  616. rgb: '3366FF'
  617. }
  618. }
  619. }
  620. } else {
  621. cellStyle.alignment.horizontal = 'left'
  622. }
  623. sheet[key].s = cellStyle
  624. }
  625. }
  626. // 表格数据转换
  627. const workBook = XLSX.utils.book_new()
  628. XLSX.utils.book_append_sheet(workBook, sheet, '行李原始报文')
  629. const tableWrite = XLSX_STYLE.write(workBook, {
  630. bookType: 'xlsx',
  631. bookSST: true,
  632. type: 'buffer',
  633. cellStyles: true
  634. })
  635. // 下载表格
  636. const { bagSN, flightNO, flightDate } = this.queryData
  637. const fileName = `行李原始报文-${bagSN}-${flightNO}-${flightDate}.xlsx`
  638. FileSaver.saveAs(new Blob([tableWrite], { type: 'application/octet-stream' }), fileName)
  639. },
  640. // 行李详情基础信息
  641. queryBaggageBasicInfo (dataContent) {
  642. return myQuery(SERVICE_ID.baggageBasicInfo, ...dataContent)
  643. },
  644. // 行李航段
  645. queryBaggageAirline (dataContent) {
  646. return myQuery(SERVICE_ID.baggageAirline, ...dataContent)
  647. },
  648. // 行李详情追踪链
  649. queryBaggageTrack (dataContent) {
  650. return myQuery(SERVICE_ID.baggageTrack, ...dataContent)
  651. },
  652. // 行李详情表格
  653. queryBaggageDetails (dataContent) {
  654. return myQuery(SERVICE_ID.baggageDetails, ...dataContent)
  655. },
  656. // 原始报文
  657. async queryBaggageMessage (dataContent) {
  658. try {
  659. const { code, returnData, message } = await BaggageMessageQuery({
  660. id: SERVICE_ID.baggageMessage,
  661. dataContent
  662. })
  663. if (Number(code) === 0) {
  664. return returnData.listValues
  665. } else {
  666. this.$message.error(message ?? '失败')
  667. }
  668. } catch (error) {
  669. this.$message.error('失败')
  670. }
  671. },
  672. async queryBasicInfo (queryData = this.queryData) {
  673. const { flightNO, flightDate, bagSN } = queryData
  674. const dataContent = [flightNO, flightDate, bagSN]
  675. try {
  676. const baggageBasicInfo = await this.queryBaggageBasicInfo(dataContent)
  677. if (baggageBasicInfo.length) {
  678. baggageBasicInfo[0].flightNO = flightNO
  679. this.baggageBasicInfo = baggageBasicInfo[0]
  680. }
  681. } catch (error) {
  682. this.$message.error('失败')
  683. }
  684. },
  685. async queryDetails (queryData = this.queryData) {
  686. function setDataSource (item) {
  687. if (item['b_type'] !== 'BSM') {
  688. const resourceCode = item['resourceFile']?.slice(-4)
  689. switch (resourceCode) {
  690. case '0100':
  691. if (
  692. (item['DeviceCode'] && item['DeviceCode'].toUpperCase() === 'STARHUB') ||
  693. (!item['DeviceCode'] && !['LOAD', 'INFL'].includes(item['nodeCode']))
  694. ) {
  695. item['dataSource'] = '星盟'
  696. } else {
  697. item['dataSource'] = 'BRS'
  698. }
  699. break
  700. case '0101':
  701. item['dataSource'] = 'RFID'
  702. break
  703. case '0102':
  704. item['dataSource'] = '首都机场'
  705. break
  706. default:
  707. break
  708. }
  709. }
  710. }
  711. const { flightNO, flightDate, bagSN } = queryData
  712. const dataContent = [flightNO, flightDate, bagSN]
  713. try {
  714. const baggageDetails = await this.queryBaggageDetails(new Array(6).fill(dataContent).flat())
  715. this.baggageTableData = baggageDetails.map((item, index) => {
  716. if (item['dealTime']) {
  717. item['dealTime'] = item['dealTime'].replace('T', ' ')
  718. }
  719. item['departureInfo'] = `${item['departureAirport']}\n${item['departureTime'] ? item['departureTime'].replace('T', '\n') : ''
  720. }`
  721. item['landingInfo'] = `${item['landingAirport']}\n${item['landingTime'] ? item['landingTime'].replace('T', '\n') : ''
  722. }`
  723. setDataSource(item)
  724. return item
  725. })
  726. this.initTableData(this.baggageTableData)
  727. } catch (error) {
  728. this.$message.error('失败')
  729. }
  730. },
  731. async queryAirline (queryData = this.queryData) {
  732. const { flightNO, flightDate, bagSN } = queryData
  733. const dataContent = [flightNO, flightDate, bagSN]
  734. try {
  735. const result = await this.queryBaggageAirline(dataContent)
  736. this.airlineList = result.map(({ flightNO, flightDate, departureAirport, arriveAirport, luggageSN }) => ({
  737. label: `${departureAirport}-${arriveAirport}`,
  738. value: `${flightNO},${flightDate},${luggageSN}`,
  739. flightNO,
  740. flightDate
  741. }))
  742. if (this.airlineList.length) {
  743. let currentIndex = this.airlineList.findIndex(
  744. ({ flightNO, flightDate }) => flightNO === queryData.flightNO && flightDate === queryData.flightDate
  745. )
  746. currentIndex = currentIndex > -1 ? currentIndex : 0
  747. this.selectedAirline = this.airlineList[currentIndex].value
  748. } else {
  749. this.selectedAirline = ''
  750. }
  751. } catch (error) {
  752. this.$message.error('失败')
  753. }
  754. },
  755. async queryTrack () {
  756. function isSameStep (code1, code2) {
  757. const sameStepCodes = ['ARRIVED', 'TRANSFER']
  758. return sameStepCodes.includes(code1) && sameStepCodes.includes(code2)
  759. }
  760. try {
  761. const result = await this.queryBaggageTrack(this.selectedAirline.split(','))
  762. this.resetStepNodes()
  763. result.forEach(({ nodeCode, nodeName, processingTime, locationId, status }) => {
  764. const replaceIndex = this.stepNodes.findIndex(
  765. stepNode => stepNode.nodeCode === nodeCode || isSameStep(stepNode.nodeCode, nodeCode)
  766. )
  767. if (replaceIndex > -1) {
  768. this.stepNodes.splice(replaceIndex, 1, {
  769. nodeCode,
  770. nodeName,
  771. processingTime: processingTime?.replace('T', '\n'),
  772. locationId,
  773. status
  774. })
  775. }
  776. })
  777. } catch (error) {
  778. this.$message.error('失败')
  779. }
  780. },
  781. async queryMessage (queryData = this.queryData) {
  782. const { flightNO, flightDate, bagSN } = queryData
  783. const dataContent = [flightNO, flightDate, bagSN]
  784. try {
  785. const result = await this.queryBaggageMessage(dataContent)
  786. const messageList = result.reduce((list, message, currentIndex, arr) => {
  787. const messageObject = typeof message === 'string' ? JSON.parse(message) : message
  788. // 确保是当前行李的报文
  789. const isCorrectBaggage = messageObject.dataContent.match(/\.N\/([0-9]{10})/)?.[1] === bagSN
  790. // 相同报文去重
  791. const index = arr.findIndex(testMessage => {
  792. const testMessageObject = typeof testMessage === 'string' ? JSON.parse(testMessage) : testMessage
  793. return (
  794. messageObject.dataContent === testMessageObject.dataContent &&
  795. messageObject.ssid === testMessageObject.ssid
  796. )
  797. })
  798. if (isCorrectBaggage && currentIndex === index) {
  799. return [...list, messageObject]
  800. } else {
  801. return list
  802. }
  803. }, [])
  804. this.messageList = this._.sortBy(messageList, 'ssid')
  805. } catch (error) {
  806. this.$message.error('失败')
  807. }
  808. }
  809. }
  810. }
  811. </script>
  812. <style
  813. lang="scss"
  814. scoped
  815. >
  816. .baggage-view {
  817. width: 100%;
  818. height: calc(100vh - 80px);
  819. overflow: hidden;
  820. background: #dfe3ea;
  821. padding: 8px 8px 0;
  822. .part1 {
  823. width: 100%;
  824. // height: 232px;
  825. background: #041741;
  826. padding: 16px 30px;
  827. overflow: hidden;
  828. position: relative;
  829. ::v-deep .title {
  830. font-size: 18px;
  831. font-weight: bold;
  832. color: #ffffff;
  833. display: flex;
  834. flex-direction: row;
  835. align-items: center;
  836. .radioBtn {
  837. margin-left: 32px;
  838. padding: 5px;
  839. background: #000d2a;
  840. .el-radio-button__inner {
  841. background: #000d2a;
  842. color: #fff;
  843. border: none;
  844. font-weight: bold;
  845. }
  846. .el-radio-button:first-child .el-radio-button__inner {
  847. border: none;
  848. }
  849. }
  850. .btn-back {
  851. color: #ffffff;
  852. }
  853. }
  854. .part1_info {
  855. width: 100%;
  856. color: #fff;
  857. font-size: 14px;
  858. font-weight: 400;
  859. color: #ffffff;
  860. max-height: 152px;
  861. overflow-x: hidden;
  862. overflow-y: auto;
  863. > .el-row > .el-col {
  864. height: 38px;
  865. line-height: 38px;
  866. display: flex;
  867. .label {
  868. flex-basis: 126px;
  869. text-align: right;
  870. }
  871. .content {
  872. flex: 1;
  873. margin: 0;
  874. white-space: nowrap;
  875. overflow: hidden;
  876. text-overflow: ellipsis;
  877. &.click {
  878. text-decoration: underline;
  879. cursor: pointer;
  880. }
  881. }
  882. }
  883. }
  884. }
  885. .part2 {
  886. margin: 8px 0;
  887. width: 100%;
  888. padding: 24px 30px 28px;
  889. overflow: hidden;
  890. background: #ffffff;
  891. display: flex;
  892. flex-direction: row;
  893. justify-content: space-between;
  894. align-items: flex-start;
  895. position: relative;
  896. .part2_info {
  897. flex: 1;
  898. display: flex;
  899. flex-direction: row;
  900. justify-content: flex-start;
  901. align-items: flex-start;
  902. line-height: 42px;
  903. .title {
  904. width: 120px;
  905. font-size: 18px;
  906. font-weight: bold;
  907. color: #303133;
  908. margin-right: 20px;
  909. }
  910. .type {
  911. font-size: 18px;
  912. font-weight: bold;
  913. margin-right: 20px;
  914. .warn {
  915. color: #df3559;
  916. }
  917. .normal {
  918. color: #519f6b;
  919. }
  920. }
  921. .airline {
  922. width: 120px;
  923. margin-right: 20px;
  924. }
  925. .baggage-track-chart {
  926. flex: 1;
  927. height: 124px;
  928. max-width: 1280px;
  929. position: relative;
  930. display: flex;
  931. flex-direction: row;
  932. justify-content: space-between;
  933. width: 100%;
  934. }
  935. .step-line {
  936. width: calc(100% - 80px);
  937. height: 10px;
  938. position: absolute;
  939. top: 16px;
  940. right: 0;
  941. left: 0;
  942. margin: auto;
  943. display: flex;
  944. .step-line-segment {
  945. width: calc(100% / 6);
  946. height: 100%;
  947. background: #afb4bf;
  948. &.step-line-active {
  949. background: #2d67e3;
  950. }
  951. }
  952. }
  953. .step-item {
  954. width: 80px;
  955. height: 100%;
  956. text-align: center;
  957. font-size: 14px;
  958. display: flex;
  959. flex-direction: column;
  960. align-items: center;
  961. justify-content: flex-start;
  962. z-index: 1;
  963. font-family: Helvetica, "Microsoft Yahei";
  964. .step-circle {
  965. width: 42px;
  966. height: 42px;
  967. border-radius: 50%;
  968. background: #aaacb2;
  969. .step-name {
  970. color: #ffffff;
  971. font-size: 14px;
  972. font-weight: bold;
  973. }
  974. }
  975. .step-info {
  976. margin-top: 15px;
  977. color: #101116;
  978. line-height: 22px;
  979. .step-status {
  980. &-normal {
  981. color: #4ab36f;
  982. }
  983. &-abnormal {
  984. color: #e9af4b;
  985. }
  986. }
  987. .step-time {
  988. white-space: pre-line;
  989. font-size: 12px;
  990. line-height: 20px;
  991. }
  992. }
  993. &.active-item .step-circle {
  994. background: #2d67e3;
  995. }
  996. }
  997. }
  998. .btns {
  999. margin-top: 6px;
  1000. }
  1001. }
  1002. .part3 {
  1003. width: 100%;
  1004. // header-80px、part1-232px、part2-128px、间隙3*8px、底部44px
  1005. // height: calc(100vh - 80px - 232px - 128px - 3 * 8px - 44px);
  1006. background: #ffffff;
  1007. ::v-deep .el-table {
  1008. width: 100%;
  1009. // &.el-table--striped {
  1010. // .el-table__body tr.el-table__row--striped td.el-table__cell,
  1011. // .el-table__header .el-table__cell {
  1012. // background: #ffffff;
  1013. // }
  1014. // }
  1015. .el-table__cell {
  1016. // background: #f0f3f7;
  1017. padding: 0;
  1018. &.cell-click {
  1019. cursor: pointer;
  1020. .cell {
  1021. color: #2d7cff;
  1022. }
  1023. }
  1024. .cell {
  1025. padding: 0;
  1026. word-spacing: 0;
  1027. font-size: 14px;
  1028. font-family: Helvetica, "Microsoft YaHei";
  1029. font-weight: 400;
  1030. color: #303133;
  1031. width: 100% !important;
  1032. .cell-content {
  1033. padding: 6px 0;
  1034. }
  1035. }
  1036. }
  1037. .el-table__body .el-table__cell .cell {
  1038. padding: 6px 10px;
  1039. .cell-content {
  1040. display: inline;
  1041. padding: 0;
  1042. }
  1043. }
  1044. }
  1045. }
  1046. .part4 {
  1047. width: 100%;
  1048. // height: calc(100vh - 80px - 232px - 2 * 8px - 44px);
  1049. .head {
  1050. padding: 16px 24px 11px 30px;
  1051. background: transparent;
  1052. display: flex;
  1053. justify-content: space-between;
  1054. .title {
  1055. line-height: 30px;
  1056. font-size: 18px;
  1057. font-weight: bold;
  1058. color: #303133;
  1059. }
  1060. }
  1061. .main {
  1062. height: calc(100% - 57px);
  1063. overflow-y: auto;
  1064. overflow-x: hidden;
  1065. ::v-deep .el-row {
  1066. flex-wrap: wrap;
  1067. .card {
  1068. width: 100%;
  1069. min-height: 440px;
  1070. padding: 20px;
  1071. background: #ffffff;
  1072. box-shadow: 0px 3px 2px 0px rgba(0, 0, 0, 0.29);
  1073. margin-bottom: 24px;
  1074. > .message-date {
  1075. width: 180px;
  1076. height: 26px;
  1077. line-height: 14px;
  1078. font-size: 14px;
  1079. font-family: Helvetica;
  1080. color: #afb4bf;
  1081. border-bottom: 1px solid #afb4bf;
  1082. margin-bottom: 18px;
  1083. }
  1084. > .message-content {
  1085. white-space: pre-line;
  1086. line-height: 24px;
  1087. font-size: 14px;
  1088. color: #303133;
  1089. word-break: break-all;
  1090. }
  1091. }
  1092. }
  1093. }
  1094. }
  1095. .btns {
  1096. height: 30px;
  1097. display: flex;
  1098. .btn-square {
  1099. margin-left: 10px;
  1100. width: 30px;
  1101. }
  1102. }
  1103. .button-toggle {
  1104. position: absolute;
  1105. bottom: 0;
  1106. left: 50%;
  1107. transform: translateX(-50%);
  1108. z-index: 1000;
  1109. width: 20px;
  1110. height: 15px;
  1111. border-radius: 4px;
  1112. background-color: #2d67e3;
  1113. color: #ffffff;
  1114. cursor: pointer;
  1115. text-align: center;
  1116. }
  1117. }
  1118. </style>
  1119. <style
  1120. scoped
  1121. lang="scss"
  1122. >
  1123. ::v-deep .baggage-view {
  1124. .el-popover {
  1125. &.popover-dark {
  1126. background: #303133;
  1127. color: #ffffff;
  1128. border: none;
  1129. }
  1130. .pre-line {
  1131. white-space: pre-line;
  1132. }
  1133. }
  1134. .el-popper[x-placement^="top"].popover-dark .popper__arrow::after {
  1135. border-top-color: #303133;
  1136. }
  1137. .el-popper[x-placement^="right"].popover-dark .popper__arrow::after {
  1138. border-right-color: #303133;
  1139. }
  1140. .el-popper[x-placement^="bottom"].popover-dark .popper__arrow::after {
  1141. border-bottom-color: #303133;
  1142. }
  1143. .el-popper[x-placement^="left"].popover-dark .popper__arrow::after {
  1144. border-left-color: #303133;
  1145. }
  1146. }
  1147. </style>