From 1a4af38bace072e69dfcb7f31eea1a2cad2543f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Wed, 17 Jun 2026 00:23:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20QR=20=E9=A2=84=E8=A7=88=20+=20=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E9=9D=A2=E6=9D=BF(PNG/SVG/=E5=A4=8D=E5=88=B6)=20+=20?= =?UTF-8?q?=E6=96=87=E6=9C=AC/URL=20=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui/src-frontend/src/App.tsx | 16 ++- .../src/components/ExportPanel.tsx | 113 +++++++++++++++++- gui/src-frontend/src/components/QrPreview.tsx | 26 +++- gui/src-frontend/src/modes/TextMode.tsx | 22 ++++ gui/src-frontend/src/modes/UrlMode.tsx | 32 +++++ 5 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 gui/src-frontend/src/modes/TextMode.tsx create mode 100644 gui/src-frontend/src/modes/UrlMode.tsx diff --git a/gui/src-frontend/src/App.tsx b/gui/src-frontend/src/App.tsx index 08c9835..21bb161 100644 --- a/gui/src-frontend/src/App.tsx +++ b/gui/src-frontend/src/App.tsx @@ -3,6 +3,8 @@ import ModePanel from './components/ModePanel'; import QrPreview from './components/QrPreview'; import ExportPanel from './components/ExportPanel'; import HistoryList from './components/HistoryList'; +import TextMode from './modes/TextMode'; +import UrlMode from './modes/UrlMode'; function AppLayout() { return ( @@ -39,11 +41,15 @@ function AppLayout() { function BottomInput() { const { state } = useQrState(); - return ( -
- 输入区 — 开发中(当前模式: {state.mode}) -
- ); + switch (state.mode) { + case 'text': return ; + case 'url': return ; + default: return ( +
+ 更多模式即将推出... +
+ ); + } } export default function App() { diff --git a/gui/src-frontend/src/components/ExportPanel.tsx b/gui/src-frontend/src/components/ExportPanel.tsx index 3d718ee..a779a8f 100644 --- a/gui/src-frontend/src/components/ExportPanel.tsx +++ b/gui/src-frontend/src/components/ExportPanel.tsx @@ -1,8 +1,119 @@ +import { useState } from 'react'; +import { useQrState } from '../store/qrContext'; +import { invoke } from '@tauri-apps/api/core'; +import { writeText } from '@tauri-apps/plugin-clipboard-manager'; +import { save } from '@tauri-apps/plugin-dialog'; +import { writeFile } from '@tauri-apps/plugin-fs'; + +function getCurrentText(state: any): string { + switch (state.mode) { + case 'url': return state.formData.url || ''; + case 'wifi': return `WIFI:T:${state.formData.encryption || 'WPA'};S:${state.formData.ssid || ''};P:${state.formData.password || ''};;`; + case 'vcard': return `BEGIN:VCARD\nVERSION:3.0\nFN:${state.formData.name || ''}\nTEL:${state.formData.phone || ''}\nEMAIL:${state.formData.email || ''}\nORG:${state.formData.company || ''}\nADR:${state.formData.address || ''}\nEND:VCARD`; + case 'email': return `mailto:${state.formData.to || ''}?subject=${encodeURIComponent(state.formData.subject || '')}&body=${encodeURIComponent(state.formData.body || '')}`; + case 'phone': return `tel:${state.formData.number || ''}`; + case 'sms': return `smsto:${state.formData.number || ''}:${state.formData.message || ''}`; + default: return state.formData.text || ''; + } +} + export default function ExportPanel() { + const { state, dispatch } = useQrState(); + const [exporting, setExporting] = useState(false); + + const handleCopySvg = async () => { + if (state.preview?.svg) { + await writeText(state.preview.svg); + } + }; + + const handleExportPng = async () => { + if (!state.preview?.svg) return; + setExporting(true); + try { + const filePath = await save({ + filters: [{ name: 'PNG 图片', extensions: ['png'] }], + defaultPath: 'qrcode.png', + }); + if (!filePath) { setExporting(false); return; } + + const bytes: number[] = await invoke('export_png', { + text: getCurrentText(state), + level: state.config.level, + margin: state.config.margin, + moduleSize: state.config.moduleSize, + }); + await writeFile(filePath, new Uint8Array(bytes)); + } catch (e) { + console.error('导出 PNG 失败:', e); + } + setExporting(false); + }; + + const handleExportSvg = async () => { + if (!state.preview?.svg) return; + try { + const filePath = await save({ + filters: [{ name: 'SVG 图片', extensions: ['svg'] }], + defaultPath: 'qrcode.svg', + }); + if (!filePath) return; + await writeFile(filePath, new TextEncoder().encode(state.preview.svg)); + } catch (e) { + console.error('导出 SVG 失败:', e); + } + }; + return (
导出选项
-
开发中...
+ + + + + + + + + +
); } diff --git a/gui/src-frontend/src/components/QrPreview.tsx b/gui/src-frontend/src/components/QrPreview.tsx index ca8f919..aca6604 100644 --- a/gui/src-frontend/src/components/QrPreview.tsx +++ b/gui/src-frontend/src/components/QrPreview.tsx @@ -1,8 +1,28 @@ +import { useQrState } from '../store/qrContext'; + export default function QrPreview() { + const { state } = useQrState(); + + if (!state.preview?.svg) { + return ( +
+
+ 输入内容生成 QR 码 +
+
+ ); + } + return ( -
-
- 输入内容生成 QR 码 +
+
+
+ 版本: {state.preview.version} + {state.preview.size}×{state.preview.size} + 掩码: {state.preview.mask}
); diff --git a/gui/src-frontend/src/modes/TextMode.tsx b/gui/src-frontend/src/modes/TextMode.tsx new file mode 100644 index 0000000..ad0d53d --- /dev/null +++ b/gui/src-frontend/src/modes/TextMode.tsx @@ -0,0 +1,22 @@ +import { useQrState } from '../store/qrContext'; +import { useQrEncode } from '../hooks/useQrEncode'; + +export default function TextMode() { + const { state, dispatch } = useQrState(); + const { encode } = useQrEncode(); + + const handleChange = (text: string) => { + dispatch({ type: 'SET_FORM_DATA', payload: { text } }); + encode(text); + }; + + return ( +