Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>超级二维码工坊 | QR Code Master</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); | |
| body { font-family: 'Noto Sans SC', sans-serif; } | |
| .glass { | |
| background: rgba(255, 255, 255, 0.9); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .color-input-wrapper { | |
| position: relative; | |
| overflow: hidden; | |
| border-radius: 0.5rem; | |
| border: 1px solid #e2e8f0; | |
| display: flex; | |
| align-items: center; | |
| padding: 0.25rem; | |
| } | |
| .color-input-wrapper input[type="color"] { | |
| border: none; | |
| width: 30px; | |
| height: 30px; | |
| cursor: pointer; | |
| background: none; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-indigo-50 to-purple-50 min-h-screen text-slate-800"> | |
| <div id="app" class="container mx-auto px-4 py-8 max-w-5xl"> | |
| <!-- Header --> | |
| <header class="text-center mb-10"> | |
| <h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-purple-600 mb-2"> | |
| <i class="fa-solid fa-qrcode mr-2"></i>超级二维码工坊 | |
| </h1> | |
| <p class="text-slate-500">专业、免费、安全的在线二维码生成器</p> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-8"> | |
| <!-- Left: Configuration --> | |
| <div class="lg:col-span-7 space-y-6"> | |
| <!-- Type Selection Tabs --> | |
| <div class="bg-white rounded-xl shadow-sm p-2 flex space-x-2 border border-slate-100"> | |
| <button @click="currentType = 'text'" | |
| :class="['flex-1 py-2 rounded-lg font-medium transition-all duration-200 flex items-center justify-center space-x-2', currentType === 'text' ? 'bg-indigo-600 text-white shadow-md' : 'text-slate-600 hover:bg-slate-50']"> | |
| <i class="fa-solid fa-link"></i> <span>文本/链接</span> | |
| </button> | |
| <button @click="currentType = 'wifi'" | |
| :class="['flex-1 py-2 rounded-lg font-medium transition-all duration-200 flex items-center justify-center space-x-2', currentType === 'wifi' ? 'bg-indigo-600 text-white shadow-md' : 'text-slate-600 hover:bg-slate-50']"> | |
| <i class="fa-solid fa-wifi"></i> <span>WiFi</span> | |
| </button> | |
| <button @click="currentType = 'vcard'" | |
| :class="['flex-1 py-2 rounded-lg font-medium transition-all duration-200 flex items-center justify-center space-x-2', currentType === 'vcard' ? 'bg-indigo-600 text-white shadow-md' : 'text-slate-600 hover:bg-slate-50']"> | |
| <i class="fa-solid fa-address-card"></i> <span>电子名片</span> | |
| </button> | |
| </div> | |
| <!-- Input Form --> | |
| <div class="bg-white rounded-2xl shadow-lg p-6 border border-slate-100"> | |
| <!-- Text/Link Inputs --> | |
| <div v-if="currentType === 'text'" class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">内容或链接</label> | |
| <textarea v-model="form.content" rows="4" | |
| class="w-full rounded-lg border-slate-200 focus:border-indigo-500 focus:ring focus:ring-indigo-200 transition-all p-3" | |
| placeholder="输入文本或 https://..."></textarea> | |
| </div> | |
| </div> | |
| <!-- WiFi Inputs --> | |
| <div v-if="currentType === 'wifi'" class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">WiFi 名称 (SSID)</label> | |
| <input type="text" v-model="form.wifi_ssid" placeholder="自动获取或手动输入" class="w-full rounded-lg border-slate-200 p-2.5 focus:ring-indigo-200 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">密码</label> | |
| <div class="relative"> | |
| <input :type="showWifiPassword ? 'text' : 'password'" v-model="form.wifi_password" class="w-full rounded-lg border-slate-200 p-2.5 focus:ring-indigo-200 focus:border-indigo-500 pr-10"> | |
| <button @click="showWifiPassword = !showWifiPassword" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-indigo-500"> | |
| <i :class="['fa-solid', showWifiPassword ? 'fa-eye-slash' : 'fa-eye']"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex space-x-4"> | |
| <div class="flex-1"> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">加密方式</label> | |
| <select v-model="form.wifi_security" class="w-full rounded-lg border-slate-200 p-2.5"> | |
| <option value="WPA">WPA/WPA2</option> | |
| <option value="WEP">WEP</option> | |
| <option value="nopass">无密码</option> | |
| </select> | |
| </div> | |
| <div class="flex items-center pt-6"> | |
| <label class="flex items-center space-x-2 cursor-pointer"> | |
| <input type="checkbox" v-model="form.wifi_hidden" class="rounded text-indigo-600 focus:ring-indigo-500"> | |
| <span class="text-sm text-slate-700">隐藏网络</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- VCard Inputs --> | |
| <div v-if="currentType === 'vcard'" class="space-y-4"> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">姓名</label> | |
| <input type="text" v-model="form.vcard_name" class="w-full rounded-lg border-slate-200 p-2.5" placeholder="Lastname;Firstname"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">显示名称</label> | |
| <input type="text" v-model="form.vcard_displayname" class="w-full rounded-lg border-slate-200 p-2.5" placeholder="张三"> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">邮箱</label> | |
| <input type="email" v-model="form.vcard_email" class="w-full rounded-lg border-slate-200 p-2.5"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">电话</label> | |
| <input type="tel" v-model="form.vcard_phone" class="w-full rounded-lg border-slate-200 p-2.5"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">个人主页/公司网址</label> | |
| <input type="url" v-model="form.vcard_url" class="w-full rounded-lg border-slate-200 p-2.5"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Customization Options --> | |
| <div class="bg-white rounded-2xl shadow-lg p-6 border border-slate-100"> | |
| <h3 class="font-bold text-lg text-slate-800 mb-4 flex items-center"> | |
| <i class="fa-solid fa-palette text-indigo-500 mr-2"></i> 美化选项 | |
| </h3> | |
| <div class="grid grid-cols-2 gap-6 mb-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-2">前景色</label> | |
| <div class="color-input-wrapper"> | |
| <input type="color" v-model="form.fg_color"> | |
| <span class="ml-2 text-sm text-slate-500 font-mono" v-text="form.fg_color"></span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-2">背景色</label> | |
| <div class="color-input-wrapper"> | |
| <input type="color" v-model="form.bg_color"> | |
| <span class="ml-2 text-sm text-slate-500 font-mono" v-text="form.bg_color"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-slate-700 mb-2">嵌入 Logo (可选)</label> | |
| <div class="flex items-center space-x-4"> | |
| <label class="cursor-pointer bg-slate-50 border border-slate-200 hover:bg-slate-100 text-slate-600 px-4 py-2 rounded-lg transition-colors flex items-center"> | |
| <i class="fa-solid fa-cloud-upload-alt mr-2"></i> 选择图片 | |
| <input type="file" @change="handleLogoUpload" class="hidden" accept="image/*"> | |
| </label> | |
| <span v-if="logoFileName" class="text-sm text-green-600 flex items-center"> | |
| <i class="fa-solid fa-check-circle mr-1"></i> <span v-text="logoFileName"></span> | |
| <button @click="clearLogo" class="ml-2 text-slate-400 hover:text-red-500"> | |
| <i class="fa-solid fa-times"></i> | |
| </button> | |
| </span> | |
| </div> | |
| </div> | |
| <button @click="generateQR" :disabled="loading" | |
| class="w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white font-bold py-3 rounded-xl shadow-lg transform transition hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center"> | |
| <span v-if="loading"><i class="fa-solid fa-circle-notch fa-spin mr-2"></i> 生成中...</span> | |
| <span v-else><i class="fa-solid fa-wand-magic-sparkles mr-2"></i> 立即生成二维码</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Right: Preview --> | |
| <div class="lg:col-span-5"> | |
| <div class="bg-white rounded-2xl shadow-xl p-6 border border-slate-100 sticky top-8 text-center"> | |
| <h3 class="font-bold text-lg text-slate-800 mb-6">预览结果</h3> | |
| <div class="aspect-square bg-slate-50 rounded-xl flex items-center justify-center border-2 border-dashed border-slate-200 mb-6 overflow-hidden relative group"> | |
| <div v-if="!resultImage && !loading" class="text-slate-400 flex flex-col items-center"> | |
| <i class="fa-solid fa-qrcode text-6xl mb-4 opacity-20"></i> | |
| <p>点击生成预览</p> | |
| </div> | |
| <div v-else-if="loading" class="text-indigo-500"> | |
| <i class="fa-solid fa-circle-notch fa-spin text-4xl"></i> | |
| </div> | |
| <img v-else :src="resultImage" class="max-w-full max-h-full object-contain shadow-sm rounded-lg"> | |
| </div> | |
| <div v-if="resultImage" class="space-y-3 animate-fade-in"> | |
| <a :href="resultImage" download="qrcode.png" | |
| class="block w-full bg-slate-900 hover:bg-slate-800 text-white font-bold py-3 rounded-xl shadow-md transition-colors"> | |
| <i class="fa-solid fa-download mr-2"></i> 下载 PNG | |
| </a> | |
| <p class="text-xs text-slate-400">建议使用手机扫码测试后再打印</p> | |
| </div> | |
| </div> | |
| <!-- Tips --> | |
| <div class="mt-6 bg-blue-50 rounded-xl p-4 text-sm text-blue-700 border border-blue-100"> | |
| <div class="flex items-start"> | |
| <i class="fa-solid fa-info-circle mt-0.5 mr-2"></i> | |
| <div> | |
| <p class="font-bold mb-1">使用小贴士</p> | |
| <ul class="list-disc list-inside space-y-1 opacity-80"> | |
| <li>Logo 建议使用背景透明的 PNG 图片</li> | |
| <li>若颜色太浅可能导致无法扫描,建议保持高对比度</li> | |
| <li>WiFi 二维码可直接扫码连接网络</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const { createApp, ref, reactive } = Vue; | |
| createApp({ | |
| setup() { | |
| const currentType = ref('text'); | |
| const loading = ref(false); | |
| const resultImage = ref(null); | |
| const logoFile = ref(null); | |
| const logoFileName = ref(''); | |
| const showWifiPassword = ref(false); | |
| // Receive detected SSID from backend (rendered by Jinja2) | |
| const detectedSSID = "{{ default_ssid }}"; | |
| const form = reactive({ | |
| content: '', | |
| wifi_ssid: detectedSSID, | |
| wifi_password: '', | |
| wifi_security: 'WPA', | |
| wifi_hidden: false, | |
| vcard_name: '', | |
| vcard_displayname: '', | |
| vcard_email: '', | |
| vcard_phone: '', | |
| vcard_url: '', | |
| fg_color: '#000000', | |
| bg_color: '#ffffff', | |
| scale: 10 | |
| }); | |
| const handleLogoUpload = (event) => { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| logoFile.value = file; | |
| logoFileName.value = file.name; | |
| } | |
| }; | |
| const clearLogo = () => { | |
| logoFile.value = null; | |
| logoFileName.value = ''; | |
| }; | |
| const generateQR = async () => { | |
| loading.value = true; | |
| resultImage.value = null; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('type', currentType.value); | |
| formData.append('fg_color', form.fg_color); | |
| formData.append('bg_color', form.bg_color); | |
| formData.append('scale', form.scale); | |
| if (currentType.value === 'text') { | |
| formData.append('content', form.content); | |
| } else if (currentType.value === 'wifi') { | |
| formData.append('wifi_ssid', form.wifi_ssid); | |
| formData.append('wifi_password', form.wifi_password); | |
| formData.append('wifi_security', form.wifi_security); | |
| formData.append('wifi_hidden', form.wifi_hidden); | |
| } else if (currentType.value === 'vcard') { | |
| formData.append('vcard_name', form.vcard_name); | |
| formData.append('vcard_displayname', form.vcard_displayname); | |
| formData.append('vcard_email', form.vcard_email); | |
| formData.append('vcard_phone', form.vcard_phone); | |
| formData.append('vcard_url', form.vcard_url); | |
| } | |
| if (logoFile.value) { | |
| formData.append('logo', logoFile.value); | |
| } | |
| const response = await fetch('/api/generate', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| resultImage.value = data.image; | |
| } else { | |
| alert('生成失败: ' + (data.error || '未知错误')); | |
| } | |
| } catch (error) { | |
| alert('请求错误: ' + error.message); | |
| } finally { | |
| loading.value = false; | |
| } | |
| }; | |
| return { | |
| currentType, | |
| form, | |
| loading, | |
| resultImage, | |
| logoFileName, | |
| showWifiPassword, | |
| handleLogoUpload, | |
| clearLogo, | |
| generateQR | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |