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 (
+
+ );
+ }
+
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 (
+