PrintTemplate.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <div
  3. class="print-wrap"
  4. v-show="true"
  5. >
  6. <div
  7. id="printMe"
  8. ref="printMe"
  9. v-if="elementStyle"
  10. :style="[elementStyle.canvas,defaultTemplate==='horizen'?{width:elementStyle.canvas.height,height:elementStyle.canvas.width}:{}]"
  11. >
  12. <div
  13. v-if="currentTicket"
  14. :style="[{ position: 'relative',width: elementStyle.canvas.width,height:elementStyle.canvas.height},defaultTemplate==='horizen'?{transform:elementStyle.canvas.reverse?'translateX(50%) rotate(90deg)':'translateX(-100%) rotate(-90deg)', transformOrigin:elementStyle.canvas.reverse?'0 0':'100% 0'}:{ transform:elementStyle.canvas.reverse?'rotate(180deg)':''}]">
  15. <div
  16. class="el sign"
  17. :style="elementStyle.sign">
  18. </div>
  19. <span
  20. v-if="projectName!=='mazu'"
  21. class="el"
  22. :style="elementStyle.num">
  23. - NO.{{ isNaN(currentTicket.index)? 1 : currentTicket.index + 1 }} -
  24. </span>
  25. <span
  26. v-else
  27. class="el"
  28. :style="elementStyle.num">
  29. - NO.{{ isNaN(currentTicket.id)? '00000000' : currentTicket.id.toString().padStart(8,'0') }} -
  30. </span>
  31. <!-- 是否使用打印价格 -->
  32. <!--
  33. <span
  34. class="el"
  35. v-if="!currentTicket.is_otaorder"
  36. :style="elementStyle.price">
  37. <i style="font-size:0.8em; font-style:normal">{{ elementStyle.price&&elementStyle.price.format }}</i> ¥ {{ currentTicket.isPrint_price_active ? currentTicket.print_price*currentTicket.checkNum : currentTicket.price }} <span v-if="showUnit">元</span>
  38. </span>
  39. <span
  40. class="el"
  41. v-if="!currentTicket.is_otaorder"
  42. :style="elementStyle.singlePrice">
  43. <i style="font-size:0.8em; font-style:normal">{{ elementStyle.singlePrice&&elementStyle.singlePrice.format }}</i> ¥ {{ currentTicket.isPrint_price_active ? currentTicket.print_price : $NP.divide(currentTicket.price, currentTicket.checkNum) }} <span v-if="showUnit">元</span>
  44. </span>
  45. -->
  46. <!-- 三河古镇定制:线上订单也打印价格 -->
  47. <template v-if="'三河古镇' === scenicName">
  48. <span
  49. class="el"
  50. :style="elementStyle.price">
  51. <i style="font-size:0.8em; font-style:normal">{{ elementStyle.price&&elementStyle.price.format }}</i> ¥ {{ currentTicket.isPrint_price_active ? currentTicket.print_price*currentTicket.checkNum : currentTicket.price }} <span v-if="showUnit">元</span>
  52. </span>
  53. <span
  54. class="el"
  55. :style="elementStyle.singlePrice">
  56. <i style="font-size:0.8em; font-style:normal">{{ elementStyle.singlePrice&&elementStyle.singlePrice.format }}</i> ¥ {{ currentTicket.isPrint_price_active ? currentTicket.print_price : $NP.divide(currentTicket.price, currentTicket.checkNum) }} <span v-if="showUnit">元</span>
  57. </span>
  58. </template>
  59. <!-- 其余景区线上订单不打印价格 -->
  60. <template v-else>
  61. <span
  62. class="el"
  63. v-if="!currentTicket.is_otaorder && !currentTicket.teamId"
  64. :style="elementStyle.price">
  65. <i style="font-size:0.8em; font-style:normal">{{ elementStyle.price&&elementStyle.price.format }}</i> ¥ {{ currentTicket.isPrint_price_active ? currentTicket.print_price*currentTicket.checkNum : currentTicket.price }} <span v-if="showUnit">元</span>
  66. </span>
  67. <span
  68. class="el"
  69. v-if="!currentTicket.is_otaorder && !currentTicket.teamId"
  70. :style="elementStyle.singlePrice">
  71. <i style="font-size:0.8em; font-style:normal">{{ elementStyle.singlePrice&&elementStyle.singlePrice.format }}</i> ¥ {{ currentTicket.isPrint_price_active ? currentTicket.print_price : $NP.divide(currentTicket.price, currentTicket.checkNum) }} <span v-if="showUnit">元</span>
  72. </span>
  73. </template>
  74. <div :style="elementStyle.orderNo">
  75. {{ elementStyle.orderNo&&elementStyle.orderNo.format || '订单号:' }}{{ currentTicket.orderNo }}
  76. </div>
  77. <div
  78. :style="elementStyle.saleAdminName"
  79. v-if="currentTicket.saleAdminName">
  80. 售票员:{{ currentTicket.saleAdminName }}
  81. </div>
  82. <div
  83. :style="elementStyle.guestName"
  84. v-if="(currentTicket.buyerName||currentTicket.guestName)">
  85. {{ elementStyle.guestName.format&&elementStyle.guestName.format||'游客:' }} {{ currentTicket.guestName || currentTicket.buyerName }}
  86. <span v-if="currentTicket.guestIdentify && elementStyle.guestName.showId">({{ currentTicket.guestIdentify.replace(/^(.{6})(?:\d+)(.{4})$/,"$1******$2") }})</span>
  87. </div>
  88. <div
  89. :style="elementStyle.teamName"
  90. v-if="currentTicket.teamName">
  91. {{ elementStyle.teamName&&elementStyle.teamName.format||'团队名称:' }} {{ currentTicket.teamName }}
  92. </div>
  93. <div :style="elementStyle.ticketId">
  94. {{ elementStyle.ticketId&&elementStyle.ticketId.format || '票号:' }}{{ currentTicket.ticketNo }}
  95. </div>
  96. <div
  97. :style="elementStyle.createTime"
  98. v-if="currentTicket.buyTime">
  99. {{ elementStyle.createTime&&elementStyle.createTime.format || '出票时间:' }}{{ fmtTime(currentTicket.buyTime) }}
  100. </div>
  101. <div :style="elementStyle.scenics">
  102. {{ scenics }}
  103. </div>
  104. <div :style="elementStyle.scenic">
  105. <div :style="((elementStyle.scenic && (elementStyle.scenic.direction==='inline'|| !elementStyle.scenic.direction))?{}:{display: 'flex','flex-direction': 'column'})">
  106. <template v-if="elementStyle.scenic.number==='0'">
  107. <span
  108. style="margin-right:5px"
  109. v-for="(item,index) in scenicList"
  110. :key="index">
  111. {{ item.scenicName }} {{ item.checkLimitTimes }}次<span v-if="index!==(scenicList.length-1)">,</span>
  112. </span>
  113. </template>
  114. <template v-else>
  115. <span
  116. style="margin-right:5px"
  117. v-for="(item,index) in scenicList.slice(0,elementStyle.scenic.number||2)"
  118. :key="index">
  119. {{ item.scenicName }} {{ item.checkLimitTimes }}次<span v-if="elementStyle.scenic.number>scenicList.length?index!==(scenicList.length-1):index!==(elementStyle.scenic.number||2)-1">,</span>
  120. </span>
  121. <span v-if="elementStyle.scenic.number<scenicList.length">...(多景点门票)</span>
  122. </template>
  123. </div>
  124. </div>
  125. <!-- <table :style="elementStyle.info">
  126. <tr>
  127. <td width="40%">
  128. 订单号:
  129. </td>
  130. <td></td>
  131. </tr>
  132. <tr>
  133. <td>票号:</td>
  134. <td>{{ currentTicket.qrcode }}</td>
  135. </tr>
  136. <tr v-if="scenicList && scenicList.length>0">
  137. <td>{{ scenicList[0].scenicName }}</td>
  138. <td>
  139. 可检 {{ scenicList[0].checkLimitTimes }} 次
  140. 单次可过人数: {{ scenicList[0].single_pass_times }} 人 <br>
  141. <span v-if="scenicList.length>1">
  142. (多景点门票)
  143. </span>
  144. </td>
  145. </tr>
  146. </table> -->
  147. <div :style="elementStyle.ticketTypeName">
  148. {{ elementStyle.ticketTypeName.format || '' }}{{ currentTicket.ticketTypeName }}
  149. </div>
  150. <div
  151. class="img"
  152. :style="[elementStyle.qrcode,{backgroundImage:`url(${qrImg})`, backgroundRepeat: 'no-repeat', backgroundPosition: 'center', backgroundSize: 'contain', display: qrImg ? 'block' : 'none'}]">
  153. </div>
  154. <!--
  155. <div
  156. v-if="currentTicket.invoiceQrcode"
  157. class="img-wrap"
  158. :style="[elementStyle.invoiceQrcode,{display:currentTicket.invoiceQrcode?'block':'none'}]">
  159. <div
  160. class="img"
  161. :style="{backgroundImage:`url(${fpImg})`,height:'100%'}">
  162. </div>
  163. <div v-if="elementStyle.invoiceQrcode">
  164. {{ elementStyle.invoiceQrcode&&elementStyle.invoiceQrcode.format }}
  165. </div>
  166. </div>
  167. -->
  168. <!-- 不需要再设置display,否则它会覆盖elementStyle里设置的display -->
  169. <div
  170. v-if="currentTicket.invoiceQrcode"
  171. class="img-wrap"
  172. :style="[elementStyle.invoiceQrcode]">
  173. <div
  174. class="img"
  175. :style="{backgroundImage:`url(${fpImg})`,height:'100%'}">
  176. </div>
  177. <div v-if="elementStyle.invoiceQrcode">
  178. {{ elementStyle.invoiceQrcode&&elementStyle.invoiceQrcode.format }}
  179. </div>
  180. </div>
  181. <!--
  182. <div
  183. v-if="currentTicket.allInvoiceQrcode"
  184. class="img-wrap"
  185. :style="[elementStyle.allInvoiceQrcode,{display:currentTicket.allInvoiceQrcode?'block':'none'}]">
  186. <div
  187. class="img"
  188. :style="{backgroundImage:`url(${afpImg})`,height:'100%'}">
  189. </div>
  190. <div v-if="elementStyle.allInvoiceQrcode">
  191. {{ elementStyle.allInvoiceQrcode.format }}
  192. </div>
  193. </div>
  194. -->
  195. <!-- 不需要再设置display,否则它会覆盖elementStyle里设置的display -->
  196. <div
  197. v-if="currentTicket.allInvoiceQrcode"
  198. class="img-wrap"
  199. :style="[elementStyle.allInvoiceQrcode]">
  200. <div
  201. class="img"
  202. :style="{backgroundImage:`url(${afpImg})`,height:'100%'}">
  203. </div>
  204. <div v-if="elementStyle.allInvoiceQrcode">
  205. {{ elementStyle.allInvoiceQrcode.format }}
  206. </div>
  207. </div>
  208. <div :style="elementStyle.des">
  209. <span v-if="currentTicket.isNeedPlaytime">
  210. {{ elementStyle.des&&elementStyle.des.format||'游玩日期:' }}{{ currentTicket.payDateBegin | formatDate }} <br>
  211. </span>
  212. <span v-else>
  213. 有效日期: {{ currentTicket.payDateBegin | formatDate }} 至 {{ currentTicket.playDateEnd | formatDate }}<br>
  214. </span>
  215. </div>
  216. <div
  217. :style="elementStyle.orderRemark"
  218. v-if="currentTicket.remark">
  219. {{ elementStyle.orderRemark&&elementStyle.orderRemark.format||'订单备注:' }}{{ currentTicket.remark }}
  220. </div>
  221. <div
  222. :style="elementStyle.performName"
  223. v-if="currentTicket.performName">
  224. <!-- {{ elementStyle.performName&&elementStyle.performName.format||'节目:' }}{{ currentTicket.performName }} -->
  225. <!-- 草房子这个景点不显示后面的"草房子剧场" -->
  226. {{ elementStyle.performName&&elementStyle.performName.format||'节目:' }}{{ '草房子' === scenicName ? '' : currentTicket.performName }}
  227. </div>
  228. <div
  229. :style="elementStyle.batchConfigName"
  230. v-if="currentTicket.batchConfigName">
  231. {{ elementStyle.batchConfigName&&elementStyle.batchConfigName.format||'场次:' }}{{ currentTicket.batchConfigName }}
  232. </div>
  233. <div
  234. :style="elementStyle.serialNumber"
  235. v-if="currentTicket.serialNumber">
  236. {{ elementStyle.serialNumber&&elementStyle.serialNumber.format||'场次内序号:' }}{{ currentTicket.serialNumber }}
  237. </div>
  238. <div
  239. :style="elementStyle.queueSerialNumber"
  240. v-if="currentTicket.queue_info">
  241. <span>{{ elementStyle.queueSerialNumber&&elementStyle.queueSerialNumber.format||'排队序列号::' }}</span>
  242. <span> {{ currentTicket.queue_info.queue_serialNumber }}</span>
  243. </div>
  244. <div
  245. :style="elementStyle.schedule"
  246. v-if="currentTicket.scheduleConfigName">
  247. {{ elementStyle.schedule&&elementStyle.schedule.format||'班次:' }}{{ currentTicket.scheduleConfigName }}
  248. </div>
  249. <div
  250. :style="elementStyle.scheduleSerialNumber"
  251. v-if="currentTicket.scheduleSerialNumber">
  252. {{ elementStyle.scheduleSerialNumber&&elementStyle.scheduleSerialNumber.format||'班次内序号:' }}{{ currentTicket.scheduleSerialNumber }}
  253. </div>
  254. <div
  255. :style="elementStyle.areaName"
  256. v-if="currentTicket.seatAreaName">
  257. {{ elementStyle.areaName&&elementStyle.areaName.format||'区域:' }}{{ currentTicket.seatAreaName }}
  258. </div>
  259. <div
  260. :style="elementStyle.pricePlan"
  261. v-if="currentTicket.ticket_price_plan_name">
  262. {{ elementStyle.pricePlan&&elementStyle.pricePlan.format||'价格方案:' }}{{ currentTicket.ticket_price_plan_name }}
  263. </div>
  264. <div
  265. :style="elementStyle.seatNameNew"
  266. v-if="currentTicket.seatNameNew">
  267. {{ elementStyle.seatNameNew&&elementStyle.seatNameNew.format||'A区' }}{{ currentTicket.seatNameNew }}
  268. </div>
  269. <div
  270. :style="elementStyle.rowTag"
  271. v-if="currentTicket.seatRowTag">
  272. {{ currentTicket.seatRowTag }}{{ elementStyle.rowTag&&elementStyle.rowTag.format||'排' }}
  273. </div>
  274. <div
  275. :style="elementStyle.seatName"
  276. v-if="currentTicket.seatName">
  277. {{ currentTicket.seatName }}{{ elementStyle.seatName&&elementStyle.seatName.format||'座' }}
  278. </div>
  279. <div
  280. :style="elementStyle.batchConfigName"
  281. v-if="currentTicket.batchConfigName">
  282. {{ elementStyle.batchConfigName&&elementStyle.batchConfigName.format||'场次:' }}{{ currentTicket.batchConfigName }}
  283. </div>
  284. <div
  285. :style="elementStyle.schedule"
  286. v-if="currentTicket.scheduleConfigName">
  287. {{ elementStyle.schedule&&elementStyle.schedule.format||'班次:' }}{{ currentTicket.scheduleConfigName }}
  288. </div>
  289. <div
  290. :style="elementStyle.areaName"
  291. v-if="currentTicket.seatAreaName">
  292. {{ elementStyle.areaName&&elementStyle.areaName.format||'区域:' }}{{ currentTicket.seatAreaName }}
  293. </div>
  294. <div
  295. v-if="elementStyle.playDateBegin"
  296. :style="elementStyle.playDateBegin">
  297. {{ elementStyle.playDateBegin.format||'开始游玩日期:' }}{{ currentTicket.payDateBegin | formatDate }}
  298. </div>
  299. <div :style="elementStyle.checkNum">
  300. {{ elementStyle.checkNum&&elementStyle.checkNum.format||'检票人数:' }}{{ currentTicket.checkNum }} 人
  301. </div>
  302. <div :style="elementStyle.remark">
  303. {{ elementStyle.remark&&elementStyle.remark.format||'' }}<span
  304. style="white-space: pre-wrap;"
  305. v-html="elementStyle.canvas.remark.replace(/\n/g,'<br>')">
  306. </span>
  307. </div>
  308. <div :style="elementStyle.fixedText">
  309. {{ elementStyle.fixedText&&elementStyle.fixedText.format||'备注2:' }}
  310. </div>
  311. </div>
  312. </div>
  313. </div>
  314. </template>
  315. <script>
  316. import QRCode from 'qrcode'
  317. export default {
  318. computed: {
  319. elementStyle () {
  320. let style = {}
  321. if (this.customStyle) {
  322. this.customStyle.elements.forEach(i => {
  323. style[i.type] = i.style
  324. })
  325. style.canvas = this.customStyle.canvas
  326. if (!style.scenic) {
  327. style.scenic = { display: 'none' }
  328. }
  329. }
  330. console.log(this.customStyle || style)
  331. return this.customStyle ? style : null
  332. },
  333. scenicList () {
  334. return this.currentTicket?.ticketScenic || this.currentTicket?.ticketCheckScenicList || []
  335. },
  336. scenics () {
  337. if (this.currentTicket) {
  338. const scenics = this.scenicList.map(i => i.scenicName)
  339. return scenics.join(' - ')
  340. } else {
  341. return ''
  342. }
  343. },
  344. printTemplate () {
  345. return this.$store.state.app.printTemplate
  346. },
  347. showUnit () {
  348. return this.scenicName !== '狼山景区' && this.scenicName !== '军山景区'
  349. },
  350. scenicName () {
  351. return this.$localStore.get('scenicName') || this.$store.state.user.scenicName
  352. }
  353. },
  354. data () {
  355. return {
  356. projectName: process.env.VUE_APP_PROJECT,
  357. qrImg: '',
  358. fpImg: '',
  359. afpImg: '',
  360. customStyle: null,
  361. defaultTemplate: ''
  362. }
  363. },
  364. props: {
  365. preview: { // 仅预览时,不打印
  366. type: Boolean,
  367. default: false
  368. },
  369. currentTicket: {
  370. type: Object,
  371. default: () => ({})
  372. }
  373. },
  374. methods: {
  375. fmtTime (time) {
  376. if (!time) return ''
  377. const d = new Date(time)
  378. const pad = n => String(n).padStart(2, '0')
  379. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
  380. },
  381. startPrint () {
  382. const webview = document.querySelector('#printWebview')
  383. let printDoc = this.$refs.printMe.outerHTML
  384. // if (this.preview) {
  385. console.log(printDoc)
  386. // return
  387. // }
  388. // webview.openDevTools()
  389. webview.send('webview-print-render', printDoc)
  390. },
  391. sendPrinter (style) {
  392. if (style) {
  393. this.customStyle = JSON.parse(JSON.stringify(style))
  394. this.defaultTemplate = this.customStyle.theme
  395. this.$nextTick(() => {
  396. this.startPrint()
  397. })
  398. } else {
  399. this.startPrint()
  400. }
  401. }
  402. },
  403. watch: {
  404. printTemplate: {
  405. handler (val) {
  406. if (val) {
  407. this.defaultTemplate = val.canvas && val.canvas.direction
  408. // console.log(this.defaultTemplate)
  409. this.customStyle = val
  410. }
  411. },
  412. deep: true,
  413. immediate: true
  414. },
  415. currentTicket: {
  416. handler (val) {
  417. if (!val) return
  418. let target
  419. const printTemplateId = val.printTemplateId || val.print_model_id
  420. console.log('current ticket printTemplateId', printTemplateId)
  421. if (printTemplateId) {
  422. target = this.$store.state.app.printTemplateList.find(i => i.id === printTemplateId)
  423. } else {
  424. let id = this.$localStore.get('defaultTemplate')
  425. target = this.$store.state.app.printTemplateList.find(i => i.id === id)
  426. }
  427. console.log('selected print template', JSON.parse(target.content))
  428. if (!target) {
  429. target = this.$store.state.app.printTemplateList.find(i => i.isDefault) || this.$store.state.app.printTemplateList[0]
  430. }
  431. this.$store.commit('SET_PRINT_TEMPLATE', target ? JSON.parse(target.content) : this.printTemplate)
  432. this.$nextTick(async () => {
  433. var opts = {
  434. errorCorrectionLevel: 'H',
  435. type: 'image/png',
  436. quality: 1,
  437. color: {
  438. dark: '#000000ff', // Blue dots
  439. light: '#0000' // Transparent background
  440. }
  441. }
  442. if (this.currentTicket.ticketNo) {
  443. this.qrImg = await QRCode.toDataURL(this.currentTicket.ticketNo, opts)
  444. }
  445. if (this.currentTicket.invoiceQrcode) {
  446. this.fpImg = await QRCode.toDataURL(this.currentTicket.invoiceQrcode, opts)
  447. }
  448. if (this.currentTicket.allInvoiceQrcode) {
  449. this.afpImg = await QRCode.toDataURL(this.currentTicket.allInvoiceQrcode, opts)
  450. }
  451. console.warn('print ticket', this.currentTicket)
  452. if (this.preview) return
  453. setTimeout(() => {
  454. this.sendPrinter()
  455. }, 100)
  456. })
  457. },
  458. immediate: true
  459. }
  460. }
  461. }
  462. </script>
  463. <style scoped>
  464. .img{
  465. height: 100%; background: no-repeat center center /contain;
  466. }
  467. </style>