duqing2026's picture
Feat: Auto-detect local WiFi SSID and add password toggle
2975274
<!DOCTYPE html>
<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>