mumu-getQrcode.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <template>
  2. <view class="canvasBox">
  3. <template v-if="isUse">
  4. <view class="box">
  5. <view class="line"></view>
  6. <view class="angle"></view>
  7. </view>
  8. <view class="box2" v-if="isUseTorch">
  9. <view class="track" @click="openTrack">
  10. <svg
  11. t="1653920715959"
  12. class="icon"
  13. viewBox="0 0 1024 1024"
  14. version="1.1"
  15. xmlns="http://www.w3.org/2000/svg"
  16. p-id="1351"
  17. width="32"
  18. height="32"
  19. >
  20. <path
  21. d="M651.353043 550.479503H378.752795L240.862609 364.315031c-3.688944-4.897391-5.660621-10.876025-5.660621-17.045466v-60.040745c0-15.773416 12.847702-28.621118 28.621118-28.621118h502.459627c15.773416 0 28.621118 12.847702 28.621118 28.621118v59.977143c0 6.105839-1.971677 12.084472-5.660621 17.045466l-137.890187 186.228074zM378.752795 598.308571v398.024348c0 15.328199 12.402484 27.667081 27.667081 27.667081h217.266087c15.328199 0 27.667081-12.402484 27.66708-27.667081V598.308571H378.752795z m136.300124 176.942112c-14.564969 0-26.331429-11.76646-26.331428-26.331428v-81.283975c0-14.564969 11.76646-26.331429 26.331428-26.331429 14.564969 0 26.331429 11.76646 26.331429 26.331429v81.283975c0 14.564969-11.76646 26.331429-26.331429 26.331428zM512 222.608696c-17.554286 0-31.801242-14.246957-31.801242-31.801243V31.801242c0-17.554286 14.246957-31.801242 31.801242-31.801242s31.801242 14.246957 31.801242 31.801242v159.006211c0 17.554286-14.246957 31.801242-31.801242 31.801243zM280.932174 205.881242c-9.47677 0-18.889938-4.197764-25.122981-12.275279L158.242981 67.991056a31.864845 31.864845 0 0 1 5.597019-44.648944 31.864845 31.864845 0 0 1 44.648944 5.597018l97.502609 125.551305a31.864845 31.864845 0 0 1-5.597019 44.648944c-5.787826 4.579379-12.656894 6.741863-19.46236 6.741863zM723.987081 205.881242c-6.805466 0-13.674534-2.162484-19.462361-6.678261a31.794882 31.794882 0 0 1-5.597018-44.648944l97.566211-125.551304a31.794882 31.794882 0 0 1 44.648944-5.597019 31.794882 31.794882 0 0 1 5.597019 44.648944l-97.566211 125.551305c-6.360248 8.077516-15.709814 12.27528-25.186584 12.275279z"
  22. fill="#ffffff"
  23. p-id="1352"
  24. ></path>
  25. </svg>
  26. {{ trackStatus ? '关闭闪光灯' : '打开闪光灯' }}
  27. </view>
  28. </view>
  29. <view class="mask1 mask" :style="'height:' + maskHeight + 'px;'"></view>
  30. <view
  31. class="mask2 mask"
  32. :style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'"
  33. ></view>
  34. <view class="mask3 mask" :style="'height:' + maskHeight + 'px;'"></view>
  35. <view
  36. class="mask4 mask"
  37. :style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'"
  38. ></view>
  39. </template>
  40. <template v-else>
  41. <slot name="error">
  42. <view class="error">
  43. <view class="on1">相机权限被拒绝,请尝试如下操作:</view>
  44. <view>· 刷新页面后重试;</view>
  45. <view>· 在系统中检测当前App或浏览器的相机权限是否被禁用;</view>
  46. <view>· 如果依然不能体验,建议在微信中打开链接;</view>
  47. </view>
  48. </slot>
  49. </template>
  50. </view>
  51. </template>
  52. <script>
  53. import jsQR from './jsQR.js'
  54. export default {
  55. props: {
  56. continue: {
  57. type: Boolean,
  58. default: false // false 监听一次 true 持续监听
  59. },
  60. exact: {
  61. type: String,
  62. default: 'environment' // environment 后摄像头 user 前摄像头
  63. },
  64. size: {
  65. type: String,
  66. default: 'whole' // whole 全屏 balf 半屏
  67. },
  68. definition: {
  69. type: Boolean,
  70. default: false // fasle 正常 true 高清
  71. }
  72. },
  73. data() {
  74. return {
  75. windowWidth: 0,
  76. windowHeight: 0,
  77. video: null,
  78. canvas2d: null,
  79. canvas2d2: null,
  80. canvasWidth: 200,
  81. canvasHeight: 200,
  82. maskWidth: 0,
  83. maskHeight: 0,
  84. inter: 0,
  85. track: null,
  86. isUseTorch: false,
  87. trackStatus: false,
  88. isParse: false,
  89. isUse: true
  90. }
  91. },
  92. mounted() {
  93. if (origin.indexOf('https') === -1) throw '请在 https 环境中使用摄像头组件。'
  94. this.windowWidth = document.documentElement.clientWidth || document.body.clientWidth
  95. this.windowHeight = document.documentElement.clientHeight || document.body.clientHeight
  96. this.windowHeight = this.size === 'whole' ? this.windowHeight : this.windowHeight / 2
  97. this.isParse = true
  98. this.$nextTick(() => {
  99. this.createMsk()
  100. this.openScan()
  101. })
  102. },
  103. destroyed() {
  104. this.closeCamera()
  105. },
  106. methods: {
  107. openScan() {
  108. const width = this.transtion(this.windowHeight)
  109. const height = this.transtion(this.windowWidth)
  110. const videoParam = {
  111. audio: false,
  112. video: {
  113. facingMode: { exact: this.exact },
  114. width,
  115. height
  116. }
  117. }
  118. navigator.mediaDevices
  119. .getUserMedia(videoParam)
  120. .then(stream => {
  121. this.video = document.createElement('video')
  122. this.video.width = this.windowWidth
  123. this.video.height = this.windowHeight
  124. const canvas = document.createElement('canvas')
  125. canvas.id = 'canvas'
  126. canvas.width = this.transtion(this.canvasWidth)
  127. canvas.height = this.transtion(this.canvasHeight)
  128. canvas.style = 'display:none;'
  129. //canvas.style = 'position: fixed;top: 0;z-index: 999;left:0'
  130. this.canvas2d = canvas.getContext('2d')
  131. // 设置当前宽高 满屏
  132. const canvasBox = document.querySelector('.canvasBox')
  133. canvasBox.append(this.video)
  134. canvasBox.append(canvas)
  135. canvasBox.style = `width:${this.windowWidth}px;height:${this.windowHeight}px;`
  136. // 创建第二个canvas
  137. const canvas2 = document.createElement('canvas')
  138. canvas2.id = 'canvas2'
  139. canvas2.width = this.canvasWidth
  140. canvas2.height = this.canvasHeight
  141. canvas2.style = 'position: absolute;top: 50%;left: 50%;z-index: 20;transform: translate(-50%, -50%);'
  142. this.canvas2d2 = canvas2.getContext('2d')
  143. canvasBox.append(canvas2)
  144. this.video.srcObject = stream
  145. this.video.setAttribute('playsinline', true)
  146. this.video.play()
  147. this.tick()
  148. this.track = stream.getVideoTracks()[0]
  149. setTimeout(() => {
  150. this.isUseTorch = this.track.getCapabilities().torch || null
  151. }, 500)
  152. })
  153. .catch(err => {
  154. this.isUse = false
  155. this.$emit('error', err)
  156. })
  157. },
  158. closeCamera() {
  159. this.isParse = false
  160. if (this.video && this.video.srcObject) {
  161. this.video.srcObject.getTracks().forEach(track => {
  162. track.stop()
  163. })
  164. }
  165. },
  166. tick() {
  167. if (!this.isParse) return
  168. if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
  169. this.canvas2d.drawImage(
  170. this.video,
  171. this.transtion(this.maskWidth),
  172. this.transtion(this.maskHeight),
  173. this.transtion(200),
  174. this.transtion(200),
  175. 0,
  176. 0,
  177. this.transtion(this.canvasWidth),
  178. this.transtion(this.canvasHeight)
  179. )
  180. const imageData = this.canvas2d.getImageData(
  181. 0,
  182. 0,
  183. this.transtion(this.canvasWidth),
  184. this.transtion(this.canvasHeight)
  185. )
  186. const code = jsQR(imageData.data, imageData.width, imageData.height, {
  187. inversionAttempts: 'dontInvert'
  188. })
  189. this.canvas2d2.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  190. if (code) {
  191. this.drawLine(code.location.topLeftCorner, code.location.topRightCorner)
  192. this.drawLine(code.location.topRightCorner, code.location.bottomRightCorner)
  193. this.drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner)
  194. this.drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner)
  195. if (code.data) {
  196. this.getData(code.data)
  197. }
  198. }
  199. }
  200. requestAnimationFrame(this.tick)
  201. },
  202. drawLine(begin, end, color = '#FF3B58') {
  203. this.canvas2d2.beginPath()
  204. this.canvas2d2.moveTo(this.nutranstion(begin.x), this.nutranstion(begin.y))
  205. this.canvas2d2.lineTo(this.nutranstion(end.x), this.nutranstion(end.y))
  206. this.canvas2d2.lineWidth = 4
  207. this.canvas2d2.strokeStyle = color
  208. this.canvas2d2.stroke()
  209. },
  210. getData(data) {
  211. this.$emit('success', data)
  212. if (!this.continue) {
  213. this.closeCamera()
  214. }
  215. },
  216. openTrack() {
  217. this.trackStatus = !this.trackStatus
  218. this.track.applyConstraints({
  219. advanced: [{ torch: this.trackStatus }]
  220. })
  221. },
  222. createMsk() {
  223. this.maskWidth = this.windowWidth / 2 - this.canvasWidth / 2
  224. this.maskHeight = this.windowHeight / 2 - this.canvasHeight / 2
  225. },
  226. transtion(number) {
  227. return this.definition ? number * 2.8 : number * 1.8
  228. },
  229. nutranstion(number) {
  230. return this.definition ? number / 2.8 : number / 1.8
  231. }
  232. }
  233. }
  234. </script>
  235. <style scoped>
  236. page {
  237. background-color: #333333;
  238. }
  239. .canvasBox {
  240. width: 100vw;
  241. height: 100vh;
  242. position: relative;
  243. background-image: linear-gradient(
  244. 0deg,
  245. transparent 24%,
  246. rgba(32, 255, 77, 0.1) 25%,
  247. rgba(32, 255, 77, 0.1) 26%,
  248. transparent 27%,
  249. transparent 74%,
  250. rgba(32, 255, 77, 0.1) 75%,
  251. rgba(32, 255, 77, 0.1) 76%,
  252. transparent 77%,
  253. transparent
  254. ),
  255. linear-gradient(
  256. 90deg,
  257. transparent 24%,
  258. rgba(32, 255, 77, 0.1) 25%,
  259. rgba(32, 255, 77, 0.1) 26%,
  260. transparent 27%,
  261. transparent 74%,
  262. rgba(32, 255, 77, 0.1) 75%,
  263. rgba(32, 255, 77, 0.1) 76%,
  264. transparent 77%,
  265. transparent
  266. );
  267. background-size: 3rem 3rem;
  268. background-position: -1rem -1rem;
  269. z-index: 10;
  270. background-color: #1110;
  271. }
  272. .box {
  273. width: 200px;
  274. height: 200px;
  275. position: absolute;
  276. left: 50%;
  277. top: 50%;
  278. transform: translate(-50%, -50%);
  279. overflow: hidden;
  280. border: 0.1rem solid rgba(0, 255, 51, 0.2);
  281. z-index: 11;
  282. }
  283. .line {
  284. height: calc(100% - 2px);
  285. width: 100%;
  286. background: linear-gradient(180deg, rgba(0, 255, 51, 0) 43%, #00ff33 211%);
  287. border-bottom: 3px solid #00ff33;
  288. transform: translateY(-100%);
  289. animation: radar-beam 2s infinite alternate;
  290. animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99);
  291. animation-delay: 1.4s;
  292. }
  293. .box:after,
  294. .box:before,
  295. .angle:after,
  296. .angle:before {
  297. content: '';
  298. display: block;
  299. position: absolute;
  300. width: 3vw;
  301. height: 3vw;
  302. z-index: 12;
  303. border: 0.2rem solid transparent;
  304. }
  305. .box:after,
  306. .box:before {
  307. top: 0;
  308. border-top-color: #00ff33;
  309. }
  310. .angle:after,
  311. .angle:before {
  312. bottom: 0;
  313. border-bottom-color: #00ff33;
  314. }
  315. .box:before,
  316. .angle:before {
  317. left: 0;
  318. border-left-color: #00ff33;
  319. }
  320. .box:after,
  321. .angle:after {
  322. right: 0;
  323. border-right-color: #00ff33;
  324. }
  325. @keyframes radar-beam {
  326. 0% {
  327. transform: translateY(-100%);
  328. }
  329. 100% {
  330. transform: translateY(0);
  331. }
  332. }
  333. .msg {
  334. text-align: center;
  335. padding: 20rpx 0;
  336. }
  337. .box2 {
  338. width: 300px;
  339. height: 200px;
  340. position: absolute;
  341. left: 50%;
  342. top: 50%;
  343. transform: translate(-50%, -50%);
  344. z-index: 20;
  345. }
  346. .track {
  347. position: absolute;
  348. bottom: -100px;
  349. left: 50%;
  350. transform: translateX(-50%);
  351. z-index: 20;
  352. color: #fff;
  353. display: flex;
  354. flex-direction: column;
  355. align-items: center;
  356. }
  357. .mask {
  358. position: absolute;
  359. z-index: 10;
  360. background-color: rgba(0, 0, 0, 0.55);
  361. }
  362. .mask1 {
  363. top: 0;
  364. left: 0;
  365. right: 0;
  366. }
  367. .mask2 {
  368. right: 0;
  369. }
  370. .mask3 {
  371. right: 0;
  372. left: 0;
  373. bottom: 0;
  374. }
  375. .mask4 {
  376. left: 0;
  377. }
  378. .error {
  379. color: #fff;
  380. padding: 40rpx;
  381. font-size: 24rpx;
  382. background-color: #333333;
  383. position: fixed;
  384. top: 50%;
  385. left: 50%;
  386. transform: translate(-50%, -50%);
  387. width: 550rpx;
  388. border-radius: 20rpx;
  389. }
  390. .error .on1 {
  391. font-size: 30rpx;
  392. }
  393. </style>