App.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <template>
  2. <div id="app">
  3. <router-view></router-view>
  4. <div class="hide">
  5. <webview
  6. id="printWebview"
  7. :src="publicPath"
  8. nodeintegration
  9. ></webview>
  10. <webview
  11. id="printSmallTicketWebview"
  12. :src="publicSmallTicketPath"
  13. nodeintegration
  14. ></webview>
  15. <!-- 打印模板 -->
  16. <PrintTemplate :current-ticket="currentTicket"></PrintTemplate>
  17. <!-- 小票打印模板 -->
  18. <SmallTicketPrintTemplate :current-order="printOrder"></SmallTicketPrintTemplate>
  19. </div>
  20. <UpdateFile></UpdateFile>
  21. </div>
  22. </template>
  23. <script>
  24. import PrintTemplate from '@/components/PrintTemplate.vue'
  25. import SmallTicketPrintTemplate from '@/components/SmallTicketPrintTemplate'
  26. import UpdateFile from './UpdateFile'
  27. const { ipcRenderer } = require('electron')
  28. const path = require('path')
  29. export default {
  30. name: 'AdminClient',
  31. components: {
  32. PrintTemplate,
  33. UpdateFile,
  34. SmallTicketPrintTemplate
  35. },
  36. computed: {
  37. faceModelIsReady () {
  38. return this.$store.state.app.faceModelIsReady
  39. },
  40. printList () {
  41. return this.$store.state.app.printList
  42. },
  43. printOrder () {
  44. return this.$store.state.app.printOrder
  45. }
  46. },
  47. data () {
  48. return {
  49. publicPath: path.join(__static, 'print.html'),
  50. publicSmallTicketPath: path.join(__static, 'smallTicket.html'),
  51. isPrinting: false, // 正在打印
  52. printers: [],
  53. currentTicket: null,
  54. currentOrder: null,
  55. printAdvanced: false, // 当前这张票是否已推进过队列(回调与超时兜底互斥,保证只推进一次)
  56. printAdvanceTimer: null // 打印回调未触发时的兜底推进定时器
  57. }
  58. },
  59. watch: {
  60. $route: {
  61. handler (val) {
  62. console.log(val.fullPath)
  63. },
  64. immediate: true
  65. },
  66. // 待打印列表
  67. printList (val) {
  68. // 打印列表不为空,且当前未在打印时,设置当前打印对象
  69. if (val.length > 0 && !this.isPrinting) {
  70. this.isPrinting = true
  71. this.currentTicket = val[0]
  72. this.$log.warn(`[print-queue] 开始打印,共 ${val.length} 张`)
  73. }
  74. }
  75. },
  76. methods: {
  77. checkConfig () {
  78. const serverUrl = this.$localStore.get('serverUrl')
  79. if (!serverUrl) {
  80. this.$router.push('/config')
  81. return false
  82. } else {
  83. // 检查更新
  84. ipcRenderer.send('updateReady', this.$localStore.get('updateUrl'))
  85. if (this.$route.name === 'config') {
  86. this.$router.push('/')
  87. }
  88. }
  89. },
  90. setPrint () {
  91. ipcRenderer.send('getPrinters')
  92. ipcRenderer.on('sendPrinters', (event, printers) => {
  93. console.log(printers)
  94. this.printers = printers
  95. this.defaultPrinter = printers.find(i => i.isDefault) && printers.find(i => i.isDefault).name
  96. })
  97. this.webview = document.querySelector('#printWebview')
  98. this.webview.removeEventListener('ipc-message', this.handlePrint)
  99. this.webview.addEventListener('ipc-message', this.handlePrint)
  100. this.smallTicketWebview = document.querySelector('#printSmallTicketWebview')
  101. this.smallTicketWebview.removeEventListener('ipc-message', this.handlePrintSmallTicket)
  102. this.smallTicketWebview.addEventListener('ipc-message', this.handlePrintSmallTicket)
  103. },
  104. // 推进打印队列:移除当前门票,有剩余则渲染并打印下一张
  105. nextPrintTicket (reason) {
  106. // 同一张票只允许推进一次:打印回调与超时兜底二选一,防止双推进丢票
  107. if (this.printAdvanced) return
  108. this.printAdvanced = true
  109. if (this.printAdvanceTimer) {
  110. clearTimeout(this.printAdvanceTimer)
  111. this.printAdvanceTimer = null
  112. }
  113. this.$store.commit('PRINT_NEW')
  114. const rest = this.$store.state.app.printList
  115. this.$log.warn(`[print-queue] 剩余待打印 ${rest.length} 张(推进原因:${reason})`)
  116. if (rest.length > 0) {
  117. this.isPrinting = true
  118. // 等上一张的作业离开 Chromium 打印栈后再发下一张,
  119. // 间隔过短连发时后续作业会被打印栈丢弃(1.0.17.8 起 N 张只打 N/2 的根因)
  120. setTimeout(() => {
  121. this.currentTicket = rest[0]
  122. }, 500)
  123. } else {
  124. this.isPrinting = false
  125. }
  126. },
  127. handlePrint (event) {
  128. if (event.channel === 'webview-print-do') {
  129. // if (this.isPrinting) return
  130. try {
  131. let printer
  132. if (this.currentTicket && this.currentTicket.printer_name && this.printers.find(v => v.name === this.currentTicket.printer_name)) {
  133. printer = this.currentTicket.printer_name || this.$localStore.get('defaultPrinter') || this.defaultPrinter
  134. } else {
  135. printer = this.$localStore.get('defaultPrinter') || this.defaultPrinter
  136. }
  137. // webview 带回它实际渲染的票号,与队列当前票号不一致说明渲染滞后/错位
  138. const renderedTicketNo = event.args && event.args[0]
  139. const currentTicketNo = this.currentTicket && this.currentTicket.ticketNo
  140. if (renderedTicketNo !== undefined && renderedTicketNo !== currentTicketNo) {
  141. this.$log.error(`[print-queue] 渲染错位!期望票号:${currentTicketNo},webview 实际渲染:${renderedTicketNo}`)
  142. }
  143. this.$log.warn(`[print-queue] 开始打印!票号:${currentTicketNo},webview 实际渲染:${renderedTicketNo || '(未知)'},打印机:${printer}`)
  144. // 打印当前门票;以打印回调推进队列,保证上一张作业完成后再发下一张(恢复 1.0.17.6 的串行节奏)
  145. try {
  146. this.printAdvanced = false
  147. this.webview.print({
  148. silent: true,
  149. printBackground: true,
  150. deviceName: printer
  151. }, (data) => {
  152. this.$log.warn('[print-queue] 打印成功回调')
  153. this.nextPrintTicket('打印回调')
  154. })
  155. // 兜底:未连打印机等场景回调不触发,超时后强制推进,避免队列卡死
  156. this.printAdvanceTimer = setTimeout(() => {
  157. this.$log.warn('[print-queue] 打印回调超时(2.5s),兜底推进')
  158. this.nextPrintTicket('超时兜底')
  159. }, 2500)
  160. } catch (e) {
  161. this.$log.error('[print-queue] 调用打印失败', e)
  162. // 调用失败时也要推进,否则队列永久卡死
  163. this.nextPrintTicket('调用失败')
  164. }
  165. } catch (e) {
  166. console.log(e)
  167. }
  168. }
  169. },
  170. handlePrintSmallTicket (event) {
  171. if (event.channel === 'webview-print-do') {
  172. // if (this.isPrinting) return
  173. try {
  174. let printer = this.$localStore.get('defaultSmallTicketPrinter') || this.defaultPrinter
  175. console.error('开始打印!', printer)
  176. this.smallTicketWebview.print({
  177. silent: true,
  178. printBackground: true,
  179. deviceName: printer
  180. }, (data) => {
  181. // console.log(data)
  182. // this.$message.success('打印成功')
  183. console.log('打印成功')
  184. // 打印成功后,更新打印列表
  185. this.isPrinting = false
  186. this.$store.commit('SET_PRINT_ORDER', null)
  187. })
  188. } catch (e) {
  189. console.log(e)
  190. }
  191. }
  192. },
  193. setConfigEvent () {
  194. ipcRenderer.on('goConfig', () => {
  195. this.$router.push('/config')
  196. })
  197. ipcRenderer.on('goPos', () => {
  198. this.$router.push('/posTool')
  199. })
  200. }
  201. },
  202. created () {
  203. this.$log.info('程序初始化完成')
  204. },
  205. mounted () {
  206. console.log('app mounted')
  207. // 检查本地配置
  208. this.checkConfig()
  209. // 设置打印事件
  210. this.setPrint()
  211. // 设置配置事件
  212. this.setConfigEvent()
  213. this.$store.dispatch('initDonsee100R')
  214. }
  215. }
  216. </script>
  217. <style lang="scss">
  218. .success {
  219. color: #67c23a;
  220. }
  221. .danger {
  222. color: #f56c6c;
  223. }
  224. i {
  225. font-style: normal;
  226. }
  227. .hidden {
  228. width: 0;
  229. height: 0;
  230. overflow: hidden;
  231. }
  232. .el-card__header {
  233. padding: 10px 20px;
  234. font-size: 16px;
  235. line-height: 22px;
  236. }
  237. .form-wrap {
  238. .el-input,
  239. .el-select,
  240. .el-date-editor,
  241. .el-date-editor.el-input,
  242. .el-input-number {
  243. width: 194px;
  244. }
  245. }
  246. .el-pagination {
  247. padding: 30px 0 20px;
  248. text-align: center;
  249. .el-select {
  250. width: auto;
  251. input {
  252. height: 28px;
  253. }
  254. }
  255. .el-input__icon {
  256. line-height: 28px;
  257. }
  258. &__jump {
  259. .el-input {
  260. width: 50px !important;
  261. }
  262. }
  263. }
  264. .el-table {
  265. .el-table__row.expanded {
  266. background: lighten(#b5bec7, 18%);
  267. }
  268. .el-table__expanded-cell {
  269. background: lighten(#e9eaee, 5%) !important;
  270. &:hover {
  271. background: lighten(#e9eaee, 5%) !important;
  272. }
  273. }
  274. .el-tag {
  275. margin: 3px 5px 3px 0;
  276. }
  277. }
  278. // 表格的详情展开面板
  279. .table-expand {
  280. .el-form-item {
  281. label {
  282. color: #99a9bf;
  283. }
  284. }
  285. }
  286. // 对话框padding太大了,30 20改成20
  287. .el-dialog__body {
  288. padding: 20px;
  289. }
  290. // 表单信息p标签不换行
  291. .p-nowrap {
  292. overflow: hidden;
  293. text-overflow: ellipsis;
  294. white-space: nowrap;
  295. }
  296. // 信息对话框的样式
  297. .info-form {
  298. display: flex;
  299. flex-wrap: wrap;
  300. .info-form-line {
  301. display: flex;
  302. width: 50%;
  303. height: 40px;
  304. line-height: 40px;
  305. .info-form-label {
  306. width: 80px;
  307. padding-right: 12px;
  308. color: #64667b;
  309. text-align: right;
  310. flex-shrink: 0;
  311. flex-grow: 0;
  312. }
  313. }
  314. }
  315. .overflow-txt {
  316. display: block;
  317. width: 180px;
  318. overflow: hidden;
  319. text-overflow: ellipsis;
  320. white-space: nowrap;
  321. }
  322. // 图片上传样式
  323. .avatar-uploader {
  324. width: 100px;
  325. height: 100px;
  326. text-align: center;
  327. background: #eee;
  328. border: 1px dashed #d9d9d9;
  329. border-radius: 6px;
  330. .el-upload {
  331. position: relative;
  332. overflow: hidden;
  333. cursor: pointer;
  334. }
  335. .el-upload:hover {
  336. border-color: #409eff;
  337. }
  338. .avatar-uploader-icon {
  339. width: 100px;
  340. height: 100px;
  341. font-size: 28px;
  342. line-height: 100px;
  343. color: #8c939d;
  344. text-align: center;
  345. }
  346. .avatar {
  347. display: block;
  348. width: 100px;
  349. height: 100px;
  350. background: center center no-repeat;
  351. background-size: contain;
  352. }
  353. }
  354. .no-wrap {
  355. width: 100%;
  356. overflow: hidden;
  357. white-space: nowrap;
  358. }
  359. .offSale-btn {
  360. color: #f56c6c;
  361. }
  362. .onSale-btn {
  363. color: #67c23a;
  364. }
  365. #printWebview {
  366. width: 500px;
  367. height: 500px;
  368. }
  369. .hide {
  370. width: 0;
  371. height: 0;
  372. overflow: hidden;
  373. }
  374. .nohide {
  375. position: absolute;
  376. top: 0;
  377. left: 0;
  378. width: 100%;
  379. height: 100%;
  380. pointer-events: none;
  381. z-index: 9999;
  382. }
  383. .card-title {
  384. display: flex;
  385. color: #333;
  386. align-items: center;
  387. justify-content: space-between;
  388. .price {
  389. b {
  390. font-size: 1.6em;
  391. color: $blue;
  392. }
  393. }
  394. }
  395. .price {
  396. b {
  397. color: $blue;
  398. font-size: 1.6em;
  399. }
  400. }
  401. .buyer-info {
  402. .el-form-item {
  403. display: inline-block;
  404. margin-right: 20px;
  405. }
  406. .el-form-item__content {
  407. font-weight: bold;
  408. }
  409. }
  410. .readCard {
  411. position: absolute;
  412. right: 0;
  413. z-index: 0;
  414. display: flex;
  415. width: 80px;
  416. height: 100%;
  417. font-size: 12px;
  418. color: #fff;
  419. cursor: pointer;
  420. align-items: center;
  421. .inner {
  422. display: flex;
  423. height: 80%;
  424. padding: 0 5px;
  425. padding-right: 30px;
  426. background: $blue;
  427. border-radius: 5px;
  428. align-items: center;
  429. }
  430. .wave {
  431. position: absolute;
  432. top: 50%;
  433. left: 50%;
  434. display: none;
  435. width: 20px;
  436. height: 20px;
  437. box-sizing: border-box;
  438. margin-top: -10px;
  439. margin-left: -10px;
  440. background: rgba(255, 255, 255, .384);
  441. border: 1px solid red;
  442. border-radius: 50%;
  443. opacity: 0;
  444. animation: wave infinite linear 1s forwards;
  445. }
  446. .wave2 {
  447. opacity: 0;
  448. animation: wave infinite 2s 1s forwards;
  449. }
  450. .svg-icon {
  451. position: relative;
  452. width: 16px;
  453. height: 16px;
  454. margin-right: 5px;
  455. font-size: 16px;
  456. }
  457. &.active {
  458. .wave {
  459. display: block;
  460. }
  461. .svg-icon {
  462. color: #fff;
  463. }
  464. }
  465. }
  466. @keyframes wave {
  467. 0% {
  468. opacity: 0;
  469. transform: scale(3);
  470. }
  471. 50% {
  472. opacity: .5;
  473. transform: scale(1);
  474. }
  475. 100% {
  476. opacity: 0;
  477. transform: scale(0);
  478. }
  479. }
  480. .dark-btn {
  481. display: flex;
  482. height: 26px;
  483. padding: 0 5px;
  484. margin-left: 10px;
  485. font-size: 12px;
  486. color: #fff;
  487. cursor: pointer;
  488. background: $blue;
  489. border-radius: 5px;
  490. align-items: center;
  491. .svg-icon {
  492. width: 16px;
  493. height: 16px;
  494. margin-right: 5px;
  495. color: #fff;
  496. }
  497. }
  498. .pay-channel-wrap {
  499. display: flex;
  500. margin-left: 20px;
  501. margin-bottom: 10px;
  502. align-items: center;
  503. .text {
  504. display: inline-block;
  505. width: 73px;
  506. padding-bottom: 5px;
  507. }
  508. }
  509. .tags-btn {
  510. flex: 1;
  511. .el-radio.is-bordered+.el-radio.is-bordered {
  512. margin-left: 0;
  513. }
  514. .el-radio--small.is-bordered .el-radio__label {
  515. font-size: 14px;
  516. line-height: 20px;
  517. }
  518. .el-radio.is-bordered.is-checked {
  519. background: #0075ff;
  520. }
  521. .el-radio__input.is-checked+.el-radio__label {
  522. color: #fff;
  523. }
  524. .el-radio.is-bordered {
  525. padding: 5px 10px;
  526. margin: 0px 10px 5px 0;
  527. }
  528. .el-radio__input {
  529. display: none;
  530. }
  531. .el-radio__label {
  532. padding: 0;
  533. }
  534. }
  535. .el-submenu .el-menu-item {
  536. padding: 0 30px;
  537. }
  538. </style>