Appearance
实现 QrCodeReader 工具
分类:developer QrCodeReader 是一款在线工具,相关计算/处理均在浏览器本地完成,不上传服务器。
2.1 功能说明
QrCodeReader 是一款在线工具,相关计算/处理均在浏览器本地完成,不上传服务器。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
本工具目录 QrCodeReader/ 下除 index.vue 外,还包含以下源文件:
jsqr.js(10092 行)
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
typeMetaFor()detectType()parseWifi()unescape()goBack()pickFile()onFileChange()onDrop()onPaste()decodeFile()runDecode()pushHistory()restore()copy()openLink()handleWindowPaste()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ArrowLeft,
UploadFilled,
Loading,
DocumentCopy,
Link,
Phone,
Message,
Location
} from '@element-plus/icons-vue'
import jsQR from './jsqr.js'
const router = useRouter()
const fileInput = ref(null)
const dragover = ref(false)
const loading = ref(false)
const errorMsg = ref('')
const result = ref(null)
const history = ref([])
const TYPE_META = {
url: { label: '网址链接', color: 'success' },
wifi: { label: 'WiFi 配置', color: 'warning' },
tel: { label: '电话号码', color: 'info' },
email: { label: '电子邮件', color: 'info' },
vcard: { label: '电子名片', color: 'info' },
geo: { label: '地理位置', color: 'warning' },
sms: { label: '短信', color: 'info' },
text: { label: '纯文本', color: 'primary' }
}
const typeMeta = computed(() => TYPE_META[result.value?.type] || TYPE_META.text)
function typeMetaFor(t) {
return TYPE_META[t] || TYPE_META.text
}
const wifi = computed(() => (result.value?.type === 'wifi' ? parseWifi(result.value.data) : {}))
function detectType(text) {
if (/^https?:\/\//i.test(text)) return 'url'
if (/^WIFI:/i.test(text)) return 'wifi'
if (/^mailto:/i.test(text)) return 'email'
if (/^MATMSG:/i.test(text)) return 'email'
if (/^(smsto|smst|mms):/i.test(text)) return 'sms'
if (/^tel:/i.test(text)) return 'tel'
if (/^geo:/i.test(text)) return 'geo'
if (/^BEGIN:VCARD/i.test(text)) return 'vcard'
return 'text'
}
function parseWifi(text) {
const unescape = (v) => (v || '').replace(/\\(.)/g, '$1')
const ssid = unescape((text.match(/S:([^;]*)/) || [])[1] || '')
const password = unescape((text.match(/P:([^;]*)/) || [])[1] || '')
const encryption = (text.match(/T:([^;]*)/) || [])[1] || ''
const hidden = /H:true/i.test(text)
return { ssid, password, encryption, hidden }
}
function goBack() {
router.back()
}
function pickFile() {
fileInput.value?.click()
}
function onFileChange(e) {
const file = e.target.files && e.target.files[0]
if (file) decodeFile(file)
e.target.value = '' // 允许重复选择同一文件
}
function onDrop(e) {
dragover.value = false
const file = e.dataTransfer?.files?.[0]
if (file) decodeFile(file)
}
function onPaste(e) {
const items = e.clipboardData?.items
if (!items) return
for (const it of items) {
if (it.type && it.type.startsWith('image/')) {
const file = it.getAsFile()
if (file) decodeFile(file)
return
}
}
}
function decodeFile(file) {
errorMsg.value = ''
result.value = null
loading.value = true
const reader = new FileReader()
reader.onload = () => {
const img = new Image()
img.onload = () => {
try {
const decoded = runDecode(img)
if (decoded) {
const data = decoded.data
result.value = {
data,
type: detectType(data),
imageSrc: reader.result
}
pushHistory({ data, type: detectType(data) })
} else {
errorMsg.value = '图片中未发现可识别的二维码,请更换更清晰的图片。'
}
} catch (err) {
console.error('二维码解析异常:', err)
errorMsg.value = '图片解析失败,请确认文件为有效的图片格式。'
} finally {
loading.value = false
}
}
img.onerror = () => {
loading.value = false
errorMsg.value = '图片加载失败,请确认文件未损坏。'
}
img.src = reader.result
}
reader.onerror = () => {
loading.value = false
errorMsg.value = '文件读取失败,请重试。'
}
reader.readAsDataURL(file)
}
function runDecode(img) {
const maxDim = 2048
const scale = Math.min(1, maxDim / Math.max(img.width, img.height))
const w = Math.max(1, Math.floor(img.width * scale))
const h = Math.max(1, Math.floor(img.height * scale))
const canvas = document.createElement('canvas')
canvas.width = w
canvas.height = h
const ctx = canvas.getContext('2d', { willReadFrequently: true })
ctx.drawImage(img, 0, 0, w, h)
const imageData = ctx.getImageData(0, 0, w, h)
// attemptBoth: 同时尝试正常与反色二维码,提升识别率
return jsQR(imageData.data, w, h, { inversionAttempts: 'attemptBoth' })
}
function pushHistory(item) {
history.value.unshift(item)
if (history.value.length > 20) history.value.pop()
}
function restore(item) {
result.value = { ...item, imageSrc: result.value?.imageSrc || '' }
}
async function copy(text) {
if (!text) return
try {
await navigator.clipboard.writeText(text)
ElMessage.success('已复制到剪贴板')
} catch (e) {
const ta = document.createElement('textarea')
ta.value = text
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
ElMessage.success('已复制到剪贴板')
}
}
function openLink(url) {
if (!url) return
window.open(url, '_blank', 'noopener')
}
// 粘贴监听挂在 window 上,保证聚焦组件外也能粘贴
function handleWindowPaste(e) {
onPaste(e)
}
onMounted(() => window.addEventListener('paste', handleWindowPaste))
onBeforeUnmount(() => window.removeEventListener('paste', handleWindowPaste))2.3 效果截图
暂未生成该工具的效果截图。