boyuehasfj-vue3-html/src/components/QRCodeDisplay.vue
2025-06-02 21:36:36 +08:00

433 lines
8.3 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.

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import QRCode from 'qrcode';
const props = defineProps({
title: {
type: String,
required: true
},
url: {
type: String,
required: true
},
id: {
type: String,
required: true
}
});
const qrcodeEl = ref<HTMLElement | null>(null);
const qrcodeImgUrl = ref('');
const isLoading = ref(true);
// 获取显示友好的URL
const getDisplayUrl = (url: string): string => {
try {
// 从URL中提取完整URL不再截断
const urlObj = new URL(url);
return url;
} catch (e) {
// 如果URL格式不正确则显示完整URL
return url;
}
};
onMounted(async () => {
try {
isLoading.value = true;
// 使用 toDataURL 方法生成二维码数据 URL
qrcodeImgUrl.value = await QRCode.toDataURL(props.url, {
width: 180,
margin: 1,
errorCorrectionLevel: 'H',
color: {
dark: '#000000',
light: '#ffffff'
}
});
} catch (error) {
console.error('生成二维码失败:', error);
// 如果生成失败,使用一个默认的二维码图片作为替代
qrcodeImgUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(props.url)}`;
} finally {
isLoading.value = false;
}
});
const onDownload = () => {
try {
if (!qrcodeImgUrl.value) {
throw new Error('二维码图片不存在');
}
// 创建下载链接
const link = document.createElement('a');
link.download = `${props.title}-${props.id}.png`;
link.href = qrcodeImgUrl.value;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error('下载二维码失败:', error);
alert('下载二维码失败,请稍后重试');
}
};
</script>
<template>
<div class="qrcode-item">
<div class="qrcode-wrapper">
<div v-if="isLoading" class="loading-overlay">
<div class="spinner"></div>
</div>
<div v-else class="qrcode">
<img :src="qrcodeImgUrl" :alt="title" />
</div>
</div>
<div class="qrcode-info">
<h4>{{ title }}</h4>
<div class="qr-url-hint">{{ getDisplayUrl(url) }}</div>
<div class="qrcode-controls">
<div class="id-container">
<span class="id-label">编号:</span>
<span class="id-value" :title="id">{{ id }}</span>
</div>
<button @click="onDownload" class="btn-download" :title="`下载${title}二维码`">
<span class="icon-download"></span>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.qrcode {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
border-radius: 4px;
overflow: hidden;
}
.qrcode img {
max-width: 100%;
height: auto;
display: block;
transition: transform 0.3s;
}
.qrcode-item {
background-color: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.4s ease;
max-width: 280px;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #f0f0f0;
position: relative;
cursor: pointer;
overflow: hidden;
}
.qrcode-item:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
border-color: rgba(0, 123, 255, 0.3);
}
.qrcode-item:hover::before {
transform: translateY(0);
}
.qrcode-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.qrcode-item:hover::after {
opacity: 1;
}
.qrcode-item::after {
content: '扫码访问';
position: absolute;
top: -10px;
right: -10px;
background-color: var(--color-primary);
color: white;
font-size: 0.75rem;
padding: 0.3rem 0.8rem;
border-radius: 20px;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
z-index: 2;
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.2);
}
.qrcode-item:hover::after {
transform: translateY(5px);
}
.qrcode-wrapper {
width: 160px;
height: 160px;
position: relative;
margin-bottom: 1rem;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed #eee;
border-radius: 4px;
padding: 5px;
background-color: #fafafa;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 4px;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.qrcode-info {
width: 100%;
text-align: center;
}
.qrcode-info h4 {
margin: 0 0 0.5rem;
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.qrcode-controls {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
color: var(--color-text-light);
background-color: #f8f9fa;
padding: 0.5rem 0.8rem;
border-radius: 30px;
margin-top: 0.8rem;
width: 100%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
transition: all 0.3s;
}
.qrcode-item:hover .qrcode-controls {
background-color: #f0f8ff;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
}
.qr-url-hint {
font-size: 0.8rem;
color: var(--color-text-light);
margin-bottom: 0.5rem;
word-break: break-all;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
background-color: #f8f9fa;
padding: 0.3rem 0.5rem;
border-radius: 4px;
text-align: left;
max-height: 3.5rem;
overflow-y: auto;
margin-bottom: 0.8rem;
}
.id-container {
display: flex;
align-items: center;
width: 70%;
overflow: hidden;
}
.id-label {
flex-shrink: 0;
margin-right: 0.3rem;
}
.id-value {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 80%;
}
.btn-download {
background-color: transparent;
border: 1px solid var(--color-primary);
border-radius: 50%;
cursor: pointer;
padding: 0.4rem;
width: 28px;
height: 28px;
color: var(--color-primary);
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
margin-left: 5px;
position: relative;
overflow: hidden;
}
.btn-download::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-primary);
transform: translateY(100%);
transition: transform 0.3s ease;
z-index: -1;
}
.btn-download:hover {
color: white;
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.3);
}
.btn-download:hover::before {
transform: translateY(0);
}
.btn-download:active {
transform: scale(0.95);
}
.icon-download {
position: relative;
width: 18px;
height: 18px;
}
.icon-download::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 10px;
height: 12px;
border: 2px solid currentColor;
border-bottom: none;
}
.icon-download::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 8px solid currentColor;
}
@media (min-width: 1200px) {
.qrcode-item {
max-width: 300px;
}
.qrcode-wrapper {
width: 180px;
height: 180px;
}
.qrcode-info h4 {
font-size: 1.2rem;
margin-bottom: 0.8rem;
}
}
@media (max-width: 992px) {
.qrcode-item {
max-width: 280px;
}
.qrcode-wrapper {
width: 160px;
height: 160px;
}
}
@media (max-width: 576px) {
.qrcode-item {
max-width: 250px;
padding: 0.8rem;
}
.qrcode-wrapper {
width: 140px;
height: 140px;
margin-bottom: 0.8rem;
}
.qrcode-info h4 {
font-size: 0.9rem;
margin-bottom: 0.3rem;
}
.qrcode-controls {
font-size: 0.75rem;
}
}
@media (max-width: 360px) {
.qrcode-item {
max-width: 200px;
padding: 0.7rem;
}
.qrcode-wrapper {
width: 130px;
height: 130px;
}
}
</style>