@forge-kit/plugin-qr-code 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/dist/app.js +72 -0
- package/dist/icon.svg +7 -0
- package/package.json +52 -0
- package/src/App.less +34 -0
- package/src/App.tsx +33 -0
- package/src/components/qr-decoder-panel.less +58 -0
- package/src/components/qr-decoder-panel.tsx +105 -0
- package/src/components/qr-generator-panel.less +57 -0
- package/src/components/qr-generator-panel.tsx +154 -0
- package/src/components/upload-file-view.less +26 -0
- package/src/components/upload-file-view.tsx +80 -0
- package/src/hooks/use-debounced-callback.ts +14 -0
- package/src/hooks/use-latest-task-guard.ts +21 -0
- package/src/hooks/use-partial-state.ts +15 -0
- package/src/hooks/use-to-ref.ts +11 -0
- package/src/main.tsx +30 -0
- package/src/utils/qr-code.ts +85 -0
package/src/main.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {StrictMode} from 'react'
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import {definePlugin} from '@forge-kit/types'
|
|
4
|
+
import {App} from '@/App.tsx'
|
|
5
|
+
|
|
6
|
+
let rootInstance: ReturnType<typeof ReactDOM.createRoot> | null = null;
|
|
7
|
+
|
|
8
|
+
const plugin = definePlugin({
|
|
9
|
+
bundleId: "com.forge-kit.plugin.qr-code",
|
|
10
|
+
name: '二维码生成与解析工具',
|
|
11
|
+
icon: `${import.meta.env.BASE_URL}icon.svg`,
|
|
12
|
+
description: "二维码生成与解析工具",
|
|
13
|
+
mount: (container, props = {}) => {
|
|
14
|
+
rootInstance = ReactDOM.createRoot(container);
|
|
15
|
+
rootInstance.render(
|
|
16
|
+
<StrictMode>
|
|
17
|
+
<App {...props}/>
|
|
18
|
+
</StrictMode>,
|
|
19
|
+
)
|
|
20
|
+
},
|
|
21
|
+
unmount: () => {
|
|
22
|
+
if (!rootInstance) return
|
|
23
|
+
rootInstance.unmount();
|
|
24
|
+
rootInstance = null;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
if (import.meta.env.DEV) plugin.mount(document.getElementById('root')!)
|
|
28
|
+
|
|
29
|
+
export default plugin
|
|
30
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import jsQR from 'jsqr'
|
|
2
|
+
import QRCode from 'qrcode'
|
|
3
|
+
import type {QRCodeErrorCorrectionLevel} from 'qrcode'
|
|
4
|
+
|
|
5
|
+
export const QR_ERROR_CORRECTION_LEVEL_OPTIONS: QRCodeErrorCorrectionLevel[] = ['L', 'M', 'Q', 'H']
|
|
6
|
+
|
|
7
|
+
export interface GenerateQrOptions {
|
|
8
|
+
width: number
|
|
9
|
+
margin: number
|
|
10
|
+
errorCorrectionLevel: QRCodeErrorCorrectionLevel
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const generateQrDataUrl = async (content: string, options: GenerateQrOptions): Promise<string> => {
|
|
14
|
+
return QRCode.toDataURL(content, options)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getDecode2dContext = (width: number, height: number): {
|
|
18
|
+
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
|
|
19
|
+
width: number
|
|
20
|
+
height: number
|
|
21
|
+
} => {
|
|
22
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
23
|
+
const offscreenCanvas = new OffscreenCanvas(width, height)
|
|
24
|
+
const context = offscreenCanvas.getContext('2d', {willReadFrequently: true})
|
|
25
|
+
if (!context) throw new Error('无法创建 Canvas 上下文')
|
|
26
|
+
return {context, width, height}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const canvas = document.createElement('canvas')
|
|
30
|
+
canvas.width = width
|
|
31
|
+
canvas.height = height
|
|
32
|
+
|
|
33
|
+
const context = canvas.getContext('2d', {willReadFrequently: true})
|
|
34
|
+
if (!context) throw new Error('无法创建 Canvas 上下文')
|
|
35
|
+
return {context, width: canvas.width, height: canvas.height}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const decodeQrFromImageUrl = async (imageUrl: string): Promise<string> => {
|
|
39
|
+
const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
40
|
+
const image = new Image()
|
|
41
|
+
image.onload = () => resolve(image)
|
|
42
|
+
image.onerror = () => reject(new Error('图片加载失败'))
|
|
43
|
+
image.src = imageUrl
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const {context, width, height} = getDecode2dContext(img.naturalWidth, img.naturalHeight)
|
|
47
|
+
|
|
48
|
+
context.drawImage(img, 0, 0)
|
|
49
|
+
const imageData = context.getImageData(0, 0, width, height)
|
|
50
|
+
|
|
51
|
+
const result = jsQR(imageData.data, imageData.width, imageData.height, {
|
|
52
|
+
inversionAttempts: 'attemptBoth',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!result?.data) {
|
|
56
|
+
throw new Error('未识别到二维码,请尝试更清晰的图片')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result.data
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const getImageFileFromFileList = (files: FileList | null): File | null => {
|
|
63
|
+
if (!files || files.length === 0) return null
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
if (file.type.startsWith('image/')) return file
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const getImageFileFromClipboard = (clipboardData: DataTransfer | null): File | null => {
|
|
71
|
+
if (!clipboardData) return null
|
|
72
|
+
for (const item of clipboardData.items) {
|
|
73
|
+
if (item.kind === 'file' && item.type.startsWith('image/')) {
|
|
74
|
+
return item.getAsFile()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const downloadDataUrl = (dataUrl: string, filename: string) => {
|
|
81
|
+
const link = document.createElement('a')
|
|
82
|
+
link.href = dataUrl
|
|
83
|
+
link.download = filename
|
|
84
|
+
link.click()
|
|
85
|
+
}
|