Skip to content

实现 QrCodeReader 工具

分类:developer QrCodeReader 是一款在线工具,相关计算/处理均在浏览器本地完成,不上传服务器。

2.1 功能说明

QrCodeReader 是一款在线工具,相关计算/处理均在浏览器本地完成,不上传服务器。

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-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 效果截图

暂未生成该工具的效果截图。

工具访问地址:https://www.i91tools.com/tools/qr-code-reader