u-barcode.vue 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. <template>
  2. <view class="u-barcode" v-if="calcSizeDone">
  3. <canvas
  4. v-if="showCanvas && !error"
  5. :id="canvasId"
  6. :canvas-id="canvasId"
  7. :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
  8. ></canvas>
  9. <image
  10. v-else-if="!showCanvas && !error"
  11. :src="barcodeImage"
  12. :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
  13. mode="aspectFit"
  14. />
  15. <view
  16. v-else
  17. class="error-container"
  18. :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
  19. >
  20. <text class="error-text">{{ error }}</text>
  21. </view>
  22. </view>
  23. </template>
  24. <script>
  25. import { nextTick } from 'vue'
  26. export default {
  27. name: 'u-barcode',
  28. props: {
  29. // 条码值
  30. value: {
  31. type: [String, Number],
  32. required: true
  33. },
  34. // 条码格式
  35. format: {
  36. type: String,
  37. default: 'auto',
  38. validator: function (value) {
  39. return [
  40. 'auto',
  41. 'CODE128', 'CODE128A', 'CODE128B', 'CODE128C',
  42. 'EAN13', 'EAN8', 'EAN5', 'EAN2',
  43. 'UPC', 'UPCA', 'UPCE',
  44. 'CODE39',
  45. 'ITF', 'ITF14',
  46. 'MSI', 'MSI10', 'MSI11', 'MSI1010', 'MSI1110',
  47. 'pharmacode',
  48. 'codabar'
  49. ].includes(value)
  50. }
  51. },
  52. // 宽度
  53. width: {
  54. type: Number,
  55. default: 200
  56. },
  57. // 高度
  58. height: {
  59. type: Number,
  60. default: 80
  61. },
  62. // 是否显示文本
  63. displayValue: {
  64. type: Boolean,
  65. default: true
  66. },
  67. // 文本内容
  68. text: {
  69. type: String,
  70. default: undefined
  71. },
  72. // 字体选项
  73. fontOptions: {
  74. type: String,
  75. default: ''
  76. },
  77. // 字体
  78. font: {
  79. type: String,
  80. default: 'monospace'
  81. },
  82. // 文本对齐方式
  83. textAlign: {
  84. type: String,
  85. default: 'center'
  86. },
  87. // 文本位置
  88. textPosition: {
  89. type: String,
  90. default: 'bottom'
  91. },
  92. // 文本边距
  93. textMargin: {
  94. type: Number,
  95. default: 2
  96. },
  97. // 字体大小
  98. fontSize: {
  99. type: Number,
  100. default: 14
  101. },
  102. // 背景色
  103. background: {
  104. type: String,
  105. default: '#ffffff'
  106. },
  107. // 条码颜色
  108. lineColor: {
  109. type: String,
  110. default: '#000000'
  111. },
  112. // 边距
  113. margin: {
  114. type: Number,
  115. default: 10
  116. },
  117. // 上边距
  118. marginTop: {
  119. type: Number,
  120. default: undefined
  121. },
  122. // 下边距
  123. marginBottom: {
  124. type: Number,
  125. default: undefined
  126. },
  127. // 左边距
  128. marginLeft: {
  129. type: Number,
  130. default: undefined
  131. },
  132. // 右边距
  133. marginRight: {
  134. type: Number,
  135. default: undefined
  136. },
  137. // 使用canvas还是生成图片
  138. useCanvas: {
  139. type: Boolean,
  140. default: true
  141. }
  142. },
  143. data() {
  144. return {
  145. canvasId: 'barcode-' + Math.random().toString(36).substr(2, 9),
  146. barcodeImage: '',
  147. showCanvas: false,
  148. canvasWidth: 0,
  149. canvasHeight: 0,
  150. calcSizeDone: false,
  151. error: ''
  152. }
  153. },
  154. watch: {
  155. value() {
  156. this.generateBarcode()
  157. },
  158. format() {
  159. this.generateBarcode()
  160. },
  161. width() {
  162. this.generateBarcode()
  163. },
  164. height() {
  165. this.generateBarcode()
  166. },
  167. displayValue() {
  168. this.generateBarcode()
  169. },
  170. text() {
  171. this.generateBarcode()
  172. },
  173. font() {
  174. this.generateBarcode()
  175. },
  176. textAlign() {
  177. this.generateBarcode()
  178. },
  179. textPosition() {
  180. this.generateBarcode()
  181. },
  182. textMargin() {
  183. this.generateBarcode()
  184. },
  185. fontSize() {
  186. this.generateBarcode()
  187. },
  188. background() {
  189. this.generateBarcode()
  190. },
  191. lineColor() {
  192. this.generateBarcode()
  193. },
  194. margin() {
  195. this.generateBarcode()
  196. }
  197. },
  198. mounted() {
  199. /**
  200. * @author jry <ijry@qq.com>
  201. */
  202. this.$nextTick(() => {
  203. this.generateBarcode()
  204. })
  205. },
  206. methods: {
  207. /**
  208. * 生成条形码
  209. * @author jry <ijry@qq.com>
  210. * @param {String|Number} value - 条码值
  211. * @param {Object} options - 条码配置选项
  212. */
  213. generateBarcode() {
  214. // 统一处理默认值
  215. const margin = this.margin
  216. const options = {
  217. format: this.format || 'auto',
  218. width: this.width,
  219. height: this.height,
  220. displayValue: this.displayValue,
  221. text: this.text,
  222. fontOptions: this.fontOptions || '',
  223. font: this.font || 'monospace',
  224. textAlign: this.textAlign || 'center',
  225. textPosition: this.textPosition || 'bottom',
  226. textMargin: this.textMargin !== undefined ? this.textMargin : 2,
  227. fontSize: this.fontSize || 20,
  228. background: this.background || '#ffffff',
  229. lineColor: this.lineColor || '#000000',
  230. margin: margin,
  231. marginTop: this.marginTop !== undefined ? this.marginTop : margin,
  232. marginBottom: this.marginBottom !== undefined ? this.marginBottom : margin,
  233. marginLeft: this.marginLeft !== undefined ? this.marginLeft : margin,
  234. marginRight: this.marginRight !== undefined ? this.marginRight : margin
  235. }
  236. // 清理未定义的选项
  237. Object.keys(options).forEach(key => {
  238. if (options[key] === undefined) {
  239. delete options[key]
  240. }
  241. })
  242. if (this.useCanvas) {
  243. // 使用canvas渲染
  244. this.showCanvas = true
  245. this.$nextTick(() => {
  246. this.renderToCanvas(options)
  247. })
  248. } else {
  249. // 生成图片
  250. this.showCanvas = false
  251. this.renderToImage(options)
  252. }
  253. },
  254. /**
  255. * 渲染条形码到canvas
  256. * @author jry <ijry@qq.com>
  257. * @param {Object} options - 条码配置选项
  258. */
  259. async renderToCanvas(options) {
  260. try {
  261. // 计算canvas尺寸
  262. this.calculateCanvasSize(options)
  263. await nextTick()
  264. // 获取canvas上下文
  265. const ctx = uni.createCanvasContext(this.canvasId, this)
  266. // 清空画布
  267. ctx.setFillStyle(options.background)
  268. ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
  269. // 生成条形码数据
  270. const barcodeData = this.encodeBarcode(this.value, options.format)
  271. if (barcodeData) {
  272. // 绘制条形码
  273. this.drawBarcode(ctx, barcodeData, options)
  274. }
  275. // 绘制到canvas
  276. ctx.draw(false, () => {
  277. // 绘制完成回调
  278. this.$emit('rendered', { type: 'canvas', id: this.canvasId })
  279. })
  280. } catch (error) {
  281. console.error('生成条码失败:', error)
  282. this.error = error.message || '生成条码失败'
  283. this.$emit('error', error)
  284. }
  285. },
  286. /**
  287. * 渲染条形码为图片
  288. * @author jry <ijry@qq.com>
  289. * @param {Object} options - 条码配置选项
  290. */
  291. renderToImage(options) {
  292. try {
  293. // 计算canvas尺寸
  294. this.calculateCanvasSize(options)
  295. // 创建临时canvas用于生成图片
  296. const tempCanvasId = 'temp-' + this.canvasId
  297. const ctx = uni.createCanvasContext(tempCanvasId, this)
  298. // 清空画布
  299. ctx.setFillStyle(options.background)
  300. ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
  301. // 生成条形码数据
  302. const barcodeData = this.encodeBarcode(this.value, options.format)
  303. if (barcodeData) {
  304. // 绘制条形码
  305. this.drawBarcode(ctx, barcodeData, options)
  306. }
  307. // 绘制到临时canvas并生成图片
  308. ctx.draw(false, () => {
  309. // 延迟一小段时间确保canvas绘制完成
  310. setTimeout(() => {
  311. // 将canvas转换为图片
  312. uni.canvasToTempFilePath({
  313. canvasId: tempCanvasId,
  314. success: (res) => {
  315. this.barcodeImage = res.tempFilePath
  316. this.$emit('rendered', { type: 'image', value: this.value, path: res.tempFilePath })
  317. },
  318. fail: (error) => {
  319. console.error('生成条码图片失败:', error)
  320. this.$emit('error', error)
  321. }
  322. }, this)
  323. }, 100)
  324. })
  325. } catch (error) {
  326. console.error('生成条码图片失败:', error)
  327. this.$emit('error', error)
  328. }
  329. },
  330. /**
  331. * 计算canvas尺寸
  332. * @author jry <ijry@qq.com>
  333. * @param {Object} options - 条码配置选项
  334. */
  335. calculateCanvasSize(options) {
  336. // 基础宽度计算
  337. let width = options.width
  338. let height = options.height
  339. // 考虑边距
  340. const marginLeft = options.marginLeft
  341. const marginRight = options.marginRight
  342. const marginTop = options.marginTop
  343. const marginBottom = options.marginBottom
  344. // 考虑文本高度
  345. let textHeight = 0
  346. if (options.displayValue !== false) {
  347. textHeight = options.fontSize + options.textMargin
  348. }
  349. // 根据文本位置调整高度
  350. if (options.textPosition === 'top' || options.textPosition === 'bottom') {
  351. height += textHeight
  352. }
  353. // 添加边距
  354. width += marginLeft + marginRight
  355. height += marginTop + marginBottom
  356. this.canvasWidth = Math.max(width, 100)
  357. this.canvasHeight = Math.max(height, 60 + textHeight)
  358. this.calcSizeDone = true
  359. },
  360. /**
  361. * 编码条形码数据
  362. * @author jry <ijry@qq.com>
  363. * @param {String|Number} value - 条码值
  364. * @param {String} format - 条码格式
  365. * @returns {String|null} 条形码编码数据
  366. */
  367. encodeBarcode(value, format) {
  368. try {
  369. switch (format) {
  370. case 'CODE128':
  371. case 'auto':
  372. return this.encodeCode128(value)
  373. case 'CODE39':
  374. return this.encodeCode39(value)
  375. case 'EAN13':
  376. return this.encodeEAN13(value)
  377. case 'EAN8':
  378. return this.encodeEAN8(value)
  379. case 'EAN5':
  380. case 'EAN2':
  381. return this.encodeEAN52(value, format)
  382. case 'UPC':
  383. case 'UPCA':
  384. return this.encodeUPCA(value)
  385. case 'UPCE':
  386. return this.encodeUPCE(value)
  387. default:
  388. // 默认使用CODE128
  389. return this.encodeCode128(value)
  390. }
  391. } catch (error) {
  392. console.error('条码编码失败:', error)
  393. throw error
  394. }
  395. },
  396. /**
  397. * CODE128编码实现
  398. * @author jry <ijry@qq.com>
  399. * @param {String} data - 要编码的数据
  400. * @returns {String|null} 编码后的条形码数据
  401. */
  402. encodeCode128(data) {
  403. const CODE128_START_CODE_B = 104
  404. const CODE128_STOP = 106
  405. // CODE128 Code B 字符集
  406. const CODE128_CODE_B_CHARS = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
  407. // 条形码模式 (B 模式)
  408. const codes = []
  409. let checksum = CODE128_START_CODE_B
  410. // 添加起始字符
  411. codes.push(CODE128_START_CODE_B)
  412. // 编码每个字符
  413. for (let i = 0; i < data.length; i++) {
  414. const char = data[i]
  415. const code = CODE128_CODE_B_CHARS.indexOf(char)
  416. if (code === -1) {
  417. throw new Error('Invalid character in CODE128: ' + char)
  418. }
  419. codes.push(code)
  420. checksum += code * (i + 1)
  421. }
  422. // 添加校验字符
  423. codes.push(checksum % 103)
  424. // 添加结束字符
  425. codes.push(CODE128_STOP)
  426. // 转换为条形码模式 (1 = 黑条, 0 = 白条)
  427. let barcode = ''
  428. for (let i = 0; i < codes.length; i++) {
  429. const code = codes[i]
  430. barcode += this.getCode128Pattern(code)
  431. }
  432. return barcode
  433. },
  434. /**
  435. * 添加右侧安静区(至少2个模块宽度的空白)
  436. * @author jry <ijry@qq.com>
  437. * @param {String} data - 要编码的数据
  438. * @returns {String|null} 编码后的条形码数据
  439. */
  440. encodeCode128(data) {
  441. const CODE128_START_CODE_B = 104
  442. const CODE128_STOP = 106
  443. // CODE128 Code B 字符集
  444. const CODE128_CODE_B_CHARS = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
  445. // 条形码模式 (B 模式)
  446. const codes = []
  447. let checksum = CODE128_START_CODE_B
  448. // 添加起始字符
  449. codes.push(CODE128_START_CODE_B)
  450. // 编码每个字符
  451. for (let i = 0; i < data.length; i++) {
  452. const char = data[i]
  453. const code = CODE128_CODE_B_CHARS.indexOf(char)
  454. if (code === -1) {
  455. throw new Error('Invalid character in CODE128: ' + char)
  456. }
  457. codes.push(code)
  458. checksum += code * (i + 1)
  459. }
  460. // 添加校验字符
  461. codes.push(checksum % 103)
  462. // 添加结束字符
  463. codes.push(CODE128_STOP)
  464. // 转换为条形码模式 (1 = 黑条, 0 = 白条)
  465. let barcode = ''
  466. for (let i = 0; i < codes.length; i++) {
  467. const code = codes[i]
  468. barcode += this.getCode128Pattern(code)
  469. }
  470. // 添加右侧安静区(至少2个模块宽度的空白)
  471. barcode += '00000'
  472. return barcode
  473. },
  474. /**
  475. * 获取CODE128编码模式
  476. * @author jry <ijry@qq.com>
  477. * @param {Number} code - 字符编码
  478. * @returns {String} 条形码二进制模式
  479. */
  480. getCode128Pattern(code) {
  481. // CODE128编码表
  482. const patterns = [
  483. "11011001100", "11001101100", "11001100110", "10010011000",
  484. "10010001100", "10001001100", "10011001000", "10011000100",
  485. "10001100100", "11001001000", "11001000100", "11000100100",
  486. "10110011100", "10011011100", "10011001110", "10111001100",
  487. "10011101100", "10011100110", "11001110010", "11001011100",
  488. "11001001110", "11011100100", "11001110100", "11101101110",
  489. "11101001100", "11100101100", "11100100110", "11101100100",
  490. "11100110100", "11100110010", "11011011000", "11011000110",
  491. "11000110110", "10100011000", "10001011000", "10001000110",
  492. "10110001000", "10001101000", "10001100010", "11010001000",
  493. "11000101000", "11000100010", "10110111000", "10110001110",
  494. "10001101110", "10111011000", "10111000110", "10001110110",
  495. "11101110110", "11010001110", "11000101110", "11011101000",
  496. "11011100010", "11011101110", "11101011000", "11101000110",
  497. "11100010110", "11101101000", "11101100010", "11100011010",
  498. "11101111010", "11001000010", "11110001010", "10100110000",
  499. "10100001100", "10010110000", "10010000110", "10000101100",
  500. "10000100110", "10110010000", "10110000100", "10011010000",
  501. "10011000010", "10000110100", "10000110010", "11000010010",
  502. "11001010000", "11110111010", "11000010100", "10001111010",
  503. "10100111100", "10010111100", "10010011110", "10111100100",
  504. "10011110100", "10011110010", "11110100100", "11110010100",
  505. "11110010010", "11011011110", "11011110110", "11110110110",
  506. "10101111000", "10100011110", "10001011110", "10111101000",
  507. "10111100010", "11110101000", "11110100010", "10111011110",
  508. "10111101110", "11101011110", "11110101110", "11010000100",
  509. "11010010000", "11010011100", "11000111010"
  510. ];
  511. return patterns[code] || "";
  512. },
  513. /**
  514. * CODE39编码实现
  515. * @author jry <ijry@qq.com>
  516. * @param {String} data - 要编码的数据
  517. * @returns {String|null} 编码后的条形码数据
  518. */
  519. encodeCode39(data) {
  520. const codes = {
  521. '0': '101000111011101',
  522. '1': '111010001010111',
  523. '2': '101110001010111',
  524. '3': '111011100010101',
  525. '4': '101000111010111',
  526. '5': '111010001110101',
  527. '6': '101110001110101',
  528. '7': '101000101110111',
  529. '8': '111010001011101',
  530. '9': '101110001011101',
  531. 'A': '111010100010111',
  532. 'B': '101110100010111',
  533. 'C': '111011101000101',
  534. 'D': '101011100010111',
  535. 'E': '111010111000101',
  536. 'F': '101110111000101',
  537. 'G': '101010001110111',
  538. 'H': '111010100011101',
  539. 'I': '101110100011101',
  540. 'J': '101011100011101',
  541. 'K': '111010101000111',
  542. 'L': '101110101000111',
  543. 'M': '111011101010001',
  544. 'N': '101011101000111',
  545. 'O': '111010111010001',
  546. 'P': '101110111010001',
  547. 'Q': '101010111000111',
  548. 'R': '111010101110001',
  549. 'S': '101110101110001',
  550. 'T': '101011101110001',
  551. 'U': '111000101010111',
  552. 'V': '100011101010111',
  553. 'W': '111000111010101',
  554. 'X': '100010111010111',
  555. 'Y': '111000101110101',
  556. 'Z': '100011101110101',
  557. '-': '100010101110111',
  558. '.': '111000101011101',
  559. ' ': '100011101011101',
  560. '*': '100010111011101', // 起始和终止字符
  561. '$': '100010001000101',
  562. '/': '100010001010001',
  563. '+': '100010100010001',
  564. '%': '101000100010001'
  565. }
  566. // 转为大写
  567. data = data.toUpperCase()
  568. // 添加起始和终止字符
  569. let barcode = codes['*']
  570. // 添加数据字符
  571. for (let i = 0; i < data.length; i++) {
  572. const char = data[i]
  573. if (codes[char]) {
  574. barcode += '0' // 字符间隔
  575. barcode += codes[char]
  576. } else {
  577. throw new Error('Invalid character in CODE39: ' + char)
  578. }
  579. }
  580. // 添加终止字符
  581. barcode += '0' // 字符间隔
  582. barcode += codes['*']
  583. return barcode
  584. },
  585. /**
  586. * EAN13编码实现
  587. * @author jry <ijry@qq.com>
  588. * @param {String} data - 13位数字字符串
  589. * @returns {String|null} 编码后的条形码数据
  590. */
  591. encodeEAN13(data) {
  592. // 确保数据是13位数字
  593. if (!/^\d{13}$/.test(data)) {
  594. throw new Error('EAN13 must be 13 digits')
  595. }
  596. // 验证校验位
  597. let sum = 0
  598. for (let i = 0; i < 12; i++) {
  599. const digit = parseInt(data[i])
  600. sum += (i % 2 === 0) ? digit : digit * 3
  601. }
  602. const checkDigit = (10 - (sum % 10)) % 10
  603. if (parseInt(data[12]) !== checkDigit) {
  604. throw new Error('Invalid EAN13 check digit')
  605. }
  606. // 左侧数据
  607. const leftData = data.substring(1, 7)
  608. const rightData = data.substring(7, 13)
  609. // 起始符
  610. let barcode = '101'
  611. // 左侧数据编码 (根据第一位数字决定编码方式)
  612. const firstDigit = parseInt(data[0])
  613. const leftPatterns = [
  614. ['LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG',
  615. 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL']
  616. ]
  617. const pattern = leftPatterns[0][firstDigit]
  618. // 左侧奇偶编码模式
  619. const leftOdd = [
  620. '0001101', '0011001', '0010011', '0111101', '0100011',
  621. '0110001', '0101111', '0111011', '0110111', '0001011'
  622. ]
  623. const leftEven = [
  624. '0100111', '0110011', '0011011', '0100001', '0011101',
  625. '0111001', '0000101', '0010001', '0001001', '0010111'
  626. ]
  627. // 编码左侧数据
  628. for (let i = 0; i < leftData.length; i++) {
  629. const digit = parseInt(leftData[i])
  630. if (pattern[i] === 'L') {
  631. barcode += leftOdd[digit]
  632. } else {
  633. barcode += leftEven[digit]
  634. }
  635. }
  636. // 中间分隔符
  637. barcode += '01010'
  638. // 右侧数据编码 (始终使用右编码)
  639. const rightCodes = [
  640. '1110010', '1100110', '1101100', '1000010', '1011100',
  641. '1001110', '1010000', '1000100', '1001000', '1110100'
  642. ]
  643. for (let i = 0; i < rightData.length; i++) {
  644. const digit = parseInt(rightData[i])
  645. barcode += rightCodes[digit]
  646. }
  647. // 结束符
  648. barcode += '101'
  649. return barcode
  650. },
  651. /**
  652. * EAN8编码实现
  653. * @author jry <ijry@qq.com>
  654. * @param {String} data - 8位数字字符串
  655. * @returns {String|null} 编码后的条形码数据
  656. */
  657. encodeEAN8(data) {
  658. // 确保数据是8位数字
  659. if (!/^\d{8}$/.test(data)) {
  660. throw new Error('EAN8 must be 8 digits')
  661. }
  662. // 验证校验位
  663. let sum = 0
  664. for (let i = 0; i < 7; i++) {
  665. const digit = parseInt(data[i])
  666. sum += digit * (i % 2 === 0 ? 3 : 1)
  667. }
  668. const checkDigit = (10 - (sum % 10)) % 10
  669. if (parseInt(data[7]) !== checkDigit) {
  670. throw new Error('Invalid EAN8 check digit')
  671. }
  672. // 左侧数据(4位)
  673. const leftData = data.substring(0, 4)
  674. // 右侧数据(4位)
  675. const rightData = data.substring(4, 8)
  676. // 起始符
  677. let barcode = '101'
  678. // 左侧奇偶编码模式
  679. const leftOdd = [
  680. '0001101', '0011001', '0010011', '0111101', '0100011',
  681. '0110001', '0101111', '0111011', '0110111', '0001011'
  682. ]
  683. // 编码左侧数据
  684. for (let i = 0; i < leftData.length; i++) {
  685. const digit = parseInt(leftData[i])
  686. barcode += leftOdd[digit]
  687. }
  688. // 中间分隔符
  689. barcode += '01010'
  690. // 右侧数据编码 (始终使用右编码)
  691. const rightCodes = [
  692. '1110010', '1100110', '1101100', '1000010', '1011100',
  693. '1001110', '1010000', '1000100', '1001000', '1110100'
  694. ]
  695. // 编码右侧数据
  696. for (let i = 0; i < rightData.length; i++) {
  697. const digit = parseInt(rightData[i])
  698. barcode += rightCodes[digit]
  699. }
  700. // 结束符
  701. barcode += '101'
  702. return barcode
  703. },
  704. /**
  705. * EAN5/EAN2编码实现
  706. * @author jry <ijry@qq.com>
  707. * @param {String} data - 2位或5位数字字符串
  708. * @param {String} format - 格式类型(EAN5或EAN2)
  709. * @returns {String|null} 编码后的条形码数据
  710. */
  711. encodeEAN52(data, format) {
  712. const length = format === 'EAN5' ? 5 : 2
  713. // 确保数据是相应位数的数字
  714. if (!new RegExp(`^\\d{${length}}$`).test(data)) {
  715. throw new Error(`${format} must be ${length} digits`)
  716. }
  717. // EAN5/2编码表
  718. const codes = [
  719. '0001101', '0011001', '0010011', '0111101', '0100011',
  720. '0110001', '0101111', '0111011', '0110111', '0001011'
  721. ]
  722. // 计算校验和
  723. let checksum = 0
  724. for (let i = 0; i < data.length; i++) {
  725. checksum += parseInt(data[i]) * (i % 2 === 0 ? 3 : 1)
  726. }
  727. // 根据校验和确定编码模式
  728. const patterns = format === 'EAN5' ?
  729. ['00001', '00010', '00100', '01000', '10000', // 0-4
  730. '00000', '00011', '00101', '00110', '01001', // 5-9
  731. '01010', '01100', '10001', '10010', '10100', // 10-14
  732. '11000', '11001', '11010', '11100'] : // 15-18
  733. ['00', '01', '10', '11'] // EAN2只有4种模式
  734. const pattern = format === 'EAN5' ?
  735. patterns[checksum % 10] :
  736. patterns[parseInt(data) % 4]
  737. // 起始符
  738. let barcode = '1011'
  739. // 编码数据
  740. for (let i = 0; i < data.length; i++) {
  741. // 添加分隔符(除了第一个字符)
  742. if (i > 0) {
  743. barcode += '01' // 字符间分隔符
  744. }
  745. // 根据模式确定编码方式
  746. const digit = parseInt(data[i])
  747. const code = codes[digit]
  748. barcode += code
  749. }
  750. return barcode
  751. },
  752. /**
  753. * UPCA编码实现
  754. * @author jry <ijry@qq.com>
  755. * @param {String} data - 11位或12位数字字符串
  756. * @returns {String|null} 编码后的条形码数据
  757. */
  758. encodeUPCA(data) {
  759. // 如果是11位,计算校验位
  760. if (/^\d{11}$/.test(data)) {
  761. let sum = 0
  762. for (let i = 0; i < 11; i++) {
  763. const digit = parseInt(data[i])
  764. sum += (i % 2 === 0) ? digit * 3 : digit
  765. }
  766. const checkDigit = (10 - (sum % 10)) % 10
  767. data += checkDigit
  768. }
  769. // 确保数据是12位数字
  770. if (!/^\d{12}$/.test(data)) {
  771. throw new Error('UPC-A must be 11 or 12 digits')
  772. }
  773. // UPCA实际上是EAN13的第一个数字为0的特殊情况
  774. return this.encodeEAN13('0' + data)
  775. },
  776. /**
  777. * UPCE编码实现
  778. * @author jry <ijry@qq.com>
  779. * @param {String} data - 6位或8位数字字符串
  780. * @returns {String|null} 编码后的条形码数据
  781. */
  782. encodeUPCE(data) {
  783. // 如果是7位,计算校验位
  784. if (/^\d{7}$/.test(data)) {
  785. let sum = 0
  786. for (let i = 0; i < 7; i++) {
  787. const digit = parseInt(data[i])
  788. sum += (i % 2 === 0) ? digit * 3 : digit
  789. }
  790. const checkDigit = (10 - (sum % 10)) % 10
  791. data += checkDigit
  792. }
  793. // 确保数据是8位数字
  794. if (!/^\d{8}$/.test(data)) {
  795. throw new Error('UPC-E must be 7 or 8 digits')
  796. }
  797. // 检查是否是有效的UPC-E格式
  798. if (data[0] !== '0' && data[0] !== '1') {
  799. throw new Error('UPC-E must start with 0 or 1')
  800. }
  801. // UPCE编码表
  802. const leftOdd = [
  803. '0001101', '0011001', '0010011', '0111101', '0100011',
  804. '0110001', '0101111', '0111011', '0110111', '0001011'
  805. ]
  806. const leftEven = [
  807. '0100111', '0110011', '0011011', '0100001', '0011101',
  808. '0111001', '0000101', '0010001', '0001001', '0010111'
  809. ]
  810. // 根据系统数字确定编码模式
  811. const systemDigit = data[0]
  812. const checkDigit = data[7]
  813. // 提取中间6位
  814. const middleData = data.substring(1, 7)
  815. // 确定编码模式
  816. let pattern
  817. if (checkDigit === '0' || checkDigit === '1' || checkDigit === '2') {
  818. pattern = 'EEEEOO' // 0,1,2
  819. } else if (checkDigit === '3') {
  820. pattern = 'EEEEOO' // 3
  821. } else if (checkDigit === '4') {
  822. pattern = 'EEEOOO' // 4
  823. } else {
  824. pattern = 'EEOOOO' // 5,6,7,8,9
  825. }
  826. // 起始符
  827. let barcode = '101'
  828. // 编码中间6位数据
  829. for (let i = 0; i < middleData.length; i++) {
  830. const digit = parseInt(middleData[i])
  831. if (pattern[i] === 'E') {
  832. barcode += leftEven[digit]
  833. } else {
  834. barcode += leftOdd[digit]
  835. }
  836. }
  837. // 中间分隔符
  838. barcode += '010101'
  839. // 结束符
  840. barcode += '101'
  841. return barcode
  842. },
  843. /**
  844. * 绘制条形码
  845. * @author jry <ijry@qq.com>
  846. * @param {Object} ctx - canvas上下文
  847. * @param {String} barcodeData - 条形码数据
  848. * @param {Object} options - 条码配置选项
  849. */
  850. drawBarcode(ctx, barcodeData, options) {
  851. if (!barcodeData) return
  852. const marginLeft = options.marginLeft
  853. const marginTop = options.marginTop
  854. const marginBottom = options.marginBottom
  855. const textHeight = options.displayValue !== false ? options.fontSize + options.textMargin : 0
  856. const height = options.height
  857. // 恢复: 根据总长度计算模块宽度
  858. const moduleWidth = Math.max(1, (this.canvasWidth - marginLeft - (options.marginRight || 10)) / barcodeData.length)
  859. ctx.setFillStyle(options.lineColor)
  860. // 计算条形码绘制的Y坐标
  861. let barcodeY = marginTop
  862. // 如果文本在顶部,需要调整条形码绘制位置
  863. if (options.displayValue !== false && options.textPosition === 'top') {
  864. barcodeY += textHeight
  865. }
  866. // 恢复: 使用计算出的模块宽度绘制条形码
  867. let x = marginLeft
  868. for (let i = 0; i < barcodeData.length; i++) {
  869. if (barcodeData[i] === '1') {
  870. // 使用计算的模块宽度绘制每个条
  871. ctx.fillRect(x, barcodeY, moduleWidth, height)
  872. }
  873. // 每个条/空都占用一个模块宽度
  874. x += moduleWidth
  875. }
  876. // 绘制文本
  877. if (options.displayValue !== false) {
  878. const text = options.text || this.value
  879. let textY
  880. ctx.setFillStyle(options.lineColor)
  881. ctx.setFontSize(options.fontSize)
  882. ctx.setTextAlign(options.textAlign)
  883. let textX
  884. switch (options.textAlign) {
  885. case 'left':
  886. textX = marginLeft
  887. break
  888. case 'right':
  889. textX = this.canvasWidth - options.marginRight
  890. break
  891. default: // center
  892. textX = this.canvasWidth / 2
  893. }
  894. // 根据文本位置确定Y坐标
  895. if (options.textPosition === 'top') {
  896. textY = marginTop + options.fontSize - 3
  897. } else { // bottom
  898. // 修复:正确计算底部文本位置,确保文本完全显示
  899. textY = barcodeY + height + options.textMargin + options.fontSize
  900. // 确保文本不会超出画布边界
  901. if (textY > this.canvasHeight - marginBottom) {
  902. textY = this.canvasHeight - marginBottom - 2
  903. }
  904. }
  905. ctx.fillText(text, textX, textY)
  906. }
  907. }
  908. }
  909. }
  910. </script>
  911. <style scoped>
  912. .u-barcode {
  913. display: flex;
  914. flex-direction: row;
  915. justify-content: center;
  916. align-items: center;
  917. }
  918. .error-container {
  919. display: flex;
  920. justify-content: center;
  921. align-items: center;
  922. background-color: #f0f0f0;
  923. color: #ff0000;
  924. }
  925. .error-text {
  926. font-size: 14px;
  927. }
  928. </style>