index.vue 35 KB

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