1001 lines
28 KiB
Vue
1001 lines
28 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="u-barcode" v-if="calcSizeDone">
|
|||
|
|
<canvas
|
|||
|
|
v-if="showCanvas && !error"
|
|||
|
|
:id="canvasId"
|
|||
|
|
:canvas-id="canvasId"
|
|||
|
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|||
|
|
></canvas>
|
|||
|
|
<image
|
|||
|
|
v-else-if="!showCanvas && !error"
|
|||
|
|
:src="barcodeImage"
|
|||
|
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|||
|
|
mode="aspectFit"
|
|||
|
|
/>
|
|||
|
|
<view
|
|||
|
|
v-else
|
|||
|
|
class="error-container"
|
|||
|
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|||
|
|
>
|
|||
|
|
<text class="error-text">{{ error }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { nextTick } from 'vue'
|
|||
|
|
import { t } from '../../libs/i18n'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'u-barcode',
|
|||
|
|
props: {
|
|||
|
|
// 条码值
|
|||
|
|
value: {
|
|||
|
|
type: [String, Number],
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
// 条码格式
|
|||
|
|
format: {
|
|||
|
|
type: String,
|
|||
|
|
default: 'auto',
|
|||
|
|
validator: function (value) {
|
|||
|
|
return [
|
|||
|
|
'auto',
|
|||
|
|
'CODE128', 'CODE128A', 'CODE128B', 'CODE128C',
|
|||
|
|
'EAN13', 'EAN8', 'EAN5', 'EAN2',
|
|||
|
|
'UPC', 'UPCA', 'UPCE',
|
|||
|
|
'CODE39',
|
|||
|
|
'ITF', 'ITF14',
|
|||
|
|
'MSI', 'MSI10', 'MSI11', 'MSI1010', 'MSI1110',
|
|||
|
|
'pharmacode',
|
|||
|
|
'codabar'
|
|||
|
|
].includes(value)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
// 宽度
|
|||
|
|
width: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 200
|
|||
|
|
},
|
|||
|
|
// 高度
|
|||
|
|
height: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 80
|
|||
|
|
},
|
|||
|
|
// 是否显示文本
|
|||
|
|
displayValue: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: true
|
|||
|
|
},
|
|||
|
|
// 文本内容
|
|||
|
|
text: {
|
|||
|
|
type: String,
|
|||
|
|
default: undefined
|
|||
|
|
},
|
|||
|
|
// 字体选项
|
|||
|
|
fontOptions: {
|
|||
|
|
type: String,
|
|||
|
|
default: ''
|
|||
|
|
},
|
|||
|
|
// 字体
|
|||
|
|
font: {
|
|||
|
|
type: String,
|
|||
|
|
default: 'monospace'
|
|||
|
|
},
|
|||
|
|
// 文本对齐方式
|
|||
|
|
textAlign: {
|
|||
|
|
type: String,
|
|||
|
|
default: 'center'
|
|||
|
|
},
|
|||
|
|
// 文本位置
|
|||
|
|
textPosition: {
|
|||
|
|
type: String,
|
|||
|
|
default: 'bottom'
|
|||
|
|
},
|
|||
|
|
// 文本边距
|
|||
|
|
textMargin: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 2
|
|||
|
|
},
|
|||
|
|
// 字体大小
|
|||
|
|
fontSize: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 14
|
|||
|
|
},
|
|||
|
|
// 背景色
|
|||
|
|
background: {
|
|||
|
|
type: String,
|
|||
|
|
default: '#ffffff'
|
|||
|
|
},
|
|||
|
|
// 条码颜色
|
|||
|
|
lineColor: {
|
|||
|
|
type: String,
|
|||
|
|
default: '#000000'
|
|||
|
|
},
|
|||
|
|
// 边距
|
|||
|
|
margin: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 10
|
|||
|
|
},
|
|||
|
|
// 上边距
|
|||
|
|
marginTop: {
|
|||
|
|
type: Number,
|
|||
|
|
default: undefined
|
|||
|
|
},
|
|||
|
|
// 下边距
|
|||
|
|
marginBottom: {
|
|||
|
|
type: Number,
|
|||
|
|
default: undefined
|
|||
|
|
},
|
|||
|
|
// 左边距
|
|||
|
|
marginLeft: {
|
|||
|
|
type: Number,
|
|||
|
|
default: undefined
|
|||
|
|
},
|
|||
|
|
// 右边距
|
|||
|
|
marginRight: {
|
|||
|
|
type: Number,
|
|||
|
|
default: undefined
|
|||
|
|
},
|
|||
|
|
// 使用canvas还是生成图片
|
|||
|
|
useCanvas: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: true
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
canvasId: 'barcode-' + Math.random().toString(36).substr(2, 9),
|
|||
|
|
barcodeImage: '',
|
|||
|
|
showCanvas: false,
|
|||
|
|
canvasWidth: 0,
|
|||
|
|
canvasHeight: 0,
|
|||
|
|
calcSizeDone: false,
|
|||
|
|
error: ''
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
watch: {
|
|||
|
|
value() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
format() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
width() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
height() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
displayValue() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
text() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
font() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
textAlign() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
textPosition() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
textMargin() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
fontSize() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
background() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
lineColor() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
},
|
|||
|
|
margin() {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
mounted() {
|
|||
|
|
/**
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
*/
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.generateBarcode()
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
/**
|
|||
|
|
* 生成条形码
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String|Number} value - 条码值
|
|||
|
|
* @param {Object} options - 条码配置选项
|
|||
|
|
*/
|
|||
|
|
generateBarcode() {
|
|||
|
|
// 统一处理默认值
|
|||
|
|
const margin = this.margin
|
|||
|
|
const options = {
|
|||
|
|
format: this.format || 'auto',
|
|||
|
|
width: this.width,
|
|||
|
|
height: this.height,
|
|||
|
|
displayValue: this.displayValue,
|
|||
|
|
text: this.text,
|
|||
|
|
fontOptions: this.fontOptions || '',
|
|||
|
|
font: this.font || 'monospace',
|
|||
|
|
textAlign: this.textAlign || 'center',
|
|||
|
|
textPosition: this.textPosition || 'bottom',
|
|||
|
|
textMargin: this.textMargin !== undefined ? this.textMargin : 2,
|
|||
|
|
fontSize: this.fontSize || 20,
|
|||
|
|
background: this.background || '#ffffff',
|
|||
|
|
lineColor: this.lineColor || '#000000',
|
|||
|
|
margin: margin,
|
|||
|
|
marginTop: this.marginTop !== undefined ? this.marginTop : margin,
|
|||
|
|
marginBottom: this.marginBottom !== undefined ? this.marginBottom : margin,
|
|||
|
|
marginLeft: this.marginLeft !== undefined ? this.marginLeft : margin,
|
|||
|
|
marginRight: this.marginRight !== undefined ? this.marginRight : margin
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理未定义的选项
|
|||
|
|
Object.keys(options).forEach(key => {
|
|||
|
|
if (options[key] === undefined) {
|
|||
|
|
delete options[key]
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (this.useCanvas) {
|
|||
|
|
// 使用canvas渲染
|
|||
|
|
this.showCanvas = true
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.renderToCanvas(options)
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
// 生成图片
|
|||
|
|
this.showCanvas = false
|
|||
|
|
this.renderToImage(options)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 渲染条形码到canvas
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {Object} options - 条码配置选项
|
|||
|
|
*/
|
|||
|
|
async renderToCanvas(options) {
|
|||
|
|
try {
|
|||
|
|
// 计算canvas尺寸
|
|||
|
|
this.calculateCanvasSize(options)
|
|||
|
|
|
|||
|
|
await nextTick()
|
|||
|
|
|
|||
|
|
// 获取canvas上下文
|
|||
|
|
const ctx = uni.createCanvasContext(this.canvasId, this)
|
|||
|
|
|
|||
|
|
// 清空画布
|
|||
|
|
ctx.setFillStyle(options.background)
|
|||
|
|
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|||
|
|
|
|||
|
|
// 生成条形码数据
|
|||
|
|
const barcodeData = this.encodeBarcode(this.value, options.format)
|
|||
|
|
|
|||
|
|
if (barcodeData) {
|
|||
|
|
// 绘制条形码
|
|||
|
|
this.drawBarcode(ctx, barcodeData, options)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绘制到canvas
|
|||
|
|
ctx.draw(false, () => {
|
|||
|
|
// 绘制完成回调
|
|||
|
|
this.$emit('rendered', { type: 'canvas', id: this.canvasId })
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('生成条码失败:', error)
|
|||
|
|
this.error = error.message || t('up.barcode.error')
|
|||
|
|
this.$emit('error', error)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 渲染条形码为图片
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {Object} options - 条码配置选项
|
|||
|
|
*/
|
|||
|
|
renderToImage(options) {
|
|||
|
|
try {
|
|||
|
|
// 计算canvas尺寸
|
|||
|
|
this.calculateCanvasSize(options)
|
|||
|
|
|
|||
|
|
// 创建临时canvas用于生成图片
|
|||
|
|
const tempCanvasId = 'temp-' + this.canvasId
|
|||
|
|
const ctx = uni.createCanvasContext(tempCanvasId, this)
|
|||
|
|
|
|||
|
|
// 清空画布
|
|||
|
|
ctx.setFillStyle(options.background)
|
|||
|
|
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|||
|
|
|
|||
|
|
// 生成条形码数据
|
|||
|
|
const barcodeData = this.encodeBarcode(this.value, options.format)
|
|||
|
|
|
|||
|
|
if (barcodeData) {
|
|||
|
|
// 绘制条形码
|
|||
|
|
this.drawBarcode(ctx, barcodeData, options)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绘制到临时canvas并生成图片
|
|||
|
|
ctx.draw(false, () => {
|
|||
|
|
// 延迟一小段时间确保canvas绘制完成
|
|||
|
|
setTimeout(() => {
|
|||
|
|
// 将canvas转换为图片
|
|||
|
|
uni.canvasToTempFilePath({
|
|||
|
|
canvasId: tempCanvasId,
|
|||
|
|
success: (res) => {
|
|||
|
|
this.barcodeImage = res.tempFilePath
|
|||
|
|
this.$emit('rendered', { type: 'image', value: this.value, path: res.tempFilePath })
|
|||
|
|
},
|
|||
|
|
fail: (error) => {
|
|||
|
|
console.error('生成条码图片失败:', error)
|
|||
|
|
this.$emit('error', error)
|
|||
|
|
}
|
|||
|
|
}, this)
|
|||
|
|
}, 100)
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('生成条码图片失败:', error)
|
|||
|
|
this.$emit('error', error)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算canvas尺寸
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {Object} options - 条码配置选项
|
|||
|
|
*/
|
|||
|
|
calculateCanvasSize(options) {
|
|||
|
|
// 基础宽度计算
|
|||
|
|
let width = options.width
|
|||
|
|
let height = options.height
|
|||
|
|
|
|||
|
|
// 考虑边距
|
|||
|
|
const marginLeft = options.marginLeft
|
|||
|
|
const marginRight = options.marginRight
|
|||
|
|
const marginTop = options.marginTop
|
|||
|
|
const marginBottom = options.marginBottom
|
|||
|
|
|
|||
|
|
// 考虑文本高度
|
|||
|
|
let textHeight = 0
|
|||
|
|
if (options.displayValue !== false) {
|
|||
|
|
textHeight = options.fontSize + options.textMargin
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据文本位置调整高度
|
|||
|
|
if (options.textPosition === 'top' || options.textPosition === 'bottom') {
|
|||
|
|
height += textHeight
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加边距
|
|||
|
|
width += marginLeft + marginRight
|
|||
|
|
height += marginTop + marginBottom
|
|||
|
|
|
|||
|
|
this.canvasWidth = Math.max(width, 100)
|
|||
|
|
this.canvasHeight = Math.max(height, 60 + textHeight)
|
|||
|
|
|
|||
|
|
this.calcSizeDone = true
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 编码条形码数据
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String|Number} value - 条码值
|
|||
|
|
* @param {String} format - 条码格式
|
|||
|
|
* @returns {String|null} 条形码编码数据
|
|||
|
|
*/
|
|||
|
|
encodeBarcode(value, format) {
|
|||
|
|
try {
|
|||
|
|
switch (format) {
|
|||
|
|
case 'CODE128':
|
|||
|
|
case 'auto':
|
|||
|
|
return this.encodeCode128(value)
|
|||
|
|
case 'CODE39':
|
|||
|
|
return this.encodeCode39(value)
|
|||
|
|
case 'EAN13':
|
|||
|
|
return this.encodeEAN13(value)
|
|||
|
|
case 'EAN8':
|
|||
|
|
return this.encodeEAN8(value)
|
|||
|
|
case 'EAN5':
|
|||
|
|
case 'EAN2':
|
|||
|
|
return this.encodeEAN52(value, format)
|
|||
|
|
case 'UPC':
|
|||
|
|
case 'UPCA':
|
|||
|
|
return this.encodeUPCA(value)
|
|||
|
|
case 'UPCE':
|
|||
|
|
return this.encodeUPCE(value)
|
|||
|
|
default:
|
|||
|
|
// 默认使用CODE128
|
|||
|
|
return this.encodeCode128(value)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('条码编码失败:', error)
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加右侧安静区(至少2个模块宽度的空白)
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String} data - 要编码的数据
|
|||
|
|
* @returns {String|null} 编码后的条形码数据
|
|||
|
|
*/
|
|||
|
|
encodeCode128(data) {
|
|||
|
|
const CODE128_START_CODE_B = 104
|
|||
|
|
const CODE128_STOP = 106
|
|||
|
|
|
|||
|
|
// CODE128 Code B 字符集
|
|||
|
|
const CODE128_CODE_B_CHARS = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
|
|||
|
|
|
|||
|
|
// 条形码模式 (B 模式)
|
|||
|
|
const codes = []
|
|||
|
|
let checksum = CODE128_START_CODE_B
|
|||
|
|
|
|||
|
|
// 添加起始字符
|
|||
|
|
codes.push(CODE128_START_CODE_B)
|
|||
|
|
|
|||
|
|
// 编码每个字符
|
|||
|
|
for (let i = 0; i < data.length; i++) {
|
|||
|
|
const char = data[i]
|
|||
|
|
const code = CODE128_CODE_B_CHARS.indexOf(char)
|
|||
|
|
|
|||
|
|
if (code === -1) {
|
|||
|
|
throw new Error('Invalid character in CODE128: ' + char)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
codes.push(code)
|
|||
|
|
checksum += code * (i + 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加校验字符
|
|||
|
|
codes.push(checksum % 103)
|
|||
|
|
|
|||
|
|
// 添加结束字符
|
|||
|
|
codes.push(CODE128_STOP)
|
|||
|
|
|
|||
|
|
// 转换为条形码模式 (1 = 黑条, 0 = 白条)
|
|||
|
|
let barcode = ''
|
|||
|
|
for (let i = 0; i < codes.length; i++) {
|
|||
|
|
const code = codes[i]
|
|||
|
|
barcode += this.getCode128Pattern(code)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加右侧安静区(至少2个模块宽度的空白)
|
|||
|
|
barcode += '00000'
|
|||
|
|
|
|||
|
|
return barcode
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取CODE128编码模式
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {Number} code - 字符编码
|
|||
|
|
* @returns {String} 条形码二进制模式
|
|||
|
|
*/
|
|||
|
|
getCode128Pattern(code) {
|
|||
|
|
// CODE128编码表
|
|||
|
|
const patterns = [
|
|||
|
|
"11011001100", "11001101100", "11001100110", "10010011000",
|
|||
|
|
"10010001100", "10001001100", "10011001000", "10011000100",
|
|||
|
|
"10001100100", "11001001000", "11001000100", "11000100100",
|
|||
|
|
"10110011100", "10011011100", "10011001110", "10111001100",
|
|||
|
|
"10011101100", "10011100110", "11001110010", "11001011100",
|
|||
|
|
"11001001110", "11011100100", "11001110100", "11101101110",
|
|||
|
|
"11101001100", "11100101100", "11100100110", "11101100100",
|
|||
|
|
"11100110100", "11100110010", "11011011000", "11011000110",
|
|||
|
|
"11000110110", "10100011000", "10001011000", "10001000110",
|
|||
|
|
"10110001000", "10001101000", "10001100010", "11010001000",
|
|||
|
|
"11000101000", "11000100010", "10110111000", "10110001110",
|
|||
|
|
"10001101110", "10111011000", "10111000110", "10001110110",
|
|||
|
|
"11101110110", "11010001110", "11000101110", "11011101000",
|
|||
|
|
"11011100010", "11011101110", "11101011000", "11101000110",
|
|||
|
|
"11100010110", "11101101000", "11101100010", "11100011010",
|
|||
|
|
"11101111010", "11001000010", "11110001010", "10100110000",
|
|||
|
|
"10100001100", "10010110000", "10010000110", "10000101100",
|
|||
|
|
"10000100110", "10110010000", "10110000100", "10011010000",
|
|||
|
|
"10011000010", "10000110100", "10000110010", "11000010010",
|
|||
|
|
"11001010000", "11110111010", "11000010100", "10001111010",
|
|||
|
|
"10100111100", "10010111100", "10010011110", "10111100100",
|
|||
|
|
"10011110100", "10011110010", "11110100100", "11110010100",
|
|||
|
|
"11110010010", "11011011110", "11011110110", "11110110110",
|
|||
|
|
"10101111000", "10100011110", "10001011110", "10111101000",
|
|||
|
|
"10111100010", "11110101000", "11110100010", "10111011110",
|
|||
|
|
"10111101110", "11101011110", "11110101110", "11010000100",
|
|||
|
|
"11010010000", "11010011100", "11000111010"
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return patterns[code] || "";
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* CODE39编码实现
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String} data - 要编码的数据
|
|||
|
|
* @returns {String|null} 编码后的条形码数据
|
|||
|
|
*/
|
|||
|
|
encodeCode39(data) {
|
|||
|
|
const codes = {
|
|||
|
|
'0': '101000111011101',
|
|||
|
|
'1': '111010001010111',
|
|||
|
|
'2': '101110001010111',
|
|||
|
|
'3': '111011100010101',
|
|||
|
|
'4': '101000111010111',
|
|||
|
|
'5': '111010001110101',
|
|||
|
|
'6': '101110001110101',
|
|||
|
|
'7': '101000101110111',
|
|||
|
|
'8': '111010001011101',
|
|||
|
|
'9': '101110001011101',
|
|||
|
|
'A': '111010100010111',
|
|||
|
|
'B': '101110100010111',
|
|||
|
|
'C': '111011101000101',
|
|||
|
|
'D': '101011100010111',
|
|||
|
|
'E': '111010111000101',
|
|||
|
|
'F': '101110111000101',
|
|||
|
|
'G': '101010001110111',
|
|||
|
|
'H': '111010100011101',
|
|||
|
|
'I': '101110100011101',
|
|||
|
|
'J': '101011100011101',
|
|||
|
|
'K': '111010101000111',
|
|||
|
|
'L': '101110101000111',
|
|||
|
|
'M': '111011101010001',
|
|||
|
|
'N': '101011101000111',
|
|||
|
|
'O': '111010111010001',
|
|||
|
|
'P': '101110111010001',
|
|||
|
|
'Q': '101010111000111',
|
|||
|
|
'R': '111010101110001',
|
|||
|
|
'S': '101110101110001',
|
|||
|
|
'T': '101011101110001',
|
|||
|
|
'U': '111000101010111',
|
|||
|
|
'V': '100011101010111',
|
|||
|
|
'W': '111000111010101',
|
|||
|
|
'X': '100010111010111',
|
|||
|
|
'Y': '111000101110101',
|
|||
|
|
'Z': '100011101110101',
|
|||
|
|
'-': '100010101110111',
|
|||
|
|
'.': '111000101011101',
|
|||
|
|
' ': '100011101011101',
|
|||
|
|
'*': '100010111011101', // 起始和终止字符
|
|||
|
|
'$': '100010001000101',
|
|||
|
|
'/': '100010001010001',
|
|||
|
|
'+': '100010100010001',
|
|||
|
|
'%': '101000100010001'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转为大写
|
|||
|
|
data = data.toUpperCase()
|
|||
|
|
|
|||
|
|
// 添加起始和终止字符
|
|||
|
|
let barcode = codes['*']
|
|||
|
|
|
|||
|
|
// 添加数据字符
|
|||
|
|
for (let i = 0; i < data.length; i++) {
|
|||
|
|
const char = data[i]
|
|||
|
|
if (codes[char]) {
|
|||
|
|
barcode += '0' // 字符间隔
|
|||
|
|
barcode += codes[char]
|
|||
|
|
} else {
|
|||
|
|
throw new Error('Invalid character in CODE39: ' + char)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加终止字符
|
|||
|
|
barcode += '0' // 字符间隔
|
|||
|
|
barcode += codes['*']
|
|||
|
|
|
|||
|
|
return barcode
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* EAN13编码实现
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String} data - 13位数字字符串
|
|||
|
|
* @returns {String|null} 编码后的条形码数据
|
|||
|
|
*/
|
|||
|
|
encodeEAN13(data) {
|
|||
|
|
// 确保数据是13位数字
|
|||
|
|
if (!/^\d{13}$/.test(data)) {
|
|||
|
|
throw new Error('EAN13 must be 13 digits')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证校验位
|
|||
|
|
let sum = 0
|
|||
|
|
for (let i = 0; i < 12; i++) {
|
|||
|
|
const digit = parseInt(data[i])
|
|||
|
|
sum += (i % 2 === 0) ? digit : digit * 3
|
|||
|
|
}
|
|||
|
|
const checkDigit = (10 - (sum % 10)) % 10
|
|||
|
|
if (parseInt(data[12]) !== checkDigit) {
|
|||
|
|
throw new Error('Invalid EAN13 check digit')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 左侧数据
|
|||
|
|
const leftData = data.substring(1, 7)
|
|||
|
|
const rightData = data.substring(7, 13)
|
|||
|
|
|
|||
|
|
// 起始符
|
|||
|
|
let barcode = '101'
|
|||
|
|
|
|||
|
|
// 左侧数据编码 (根据第一位数字决定编码方式)
|
|||
|
|
const firstDigit = parseInt(data[0])
|
|||
|
|
const leftPatterns = [
|
|||
|
|
['LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG',
|
|||
|
|
'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL']
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const pattern = leftPatterns[0][firstDigit]
|
|||
|
|
|
|||
|
|
// 左侧奇偶编码模式
|
|||
|
|
const leftOdd = [
|
|||
|
|
'0001101', '0011001', '0010011', '0111101', '0100011',
|
|||
|
|
'0110001', '0101111', '0111011', '0110111', '0001011'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const leftEven = [
|
|||
|
|
'0100111', '0110011', '0011011', '0100001', '0011101',
|
|||
|
|
'0111001', '0000101', '0010001', '0001001', '0010111'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 编码左侧数据
|
|||
|
|
for (let i = 0; i < leftData.length; i++) {
|
|||
|
|
const digit = parseInt(leftData[i])
|
|||
|
|
if (pattern[i] === 'L') {
|
|||
|
|
barcode += leftOdd[digit]
|
|||
|
|
} else {
|
|||
|
|
barcode += leftEven[digit]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 中间分隔符
|
|||
|
|
barcode += '01010'
|
|||
|
|
|
|||
|
|
// 右侧数据编码 (始终使用右编码)
|
|||
|
|
const rightCodes = [
|
|||
|
|
'1110010', '1100110', '1101100', '1000010', '1011100',
|
|||
|
|
'1001110', '1010000', '1000100', '1001000', '1110100'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for (let i = 0; i < rightData.length; i++) {
|
|||
|
|
const digit = parseInt(rightData[i])
|
|||
|
|
barcode += rightCodes[digit]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 结束符
|
|||
|
|
barcode += '101'
|
|||
|
|
|
|||
|
|
return barcode
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* EAN8编码实现
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String} data - 8位数字字符串
|
|||
|
|
* @returns {String|null} 编码后的条形码数据
|
|||
|
|
*/
|
|||
|
|
encodeEAN8(data) {
|
|||
|
|
// 确保数据是8位数字
|
|||
|
|
if (!/^\d{8}$/.test(data)) {
|
|||
|
|
throw new Error('EAN8 must be 8 digits')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证校验位
|
|||
|
|
let sum = 0
|
|||
|
|
for (let i = 0; i < 7; i++) {
|
|||
|
|
const digit = parseInt(data[i])
|
|||
|
|
sum += digit * (i % 2 === 0 ? 3 : 1)
|
|||
|
|
}
|
|||
|
|
const checkDigit = (10 - (sum % 10)) % 10
|
|||
|
|
if (parseInt(data[7]) !== checkDigit) {
|
|||
|
|
throw new Error('Invalid EAN8 check digit')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 左侧数据(4位)
|
|||
|
|
const leftData = data.substring(0, 4)
|
|||
|
|
// 右侧数据(4位)
|
|||
|
|
const rightData = data.substring(4, 8)
|
|||
|
|
|
|||
|
|
// 起始符
|
|||
|
|
let barcode = '101'
|
|||
|
|
|
|||
|
|
// 左侧奇偶编码模式
|
|||
|
|
const leftOdd = [
|
|||
|
|
'0001101', '0011001', '0010011', '0111101', '0100011',
|
|||
|
|
'0110001', '0101111', '0111011', '0110111', '0001011'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 编码左侧数据
|
|||
|
|
for (let i = 0; i < leftData.length; i++) {
|
|||
|
|
const digit = parseInt(leftData[i])
|
|||
|
|
barcode += leftOdd[digit]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 中间分隔符
|
|||
|
|
barcode += '01010'
|
|||
|
|
|
|||
|
|
// 右侧数据编码 (始终使用右编码)
|
|||
|
|
const rightCodes = [
|
|||
|
|
'1110010', '1100110', '1101100', '1000010', '1011100',
|
|||
|
|
'1001110', '1010000', '1000100', '1001000', '1110100'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 编码右侧数据
|
|||
|
|
for (let i = 0; i < rightData.length; i++) {
|
|||
|
|
const digit = parseInt(rightData[i])
|
|||
|
|
barcode += rightCodes[digit]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 结束符
|
|||
|
|
barcode += '101'
|
|||
|
|
|
|||
|
|
return barcode
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* EAN5/EAN2编码实现
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String} data - 2位或5位数字字符串
|
|||
|
|
* @param {String} format - 格式类型(EAN5或EAN2)
|
|||
|
|
* @returns {String|null} 编码后的条形码数据
|
|||
|
|
*/
|
|||
|
|
encodeEAN52(data, format) {
|
|||
|
|
const length = format === 'EAN5' ? 5 : 2
|
|||
|
|
// 确保数据是相应位数的数字
|
|||
|
|
if (!new RegExp(`^\\d{${length}}$`).test(data)) {
|
|||
|
|
throw new Error(`${format} must be ${length} digits`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// EAN5/2编码表
|
|||
|
|
const codes = [
|
|||
|
|
'0001101', '0011001', '0010011', '0111101', '0100011',
|
|||
|
|
'0110001', '0101111', '0111011', '0110111', '0001011'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 计算校验和
|
|||
|
|
let checksum = 0
|
|||
|
|
for (let i = 0; i < data.length; i++) {
|
|||
|
|
checksum += parseInt(data[i]) * (i % 2 === 0 ? 3 : 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据校验和确定编码模式
|
|||
|
|
const patterns = format === 'EAN5' ?
|
|||
|
|
['00001', '00010', '00100', '01000', '10000', // 0-4
|
|||
|
|
'00000', '00011', '00101', '00110', '01001', // 5-9
|
|||
|
|
'01010', '01100', '10001', '10010', '10100', // 10-14
|
|||
|
|
'11000', '11001', '11010', '11100'] : // 15-18
|
|||
|
|
['00', '01', '10', '11'] // EAN2只有4种模式
|
|||
|
|
|
|||
|
|
const pattern = format === 'EAN5' ?
|
|||
|
|
patterns[checksum % 10] :
|
|||
|
|
patterns[parseInt(data) % 4]
|
|||
|
|
|
|||
|
|
// 起始符
|
|||
|
|
let barcode = '1011'
|
|||
|
|
|
|||
|
|
// 编码数据
|
|||
|
|
for (let i = 0; i < data.length; i++) {
|
|||
|
|
// 添加分隔符(除了第一个字符)
|
|||
|
|
if (i > 0) {
|
|||
|
|
barcode += '01' // 字符间分隔符
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据模式确定编码方式
|
|||
|
|
const digit = parseInt(data[i])
|
|||
|
|
const code = codes[digit]
|
|||
|
|
|
|||
|
|
barcode += code
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return barcode
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* UPCA编码实现
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String} data - 11位或12位数字字符串
|
|||
|
|
* @returns {String|null} 编码后的条形码数据
|
|||
|
|
*/
|
|||
|
|
encodeUPCA(data) {
|
|||
|
|
// 如果是11位,计算校验位
|
|||
|
|
if (/^\d{11}$/.test(data)) {
|
|||
|
|
let sum = 0
|
|||
|
|
for (let i = 0; i < 11; i++) {
|
|||
|
|
const digit = parseInt(data[i])
|
|||
|
|
sum += (i % 2 === 0) ? digit * 3 : digit
|
|||
|
|
}
|
|||
|
|
const checkDigit = (10 - (sum % 10)) % 10
|
|||
|
|
data += checkDigit
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保数据是12位数字
|
|||
|
|
if (!/^\d{12}$/.test(data)) {
|
|||
|
|
throw new Error('UPC-A must be 11 or 12 digits')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UPCA实际上是EAN13的第一个数字为0的特殊情况
|
|||
|
|
return this.encodeEAN13('0' + data)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* UPCE编码实现
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {String} data - 6位或8位数字字符串
|
|||
|
|
* @returns {String|null} 编码后的条形码数据
|
|||
|
|
*/
|
|||
|
|
encodeUPCE(data) {
|
|||
|
|
// 如果是7位,计算校验位
|
|||
|
|
if (/^\d{7}$/.test(data)) {
|
|||
|
|
let sum = 0
|
|||
|
|
for (let i = 0; i < 7; i++) {
|
|||
|
|
const digit = parseInt(data[i])
|
|||
|
|
sum += (i % 2 === 0) ? digit * 3 : digit
|
|||
|
|
}
|
|||
|
|
const checkDigit = (10 - (sum % 10)) % 10
|
|||
|
|
data += checkDigit
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保数据是8位数字
|
|||
|
|
if (!/^\d{8}$/.test(data)) {
|
|||
|
|
throw new Error('UPC-E must be 7 or 8 digits')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否是有效的UPC-E格式
|
|||
|
|
if (data[0] !== '0' && data[0] !== '1') {
|
|||
|
|
throw new Error('UPC-E must start with 0 or 1')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UPCE编码表
|
|||
|
|
const leftOdd = [
|
|||
|
|
'0001101', '0011001', '0010011', '0111101', '0100011',
|
|||
|
|
'0110001', '0101111', '0111011', '0110111', '0001011'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const leftEven = [
|
|||
|
|
'0100111', '0110011', '0011011', '0100001', '0011101',
|
|||
|
|
'0111001', '0000101', '0010001', '0001001', '0010111'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 根据系统数字确定编码模式
|
|||
|
|
const systemDigit = data[0]
|
|||
|
|
const checkDigit = data[7]
|
|||
|
|
|
|||
|
|
// 提取中间6位
|
|||
|
|
const middleData = data.substring(1, 7)
|
|||
|
|
|
|||
|
|
// 确定编码模式
|
|||
|
|
let pattern
|
|||
|
|
if (checkDigit === '0' || checkDigit === '1' || checkDigit === '2') {
|
|||
|
|
pattern = 'EEEEOO' // 0,1,2
|
|||
|
|
} else if (checkDigit === '3') {
|
|||
|
|
pattern = 'EEEEOO' // 3
|
|||
|
|
} else if (checkDigit === '4') {
|
|||
|
|
pattern = 'EEEOOO' // 4
|
|||
|
|
} else {
|
|||
|
|
pattern = 'EEOOOO' // 5,6,7,8,9
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 起始符
|
|||
|
|
let barcode = '101'
|
|||
|
|
|
|||
|
|
// 编码中间6位数据
|
|||
|
|
for (let i = 0; i < middleData.length; i++) {
|
|||
|
|
const digit = parseInt(middleData[i])
|
|||
|
|
if (pattern[i] === 'E') {
|
|||
|
|
barcode += leftEven[digit]
|
|||
|
|
} else {
|
|||
|
|
barcode += leftOdd[digit]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 中间分隔符
|
|||
|
|
barcode += '010101'
|
|||
|
|
|
|||
|
|
// 结束符
|
|||
|
|
barcode += '101'
|
|||
|
|
|
|||
|
|
return barcode
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 绘制条形码
|
|||
|
|
* @author jry <ijry@qq.com>
|
|||
|
|
* @param {Object} ctx - canvas上下文
|
|||
|
|
* @param {String} barcodeData - 条形码数据
|
|||
|
|
* @param {Object} options - 条码配置选项
|
|||
|
|
*/
|
|||
|
|
drawBarcode(ctx, barcodeData, options) {
|
|||
|
|
if (!barcodeData) return
|
|||
|
|
|
|||
|
|
const marginLeft = options.marginLeft
|
|||
|
|
const marginTop = options.marginTop
|
|||
|
|
const marginBottom = options.marginBottom
|
|||
|
|
const textHeight = options.displayValue !== false ? options.fontSize + options.textMargin : 0
|
|||
|
|
const height = options.height
|
|||
|
|
// 恢复: 根据总长度计算模块宽度
|
|||
|
|
const moduleWidth = Math.max(1, (this.canvasWidth - marginLeft - (options.marginRight || 10)) / barcodeData.length)
|
|||
|
|
|
|||
|
|
ctx.setFillStyle(options.lineColor)
|
|||
|
|
|
|||
|
|
// 计算条形码绘制的Y坐标
|
|||
|
|
let barcodeY = marginTop
|
|||
|
|
// 如果文本在顶部,需要调整条形码绘制位置
|
|||
|
|
if (options.displayValue !== false && options.textPosition === 'top') {
|
|||
|
|
barcodeY += textHeight
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 恢复: 使用计算出的模块宽度绘制条形码
|
|||
|
|
let x = marginLeft
|
|||
|
|
for (let i = 0; i < barcodeData.length; i++) {
|
|||
|
|
if (barcodeData[i] === '1') {
|
|||
|
|
// 使用计算的模块宽度绘制每个条
|
|||
|
|
ctx.fillRect(x, barcodeY, moduleWidth, height)
|
|||
|
|
}
|
|||
|
|
// 每个条/空都占用一个模块宽度
|
|||
|
|
x += moduleWidth
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绘制文本
|
|||
|
|
if (options.displayValue !== false) {
|
|||
|
|
const text = options.text || this.value
|
|||
|
|
let textY
|
|||
|
|
|
|||
|
|
ctx.setFillStyle(options.lineColor)
|
|||
|
|
ctx.setFontSize(options.fontSize)
|
|||
|
|
ctx.setTextAlign(options.textAlign)
|
|||
|
|
|
|||
|
|
let textX
|
|||
|
|
switch (options.textAlign) {
|
|||
|
|
case 'left':
|
|||
|
|
textX = marginLeft
|
|||
|
|
break
|
|||
|
|
case 'right':
|
|||
|
|
textX = this.canvasWidth - options.marginRight
|
|||
|
|
break
|
|||
|
|
default: // center
|
|||
|
|
textX = this.canvasWidth / 2
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据文本位置确定Y坐标
|
|||
|
|
if (options.textPosition === 'top') {
|
|||
|
|
textY = marginTop + options.fontSize - 3
|
|||
|
|
} else { // bottom
|
|||
|
|
// 修复:正确计算底部文本位置,确保文本完全显示
|
|||
|
|
textY = barcodeY + height + options.textMargin + options.fontSize
|
|||
|
|
// 确保文本不会超出画布边界
|
|||
|
|
if (textY > this.canvasHeight - marginBottom) {
|
|||
|
|
textY = this.canvasHeight - marginBottom - 2
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ctx.fillText(text, textX, textY)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.u-barcode {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: row;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-container {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
background-color: #f0f0f0;
|
|||
|
|
color: #ff0000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-text {
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
</style>
|