-
8api/book.js
-
20components/book-list-item/book-list-item.vue
-
210pages/home/home.vue
-
13pages/lendCar/lendCar.vue
-
41pages/login/login.vue
-
17pages/search/search.vue
-
93pages/user/user.vue
-
4static/iconfont.css
-
BINstatic/iconfont.ttf
-
BINstatic/images/activity1.png
-
BINstatic/images/activity2.png
-
BINstatic/images/avatar.png
-
BINstatic/images/card-img1.png
-
BINstatic/images/card-img2.png
-
BINstatic/images/default-avatar.png
-
BINstatic/images/default-book.png
-
BINstatic/images/logo.jpg
-
BINstatic/images/menu-jy.png
-
BINstatic/images/menu-sc.png
-
BINstatic/images/mingqi-beij@2x.png
-
BINstatic/images/mylending-img1.png
-
BINstatic/images/mylending-img2.png
-
BINstatic/images/mylending-img3.png
-
BINstatic/images/ranking-bg.png
-
49styles/global.scss
-
219subpkg/pages/activity-detail/activity-detail.vue
-
282subpkg/pages/activity-list/activity-list.vue
-
43subpkg/pages/book-detail/book-detail.vue
-
75subpkg/pages/book-list/book-list.vue
-
36subpkg/pages/collect-list/collect-list.vue
-
2subpkg/pages/feedback-list/feedback-list.vue
-
68subpkg/pages/myLending/myLending.vue
-
99subpkg/pages/reader-card/reader-card.vue
-
122subpkg/pages/user-info/user-info.vue
-
16uni_modules/w-qrcode/changelog.md
-
732uni_modules/w-qrcode/components/w-qrcode/w-qrcode.vue
-
619uni_modules/w-qrcode/js_sdk/qrcode.js
-
104uni_modules/w-qrcode/package.json
-
229uni_modules/w-qrcode/readme.md
|
After Width: 675 | Height: 849 | Size: 107 KiB |
|
After Width: 673 | Height: 1102 | Size: 188 KiB |
|
Before Width: 192 | Height: 192 | Size: 4.3 KiB |
|
Before Width: 610 | Height: 245 | Size: 100 KiB After Width: 610 | Height: 245 | Size: 26 KiB |
|
Before Width: 80 | Height: 80 | Size: 8.8 KiB After Width: 80 | Height: 80 | Size: 3.2 KiB |
|
Before Width: 124 | Height: 124 | Size: 5.3 KiB After Width: 124 | Height: 124 | Size: 1.4 KiB |
|
Before Width: 756 | Height: 1063 | Size: 8.1 KiB After Width: 574 | Height: 800 | Size: 157 KiB |
|
After Width: 640 | Height: 640 | Size: 18 KiB |
|
Before Width: 64 | Height: 64 | Size: 1.7 KiB After Width: 64 | Height: 64 | Size: 663 B |
|
Before Width: 64 | Height: 64 | Size: 1.9 KiB After Width: 64 | Height: 64 | Size: 684 B |
|
Before Width: 750 | Height: 396 | Size: 181 KiB After Width: 750 | Height: 396 | Size: 36 KiB |
|
Before Width: 122 | Height: 107 | Size: 12 KiB After Width: 122 | Height: 107 | Size: 3.1 KiB |
|
Before Width: 122 | Height: 107 | Size: 7.4 KiB After Width: 122 | Height: 107 | Size: 2.4 KiB |
|
Before Width: 122 | Height: 107 | Size: 11 KiB After Width: 122 | Height: 107 | Size: 2.9 KiB |
|
Before Width: 750 | Height: 270 | Size: 125 KiB After Width: 750 | Height: 270 | Size: 46 KiB |
@ -0,0 +1,16 @@ |
|||
## 1.0.1(2025-12-30) |
|||
## 1.0.1(2025-12-30) |
|||
- 优化二维码生成逻辑 |
|||
## 1.0.0(2025-12-29) |
|||
- 初始版本发布 |
|||
- 纯 JavaScript 实现二维码生成算法 |
|||
- 支持 Vue2 和 Vue3 |
|||
- 支持自定义前景色、背景色 |
|||
- 支持添加 Logo 图片 |
|||
- 支持多种纠错级别(L/M/Q/H) |
|||
- 支持保存到相册 |
|||
- 支持导出临时文件和 Base64 |
|||
- 兼容全平台:H5、App、各类小程序 |
|||
|
|||
## 1.0.1(2025-12-30) |
|||
- 优化二维码生成逻辑 |
|||
@ -0,0 +1,732 @@ |
|||
<template> |
|||
<view class="w-qrcode" :style="rootStyle"> |
|||
<!-- #ifdef MP-WEIXIN --> |
|||
<canvas |
|||
v-if="canvas2d" |
|||
:id="canvasId" |
|||
:canvas-id="canvasId" |
|||
type="2d" |
|||
:style="canvasStyle" |
|||
@longpress="onLongpress" |
|||
></canvas> |
|||
<canvas |
|||
v-else |
|||
:id="canvasId" |
|||
:canvas-id="canvasId" |
|||
:style="canvasStyle" |
|||
@longpress="onLongpress" |
|||
></canvas> |
|||
<!-- #endif --> |
|||
|
|||
<!-- #ifdef H5 --> |
|||
<canvas |
|||
:id="canvasId" |
|||
:canvas-id="canvasId" |
|||
:style="canvasStyle" |
|||
:width="canvasWidth" |
|||
:height="canvasHeight" |
|||
@longpress="onLongpress" |
|||
></canvas> |
|||
<!-- #endif --> |
|||
|
|||
<!-- #ifdef APP-PLUS --> |
|||
<canvas |
|||
:id="canvasId" |
|||
:canvas-id="canvasId" |
|||
:style="canvasStyle" |
|||
@longpress="onLongpress" |
|||
></canvas> |
|||
<!-- #endif --> |
|||
|
|||
<!-- #ifdef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU || MP-LARK || MP-JD --> |
|||
<canvas |
|||
:id="canvasId" |
|||
:canvas-id="canvasId" |
|||
:style="canvasStyle" |
|||
@longpress="onLongpress" |
|||
></canvas> |
|||
<!-- #endif --> |
|||
|
|||
<!-- 用于非Canvas渲染场景的备用方案 --> |
|||
<view v-if="showFallback" class="w-qrcode__fallback" :style="fallbackStyle"> |
|||
<view |
|||
v-for="(row, rowIndex) in modules" |
|||
:key="rowIndex" |
|||
class="w-qrcode__row" |
|||
:style="rowStyle" |
|||
> |
|||
<view |
|||
v-for="(cell, colIndex) in row" |
|||
:key="colIndex" |
|||
class="w-qrcode__cell" |
|||
:style="cell ? darkCellStyle : lightCellStyle" |
|||
></view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import QRCode from '../../js_sdk/qrcode.js'; |
|||
|
|||
// 生成唯一ID |
|||
let uid = 0; |
|||
const generateId = () => `w-qrcode-${++uid}-${Date.now()}`; |
|||
|
|||
// 获取系统信息用于 rpx 转换 |
|||
let systemInfo = null; |
|||
const getSystemInfo = () => { |
|||
if (!systemInfo) { |
|||
systemInfo = uni.getSystemInfoSync(); |
|||
} |
|||
return systemInfo; |
|||
}; |
|||
|
|||
// 单位转换:支持 px、rpx、数字 |
|||
const parseSize = (size) => { |
|||
if (typeof size === 'number') { |
|||
return size; |
|||
} |
|||
const str = String(size).trim(); |
|||
if (str.endsWith('rpx')) { |
|||
const rpxValue = parseFloat(str); |
|||
const { windowWidth } = getSystemInfo(); |
|||
return Math.round(rpxValue * windowWidth / 750); |
|||
} |
|||
if (str.endsWith('px')) { |
|||
return parseFloat(str); |
|||
} |
|||
return parseFloat(str) || 0; |
|||
}; |
|||
|
|||
export default { |
|||
name: 'w-qrcode', |
|||
|
|||
emits: ['generated', 'error', 'longpress'], |
|||
|
|||
props: { |
|||
// 要编码的文本 |
|||
value: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 二维码尺寸(支持 px、rpx) |
|||
size: { |
|||
type: [Number, String], |
|||
default: 200 |
|||
}, |
|||
// 前景色 |
|||
foreground: { |
|||
type: String, |
|||
default: '#000000' |
|||
}, |
|||
// 背景色 |
|||
background: { |
|||
type: String, |
|||
default: '#FFFFFF' |
|||
}, |
|||
// 纠错级别 L/M/Q/H |
|||
errorCorrectionLevel: { |
|||
type: String, |
|||
default: 'M', |
|||
validator: (val) => ['L', 'M', 'Q', 'H'].includes(val) |
|||
}, |
|||
// Logo 图片路径 |
|||
logo: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// Logo 尺寸(支持 px、rpx) |
|||
logoSize: { |
|||
type: [Number, String], |
|||
default: 50 |
|||
}, |
|||
// Logo 边距 |
|||
logoMargin: { |
|||
type: [Number, String], |
|||
default: 5 |
|||
}, |
|||
// Logo 圆角 |
|||
logoRadius: { |
|||
type: [Number, String], |
|||
default: 4 |
|||
}, |
|||
// Logo 背景色 |
|||
logoBackground: { |
|||
type: String, |
|||
default: '#FFFFFF' |
|||
}, |
|||
// 是否使用 Canvas 2D(仅微信小程序) |
|||
canvas2d: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
canvasId: generateId(), |
|||
modules: [], |
|||
showFallback: false, |
|||
isReady: false, |
|||
ctx: null, |
|||
canvasNode: null, |
|||
pixelRatio: 1 |
|||
}; |
|||
}, |
|||
|
|||
computed: { |
|||
// 转换后的像素尺寸 |
|||
sizePx() { |
|||
return Math.floor(parseSize(this.size)); |
|||
}, |
|||
logoSizePx() { |
|||
return Math.floor(parseSize(this.logoSize)); |
|||
}, |
|||
logoMarginPx() { |
|||
return Math.floor(parseSize(this.logoMargin)); |
|||
}, |
|||
logoRadiusPx() { |
|||
return Math.floor(parseSize(this.logoRadius)); |
|||
}, |
|||
// H5 canvas 属性 |
|||
canvasWidth() { |
|||
return this.sizePx; |
|||
}, |
|||
canvasHeight() { |
|||
return this.sizePx; |
|||
}, |
|||
rootStyle() { |
|||
const size = this.sizePx; |
|||
return `width: ${size}px; height: ${size}px; display: inline-block;`; |
|||
}, |
|||
canvasStyle() { |
|||
const size = this.sizePx; |
|||
return `width: ${size}px; height: ${size}px; display: block;`; |
|||
}, |
|||
fallbackStyle() { |
|||
const size = this.sizePx; |
|||
return { |
|||
width: `${size}px`, |
|||
height: `${size}px`, |
|||
backgroundColor: this.background |
|||
}; |
|||
}, |
|||
// 计算每个模块的像素大小 |
|||
cellSizePx() { |
|||
if (!this.modules.length) return 0; |
|||
const moduleCount = this.modules.length; |
|||
return Math.floor(this.sizePx / moduleCount); |
|||
}, |
|||
// 偏移量(居中) |
|||
offset() { |
|||
if (!this.modules.length) return 0; |
|||
const actualSize = this.cellSizePx * this.modules.length; |
|||
return Math.floor((this.sizePx - actualSize) / 2); |
|||
}, |
|||
rowStyle() { |
|||
return { |
|||
height: `${this.cellSizePx}px` |
|||
}; |
|||
}, |
|||
darkCellStyle() { |
|||
return { |
|||
width: `${this.cellSizePx}px`, |
|||
height: `${this.cellSizePx}px`, |
|||
backgroundColor: this.foreground |
|||
}; |
|||
}, |
|||
lightCellStyle() { |
|||
return { |
|||
width: `${this.cellSizePx}px`, |
|||
height: `${this.cellSizePx}px`, |
|||
backgroundColor: this.background |
|||
}; |
|||
} |
|||
}, |
|||
|
|||
watch: { |
|||
value: { |
|||
handler(val) { |
|||
if (val) { |
|||
this.$nextTick(() => { |
|||
this.generate(); |
|||
}); |
|||
} |
|||
}, |
|||
immediate: true |
|||
}, |
|||
foreground() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
background() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
size() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
logo() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
logoSize() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
logoRadius() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
logoMargin() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
logoBackground() { |
|||
this.$nextTick(() => this.draw()); |
|||
}, |
|||
errorCorrectionLevel() { |
|||
this.$nextTick(() => this.generate()); |
|||
} |
|||
}, |
|||
|
|||
mounted() { |
|||
this.pixelRatio = getSystemInfo().pixelRatio || 1; |
|||
this.isReady = true; |
|||
if (this.value) { |
|||
// 延迟执行确保 canvas 已渲染 |
|||
setTimeout(() => { |
|||
this.generate(); |
|||
}, 50); |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
// 生成二维码数据 |
|||
generate() { |
|||
if (!this.value) { |
|||
this.modules = []; |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
const result = QRCode.generate(this.value, { |
|||
errorCorrectionLevel: this.errorCorrectionLevel |
|||
}); |
|||
|
|||
this.modules = result.modules; |
|||
this.$emit('generated', { |
|||
version: result.version, |
|||
size: result.size, |
|||
errorCorrectionLevel: result.errorCorrectionLevel |
|||
}); |
|||
|
|||
this.$nextTick(() => { |
|||
setTimeout(() => { |
|||
this.draw(); |
|||
}, 20); |
|||
}); |
|||
} catch (error) { |
|||
console.error('[w-qrcode] Generate error:', error); |
|||
this.$emit('error', error); |
|||
} |
|||
}, |
|||
|
|||
// 绘制二维码 |
|||
draw() { |
|||
if (!this.modules.length || !this.isReady) return; |
|||
|
|||
// #ifdef MP-WEIXIN |
|||
if (this.canvas2d) { |
|||
this.drawCanvas2D(); |
|||
return; |
|||
} |
|||
this.drawCanvasLegacy(); |
|||
return; |
|||
// #endif |
|||
|
|||
// #ifdef APP-NVUE |
|||
this.showFallback = true; |
|||
return; |
|||
// #endif |
|||
|
|||
// #ifdef H5 |
|||
this.drawCanvasH5(); |
|||
return; |
|||
// #endif |
|||
|
|||
// #ifdef APP-PLUS |
|||
this.drawCanvasLegacy(); |
|||
return; |
|||
// #endif |
|||
|
|||
// #ifndef MP-WEIXIN || APP-NVUE || H5 || APP-PLUS |
|||
this.drawCanvasLegacy(); |
|||
// #endif |
|||
}, |
|||
|
|||
// H5 Canvas 绘制 |
|||
drawCanvasH5() { |
|||
const query = uni.createSelectorQuery().in(this); |
|||
query.select(`#${this.canvasId}`) |
|||
.fields({ node: true, size: true }) |
|||
.exec((res) => { |
|||
if (res && res[0] && res[0].node) { |
|||
const canvas = res[0].node; |
|||
const ctx = canvas.getContext('2d'); |
|||
|
|||
// 设置 canvas 实际像素大小 |
|||
const dpr = this.pixelRatio; |
|||
canvas.width = this.sizePx * dpr; |
|||
canvas.height = this.sizePx * dpr; |
|||
ctx.scale(dpr, dpr); |
|||
|
|||
this.canvasNode = canvas; |
|||
this.ctx = ctx; |
|||
this.renderQRCode(ctx, canvas, true); |
|||
} else { |
|||
// 降级处理 |
|||
this.drawCanvasLegacy(); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// Canvas 2D 绘制(微信小程序) |
|||
drawCanvas2D() { |
|||
const query = uni.createSelectorQuery().in(this); |
|||
query.select(`#${this.canvasId}`) |
|||
.fields({ node: true, size: true }) |
|||
.exec((res) => { |
|||
if (!res || !res[0] || !res[0].node) { |
|||
this.drawCanvasLegacy(); |
|||
return; |
|||
} |
|||
|
|||
const canvas = res[0].node; |
|||
const ctx = canvas.getContext('2d'); |
|||
const dpr = this.pixelRatio; |
|||
|
|||
canvas.width = this.sizePx * dpr; |
|||
canvas.height = this.sizePx * dpr; |
|||
ctx.scale(dpr, dpr); |
|||
|
|||
this.canvasNode = canvas; |
|||
this.ctx = ctx; |
|||
|
|||
this.renderQRCode(ctx, canvas, true); |
|||
}); |
|||
}, |
|||
|
|||
// 旧版 Canvas 绘制 |
|||
drawCanvasLegacy() { |
|||
const ctx = uni.createCanvasContext(this.canvasId, this); |
|||
this.ctx = ctx; |
|||
this.renderQRCodeLegacy(ctx); |
|||
}, |
|||
|
|||
// 统一的二维码渲染方法(Canvas 2D / H5) |
|||
renderQRCode(ctx, canvas, isCanvas2D) { |
|||
const size = this.sizePx; |
|||
const modules = this.modules; |
|||
const moduleCount = modules.length; |
|||
const cellSize = this.cellSizePx; |
|||
const offset = this.offset; |
|||
|
|||
// 清空并绘制背景 |
|||
ctx.clearRect(0, 0, size, size); |
|||
ctx.fillStyle = this.background; |
|||
ctx.fillRect(0, 0, size, size); |
|||
|
|||
// 绘制二维码模块 |
|||
ctx.fillStyle = this.foreground; |
|||
for (let row = 0; row < moduleCount; row++) { |
|||
for (let col = 0; col < moduleCount; col++) { |
|||
if (modules[row][col]) { |
|||
const x = offset + col * cellSize; |
|||
const y = offset + row * cellSize; |
|||
ctx.fillRect(x, y, cellSize, cellSize); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 绘制 Logo |
|||
if (this.logo && canvas) { |
|||
this.renderLogo(ctx, canvas); |
|||
} |
|||
}, |
|||
|
|||
// 旧版 Canvas 渲染 |
|||
renderQRCodeLegacy(ctx) { |
|||
const size = this.sizePx; |
|||
const modules = this.modules; |
|||
const moduleCount = modules.length; |
|||
const cellSize = this.cellSizePx; |
|||
const offset = this.offset; |
|||
|
|||
// 绘制背景 |
|||
ctx.setFillStyle(this.background); |
|||
ctx.fillRect(0, 0, size, size); |
|||
|
|||
// 绘制二维码模块 |
|||
ctx.setFillStyle(this.foreground); |
|||
for (let row = 0; row < moduleCount; row++) { |
|||
for (let col = 0; col < moduleCount; col++) { |
|||
if (modules[row][col]) { |
|||
const x = offset + col * cellSize; |
|||
const y = offset + row * cellSize; |
|||
ctx.fillRect(x, y, cellSize, cellSize); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 绘制 Logo |
|||
if (this.logo) { |
|||
this.renderLogoLegacy(ctx); |
|||
} else { |
|||
ctx.draw(); |
|||
} |
|||
}, |
|||
|
|||
// 绘制 Logo(Canvas 2D / H5) |
|||
renderLogo(ctx, canvas) { |
|||
const size = this.sizePx; |
|||
const logoSize = this.logoSizePx; |
|||
const logoMargin = this.logoMarginPx; |
|||
const logoRadius = this.logoRadiusPx; |
|||
const x = Math.floor((size - logoSize) / 2); |
|||
const y = Math.floor((size - logoSize) / 2); |
|||
const bgSize = logoSize + logoMargin * 2; |
|||
const bgX = Math.floor((size - bgSize) / 2); |
|||
const bgY = Math.floor((size - bgSize) / 2); |
|||
|
|||
// 绘制 Logo 背景 |
|||
ctx.fillStyle = this.logoBackground; |
|||
this.drawRoundRect(ctx, bgX, bgY, bgSize, bgSize, logoRadius + logoMargin / 2); |
|||
ctx.fill(); |
|||
|
|||
// 绘制 Logo 图片 |
|||
const img = canvas.createImage ? canvas.createImage() : new Image(); |
|||
img.onload = () => { |
|||
ctx.save(); |
|||
this.drawRoundRect(ctx, x, y, logoSize, logoSize, logoRadius); |
|||
ctx.clip(); |
|||
ctx.drawImage(img, x, y, logoSize, logoSize); |
|||
ctx.restore(); |
|||
}; |
|||
img.onerror = (err) => { |
|||
console.error('[w-qrcode] Logo load error:', err); |
|||
}; |
|||
img.src = this.logo; |
|||
}, |
|||
|
|||
// 绘制 Logo(旧版 Canvas) |
|||
renderLogoLegacy(ctx) { |
|||
const size = this.sizePx; |
|||
const logoSize = this.logoSizePx; |
|||
const logoMargin = this.logoMarginPx; |
|||
const logoRadius = this.logoRadiusPx; |
|||
const x = Math.floor((size - logoSize) / 2); |
|||
const y = Math.floor((size - logoSize) / 2); |
|||
const bgSize = logoSize + logoMargin * 2; |
|||
const bgX = Math.floor((size - bgSize) / 2); |
|||
const bgY = Math.floor((size - bgSize) / 2); |
|||
|
|||
// 绘制 Logo 背景 |
|||
ctx.setFillStyle(this.logoBackground); |
|||
this.drawRoundRect(ctx, bgX, bgY, bgSize, bgSize, logoRadius + logoMargin / 2); |
|||
ctx.fill(); |
|||
|
|||
// 绘制 Logo 图片 |
|||
ctx.save(); |
|||
this.drawRoundRect(ctx, x, y, logoSize, logoSize, logoRadius); |
|||
ctx.clip(); |
|||
ctx.drawImage(this.logo, x, y, logoSize, logoSize); |
|||
ctx.restore(); |
|||
ctx.draw(); |
|||
}, |
|||
|
|||
// 绘制圆角矩形 |
|||
drawRoundRect(ctx, x, y, width, height, radius) { |
|||
if (radius <= 0) { |
|||
ctx.beginPath(); |
|||
ctx.rect(x, y, width, height); |
|||
ctx.closePath(); |
|||
return; |
|||
} |
|||
ctx.beginPath(); |
|||
ctx.moveTo(x + radius, y); |
|||
ctx.lineTo(x + width - radius, y); |
|||
ctx.arcTo(x + width, y, x + width, y + radius, radius); |
|||
ctx.lineTo(x + width, y + height - radius); |
|||
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius); |
|||
ctx.lineTo(x + radius, y + height); |
|||
ctx.arcTo(x, y + height, x, y + height - radius, radius); |
|||
ctx.lineTo(x, y + radius); |
|||
ctx.arcTo(x, y, x + radius, y, radius); |
|||
ctx.closePath(); |
|||
}, |
|||
|
|||
// 长按事件 |
|||
onLongpress(e) { |
|||
this.$emit('longpress', e); |
|||
}, |
|||
|
|||
// 保存到相册 |
|||
saveToAlbum() { |
|||
return new Promise((resolve, reject) => { |
|||
// #ifdef MP-WEIXIN |
|||
if (this.canvas2d && this.canvasNode) { |
|||
uni.canvasToTempFilePath({ |
|||
canvas: this.canvasNode, |
|||
success: (res) => { |
|||
this.saveImage(res.tempFilePath).then(resolve).catch(reject); |
|||
}, |
|||
fail: reject |
|||
}); |
|||
return; |
|||
} |
|||
// #endif |
|||
|
|||
// #ifdef H5 |
|||
if (this.canvasNode) { |
|||
try { |
|||
const dataURL = this.canvasNode.toDataURL('image/png'); |
|||
// H5 下载图片 |
|||
const link = document.createElement('a'); |
|||
link.download = 'qrcode.png'; |
|||
link.href = dataURL; |
|||
link.click(); |
|||
resolve({ tempFilePath: dataURL }); |
|||
return; |
|||
} catch (e) { |
|||
reject(e); |
|||
return; |
|||
} |
|||
} |
|||
// #endif |
|||
|
|||
uni.canvasToTempFilePath({ |
|||
canvasId: this.canvasId, |
|||
success: (res) => { |
|||
this.saveImage(res.tempFilePath).then(resolve).catch(reject); |
|||
}, |
|||
fail: reject |
|||
}, this); |
|||
}); |
|||
}, |
|||
|
|||
// 保存图片 |
|||
saveImage(tempFilePath) { |
|||
return new Promise((resolve, reject) => { |
|||
// #ifdef H5 |
|||
resolve({ tempFilePath }); |
|||
return; |
|||
// #endif |
|||
|
|||
// #ifndef H5 |
|||
uni.saveImageToPhotosAlbum({ |
|||
filePath: tempFilePath, |
|||
success: () => { |
|||
uni.showToast({ |
|||
title: '保存成功', |
|||
icon: 'success' |
|||
}); |
|||
resolve({ tempFilePath }); |
|||
}, |
|||
fail: (err) => { |
|||
if (err.errMsg && err.errMsg.includes('auth deny')) { |
|||
uni.showModal({ |
|||
title: '提示', |
|||
content: '需要您授权保存相册权限', |
|||
showCancel: false |
|||
}); |
|||
} |
|||
reject(err); |
|||
} |
|||
}); |
|||
// #endif |
|||
}); |
|||
}, |
|||
|
|||
// 获取临时文件路径 |
|||
toTempFilePath(options = {}) { |
|||
return new Promise((resolve, reject) => { |
|||
const defaultOptions = { |
|||
fileType: 'png', |
|||
quality: 1, |
|||
...options |
|||
}; |
|||
|
|||
// #ifdef MP-WEIXIN |
|||
if (this.canvas2d && this.canvasNode) { |
|||
uni.canvasToTempFilePath({ |
|||
...defaultOptions, |
|||
canvas: this.canvasNode, |
|||
success: resolve, |
|||
fail: reject |
|||
}); |
|||
return; |
|||
} |
|||
// #endif |
|||
|
|||
// #ifdef H5 |
|||
if (this.canvasNode) { |
|||
try { |
|||
const dataURL = this.canvasNode.toDataURL('image/png'); |
|||
resolve({ tempFilePath: dataURL }); |
|||
return; |
|||
} catch (e) { |
|||
reject(e); |
|||
return; |
|||
} |
|||
} |
|||
// #endif |
|||
|
|||
uni.canvasToTempFilePath({ |
|||
...defaultOptions, |
|||
canvasId: this.canvasId, |
|||
success: resolve, |
|||
fail: reject |
|||
}, this); |
|||
}); |
|||
}, |
|||
|
|||
// 获取 Base64 |
|||
toDataURL(type = 'image/png', quality = 1) { |
|||
return new Promise((resolve, reject) => { |
|||
// #ifdef H5 |
|||
if (this.canvasNode) { |
|||
try { |
|||
resolve(this.canvasNode.toDataURL(type, quality)); |
|||
return; |
|||
} catch (e) { |
|||
reject(e); |
|||
return; |
|||
} |
|||
} |
|||
// #endif |
|||
|
|||
reject(new Error('toDataURL is only supported on H5')); |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.w-qrcode { |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.w-qrcode__fallback { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.w-qrcode__row { |
|||
display: flex; |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.w-qrcode__cell { |
|||
flex-shrink: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,619 @@ |
|||
/** |
|||
* QRCode Generator - 纯JavaScript实现 |
|||
* 基于 ISO/IEC 18004 标准 |
|||
* @version 2.0.0 |
|||
*/ |
|||
|
|||
// 纠错级别 - 使用标准索引
|
|||
const ECL = { L: 0, M: 1, Q: 2, H: 3 }; |
|||
|
|||
// 模式指示符
|
|||
const MODE = { |
|||
Numeric: 0b0001, |
|||
Alphanumeric: 0b0010, |
|||
Byte: 0b0100, |
|||
Kanji: 0b1000 |
|||
}; |
|||
|
|||
// 字母数字字符映射
|
|||
const ALPHANUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; |
|||
|
|||
// 各版本各纠错级别的数据容量(字节模式)
|
|||
const CAPACITIES = [ |
|||
[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44], |
|||
[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119], |
|||
[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220], |
|||
[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382], |
|||
[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535], |
|||
[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742], |
|||
[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983], |
|||
[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273] |
|||
]; |
|||
|
|||
// RS块信息表 [总码字, [各纠错级别的纠错码字数], [各纠错级别的块数(组1,组2)]]
|
|||
const RS_BLOCKS = [ |
|||
[[26,[7,10,13,17],[[1,0],[1,0],[1,0],[1,0]]]], |
|||
[[44,[10,16,22,28],[[1,0],[1,0],[1,0],[1,0]]]], |
|||
[[70,[15,26,18,22],[[1,0],[1,0],[2,0],[2,0]]]], |
|||
[[100,[20,18,26,16],[[1,0],[2,0],[2,0],[4,0]]]], |
|||
[[134,[26,24,18,22],[[1,0],[2,0],[4,0],[4,0]]]], |
|||
[[172,[18,16,24,28],[[2,0],[4,0],[4,0],[4,0]]]], |
|||
[[196,[20,18,18,26],[[2,0],[4,0],[6,0],[5,0]]]], |
|||
[[242,[24,22,22,26],[[2,0],[4,0],[6,0],[6,0]]]], |
|||
[[292,[30,22,20,24],[[2,0],[5,0],[8,0],[8,0]]]], |
|||
[[346,[18,26,24,28],[[4,0],[5,0],[8,0],[8,0]]]], |
|||
[[404,[20,30,28,24],[[4,0],[5,0],[8,0],[11,0]]]], |
|||
[[466,[24,22,26,28],[[4,0],[8,0],[10,0],[11,0]]]], |
|||
[[532,[26,22,24,22],[[4,0],[9,0],[12,0],[16,0]]]], |
|||
[[581,[30,24,20,24],[[4,0],[9,0],[16,0],[16,0]]]], |
|||
[[655,[22,24,30,24],[[6,0],[10,0],[12,0],[18,0]]]], |
|||
[[733,[24,28,24,30],[[6,0],[10,0],[17,0],[16,0]]]], |
|||
[[815,[28,28,28,28],[[6,0],[11,0],[16,0],[19,0]]]], |
|||
[[901,[30,26,28,28],[[6,0],[13,0],[18,0],[21,0]]]], |
|||
[[991,[28,26,26,26],[[7,0],[14,0],[21,0],[25,0]]]], |
|||
[[1085,[28,26,30,28],[[8,0],[16,0],[20,0],[25,0]]]], |
|||
[[1156,[28,26,28,30],[[8,0],[17,0],[23,0],[25,0]]]], |
|||
[[1258,[28,28,30,24],[[9,0],[17,0],[23,0],[34,0]]]], |
|||
[[1364,[30,28,30,30],[[9,0],[18,0],[25,0],[30,0]]]], |
|||
[[1474,[30,28,30,30],[[10,0],[20,0],[27,0],[32,0]]]], |
|||
[[1588,[26,28,30,30],[[12,0],[21,0],[29,0],[35,0]]]], |
|||
[[1706,[28,28,28,30],[[12,0],[23,0],[34,0],[37,0]]]], |
|||
[[1828,[30,28,30,30],[[12,0],[25,0],[34,0],[40,0]]]], |
|||
[[1921,[30,28,30,30],[[13,0],[26,0],[35,0],[42,0]]]], |
|||
[[2051,[30,28,30,30],[[14,0],[28,0],[38,0],[45,0]]]], |
|||
[[2185,[30,28,30,30],[[15,0],[29,0],[40,0],[48,0]]]], |
|||
[[2323,[30,28,30,30],[[16,0],[31,0],[43,0],[51,0]]]], |
|||
[[2465,[30,28,30,30],[[17,0],[33,0],[45,0],[54,0]]]], |
|||
[[2611,[30,28,30,30],[[18,0],[35,0],[48,0],[57,0]]]], |
|||
[[2761,[30,28,30,30],[[19,0],[37,0],[51,0],[60,0]]]], |
|||
[[2876,[30,28,30,30],[[19,0],[38,0],[53,0],[63,0]]]], |
|||
[[3034,[30,28,30,30],[[20,0],[40,0],[56,0],[66,0]]]], |
|||
[[3196,[30,28,30,30],[[21,0],[43,0],[59,0],[70,0]]]], |
|||
[[3362,[30,28,30,30],[[22,0],[45,0],[62,0],[74,0]]]], |
|||
[[3532,[30,28,30,30],[[24,0],[47,0],[65,0],[77,0]]]], |
|||
[[3706,[30,28,30,30],[[25,0],[49,0],[68,0],[81,0]]]] |
|||
]; |
|||
|
|||
// 更完整的RS块信息
|
|||
const EC_PARAMS = [ |
|||
// [纠错码字每块, 数据码字每块组1, 块数组1, 数据码字每块组2, 块数组2]
|
|||
// L, M, Q, H for each version
|
|||
[[7,19,1,0,0],[10,16,1,0,0],[13,13,1,0,0],[17,9,1,0,0]], // v1
|
|||
[[10,34,1,0,0],[16,28,1,0,0],[22,22,1,0,0],[28,16,1,0,0]], // v2
|
|||
[[15,55,1,0,0],[26,44,1,0,0],[18,17,2,0,0],[22,13,2,0,0]], // v3
|
|||
[[20,80,1,0,0],[18,32,2,0,0],[26,24,2,0,0],[16,9,4,0,0]], // v4
|
|||
[[26,108,1,0,0],[24,43,2,0,0],[18,15,2,16,2],[22,11,2,12,2]], // v5
|
|||
[[18,68,2,0,0],[16,27,4,0,0],[24,19,4,0,0],[28,15,4,0,0]], // v6
|
|||
[[20,78,2,0,0],[18,31,4,0,0],[18,14,2,15,4],[26,13,4,14,1]], // v7
|
|||
[[24,97,2,0,0],[22,38,2,39,2],[22,18,4,19,2],[26,14,4,15,2]], // v8
|
|||
[[30,116,2,0,0],[22,36,3,37,2],[20,16,4,17,4],[24,12,4,13,4]], // v9
|
|||
[[18,68,2,69,2],[26,43,4,44,1],[24,19,6,20,2],[28,15,6,16,2]], // v10
|
|||
[[20,81,4,0,0],[30,50,1,51,4],[28,22,4,23,4],[24,12,3,13,8]], // v11
|
|||
[[24,92,2,93,2],[22,36,6,37,2],[26,20,4,21,6],[28,14,7,15,4]], // v12
|
|||
[[26,107,4,0,0],[22,37,8,38,1],[24,20,8,21,4],[22,11,12,12,4]], // v13
|
|||
[[30,115,3,116,1],[24,40,4,41,5],[20,16,11,17,5],[24,12,11,13,5]], // v14
|
|||
[[22,87,5,88,1],[24,41,5,42,5],[30,24,5,25,7],[24,12,11,13,7]], // v15
|
|||
[[24,98,5,99,1],[28,45,7,46,3],[24,19,15,20,2],[30,15,3,16,13]], // v16
|
|||
[[28,107,1,108,5],[28,46,10,47,1],[28,22,1,23,15],[28,14,2,15,17]], // v17
|
|||
[[30,120,5,121,1],[26,43,9,44,4],[28,22,17,23,1],[28,14,2,15,19]], // v18
|
|||
[[28,113,3,114,4],[26,44,3,45,11],[26,21,17,22,4],[26,13,9,14,16]], // v19
|
|||
[[28,107,3,108,5],[26,41,3,42,13],[30,24,15,25,5],[28,15,15,16,10]], // v20
|
|||
[[28,116,4,117,4],[26,42,17,0,0],[28,22,17,23,6],[30,16,19,17,6]], // v21
|
|||
[[28,111,2,112,7],[28,46,17,0,0],[30,24,7,25,16],[24,13,34,0,0]], // v22
|
|||
[[30,121,4,122,5],[28,47,4,48,14],[30,24,11,25,14],[30,15,16,16,14]], // v23
|
|||
[[30,117,6,118,4],[28,45,6,46,14],[30,24,11,25,16],[30,16,30,17,2]], // v24
|
|||
[[26,106,8,107,4],[28,47,8,48,13],[30,24,7,25,22],[30,15,22,16,13]], // v25
|
|||
[[28,114,10,115,2],[28,46,19,47,4],[28,22,28,23,6],[30,16,33,17,4]], // v26
|
|||
[[30,122,8,123,4],[28,45,22,46,3],[30,23,8,24,26],[30,15,12,16,28]], // v27
|
|||
[[30,117,3,118,10],[28,45,3,46,23],[30,24,4,25,31],[30,15,11,16,31]], // v28
|
|||
[[30,116,7,117,7],[28,45,21,46,7],[30,23,1,24,37],[30,15,19,16,26]], // v29
|
|||
[[30,115,5,116,10],[28,47,19,48,10],[30,24,15,25,25],[30,15,23,16,25]], // v30
|
|||
[[30,115,13,116,3],[28,46,2,47,29],[30,24,42,25,1],[30,15,23,16,28]], // v31
|
|||
[[30,115,17,0,0],[28,46,10,47,23],[30,24,10,25,35],[30,15,19,16,35]], // v32
|
|||
[[30,115,17,116,1],[28,46,14,47,21],[30,24,29,25,19],[30,15,11,16,46]], // v33
|
|||
[[30,115,13,116,6],[28,46,14,47,23],[30,24,44,25,7],[30,16,59,17,1]], // v34
|
|||
[[30,121,12,122,7],[28,47,12,48,26],[30,24,39,25,14],[30,15,22,16,41]], // v35
|
|||
[[30,121,6,122,14],[28,47,6,48,34],[30,24,46,25,10],[30,15,2,16,64]], // v36
|
|||
[[30,122,17,123,4],[28,46,29,47,14],[30,24,49,25,10],[30,15,24,16,46]], // v37
|
|||
[[30,122,4,123,18],[28,46,13,47,32],[30,24,48,25,14],[30,15,42,16,32]], // v38
|
|||
[[30,117,20,118,4],[28,47,40,48,7],[30,24,43,25,22],[30,15,10,16,67]], // v39
|
|||
[[30,118,19,119,6],[28,47,18,48,31],[30,24,34,25,34],[30,15,20,16,61]] // v40
|
|||
]; |
|||
|
|||
// 对齐图案位置
|
|||
const ALIGN_POS = [ |
|||
[], [6,18], [6,22], [6,26], [6,30], [6,34], |
|||
[6,22,38], [6,24,42], [6,26,46], [6,28,50], [6,30,54], [6,32,58], [6,34,62], |
|||
[6,26,46,66], [6,26,48,70], [6,26,50,74], [6,30,54,78], [6,30,56,82], [6,30,58,86], [6,34,62,90], |
|||
[6,28,50,72,94], [6,26,50,74,98], [6,30,54,78,102], [6,28,54,80,106], [6,32,58,84,110], |
|||
[6,30,58,86,114], [6,34,62,90,118], [6,26,50,74,98,122], [6,30,54,78,102,126], |
|||
[6,26,52,78,104,130], [6,30,56,82,108,134], [6,34,60,86,112,138], [6,30,58,86,114,142], |
|||
[6,34,62,90,118,146], [6,30,54,78,102,126,150], [6,24,50,76,102,128,154], |
|||
[6,28,54,80,106,132,158], [6,32,58,84,110,136,162], [6,26,54,82,110,138,166], [6,30,58,86,114,142,170] |
|||
]; |
|||
|
|||
// 格式信息预计算 (ecl*8+mask) -> formatBits
|
|||
const FORMAT_BITS = [ |
|||
0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, |
|||
0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, |
|||
0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, |
|||
0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b |
|||
]; |
|||
|
|||
// 版本信息 (版本7+)
|
|||
const VERSION_BITS = [ |
|||
0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, |
|||
0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, |
|||
0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, |
|||
0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, |
|||
0x27541, 0x28c69 |
|||
]; |
|||
|
|||
// GF(2^8) 运算表
|
|||
const EXP = new Uint8Array(512); |
|||
const LOG = new Uint8Array(256); |
|||
(() => { |
|||
let x = 1; |
|||
for (let i = 0; i < 255; i++) { |
|||
EXP[i] = x; |
|||
LOG[x] = i; |
|||
x = (x << 1) ^ (x >= 128 ? 0x11d : 0); |
|||
} |
|||
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]; |
|||
})(); |
|||
|
|||
// RS编码
|
|||
function rsEncode(data, ecLen) { |
|||
const gen = new Uint8Array(ecLen + 1); |
|||
gen[0] = 1; |
|||
for (let i = 0; i < ecLen; i++) { |
|||
for (let j = i + 1; j >= 1; j--) { |
|||
gen[j] = gen[j] ? EXP[LOG[gen[j]] + i] ^ gen[j-1] : gen[j-1]; |
|||
} |
|||
gen[0] = EXP[LOG[gen[0]] + i]; |
|||
} |
|||
|
|||
const result = new Uint8Array(ecLen); |
|||
for (let i = 0; i < data.length; i++) { |
|||
const coef = data[i] ^ result[0]; |
|||
result.copyWithin(0, 1); |
|||
result[ecLen - 1] = 0; |
|||
if (coef) { |
|||
for (let j = 0; j < ecLen; j++) { |
|||
result[j] ^= EXP[LOG[gen[ecLen - 1 - j]] + LOG[coef]]; |
|||
} |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
// 获取数据模式
|
|||
function getMode(text) { |
|||
if (/^\d+$/.test(text)) return MODE.Numeric; |
|||
if (/^[0-9A-Z $%*+\-./:]+$/.test(text)) return MODE.Alphanumeric; |
|||
return MODE.Byte; |
|||
} |
|||
|
|||
// 获取字符计数位数
|
|||
function getCharCountBits(ver, mode) { |
|||
const idx = ver < 10 ? 0 : ver < 27 ? 1 : 2; |
|||
return [[10,9,8,8],[12,11,16,10],[14,13,16,12]][idx][[MODE.Numeric,MODE.Alphanumeric,MODE.Byte,MODE.Kanji].indexOf(mode)]; |
|||
} |
|||
|
|||
// UTF-8编码
|
|||
function toUtf8(str) { |
|||
const bytes = []; |
|||
for (let i = 0; i < str.length; i++) { |
|||
let c = str.charCodeAt(i); |
|||
if (c < 0x80) { |
|||
bytes.push(c); |
|||
} else if (c < 0x800) { |
|||
bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); |
|||
} else if (c >= 0xd800 && c < 0xdc00 && i + 1 < str.length) { |
|||
const c2 = str.charCodeAt(++i); |
|||
c = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff); |
|||
bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); |
|||
} else { |
|||
bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); |
|||
} |
|||
} |
|||
return bytes; |
|||
} |
|||
|
|||
// 获取最小版本
|
|||
function getMinVersion(text, ecl) { |
|||
const mode = getMode(text); |
|||
const len = mode === MODE.Byte ? toUtf8(text).length : text.length; |
|||
for (let v = 1; v <= 40; v++) { |
|||
if (len <= CAPACITIES[v-1][ecl]) return v; |
|||
} |
|||
return -1; |
|||
} |
|||
|
|||
// 编码数据
|
|||
function encodeData(text, ver, ecl) { |
|||
const mode = getMode(text); |
|||
const bits = []; |
|||
|
|||
// 写入位
|
|||
const write = (val, len) => { |
|||
for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1); |
|||
}; |
|||
|
|||
// 模式指示符
|
|||
write(mode, 4); |
|||
|
|||
// 字符计数
|
|||
const utf8 = mode === MODE.Byte ? toUtf8(text) : null; |
|||
const charCount = utf8 ? utf8.length : text.length; |
|||
write(charCount, getCharCountBits(ver, mode)); |
|||
|
|||
// 数据编码
|
|||
if (mode === MODE.Numeric) { |
|||
for (let i = 0; i < text.length; i += 3) { |
|||
const chunk = text.substr(i, 3); |
|||
write(parseInt(chunk, 10), chunk.length * 3 + 1); |
|||
} |
|||
} else if (mode === MODE.Alphanumeric) { |
|||
for (let i = 0; i < text.length; i += 2) { |
|||
if (i + 1 < text.length) { |
|||
write(ALPHANUM.indexOf(text[i]) * 45 + ALPHANUM.indexOf(text[i+1]), 11); |
|||
} else { |
|||
write(ALPHANUM.indexOf(text[i]), 6); |
|||
} |
|||
} |
|||
} else { |
|||
for (const b of utf8) write(b, 8); |
|||
} |
|||
|
|||
// 获取数据容量
|
|||
const params = EC_PARAMS[ver - 1][ecl]; |
|||
const [ecPerBlock, dc1, bc1, dc2, bc2] = params; |
|||
const totalDC = dc1 * bc1 + dc2 * bc2; |
|||
const capacity = totalDC * 8; |
|||
|
|||
// 终止符
|
|||
const termLen = Math.min(4, capacity - bits.length); |
|||
for (let i = 0; i < termLen; i++) bits.push(0); |
|||
|
|||
// 对齐到字节
|
|||
while (bits.length % 8) bits.push(0); |
|||
|
|||
// 填充
|
|||
const pads = [0xec, 0x11]; |
|||
let padIdx = 0; |
|||
while (bits.length < capacity) { |
|||
write(pads[padIdx++ % 2], 8); |
|||
} |
|||
|
|||
// 转换为字节
|
|||
const bytes = []; |
|||
for (let i = 0; i < bits.length; i += 8) { |
|||
let b = 0; |
|||
for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j]; |
|||
bytes.push(b); |
|||
} |
|||
|
|||
// 分块并添加纠错码
|
|||
const blocks = []; |
|||
const ecBlocks = []; |
|||
let offset = 0; |
|||
|
|||
for (let i = 0; i < bc1; i++) { |
|||
const block = bytes.slice(offset, offset + dc1); |
|||
blocks.push(block); |
|||
ecBlocks.push(rsEncode(new Uint8Array(block), ecPerBlock)); |
|||
offset += dc1; |
|||
} |
|||
for (let i = 0; i < bc2; i++) { |
|||
const block = bytes.slice(offset, offset + dc2); |
|||
blocks.push(block); |
|||
ecBlocks.push(rsEncode(new Uint8Array(block), ecPerBlock)); |
|||
offset += dc2; |
|||
} |
|||
|
|||
// 交织
|
|||
const result = []; |
|||
const maxDC = Math.max(dc1, dc2); |
|||
for (let i = 0; i < maxDC; i++) { |
|||
for (const block of blocks) { |
|||
if (i < block.length) result.push(block[i]); |
|||
} |
|||
} |
|||
for (let i = 0; i < ecPerBlock; i++) { |
|||
for (const ec of ecBlocks) { |
|||
result.push(ec[i]); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
// 创建二维码矩阵
|
|||
function createMatrix(ver) { |
|||
const size = ver * 4 + 17; |
|||
const matrix = []; |
|||
const reserved = []; |
|||
for (let i = 0; i < size; i++) { |
|||
matrix.push(new Array(size).fill(0)); |
|||
reserved.push(new Array(size).fill(false)); |
|||
} |
|||
|
|||
// 标记保留区域
|
|||
const mark = (r, c) => { |
|||
if (r >= 0 && r < size && c >= 0 && c < size) reserved[r][c] = true; |
|||
}; |
|||
|
|||
// 查找图案
|
|||
const placeFinder = (r, c) => { |
|||
for (let dr = -1; dr <= 7; dr++) { |
|||
for (let dc = -1; dc <= 7; dc++) { |
|||
const nr = r + dr, nc = c + dc; |
|||
if (nr < 0 || nr >= size || nc < 0 || nc >= size) continue; |
|||
mark(nr, nc); |
|||
if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) { |
|||
const isBlack = dr === 0 || dr === 6 || dc === 0 || dc === 6 || |
|||
(dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4); |
|||
matrix[nr][nc] = isBlack ? 1 : 0; |
|||
} else { |
|||
matrix[nr][nc] = 0; |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
placeFinder(0, 0); |
|||
placeFinder(0, size - 7); |
|||
placeFinder(size - 7, 0); |
|||
|
|||
// 对齐图案
|
|||
if (ver >= 2) { |
|||
const positions = ALIGN_POS[ver - 1]; |
|||
for (const r of positions) { |
|||
for (const c of positions) { |
|||
if (reserved[r][c]) continue; |
|||
for (let dr = -2; dr <= 2; dr++) { |
|||
for (let dc = -2; dc <= 2; dc++) { |
|||
mark(r + dr, c + dc); |
|||
const isBlack = Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0); |
|||
matrix[r + dr][c + dc] = isBlack ? 1 : 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 时序图案
|
|||
for (let i = 8; i < size - 8; i++) { |
|||
const v = i % 2 === 0 ? 1 : 0; |
|||
if (!reserved[6][i]) { matrix[6][i] = v; mark(6, i); } |
|||
if (!reserved[i][6]) { matrix[i][6] = v; mark(i, 6); } |
|||
} |
|||
|
|||
// 暗模块
|
|||
matrix[size - 8][8] = 1; |
|||
mark(size - 8, 8); |
|||
|
|||
// 格式信息区域
|
|||
for (let i = 0; i < 9; i++) { mark(8, i); mark(i, 8); } |
|||
for (let i = 0; i < 8; i++) { mark(8, size - 1 - i); mark(size - 1 - i, 8); } |
|||
|
|||
// 版本信息区域
|
|||
if (ver >= 7) { |
|||
for (let i = 0; i < 6; i++) { |
|||
for (let j = 0; j < 3; j++) { |
|||
mark(i, size - 11 + j); |
|||
mark(size - 11 + j, i); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return { matrix, reserved, size }; |
|||
} |
|||
|
|||
// 放置数据
|
|||
function placeData(matrix, reserved, data) { |
|||
const size = matrix.length; |
|||
let bitIdx = 0; |
|||
let upward = true; |
|||
|
|||
for (let col = size - 1; col >= 1; col -= 2) { |
|||
if (col === 6) col = 5; |
|||
|
|||
for (let i = 0; i < size; i++) { |
|||
const row = upward ? size - 1 - i : i; |
|||
|
|||
for (let dc = 0; dc < 2; dc++) { |
|||
const c = col - dc; |
|||
if (!reserved[row][c]) { |
|||
const bit = bitIdx < data.length * 8 |
|||
? (data[Math.floor(bitIdx / 8)] >> (7 - bitIdx % 8)) & 1 |
|||
: 0; |
|||
matrix[row][c] = bit; |
|||
bitIdx++; |
|||
} |
|||
} |
|||
} |
|||
upward = !upward; |
|||
} |
|||
} |
|||
|
|||
// 应用掩码
|
|||
function applyMask(matrix, reserved, mask) { |
|||
const size = matrix.length; |
|||
const result = matrix.map(row => [...row]); |
|||
|
|||
const masks = [ |
|||
(r, c) => (r + c) % 2 === 0, |
|||
(r, c) => r % 2 === 0, |
|||
(r, c) => c % 3 === 0, |
|||
(r, c) => (r + c) % 3 === 0, |
|||
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0, |
|||
(r, c) => (r * c) % 2 + (r * c) % 3 === 0, |
|||
(r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0, |
|||
(r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0 |
|||
]; |
|||
|
|||
for (let r = 0; r < size; r++) { |
|||
for (let c = 0; c < size; c++) { |
|||
if (!reserved[r][c] && masks[mask](r, c)) { |
|||
result[r][c] ^= 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
// 放置格式信息
|
|||
function placeFormatInfo(matrix, ecl, mask) { |
|||
const size = matrix.length; |
|||
const bits = FORMAT_BITS[ecl * 8 + mask]; |
|||
|
|||
// 左上水平
|
|||
for (let i = 0; i <= 5; i++) matrix[8][i] = (bits >> (14 - i)) & 1; |
|||
matrix[8][7] = (bits >> 8) & 1; |
|||
matrix[8][8] = (bits >> 7) & 1; |
|||
matrix[7][8] = (bits >> 6) & 1; |
|||
for (let i = 0; i <= 5; i++) matrix[i][8] = (bits >> i) & 1; |
|||
|
|||
// 右上和左下
|
|||
for (let i = 0; i <= 7; i++) matrix[8][size - 1 - i] = (bits >> i) & 1; |
|||
for (let i = 0; i <= 6; i++) matrix[size - 1 - i][8] = (bits >> (14 - i)) & 1; |
|||
} |
|||
|
|||
// 放置版本信息
|
|||
function placeVersionInfo(matrix, ver) { |
|||
if (ver < 7) return; |
|||
const size = matrix.length; |
|||
const bits = VERSION_BITS[ver - 7]; |
|||
|
|||
for (let i = 0; i < 6; i++) { |
|||
for (let j = 0; j < 3; j++) { |
|||
const bit = (bits >> (i * 3 + j)) & 1; |
|||
matrix[i][size - 11 + j] = bit; |
|||
matrix[size - 11 + j][i] = bit; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 计算惩罚分数
|
|||
function calcPenalty(matrix) { |
|||
const size = matrix.length; |
|||
let penalty = 0; |
|||
|
|||
// 规则1: 连续同色
|
|||
for (let r = 0; r < size; r++) { |
|||
let cnt = 1; |
|||
for (let c = 1; c < size; c++) { |
|||
if (matrix[r][c] === matrix[r][c-1]) cnt++; |
|||
else { if (cnt >= 5) penalty += cnt - 2; cnt = 1; } |
|||
} |
|||
if (cnt >= 5) penalty += cnt - 2; |
|||
} |
|||
for (let c = 0; c < size; c++) { |
|||
let cnt = 1; |
|||
for (let r = 1; r < size; r++) { |
|||
if (matrix[r][c] === matrix[r-1][c]) cnt++; |
|||
else { if (cnt >= 5) penalty += cnt - 2; cnt = 1; } |
|||
} |
|||
if (cnt >= 5) penalty += cnt - 2; |
|||
} |
|||
|
|||
// 规则2: 2x2块
|
|||
for (let r = 0; r < size - 1; r++) { |
|||
for (let c = 0; c < size - 1; c++) { |
|||
const v = matrix[r][c]; |
|||
if (v === matrix[r][c+1] && v === matrix[r+1][c] && v === matrix[r+1][c+1]) { |
|||
penalty += 3; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 规则3: 特定图案
|
|||
const p1 = [1,0,1,1,1,0,1,0,0,0,0]; |
|||
const p2 = [0,0,0,0,1,0,1,1,1,0,1]; |
|||
for (let r = 0; r < size; r++) { |
|||
for (let c = 0; c <= size - 11; c++) { |
|||
let m1 = true, m2 = true; |
|||
for (let i = 0; i < 11; i++) { |
|||
if (matrix[r][c+i] !== p1[i]) m1 = false; |
|||
if (matrix[r][c+i] !== p2[i]) m2 = false; |
|||
} |
|||
if (m1 || m2) penalty += 40; |
|||
} |
|||
} |
|||
for (let c = 0; c < size; c++) { |
|||
for (let r = 0; r <= size - 11; r++) { |
|||
let m1 = true, m2 = true; |
|||
for (let i = 0; i < 11; i++) { |
|||
if (matrix[r+i][c] !== p1[i]) m1 = false; |
|||
if (matrix[r+i][c] !== p2[i]) m2 = false; |
|||
} |
|||
if (m1 || m2) penalty += 40; |
|||
} |
|||
} |
|||
|
|||
// 规则4: 黑白比例
|
|||
let dark = 0; |
|||
for (let r = 0; r < size; r++) { |
|||
for (let c = 0; c < size; c++) { |
|||
if (matrix[r][c]) dark++; |
|||
} |
|||
} |
|||
const ratio = dark / (size * size); |
|||
penalty += Math.floor(Math.abs(ratio - 0.5) / 0.05) * 10; |
|||
|
|||
return penalty; |
|||
} |
|||
|
|||
// 选择最佳掩码
|
|||
function selectMask(matrix, reserved, ecl, ver) { |
|||
let bestMask = 0; |
|||
let bestPenalty = Infinity; |
|||
|
|||
for (let mask = 0; mask < 8; mask++) { |
|||
const masked = applyMask(matrix, reserved, mask); |
|||
placeFormatInfo(masked, ecl, mask); |
|||
placeVersionInfo(masked, ver); |
|||
const p = calcPenalty(masked); |
|||
if (p < bestPenalty) { |
|||
bestPenalty = p; |
|||
bestMask = mask; |
|||
} |
|||
} |
|||
|
|||
return bestMask; |
|||
} |
|||
|
|||
/** |
|||
* 生成二维码 |
|||
*/ |
|||
function generate(text, options = {}) { |
|||
const eclName = (options.errorCorrectionLevel || 'M').toUpperCase(); |
|||
const ecl = ECL[eclName] !== undefined ? ECL[eclName] : ECL.M; |
|||
|
|||
let ver = options.version || getMinVersion(text, ecl); |
|||
if (ver < 1) throw new Error('数据过长'); |
|||
if (ver > 40) ver = 40; |
|||
|
|||
const data = encodeData(text, ver, ecl); |
|||
const { matrix, reserved, size } = createMatrix(ver); |
|||
|
|||
placeData(matrix, reserved, data); |
|||
|
|||
const mask = selectMask(matrix, reserved, ecl, ver); |
|||
const final = applyMask(matrix, reserved, mask); |
|||
placeFormatInfo(final, ecl, mask); |
|||
placeVersionInfo(final, ver); |
|||
|
|||
return { |
|||
version: ver, |
|||
size, |
|||
modules: final, |
|||
errorCorrectionLevel: eclName |
|||
}; |
|||
} |
|||
|
|||
export default { generate, ECL, MODE }; |
|||
export { generate, ECL, MODE }; |
|||
@ -0,0 +1,104 @@ |
|||
{ |
|||
"id": "w-qrcode", |
|||
"displayName": "w-qrcode 二维码生成器", |
|||
"version": "1.0.1", |
|||
"description": "高性能二维码生成组件,纯JavaScript实现,支持自定义样式、Logo、多种纠错级别,兼容Vue2/Vue3,支持全平台", |
|||
"keywords": [ |
|||
"qrcode", |
|||
"二维码", |
|||
"qr-code", |
|||
"vue2", |
|||
"vue3" |
|||
], |
|||
"repository": "", |
|||
"engines": { |
|||
"HBuilderX": "^3.1.0", |
|||
"uni-app": "^3.6.15", |
|||
"uni-app-x": "" |
|||
}, |
|||
"dcloudext": { |
|||
"type": "component-vue", |
|||
"sale": { |
|||
"regular": { |
|||
"price": "0.00" |
|||
}, |
|||
"sourcecode": { |
|||
"price": "0.00" |
|||
} |
|||
}, |
|||
"contact": { |
|||
"qq": "" |
|||
}, |
|||
"declaration": { |
|||
"ads": "无", |
|||
"data": "无", |
|||
"permissions": "无" |
|||
}, |
|||
"npmurl": "", |
|||
"darkmode": "x", |
|||
"i18n": "x", |
|||
"widescreen": "x" |
|||
}, |
|||
"uni_modules": { |
|||
"dependencies": [], |
|||
"encrypt": [], |
|||
"platforms": { |
|||
"cloud": { |
|||
"tcb": "√", |
|||
"aliyun": "√", |
|||
"alipay": "x" |
|||
}, |
|||
"client": { |
|||
"uni-app": { |
|||
"vue": { |
|||
"vue2": { |
|||
}, |
|||
"vue3": { |
|||
} |
|||
}, |
|||
"web": { |
|||
"safari": "√", |
|||
"chrome": "√" |
|||
}, |
|||
"app": { |
|||
"vue": "√", |
|||
"nvue": "√", |
|||
"android": "-", |
|||
"ios": "-", |
|||
"harmony": "-" |
|||
}, |
|||
"mp": { |
|||
"weixin": "√", |
|||
"alipay": "-", |
|||
"toutiao": "-", |
|||
"baidu": "-", |
|||
"kuaishou": "-", |
|||
"jd": "-", |
|||
"harmony": "-", |
|||
"qq": "-", |
|||
"lark": "-" |
|||
}, |
|||
"quickapp": { |
|||
"huawei": "-", |
|||
"union": "-" |
|||
} |
|||
}, |
|||
"uni-app-x": { |
|||
"web": { |
|||
"safari": "-", |
|||
"chrome": "-" |
|||
}, |
|||
"app": { |
|||
"android": "-", |
|||
"ios": "-", |
|||
"harmony": "-" |
|||
}, |
|||
"mp": { |
|||
"weixin": "-" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"main": "js_sdk/qrcode.js" |
|||
} |
|||
@ -0,0 +1,229 @@ |
|||
# w-qrcode 二维码生成器 |
|||
|
|||
高性能二维码生成组件,纯 JavaScript 实现,无外部依赖。 |
|||
|
|||
## 特性 |
|||
|
|||
- 纯 JavaScript 实现,无需任何外部依赖 |
|||
- 支持 Vue2 和 Vue3 |
|||
- 支持全平台:H5、App(Vue/Nvue)、微信/支付宝/百度/字节跳动/QQ 等小程序 |
|||
- 支持自定义前景色、背景色 |
|||
- 支持添加 Logo 图片 |
|||
- 支持多种纠错级别(L/M/Q/H) |
|||
- 支持保存到相册 |
|||
- 支持导出为图片 |
|||
|
|||
## 安装 |
|||
|
|||
将 `w-qrcode` 目录复制到项目的 `uni_modules` 目录下即可。 |
|||
|
|||
## 基本使用 |
|||
|
|||
```vue |
|||
<template> |
|||
<w-qrcode value="https://example.com" /> |
|||
</template> |
|||
``` |
|||
|
|||
## 完整示例 |
|||
|
|||
```vue |
|||
<template> |
|||
<view class="container"> |
|||
<!-- 基础用法 --> |
|||
<w-qrcode |
|||
value="https://example.com" |
|||
:size="200" |
|||
/> |
|||
|
|||
<!-- 自定义颜色 --> |
|||
<w-qrcode |
|||
value="https://example.com" |
|||
:size="200" |
|||
foreground="#1989fa" |
|||
background="#f5f5f5" |
|||
/> |
|||
|
|||
<!-- 带 Logo --> |
|||
<w-qrcode |
|||
ref="qrcodeRef" |
|||
value="https://example.com" |
|||
:size="200" |
|||
logo="/static/logo.png" |
|||
:logo-size="50" |
|||
:logo-radius="8" |
|||
error-correction-level="H" |
|||
@generated="onGenerated" |
|||
@longpress="onLongpress" |
|||
/> |
|||
|
|||
<button @click="saveToAlbum">保存到相册</button> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
methods: { |
|||
onGenerated(info) { |
|||
console.log('二维码生成成功:', info); |
|||
// { version: 2, size: 25, errorCorrectionLevel: 'H' } |
|||
}, |
|||
onLongpress(e) { |
|||
console.log('长按事件'); |
|||
}, |
|||
saveToAlbum() { |
|||
this.$refs.qrcodeRef.saveToAlbum() |
|||
.then(res => { |
|||
console.log('保存成功', res); |
|||
}) |
|||
.catch(err => { |
|||
console.error('保存失败', err); |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
``` |
|||
|
|||
## Vue3 组合式 API 示例 |
|||
|
|||
```vue |
|||
<template> |
|||
<w-qrcode |
|||
ref="qrcodeRef" |
|||
:value="qrValue" |
|||
:size="200" |
|||
/> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref } from 'vue'; |
|||
|
|||
const qrcodeRef = ref(null); |
|||
const qrValue = ref('https://example.com'); |
|||
|
|||
const saveToAlbum = () => { |
|||
qrcodeRef.value.saveToAlbum(); |
|||
}; |
|||
</script> |
|||
``` |
|||
|
|||
## Props 属性 |
|||
|
|||
| 属性名 | 类型 | 默认值 | 说明 | |
|||
|--------|------|--------|------| |
|||
| value | String | '' | 要编码的文本内容 | |
|||
| size | Number/String | 200 | 二维码尺寸(单位:px) | |
|||
| foreground | String | '#000000' | 前景色(二维码颜色) | |
|||
| background | String | '#FFFFFF' | 背景色 | |
|||
| errorCorrectionLevel | String | 'M' | 纠错级别,可选值:L/M/Q/H | |
|||
| margin | Number/String | 2 | 二维码边距(单位:模块) | |
|||
| logo | String | '' | Logo 图片路径 | |
|||
| logoSize | Number/String | 50 | Logo 尺寸(单位:px) | |
|||
| logoMargin | Number/String | 5 | Logo 外边距(单位:px) | |
|||
| logoRadius | Number/String | 8 | Logo 圆角(单位:px) | |
|||
| logoBackground | String | '#FFFFFF' | Logo 背景色 | |
|||
| canvas2d | Boolean | true | 是否使用 Canvas 2D(仅小程序有效) | |
|||
|
|||
## 纠错级别说明 |
|||
|
|||
| 级别 | 纠错能力 | 说明 | |
|||
|------|----------|------| |
|||
| L | 约 7% | 数据恢复能力最低 | |
|||
| M | 约 15% | 默认级别,平衡 | |
|||
| Q | 约 25% | 较高的数据恢复能力 | |
|||
| H | 约 30% | 最高级别,适合添加 Logo | |
|||
|
|||
> 注意:如果需要在二维码中添加 Logo,建议使用 `H` 级别,以保证扫描成功率。 |
|||
|
|||
## Events 事件 |
|||
|
|||
| 事件名 | 说明 | 回调参数 | |
|||
|--------|------|----------| |
|||
| generated | 二维码生成成功时触发 | { version, size, errorCorrectionLevel } | |
|||
| error | 生成失败时触发 | Error 对象 | |
|||
| longpress | 长按二维码时触发 | Event 对象 | |
|||
|
|||
## Methods 方法 |
|||
|
|||
通过 ref 获取组件实例后调用: |
|||
|
|||
| 方法名 | 说明 | 参数 | 返回值 | |
|||
|--------|------|------|--------| |
|||
| saveToAlbum | 保存到相册 | - | Promise | |
|||
| toTempFilePath | 获取临时文件路径 | options | Promise<{tempFilePath}> | |
|||
| toDataURL | 获取 Base64(仅 H5) | type, quality | Promise<string> | |
|||
| generate | 重新生成二维码 | - | - | |
|||
|
|||
### toTempFilePath 参数 |
|||
|
|||
```javascript |
|||
this.$refs.qrcodeRef.toTempFilePath({ |
|||
fileType: 'png', // 'jpg' | 'png' |
|||
quality: 1, // 0-1,jpg 时有效 |
|||
destWidth: 300, // 输出图片宽度 |
|||
destHeight: 300 // 输出图片高度 |
|||
}); |
|||
``` |
|||
|
|||
## 直接使用 JS SDK |
|||
|
|||
如果只需要生成二维码数据,可以直接使用 JS SDK: |
|||
|
|||
```javascript |
|||
import QRCode from '@/uni_modules/w-qrcode/js_sdk/qrcode.js'; |
|||
|
|||
// 生成二维码数据 |
|||
const result = QRCode.generate('https://example.com', { |
|||
errorCorrectionLevel: 'M' // L/M/Q/H |
|||
}); |
|||
|
|||
console.log(result); |
|||
// { |
|||
// version: 2, // 版本号 1-40 |
|||
// size: 25, // 矩阵尺寸 |
|||
// modules: [[...], ...], // 二维码矩阵,1表示黑色,0表示白色 |
|||
// errorCorrectionLevel: 'M' |
|||
// } |
|||
``` |
|||
|
|||
## 平台差异 |
|||
|
|||
| 平台 | 渲染方式 | 说明 | |
|||
|------|----------|------| |
|||
| H5 | Canvas | 完全支持 | |
|||
| App-Vue | Canvas | 完全支持 | |
|||
| App-Nvue | View 布局 | 使用 Fallback 渲染 | |
|||
| 微信小程序 | Canvas 2D | 推荐使用 Canvas 2D | |
|||
| 其他小程序 | Canvas | 使用旧版 Canvas API | |
|||
|
|||
## 常见问题 |
|||
|
|||
### 1. 二维码扫描不出来? |
|||
|
|||
- 检查内容是否正确 |
|||
- 增加纠错级别(使用 'H') |
|||
- 减小 Logo 尺寸(不超过二维码的 30%) |
|||
- 增加二维码尺寸 |
|||
|
|||
### 2. 保存到相册失败? |
|||
|
|||
- 小程序需要用户授权 |
|||
- App 需要配置相册权限 |
|||
- 检查图片路径是否正确 |
|||
|
|||
### 3. Logo 不显示? |
|||
|
|||
- 确保图片路径正确 |
|||
- 本地图片使用绝对路径 |
|||
- 网络图片需要在小程序后台配置域名白名单 |
|||
|
|||
## 更新日志 |
|||
|
|||
### 1.0.0 |
|||
|
|||
- 初始版本发布 |
|||
- 支持基础二维码生成 |
|||
- 支持自定义样式 |
|||
- 支持 Logo 图片 |
|||
- 支持多平台 |
|||