u-barcode.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  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. * 添加右侧安静区(至少2个模块宽度的空白)
  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. // 添加右侧安静区(至少2个模块宽度的空白)
  433. barcode += '00000'
  434. return barcode
  435. },
  436. /**
  437. * 获取CODE128编码模式
  438. * @author jry <ijry@qq.com>
  439. * @param {Number} code - 字符编码
  440. * @returns {String} 条形码二进制模式
  441. */
  442. getCode128Pattern(code) {
  443. // CODE128编码表
  444. const patterns = [
  445. "11011001100", "11001101100", "11001100110", "10010011000",
  446. "10010001100", "10001001100", "10011001000", "10011000100",
  447. "10001100100", "11001001000", "11001000100", "11000100100",
  448. "10110011100", "10011011100", "10011001110", "10111001100",
  449. "10011101100", "10011100110", "11001110010", "11001011100",
  450. "11001001110", "11011100100", "11001110100", "11101101110",
  451. "11101001100", "11100101100", "11100100110", "11101100100",
  452. "11100110100", "11100110010", "11011011000", "11011000110",
  453. "11000110110", "10100011000", "10001011000", "10001000110",
  454. "10110001000", "10001101000", "10001100010", "11010001000",
  455. "11000101000", "11000100010", "10110111000", "10110001110",
  456. "10001101110", "10111011000", "10111000110", "10001110110",
  457. "11101110110", "11010001110", "11000101110", "11011101000",
  458. "11011100010", "11011101110", "11101011000", "11101000110",
  459. "11100010110", "11101101000", "11101100010", "11100011010",
  460. "11101111010", "11001000010", "11110001010", "10100110000",
  461. "10100001100", "10010110000", "10010000110", "10000101100",
  462. "10000100110", "10110010000", "10110000100", "10011010000",
  463. "10011000010", "10000110100", "10000110010", "11000010010",
  464. "11001010000", "11110111010", "11000010100", "10001111010",
  465. "10100111100", "10010111100", "10010011110", "10111100100",
  466. "10011110100", "10011110010", "11110100100", "11110010100",
  467. "11110010010", "11011011110", "11011110110", "11110110110",
  468. "10101111000", "10100011110", "10001011110", "10111101000",
  469. "10111100010", "11110101000", "11110100010", "10111011110",
  470. "10111101110", "11101011110", "11110101110", "11010000100",
  471. "11010010000", "11010011100", "11000111010"
  472. ];
  473. return patterns[code] || "";
  474. },
  475. /**
  476. * CODE39编码实现
  477. * @author jry <ijry@qq.com>
  478. * @param {String} data - 要编码的数据
  479. * @returns {String|null} 编码后的条形码数据
  480. */
  481. encodeCode39(data) {
  482. const codes = {
  483. '0': '101000111011101',
  484. '1': '111010001010111',
  485. '2': '101110001010111',
  486. '3': '111011100010101',
  487. '4': '101000111010111',
  488. '5': '111010001110101',
  489. '6': '101110001110101',
  490. '7': '101000101110111',
  491. '8': '111010001011101',
  492. '9': '101110001011101',
  493. 'A': '111010100010111',
  494. 'B': '101110100010111',
  495. 'C': '111011101000101',
  496. 'D': '101011100010111',
  497. 'E': '111010111000101',
  498. 'F': '101110111000101',
  499. 'G': '101010001110111',
  500. 'H': '111010100011101',
  501. 'I': '101110100011101',
  502. 'J': '101011100011101',
  503. 'K': '111010101000111',
  504. 'L': '101110101000111',
  505. 'M': '111011101010001',
  506. 'N': '101011101000111',
  507. 'O': '111010111010001',
  508. 'P': '101110111010001',
  509. 'Q': '101010111000111',
  510. 'R': '111010101110001',
  511. 'S': '101110101110001',
  512. 'T': '101011101110001',
  513. 'U': '111000101010111',
  514. 'V': '100011101010111',
  515. 'W': '111000111010101',
  516. 'X': '100010111010111',
  517. 'Y': '111000101110101',
  518. 'Z': '100011101110101',
  519. '-': '100010101110111',
  520. '.': '111000101011101',
  521. ' ': '100011101011101',
  522. '*': '100010111011101', // 起始和终止字符
  523. '$': '100010001000101',
  524. '/': '100010001010001',
  525. '+': '100010100010001',
  526. '%': '101000100010001'
  527. }
  528. // 转为大写
  529. data = data.toUpperCase()
  530. // 添加起始和终止字符
  531. let barcode = codes['*']
  532. // 添加数据字符
  533. for (let i = 0; i < data.length; i++) {
  534. const char = data[i]
  535. if (codes[char]) {
  536. barcode += '0' // 字符间隔
  537. barcode += codes[char]
  538. } else {
  539. throw new Error('Invalid character in CODE39: ' + char)
  540. }
  541. }
  542. // 添加终止字符
  543. barcode += '0' // 字符间隔
  544. barcode += codes['*']
  545. return barcode
  546. },
  547. /**
  548. * EAN13编码实现
  549. * @author jry <ijry@qq.com>
  550. * @param {String} data - 13位数字字符串
  551. * @returns {String|null} 编码后的条形码数据
  552. */
  553. encodeEAN13(data) {
  554. // 确保数据是13位数字
  555. if (!/^\d{13}$/.test(data)) {
  556. throw new Error('EAN13 must be 13 digits')
  557. }
  558. // 验证校验位
  559. let sum = 0
  560. for (let i = 0; i < 12; i++) {
  561. const digit = parseInt(data[i])
  562. sum += (i % 2 === 0) ? digit : digit * 3
  563. }
  564. const checkDigit = (10 - (sum % 10)) % 10
  565. if (parseInt(data[12]) !== checkDigit) {
  566. throw new Error('Invalid EAN13 check digit')
  567. }
  568. // 左侧数据
  569. const leftData = data.substring(1, 7)
  570. const rightData = data.substring(7, 13)
  571. // 起始符
  572. let barcode = '101'
  573. // 左侧数据编码 (根据第一位数字决定编码方式)
  574. const firstDigit = parseInt(data[0])
  575. const leftPatterns = [
  576. ['LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG',
  577. 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL']
  578. ]
  579. const pattern = leftPatterns[0][firstDigit]
  580. // 左侧奇偶编码模式
  581. const leftOdd = [
  582. '0001101', '0011001', '0010011', '0111101', '0100011',
  583. '0110001', '0101111', '0111011', '0110111', '0001011'
  584. ]
  585. const leftEven = [
  586. '0100111', '0110011', '0011011', '0100001', '0011101',
  587. '0111001', '0000101', '0010001', '0001001', '0010111'
  588. ]
  589. // 编码左侧数据
  590. for (let i = 0; i < leftData.length; i++) {
  591. const digit = parseInt(leftData[i])
  592. if (pattern[i] === 'L') {
  593. barcode += leftOdd[digit]
  594. } else {
  595. barcode += leftEven[digit]
  596. }
  597. }
  598. // 中间分隔符
  599. barcode += '01010'
  600. // 右侧数据编码 (始终使用右编码)
  601. const rightCodes = [
  602. '1110010', '1100110', '1101100', '1000010', '1011100',
  603. '1001110', '1010000', '1000100', '1001000', '1110100'
  604. ]
  605. for (let i = 0; i < rightData.length; i++) {
  606. const digit = parseInt(rightData[i])
  607. barcode += rightCodes[digit]
  608. }
  609. // 结束符
  610. barcode += '101'
  611. return barcode
  612. },
  613. /**
  614. * EAN8编码实现
  615. * @author jry <ijry@qq.com>
  616. * @param {String} data - 8位数字字符串
  617. * @returns {String|null} 编码后的条形码数据
  618. */
  619. encodeEAN8(data) {
  620. // 确保数据是8位数字
  621. if (!/^\d{8}$/.test(data)) {
  622. throw new Error('EAN8 must be 8 digits')
  623. }
  624. // 验证校验位
  625. let sum = 0
  626. for (let i = 0; i < 7; i++) {
  627. const digit = parseInt(data[i])
  628. sum += digit * (i % 2 === 0 ? 3 : 1)
  629. }
  630. const checkDigit = (10 - (sum % 10)) % 10
  631. if (parseInt(data[7]) !== checkDigit) {
  632. throw new Error('Invalid EAN8 check digit')
  633. }
  634. // 左侧数据(4位)
  635. const leftData = data.substring(0, 4)
  636. // 右侧数据(4位)
  637. const rightData = data.substring(4, 8)
  638. // 起始符
  639. let barcode = '101'
  640. // 左侧奇偶编码模式
  641. const leftOdd = [
  642. '0001101', '0011001', '0010011', '0111101', '0100011',
  643. '0110001', '0101111', '0111011', '0110111', '0001011'
  644. ]
  645. // 编码左侧数据
  646. for (let i = 0; i < leftData.length; i++) {
  647. const digit = parseInt(leftData[i])
  648. barcode += leftOdd[digit]
  649. }
  650. // 中间分隔符
  651. barcode += '01010'
  652. // 右侧数据编码 (始终使用右编码)
  653. const rightCodes = [
  654. '1110010', '1100110', '1101100', '1000010', '1011100',
  655. '1001110', '1010000', '1000100', '1001000', '1110100'
  656. ]
  657. // 编码右侧数据
  658. for (let i = 0; i < rightData.length; i++) {
  659. const digit = parseInt(rightData[i])
  660. barcode += rightCodes[digit]
  661. }
  662. // 结束符
  663. barcode += '101'
  664. return barcode
  665. },
  666. /**
  667. * EAN5/EAN2编码实现
  668. * @author jry <ijry@qq.com>
  669. * @param {String} data - 2位或5位数字字符串
  670. * @param {String} format - 格式类型(EAN5或EAN2)
  671. * @returns {String|null} 编码后的条形码数据
  672. */
  673. encodeEAN52(data, format) {
  674. const length = format === 'EAN5' ? 5 : 2
  675. // 确保数据是相应位数的数字
  676. if (!new RegExp(`^\\d{${length}}$`).test(data)) {
  677. throw new Error(`${format} must be ${length} digits`)
  678. }
  679. // EAN5/2编码表
  680. const codes = [
  681. '0001101', '0011001', '0010011', '0111101', '0100011',
  682. '0110001', '0101111', '0111011', '0110111', '0001011'
  683. ]
  684. // 计算校验和
  685. let checksum = 0
  686. for (let i = 0; i < data.length; i++) {
  687. checksum += parseInt(data[i]) * (i % 2 === 0 ? 3 : 1)
  688. }
  689. // 根据校验和确定编码模式
  690. const patterns = format === 'EAN5' ?
  691. ['00001', '00010', '00100', '01000', '10000', // 0-4
  692. '00000', '00011', '00101', '00110', '01001', // 5-9
  693. '01010', '01100', '10001', '10010', '10100', // 10-14
  694. '11000', '11001', '11010', '11100'] : // 15-18
  695. ['00', '01', '10', '11'] // EAN2只有4种模式
  696. const pattern = format === 'EAN5' ?
  697. patterns[checksum % 10] :
  698. patterns[parseInt(data) % 4]
  699. // 起始符
  700. let barcode = '1011'
  701. // 编码数据
  702. for (let i = 0; i < data.length; i++) {
  703. // 添加分隔符(除了第一个字符)
  704. if (i > 0) {
  705. barcode += '01' // 字符间分隔符
  706. }
  707. // 根据模式确定编码方式
  708. const digit = parseInt(data[i])
  709. const code = codes[digit]
  710. barcode += code
  711. }
  712. return barcode
  713. },
  714. /**
  715. * UPCA编码实现
  716. * @author jry <ijry@qq.com>
  717. * @param {String} data - 11位或12位数字字符串
  718. * @returns {String|null} 编码后的条形码数据
  719. */
  720. encodeUPCA(data) {
  721. // 如果是11位,计算校验位
  722. if (/^\d{11}$/.test(data)) {
  723. let sum = 0
  724. for (let i = 0; i < 11; i++) {
  725. const digit = parseInt(data[i])
  726. sum += (i % 2 === 0) ? digit * 3 : digit
  727. }
  728. const checkDigit = (10 - (sum % 10)) % 10
  729. data += checkDigit
  730. }
  731. // 确保数据是12位数字
  732. if (!/^\d{12}$/.test(data)) {
  733. throw new Error('UPC-A must be 11 or 12 digits')
  734. }
  735. // UPCA实际上是EAN13的第一个数字为0的特殊情况
  736. return this.encodeEAN13('0' + data)
  737. },
  738. /**
  739. * UPCE编码实现
  740. * @author jry <ijry@qq.com>
  741. * @param {String} data - 6位或8位数字字符串
  742. * @returns {String|null} 编码后的条形码数据
  743. */
  744. encodeUPCE(data) {
  745. // 如果是7位,计算校验位
  746. if (/^\d{7}$/.test(data)) {
  747. let sum = 0
  748. for (let i = 0; i < 7; i++) {
  749. const digit = parseInt(data[i])
  750. sum += (i % 2 === 0) ? digit * 3 : digit
  751. }
  752. const checkDigit = (10 - (sum % 10)) % 10
  753. data += checkDigit
  754. }
  755. // 确保数据是8位数字
  756. if (!/^\d{8}$/.test(data)) {
  757. throw new Error('UPC-E must be 7 or 8 digits')
  758. }
  759. // 检查是否是有效的UPC-E格式
  760. if (data[0] !== '0' && data[0] !== '1') {
  761. throw new Error('UPC-E must start with 0 or 1')
  762. }
  763. // UPCE编码表
  764. const leftOdd = [
  765. '0001101', '0011001', '0010011', '0111101', '0100011',
  766. '0110001', '0101111', '0111011', '0110111', '0001011'
  767. ]
  768. const leftEven = [
  769. '0100111', '0110011', '0011011', '0100001', '0011101',
  770. '0111001', '0000101', '0010001', '0001001', '0010111'
  771. ]
  772. // 根据系统数字确定编码模式
  773. const systemDigit = data[0]
  774. const checkDigit = data[7]
  775. // 提取中间6位
  776. const middleData = data.substring(1, 7)
  777. // 确定编码模式
  778. let pattern
  779. if (checkDigit === '0' || checkDigit === '1' || checkDigit === '2') {
  780. pattern = 'EEEEOO' // 0,1,2
  781. } else if (checkDigit === '3') {
  782. pattern = 'EEEEOO' // 3
  783. } else if (checkDigit === '4') {
  784. pattern = 'EEEOOO' // 4
  785. } else {
  786. pattern = 'EEOOOO' // 5,6,7,8,9
  787. }
  788. // 起始符
  789. let barcode = '101'
  790. // 编码中间6位数据
  791. for (let i = 0; i < middleData.length; i++) {
  792. const digit = parseInt(middleData[i])
  793. if (pattern[i] === 'E') {
  794. barcode += leftEven[digit]
  795. } else {
  796. barcode += leftOdd[digit]
  797. }
  798. }
  799. // 中间分隔符
  800. barcode += '010101'
  801. // 结束符
  802. barcode += '101'
  803. return barcode
  804. },
  805. /**
  806. * 绘制条形码
  807. * @author jry <ijry@qq.com>
  808. * @param {Object} ctx - canvas上下文
  809. * @param {String} barcodeData - 条形码数据
  810. * @param {Object} options - 条码配置选项
  811. */
  812. drawBarcode(ctx, barcodeData, options) {
  813. if (!barcodeData) return
  814. const marginLeft = options.marginLeft
  815. const marginTop = options.marginTop
  816. const marginBottom = options.marginBottom
  817. const textHeight = options.displayValue !== false ? options.fontSize + options.textMargin : 0
  818. const height = options.height
  819. // 恢复: 根据总长度计算模块宽度
  820. const moduleWidth = Math.max(1, (this.canvasWidth - marginLeft - (options.marginRight || 10)) / barcodeData.length)
  821. ctx.setFillStyle(options.lineColor)
  822. // 计算条形码绘制的Y坐标
  823. let barcodeY = marginTop
  824. // 如果文本在顶部,需要调整条形码绘制位置
  825. if (options.displayValue !== false && options.textPosition === 'top') {
  826. barcodeY += textHeight
  827. }
  828. // 恢复: 使用计算出的模块宽度绘制条形码
  829. let x = marginLeft
  830. for (let i = 0; i < barcodeData.length; i++) {
  831. if (barcodeData[i] === '1') {
  832. // 使用计算的模块宽度绘制每个条
  833. ctx.fillRect(x, barcodeY, moduleWidth, height)
  834. }
  835. // 每个条/空都占用一个模块宽度
  836. x += moduleWidth
  837. }
  838. // 绘制文本
  839. if (options.displayValue !== false) {
  840. const text = options.text || this.value
  841. let textY
  842. ctx.setFillStyle(options.lineColor)
  843. ctx.setFontSize(options.fontSize)
  844. ctx.setTextAlign(options.textAlign)
  845. let textX
  846. switch (options.textAlign) {
  847. case 'left':
  848. textX = marginLeft
  849. break
  850. case 'right':
  851. textX = this.canvasWidth - options.marginRight
  852. break
  853. default: // center
  854. textX = this.canvasWidth / 2
  855. }
  856. // 根据文本位置确定Y坐标
  857. if (options.textPosition === 'top') {
  858. textY = marginTop + options.fontSize - 3
  859. } else { // bottom
  860. // 修复:正确计算底部文本位置,确保文本完全显示
  861. textY = barcodeY + height + options.textMargin + options.fontSize
  862. // 确保文本不会超出画布边界
  863. if (textY > this.canvasHeight - marginBottom) {
  864. textY = this.canvasHeight - marginBottom - 2
  865. }
  866. }
  867. ctx.fillText(text, textX, textY)
  868. }
  869. }
  870. }
  871. }
  872. </script>
  873. <style scoped>
  874. .u-barcode {
  875. display: flex;
  876. flex-direction: row;
  877. justify-content: center;
  878. align-items: center;
  879. }
  880. .error-container {
  881. display: flex;
  882. justify-content: center;
  883. align-items: center;
  884. background-color: #f0f0f0;
  885. color: #ff0000;
  886. }
  887. .error-text {
  888. font-size: 14px;
  889. }
  890. </style>