tool.xpcool.com/src/views/qrcode/QrcodeView.vue
夏犀麟 6e25de5c21 feat: 全站拖拽上传 + 图片压缩批量与多选项优化
- 新增 UploadDrop 通用组件:点击选择 + 拖拽上传、图片缩略图回显、
  批量/单文件、删除,替代各页面的 t-upload 文件模式
- 图片压缩重写:批量(最多 20 张)、按质量/限制大小双模式、
  等比缩放、保持原格式、JPG 透明图白底填充、jszip 批量下载
- 修复压缩页历史解析正则 bug(/| jpg/ 恒为 true)与 slider label 类型错误
- 裁剪/二维码/Base64/哈希/OCR 页面统一接入 UploadDrop,文件变化改用 watch 响应
- 更新 agents.md 与 README:UploadDrop 约定、新坑位与依赖说明
2026-08-21 15:37:06 +08:00

252 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-wrap">
<h2 class="page-title">二维码生成 / 解析</h2>
<p class="page-desc">输入文本或链接生成二维码上传二维码图片解析其内容全部在浏览器本地完成</p>
<t-tabs v-model="tab">
<!-- ==================== 生成 ==================== -->
<t-tab-panel value="generate" label="生成二维码">
<t-card class="tool-section" title="内容与样式">
<t-textarea
v-model="text"
placeholder="输入要生成二维码的文本或链接,例如 https://example.com"
:maxlength="2000"
:autosize="{ minRows: 3, maxRows: 6 }"
/>
<div class="form-row" style="margin-top: 12px">
<span class="form-label">尺寸</span>
<t-select v-model="size" :options="sizeOptions" class="form-select" />
<span class="form-label">容错率</span>
<t-select v-model="level" :options="levelOptions" class="form-select" />
</div>
<div class="form-row" style="margin-top: 12px">
<span class="form-label">前景色</span>
<input v-model="dark" type="color" class="color-input" />
<span class="form-label">背景色</span>
<input v-model="light" type="color" class="color-input" />
</div>
<div class="form-actions">
<t-button theme="primary" :loading="genBusy" @click="generate">生成二维码</t-button>
<t-button variant="outline" @click="clearGen">清空</t-button>
</div>
</t-card>
<ErrorAlert :message="genError" @close="genError = ''" />
<t-card v-if="qrUrl" class="tool-section" title="生成结果">
<div class="qr-result">
<img :src="qrUrl" alt="生成的二维码" class="qr-img" />
<div class="form-actions">
<DownloadButton :filename="`qrcode_${size}.png`" :get-blob="getQrBlob" label="下载 PNG" />
<CopyButton :text="text" label="复制内容" />
</div>
</div>
</t-card>
<HistoryPanel
:items="history.items"
@select="(t: string) => (text = t)"
@remove="history.remove"
@clear="history.clear"
/>
</t-tab-panel>
<!-- ==================== 解析 ==================== -->
<t-tab-panel value="parse" label="解析二维码">
<t-card class="tool-section" title="上传二维码图片">
<UploadDrop v-model:files="parseFiles" accept="image/*" tip="上传一张二维码图片自动解析" />
</t-card>
<ErrorAlert :message="parseError" @close="parseError = ''" />
<t-card v-if="parseResult" class="tool-section" title="解析结果">
<div class="code-block">{{ parseResult }}</div>
<div class="form-actions">
<CopyButton :text="parseResult" />
</div>
</t-card>
<t-card v-else-if="parsed" class="tool-section" title="解析结果">
<t-alert theme="warning" message="未能从图片中识别出二维码,请确认图片清晰且包含完整二维码" />
</t-card>
<HistoryPanel
:items="parseHistory.items"
@select="(t: string) => (parseResult = t)"
@remove="parseHistory.remove"
@clear="parseHistory.clear"
/>
</t-tab-panel>
</t-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import QRCode from 'qrcode'
import jsQR from 'jsqr'
import UploadDrop from '@/components/common/UploadDrop.vue'
import ErrorAlert from '@/components/common/ErrorAlert.vue'
import CopyButton from '@/components/common/CopyButton.vue'
import DownloadButton from '@/components/common/DownloadButton.vue'
import HistoryPanel from '@/components/common/HistoryPanel.vue'
import { useHistory } from '@/composables/useHistory'
import { dataUrlToBlob } from '@/utils'
const tab = ref('generate')
/* ---------- 生成 ---------- */
const sizeOptions = [
{ label: '128 × 128', value: 128 },
{ label: '256 × 256', value: 256 },
{ label: '512 × 512', value: 512 },
{ label: '1024 × 1024', value: 1024 },
]
const levelOptions = ['L', 'M', 'Q', 'H'].map((v) => ({ label: v, value: v }))
const text = ref('')
const size = ref(256)
const level = ref('M')
const dark = ref('#000000')
const light = ref('#ffffff')
const genBusy = ref(false)
const genError = ref('')
const qrUrl = ref('')
const history = useHistory('qrcode-generate')
/** 生成二维码qrcode 库渲染到离屏 canvas 后导出 DataURL */
async function generate() {
if (!text.value.trim()) {
genError.value = '请输入要生成二维码的内容'
return
}
genBusy.value = true
genError.value = ''
qrUrl.value = ''
try {
const canvas = document.createElement('canvas')
await QRCode.toCanvas(canvas, text.value, {
width: size.value,
margin: 2,
errorCorrectionLevel: level.value as 'L' | 'M' | 'Q' | 'H',
color: { dark: dark.value, light: light.value },
})
qrUrl.value = canvas.toDataURL('image/png')
history.add(text.value)
} catch {
genError.value = '生成失败:内容过长或参数异常,请缩短内容后重试'
} finally {
genBusy.value = false
}
}
function getQrBlob(): Blob | null {
if (!qrUrl.value) return null
try {
return dataUrlToBlob(qrUrl.value)
} catch {
return null
}
}
function clearGen() {
text.value = ''
qrUrl.value = ''
genError.value = ''
}
/* ---------- 解析 ---------- */
const parseFiles = ref<File[]>([])
const parseError = ref('')
const parseResult = ref('')
const parsed = ref(false)
const parseHistory = useHistory('qrcode-parse')
/** 文件变化时(含拖拽删除/清空)自动解析 */
watch(parseFiles, (list) => {
const raw = list[0]
if (!(raw instanceof File)) {
parseError.value = ''
parseResult.value = ''
parsed.value = false
return
}
void parseImage(raw)
})
async function parseImage(raw: File) {
parseError.value = ''
parseResult.value = ''
parsed.value = false
try {
// 将图片绘制到 canvas 获取像素数据,再用 jsqr 识别
const dataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = () => reject(reader.error)
reader.readAsDataURL(raw)
})
const img = new Image()
img.src = dataUrl
await new Promise((resolve, reject) => {
img.onload = resolve
img.onerror = () => reject(new Error('图片加载失败'))
})
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('无法创建画布')
ctx.drawImage(img, 0, 0)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const result = jsQR(imageData.data, imageData.width, imageData.height)
parsed.value = true
if (result?.data) {
parseResult.value = result.data
parseHistory.add(result.data)
}
} catch {
parseError.value = '解析失败:图片格式不受支持,请换一张图片重试'
}
}
</script>
<style scoped>
.form-label {
width: 72px;
flex-shrink: 0;
font-size: 14px;
color: var(--td-text-color-secondary);
}
.form-select {
width: 150px;
}
.color-input {
width: 44px;
height: 32px;
padding: 0 2px;
border: 1px solid var(--td-border-level-2-color);
border-radius: 6px;
background: transparent;
cursor: pointer;
}
.form-actions {
display: flex;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
.qr-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.qr-img {
max-width: 280px;
border-radius: 8px;
border: 1px solid var(--td-border-level-1-color);
}
</style>