update: 二维码

This commit is contained in:
ctwj
2025-10-27 23:41:35 +08:00
parent 22fd1dcf81
commit 1ad3a07930
13 changed files with 862 additions and 61 deletions

View File

@@ -0,0 +1,285 @@
<template>
<n-modal
:show="show"
:mask-closable="true"
preset="card"
title="选择二维码样式"
style="width: 90%; max-width: 900px;"
@close="handleClose"
@update:show="handleShowUpdate"
>
<div class="qr-style-selector">
<!-- 样式选择区域 -->
<div class="styles-section">
<div class="styles-grid">
<div
v-for="preset in allQrCodePresets"
:key="preset.name"
class="style-item"
:class="{ active: selectedPreset?.name === preset.name }"
@click="selectPreset(preset)"
>
<div class="qr-preview" :style="preset.style">
<div :ref="el => setQRContainer(el, preset.name)" v-if="preset"></div>
<!-- 选中状态指示器 -->
<div v-if="selectedPreset?.name === preset.name" class="selected-indicator">
<i class="fas fa-check"></i>
</div>
</div>
<div class="style-name">{{ preset.name }}</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<n-button @click="handleClose">取消</n-button>
<n-button type="primary" @click="confirmSelection">
确认选择
</n-button>
</div>
</div>
</n-modal>
</template>
<script setup lang="ts">
import QRCodeStyling from 'qr-code-styling'
import { allQrCodePresets, type Preset } from '~/components/QRCode/presets'
// Props
const props = defineProps<{
show: boolean
currentStyle?: string
}>()
// Emits
const emit = defineEmits<{
'update:show': [value: boolean]
'select': [preset: Preset]
}>()
// 示例数据
const sampleData = ref('https://pan.l9.lc')
// 当前选中的预设
const selectedPreset = ref<Preset | null>(null)
// QR码实例映射
const qrInstances = ref<Map<string, QRCodeStyling>>(new Map())
// 监听显示状态变化
watch(() => props.show, (newShow) => {
if (newShow) {
// 查找当前样式对应的预设
const currentPreset = allQrCodePresets.find(preset => preset.name === props.currentStyle)
selectedPreset.value = currentPreset || allQrCodePresets[0] // 默认选择 Plain
// 延迟渲染QR码确保DOM已经准备好
nextTick(() => {
renderAllQRCodes()
})
}
})
// 设置QR码容器
const setQRContainer = (el: HTMLElement, presetName: string) => {
if (el) {
// 先清空容器内容,防止重复添加
el.innerHTML = ''
const preset = allQrCodePresets.find(p => p.name === presetName)
if (preset) {
const qrInstance = new QRCodeStyling({
data: sampleData.value,
...preset,
width: 80,
height: 80
})
qrInstance.append(el)
// 保存实例引用
qrInstances.value.set(presetName, qrInstance)
}
}
}
// 渲染所有QR码
const renderAllQRCodes = () => {
// 这个函数会在 setQRContainer 中被调用
// 这里不需要额外操作,因为每个组件都会自己渲染
}
// 选择预设
const selectPreset = (preset: Preset) => {
selectedPreset.value = preset
}
// 确认选择
const confirmSelection = () => {
if (selectedPreset.value) {
emit('select', selectedPreset.value)
handleClose()
}
}
// 处理显示状态更新
const handleShowUpdate = (value: boolean) => {
emit('update:show', value)
}
// 关闭弹窗
const handleClose = () => {
emit('update:show', false)
}
</script>
<style scoped>
.qr-style-selector {
padding: 20px;
}
/* 样式选择区域 */
.styles-section h3 {
margin-bottom: 20px;
color: var(--color-text-1);
font-size: 18px;
font-weight: 600;
}
.styles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
margin-bottom: 30px;
max-height: 400px;
overflow-y: auto;
padding: 10px;
}
.style-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 15px;
border: 2px solid transparent;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
background: var(--color-card-bg);
}
.style-item:hover {
border-color: var(--color-primary-soft);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.style-item.active {
border-color: var(--color-primary);
background: var(--color-primary-soft);
box-shadow: 0 0 0 2px rgba(24, 160, 88, 0.2);
}
.qr-preview {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
border-radius: 6px;
transition: all 0.3s ease;
position: relative;
}
/* 选中状态指示器 */
.selected-indicator {
position: absolute;
top: 5px;
right: 5px;
width: 24px;
height: 24px;
background: var(--color-primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 10;
}
.style-name {
font-size: 12px;
font-weight: 500;
color: var(--color-text-2);
text-align: center;
}
.style-item.active .style-name {
color: var(--color-primary);
font-weight: 600;
}
/* 操作按钮 */
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 20px;
border-top: 1px solid var(--color-border);
}
/* 暗色主题适配 */
.dark .styles-grid {
background: var(--color-dark-bg);
}
.dark .style-item {
background: var(--color-dark-card);
}
.dark .style-item:hover {
background: var(--color-dark-card-hover);
}
.dark .style-item.active {
background: rgba(24, 160, 88, 0.1);
}
/* 响应式 */
@media (max-width: 768px) {
.qr-style-selector {
padding: 15px;
}
.styles-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
max-height: 300px;
}
.style-item {
padding: 10px;
}
}
/* 滚动条样式 */
.styles-grid::-webkit-scrollbar {
width: 6px;
}
.styles-grid::-webkit-scrollbar-track {
background: var(--color-border);
border-radius: 3px;
}
.styles-grid::-webkit-scrollbar-thumb {
background: var(--color-text-3);
border-radius: 3px;
}
.styles-grid::-webkit-scrollbar-thumb:hover {
background: var(--color-text-2);
}
</style>

View File

@@ -11,7 +11,14 @@
<i class="fa fa-qrcode" style="color: #27ae60;"></i>
<div class="hover-show-con dropdown-menu qrcode-btn" style="width: 150px; height: auto;">
<div class="qrcode" data-size="100">
<n-qr-code :value="currentUrl" :size="100" :bordered="false" />
<QRCodeDisplay
v-if="qrCodePreset"
:data="currentUrl"
:preset="qrCodePreset"
:width="100"
:height="100"
/>
<n-qr-code v-else :value="currentUrl" :size="100" :bordered="false" />
</div>
<div class="mt6 px12 muted-color">扫一扫在手机上体验</div>
</div>
@@ -44,6 +51,7 @@
<script setup lang="ts">
// 导入系统配置store
import { useSystemConfigStore } from '~/stores/systemConfig'
import { QRCodeDisplay, findPresetByName } from '~/components/QRCode'
// 获取系统配置store
const systemConfigStore = useSystemConfigStore()
@@ -66,6 +74,12 @@ const telegramQrImage = computed(() => {
return systemConfigStore.config?.telegram_qr_image || ''
})
// 计算属性:二维码样式预设
const qrCodePreset = computed(() => {
const styleName = systemConfigStore.config?.qr_code_style || 'Plain'
return findPresetByName(styleName)
})
// 滚动到顶部
const scrollToTop = () => {
window.scrollTo({

View File

@@ -42,6 +42,7 @@ interface Props {
preset?: Preset
borderRadius?: string
background?: string
className?: string
customImage?: string
customImageOptions?: {
margin?: number
@@ -75,17 +76,37 @@ let qrCodeInstance: QRCodeStyling | null = null
// 计算容器样式
const containerStyle = computed(() => {
if (props.preset) {
return {
const style = {
borderRadius: props.preset.style.borderRadius || '0px',
background: props.preset.style.background || 'transparent',
padding: '16px'
}
// 如果预设有className添加到样式中
if (props.preset.style.className) {
return {
...style,
class: props.preset.style.className
}
}
return style
}
return {
const style = {
borderRadius: props.borderRadius,
background: props.background,
padding: '16px'
}
// 如果props有className添加到样式中
if (props.className) {
return {
...style,
class: props.className
}
}
return style
})
// 生成配置键,用于缓存

View File

@@ -117,6 +117,236 @@ export const techPreset: Preset = {
style: { borderRadius: '0px', background: '#000000' }
}
// 透明预设
export const transparentPreset: Preset = {
...defaultPresetOptions,
name: 'Transparent',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%23374151',
dotsOptions: { color: '#374151', type: 'dots' },
cornersSquareOptions: { color: '#374151', type: 'dot' },
cornersDotOptions: { color: '#374151', type: 'dot' },
imageOptions: { margin: 8 },
style: { borderRadius: '8px', background: 'transparent' }
}
// 渐变预设 - 二维码组成部分使用渐变
export const gradientModernPreset: Preset = {
...defaultPresetOptions,
name: 'Gradient Modern',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%23667eea',
dotsOptions: {
type: 'rounded',
gradient: {
type: 'linear',
rotation: 45,
colorStops: [
{ offset: 0, color: '#667eea' },
{ offset: 0.5, color: '#764ba2' },
{ offset: 1, color: '#f093fb' }
]
}
},
cornersSquareOptions: {
type: 'extra-rounded',
gradient: {
type: 'radial',
colorStops: [
{ offset: 0, color: '#f093fb' },
{ offset: 1, color: '#f5576c' }
]
}
},
cornersDotOptions: {
type: 'dot',
gradient: {
type: 'linear',
rotation: 90,
colorStops: [
{ offset: 0, color: '#fda085' },
{ offset: 1, color: '#f5576c' }
]
}
},
imageOptions: { margin: 8 },
style: {
borderRadius: '16px',
background: '#F8FAFC'
}
}
// 彩虹渐变预设 - 二维码组成部分使用彩虹渐变
export const rainbowPreset: Preset = {
...defaultPresetOptions,
name: 'Rainbow',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%23ff0000',
dotsOptions: {
type: 'dots',
gradient: {
type: 'linear',
rotation: 45,
colorStops: [
{ offset: 0, color: '#ff0000' },
{ offset: 0.14, color: '#ff7f00' },
{ offset: 0.28, color: '#ffff00' },
{ offset: 0.42, color: '#00ff00' },
{ offset: 0.57, color: '#0000ff' },
{ offset: 0.71, color: '#4b0082' },
{ offset: 0.85, color: '#9400d3' },
{ offset: 1, color: '#ff0000' }
]
}
},
cornersSquareOptions: {
type: 'extra-rounded',
gradient: {
type: 'radial',
colorStops: [
{ offset: 0, color: '#ffff00' },
{ offset: 0.5, color: '#00ff00' },
{ offset: 1, color: '#0000ff' }
]
}
},
cornersDotOptions: {
type: 'dot',
gradient: {
type: 'linear',
rotation: 90,
colorStops: [
{ offset: 0, color: '#ff7f00' },
{ offset: 0.5, color: '#ff00ff' },
{ offset: 1, color: '#00ffff' }
]
}
},
imageOptions: { margin: 8 },
style: {
borderRadius: '20px',
background: '#FEFEFE'
}
}
// 动态颜色预设 - 二维码组成部分使用动态渐变
export const dynamicPreset: Preset = {
...defaultPresetOptions,
name: 'Dynamic',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%23ee7752',
dotsOptions: {
type: 'rounded',
gradient: {
type: 'linear',
rotation: -45,
colorStops: [
{ offset: 0, color: '#ee7752' },
{ offset: 0.33, color: '#e73c7e' },
{ offset: 0.66, color: '#23a6d5' },
{ offset: 1, color: '#23d5ab' }
]
}
},
cornersSquareOptions: {
type: 'extra-rounded',
gradient: {
type: 'radial',
colorStops: [
{ offset: 0, color: '#23d5ab' },
{ offset: 0.5, color: '#ee7752' },
{ offset: 1, color: '#e73c7e' }
]
}
},
cornersDotOptions: {
type: 'dot',
gradient: {
type: 'linear',
rotation: 45,
colorStops: [
{ offset: 0, color: '#23a6d5' },
{ offset: 0.5, color: '#23d5ab' },
{ offset: 1, color: '#ee7752' }
]
}
},
imageOptions: { margin: 8 },
style: {
borderRadius: '12px',
background: '#F5F5F5',
className: 'qr-dynamic'
}
}
// 玻璃态预设
export const glassPreset: Preset = {
...defaultPresetOptions,
name: 'Glass',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%231F2937',
dotsOptions: { color: '#1F2937', type: 'dots' },
cornersSquareOptions: { color: '#1F2937', type: 'dot' },
cornersDotOptions: { color: '#1F2937', type: 'dot' },
imageOptions: { margin: 8 },
style: {
borderRadius: '16px',
background: 'rgba(255, 255, 255, 0.25)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.18)'
}
}
// 霓虹预设 - 二维码组成部分使用霓虹渐变
export const neonPreset: Preset = {
...defaultPresetOptions,
name: 'Neon',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%2300FF88',
dotsOptions: {
type: 'square',
gradient: {
type: 'linear',
rotation: 45,
colorStops: [
{ offset: 0, color: '#00FF88' },
{ offset: 0.5, color: '#00FFAA' },
{ offset: 1, color: '#00FFCC' }
]
}
},
cornersSquareOptions: {
type: 'square',
gradient: {
type: 'radial',
colorStops: [
{ offset: 0, color: '#FF00FF' },
{ offset: 0.5, color: '#FF00AA' },
{ offset: 1, color: '#FF0088' }
]
}
},
cornersDotOptions: {
type: 'square',
gradient: {
type: 'linear',
rotation: 90,
colorStops: [
{ offset: 0, color: '#00FFFF' },
{ offset: 0.5, color: '#00FFEE' },
{ offset: 1, color: '#00FFCC' }
]
}
},
imageOptions: { margin: 8 },
style: {
borderRadius: '8px',
background: '#1a1a1a',
boxShadow: '0 0 20px rgba(0, 255, 136, 0.3), 0 0 40px rgba(0, 255, 136, 0.2), 0 0 60px rgba(0, 255, 136, 0.1)',
className: 'qr-neon'
}
}
export const naturePreset: Preset = {
...defaultPresetOptions,
name: 'Nature',
@@ -153,6 +383,150 @@ export const coolPreset: Preset = {
style: { borderRadius: '20px', background: '#EFF6FF' }
}
// 新增:金属渐变预设
export const metallicPreset: Preset = {
...defaultPresetOptions,
name: 'Metallic',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%23FFD700',
dotsOptions: {
type: 'rounded',
gradient: {
type: 'linear',
rotation: 135,
colorStops: [
{ offset: 0, color: '#C0C0C0' },
{ offset: 0.25, color: '#E5E5E5' },
{ offset: 0.5, color: '#FFD700' },
{ offset: 0.75, color: '#E5E5E5' },
{ offset: 1, color: '#C0C0C0' }
]
}
},
cornersSquareOptions: {
type: 'extra-rounded',
gradient: {
type: 'radial',
colorStops: [
{ offset: 0, color: '#FFD700' },
{ offset: 0.5, color: '#C0C0C0' },
{ offset: 1, color: '#808080' }
]
}
},
cornersDotOptions: {
type: 'dot',
gradient: {
type: 'linear',
rotation: 45,
colorStops: [
{ offset: 0, color: '#FFD700' },
{ offset: 1, color: '#B8860B' }
]
}
},
imageOptions: { margin: 8 },
style: {
borderRadius: '16px',
background: '#F8F8F8'
}
}
// 新增:海洋渐变预设
export const oceanPreset: Preset = {
...defaultPresetOptions,
name: 'Ocean',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%2300CED1',
dotsOptions: {
type: 'dots',
gradient: {
type: 'radial',
colorStops: [
{ offset: 0, color: '#00CED1' },
{ offset: 0.5, color: '#4682B4' },
{ offset: 1, color: '#191970' }
]
}
},
cornersSquareOptions: {
type: 'square',
gradient: {
type: 'linear',
rotation: 90,
colorStops: [
{ offset: 0, color: '#00FFFF' },
{ offset: 0.5, color: '#00CED1' },
{ offset: 1, color: '#0000CD' }
]
}
},
cornersDotOptions: {
type: 'dot',
gradient: {
type: 'linear',
rotation: 45,
colorStops: [
{ offset: 0, color: '#87CEEB' },
{ offset: 1, color: '#4682B4' }
]
}
},
imageOptions: { margin: 8 },
style: {
borderRadius: '20px',
background: '#E0F2FE'
}
}
// 新增:火焰渐变预设
export const firePreset: Preset = {
...defaultPresetOptions,
name: 'Fire',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%23FF4500',
dotsOptions: {
type: 'classy-rounded',
gradient: {
type: 'radial',
colorStops: [
{ offset: 0, color: '#FFFF00' },
{ offset: 0.3, color: '#FFA500' },
{ offset: 0.7, color: '#FF4500' },
{ offset: 1, color: '#8B0000' }
]
}
},
cornersSquareOptions: {
type: 'extra-rounded',
gradient: {
type: 'linear',
rotation: 45,
colorStops: [
{ offset: 0, color: '#FF6347' },
{ offset: 0.5, color: '#FF4500' },
{ offset: 1, color: '#DC143C' }
]
}
},
cornersDotOptions: {
type: 'square',
gradient: {
type: 'linear',
rotation: 90,
colorStops: [
{ offset: 0, color: '#FFA500' },
{ offset: 1, color: '#FF4500' }
]
}
},
imageOptions: { margin: 8 },
style: {
borderRadius: '12px',
background: '#FFF7ED'
}
}
// 原项目预设
export const padletPreset: Preset = {
...defaultPresetOptions,
@@ -166,17 +540,6 @@ export const padletPreset: Preset = {
style: { borderRadius: '24px', background: '#000000' }
}
export const vercelLightPreset: Preset = {
...defaultPresetOptions,
name: 'Vercel Light',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:logo-vercel.svg?color=%23000',
dotsOptions: { color: '#000000', type: 'classy' },
cornersSquareOptions: { color: '#000000', type: 'square' },
cornersDotOptions: { color: '#000000', type: 'square' },
imageOptions: { margin: 8 },
style: { borderRadius: '0px', background: '#FFFFFF' }
}
export const vercelDarkPreset: Preset = {
...defaultPresetOptions,
@@ -190,29 +553,6 @@ export const vercelDarkPreset: Preset = {
style: { borderRadius: '0px', background: '#000000' }
}
export const supabaseGreenPreset: Preset = {
...defaultPresetOptions,
name: 'Supabase Green',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/logos:supabase-icon.svg',
dotsOptions: { color: '#3ecf8e', type: 'classy-rounded' },
cornersSquareOptions: { color: '#3ecf8e', type: 'square' },
cornersDotOptions: { color: '#3ecf8e', type: 'square' },
imageOptions: { margin: 8 },
style: { borderRadius: '12px', background: '#000000' }
}
export const supabasePurplePreset: Preset = {
...defaultPresetOptions,
name: 'Supabase Purple',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%237700ff',
dotsOptions: { color: '#7700ff', type: 'classy-rounded' },
cornersSquareOptions: { color: '#7700ff', type: 'square' },
cornersDotOptions: { color: '#7700ff', type: 'square' },
imageOptions: { margin: 8 },
style: { borderRadius: '12px', background: '#000000' }
}
export const uiliciousPreset: Preset = {
...defaultPresetOptions,
@@ -250,17 +590,6 @@ export const vueJsPreset: Preset = {
style: { borderRadius: '12px', background: '#000000' }
}
export const vuei18nPreset: Preset = {
...defaultPresetOptions,
name: 'Vue i18n',
data: 'https://pan.l9.lc',
image: 'https://api.iconify.design/ion:qr-code-outline.svg?color=%23FF6B6B',
dotsOptions: { color: '#FF6B6B', type: 'classy-rounded' },
cornersSquareOptions: { color: '#FF6B6B', type: 'square' },
cornersDotOptions: { color: '#FF6B6B', type: 'square' },
imageOptions: { margin: 8 },
style: { borderRadius: '12px', background: '#000000' }
}
export const lyqhtPreset: Preset = {
...defaultPresetOptions,
@@ -359,19 +688,25 @@ export const builtInPresets: Preset[] = [
gradientPreset,
minimalPreset,
techPreset,
// 高级样式预设
transparentPreset,
gradientModernPreset,
rainbowPreset,
dynamicPreset,
glassPreset,
neonPreset,
naturePreset,
warmPreset,
coolPreset,
metallicPreset,
oceanPreset,
firePreset,
// 原项目预设
padletPreset,
vercelLightPreset,
vercelDarkPreset,
supabaseGreenPreset,
supabasePurplePreset,
uiliciousPreset,
viteConf2023Preset,
vueJsPreset,
vuei18nPreset,
lyqhtPreset,
pejuangKodePreset,
geeksHackingPreset,

View File

@@ -96,7 +96,14 @@
<div v-if="isQuarkLink" class="space-y-4">
<div class=" flex justify-center">
<div class="flex qr-container items-center justify-center w-full">
<QRCodeDisplay :data="save_url || url" :width="size" :height="size" />
<QRCodeDisplay
v-if="qrCodePreset"
:data="save_url || url"
:preset="qrCodePreset"
:width="size"
:height="size"
/>
<QRCodeDisplay v-else :data="save_url || url" :width="size" :height="size" />
</div>
</div>
<div class="text-center">
@@ -109,12 +116,19 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">请使用手机扫码操作</p>
</div>
</div>
<!-- 其他链接同时显示链接和二维码 -->
<div v-else class="space-y-4">
<div class="mb-4 flex justify-center">
<div class="flex qr-container items-center justify-center w-full">
<QRCodeDisplay :data="save_url || url" :preset="supabaseGreenPreset" :width="size" :height="size" />
<QRCodeDisplay
v-if="qrCodePreset"
:data="save_url || url"
:preset="qrCodePreset"
:width="size"
:height="size"
/>
<QRCodeDisplay v-else :data="save_url || url" :width="size" :height="size" />
</div>
</div>
@@ -149,7 +163,9 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { QRCodeDisplay, supabaseGreenPreset, preloadCommonLogos } from './QRCode'
import { QRCodeDisplay, preloadCommonLogos } from './QRCode'
import { useSystemConfigStore } from '~/stores/systemConfig'
import { findPresetByName } from './QRCode/presets'
interface Props {
visible: boolean
@@ -173,10 +189,19 @@ const props = withDefaults(defineProps<Props>(), {
})
const emit = defineEmits<Emits>()
// 获取系统配置store
const systemConfigStore = useSystemConfigStore()
const size = ref(180)
const color = ref('#409eff')
const backgroundColor = ref('#F5F5F5')
// 计算二维码样式预设
const qrCodePreset = computed(() => {
const styleName = systemConfigStore.config?.qr_code_style || 'Plain'
return findPresetByName(styleName)
})
// 检测是否为移动设备
const isMobile = ref(false)