diff --git a/docs/superpowers/plans/2026-06-17-qrcode-gui-plan.md b/docs/superpowers/plans/2026-06-17-qrcode-gui-plan.md
new file mode 100644
index 0000000..c0a8c57
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-17-qrcode-gui-plan.md
@@ -0,0 +1,1661 @@
+# QRGen GUI 版 实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 为 QRGen 添加 Tauri 2 + React 桌面 GUI,支持 7 种编码模式、实时预览、多格式导出和历史记录
+
+**Architecture:** 在现有 workspace 添加 `gui/` crate,Tauri 2 (Rust 后端, thin wrapper over qr-core) + React 18 + TypeScript + TailwindCSS (前端)
+
+**Tech Stack:** Tauri 2.6, React 18, TypeScript, TailwindCSS 4, Vite, pnpm
+
+---
+
+## 文件清单
+
+### 修改已有文件
+| 文件 | 变更 |
+|------|------|
+| `Cargo.toml` | workspace members 加 `"gui"` |
+| `core/Cargo.toml` | 加 `serde` derive 依赖 |
+
+### 新增文件
+| 文件 | 职责 |
+|------|------|
+| `gui/Cargo.toml` | Tauri crate 配置 |
+| `gui/tauri.conf.json` | Tauri 窗口/权限配置 |
+| `gui/build.rs` | Tauri 构建脚本 |
+| `gui/src/main.rs` | Tauri 入口 |
+| `gui/src/lib.rs` | Tauri commands(encode_qr, export, history CRUD) |
+| `gui/src-frontend/package.json` | 前端依赖 |
+| `gui/src-frontend/vite.config.ts` | Vite 构建配置 |
+| `gui/src-frontend/tsconfig.json` | TypeScript 配置 |
+| `gui/src-frontend/index.html` | HTML 入口 |
+| `gui/src-frontend/src/main.tsx` | React 入口 |
+| `gui/src-frontend/src/index.css` | TailwindCSS + 全局样式 |
+| `gui/src-frontend/src/App.tsx` | 主布局(三栏+底部输入) |
+| `gui/src-frontend/src/types/index.ts` | TypeScript 类型定义 |
+| `gui/src-frontend/src/hooks/useQrEncode.ts` | 编码 hook(防抖+Tauri调用) |
+| `gui/src-frontend/src/store/qrContext.tsx` | React Context + useReducer |
+| `gui/src-frontend/src/store/history.ts` | 历史记录持久化(tauri-plugin-store) |
+| `gui/src-frontend/src/components/ModePanel.tsx` | 左侧模式面板 |
+| `gui/src-frontend/src/components/QrPreview.tsx` | 中间预览区 |
+| `gui/src-frontend/src/components/ExportPanel.tsx` | 右侧导出面板 |
+| `gui/src-frontend/src/components/HistoryList.tsx` | 历史记录列表 |
+| `gui/src-frontend/src/modes/TextMode.tsx` | 文本模式表单 |
+| `gui/src-frontend/src/modes/UrlMode.tsx` | URL 模式表单 |
+| `gui/src-frontend/src/modes/WifiMode.tsx` | WiFi 模式表单 |
+| `gui/src-frontend/src/modes/VCardMode.tsx` | vCard 模式表单 |
+| `gui/src-frontend/src/modes/EmailMode.tsx` | Email 模式表单 |
+| `gui/src-frontend/src/modes/PhoneMode.tsx` | 电话模式表单 |
+| `gui/src-frontend/src/modes/SmsMode.tsx` | SMS 模式表单 |
+
+---
+
+### Task 1: 准备 qr-core + Workspace
+
+**Files:**
+- Modify: `Cargo.toml` (workspace root)
+- Modify: `core/Cargo.toml`
+
+- [ ] **Step 1: 更新 workspace members**
+
+`Cargo.toml`:
+```toml
+[workspace]
+resolver = "2"
+members = ["core", "cli", "gui"]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+authors = ["刘航宇"]
+```
+
+- [ ] **Step 2: 给 qr-core 添加 serde 支持**
+
+`core/Cargo.toml` 追加:
+```toml
+serde = { version = "1", features = ["derive"] }
+```
+
+在 `core/src/version.rs` 中给 `EcLevel` 加 `Serialize` derive:
+```rust
+use serde::Serialize;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+pub enum EcLevel { ... }
+```
+
+给 `Version` 加 derive:
+```rust
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+pub struct Version(pub u8);
+```
+
+- [ ] **Step 3: 验证编译**
+
+```bash
+cd D:\Code\doing_exercises\programs\QRGen && cargo build
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add -A && git commit -m "chore: workspace 加 gui 成员,qr-core 加 serde"
+```
+
+---
+
+### Task 2: Tauri 2 + React 脚手架
+
+**Files:**
+- Create: `gui/Cargo.toml`
+- Create: `gui/tauri.conf.json`
+- Create: `gui/build.rs`
+- Create: `gui/src/main.rs`
+- Create: `gui/src/lib.rs` (stub)
+- Create: `gui/src-frontend/package.json`
+- Create: `gui/src-frontend/vite.config.ts`
+- Create: `gui/src-frontend/tsconfig.json`
+- Create: `gui/src-frontend/index.html`
+- Create: `gui/src-frontend/src/main.tsx`
+- Create: `gui/src-frontend/src/index.css`
+- Create: `gui/src-frontend/src/App.tsx` (stub)
+
+- [ ] **Step 1: 创建 gui/Cargo.toml**
+
+```toml
+[package]
+name = "qrgen-gui"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+
+[lib]
+name = "app_lib"
+crate-type = ["staticlib", "rlib"]
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+qr-core = { path = "../core" }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tauri = { version = "2", features = [] }
+tauri-plugin-store = "2"
+tauri-plugin-dialog = "2"
+tauri-plugin-clipboard-manager = "2"
+```
+
+- [ ] **Step 2: 创建 gui/tauri.conf.json**
+
+```json
+{
+ "$schema": "https://raw.githubusercontent.com/nicehash/tauri/main/crates/tauri-cli/config.schema.json",
+ "productName": "QRGen",
+ "version": "0.1.0",
+ "identifier": "com.liuhangyu.qrgen",
+ "build": {
+ "frontendDist": "../src-frontend/dist",
+ "devUrl": "http://localhost:1420",
+ "beforeDevCommand": "cd src-frontend && pnpm dev",
+ "beforeBuildCommand": "cd src-frontend && pnpm build"
+ },
+ "app": {
+ "withGlobalTauri": true,
+ "windows": [
+ {
+ "title": "QRGen - QR 码生成器",
+ "width": 900,
+ "height": 650,
+ "minWidth": 900,
+ "minHeight": 650,
+ "resizable": true,
+ "decorations": true
+ }
+ ],
+ "security": {
+ "csp": null
+ }
+ },
+ "plugins": {
+ "store": {}
+ }
+}
+```
+
+- [ ] **Step 3: 创建 gui/build.rs**
+
+```rust
+fn main() {
+ tauri_build::build()
+}
+```
+
+- [ ] **Step 4: 创建 gui/src/main.rs**
+
+```rust
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+fn main() {
+ app_lib::run()
+}
+```
+
+- [ ] **Step 5: 创建 gui/src/lib.rs(占位)**
+
+```rust
+use tauri::Manager;
+
+#[tauri::command]
+fn greet(name: &str) -> String {
+ format!("你好, {}! QRGen GUI 已就绪。", name)
+}
+
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_store::Builder::new().build())
+ .plugin(tauri_plugin_dialog::init())
+ .plugin(tauri_plugin_clipboard_manager::init())
+ .invoke_handler(tauri::generate_handler![greet])
+ .run(tauri::generate_context!())
+ .expect("启动 QRGen GUI 失败");
+}
+```
+
+- [ ] **Step 6: 创建前端工程文件**
+
+`gui/src-frontend/package.json`:
+```json
+{
+ "name": "qrgen-frontend",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 1420",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-store": "^2",
+ "@tauri-apps/plugin-clipboard-manager": "^2",
+ "@tauri-apps/plugin-dialog": "^2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.16",
+ "typescript": "^5.6.3",
+ "vite": "^6.0.3"
+ }
+}
+```
+
+`gui/src-frontend/vite.config.ts`:
+```typescript
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ clearScreen: false,
+ server: { port: 1420, strictPort: true },
+ envPrefix: ["VITE_", "TAURI_"],
+ build: { target: "esnext", minify: !process.env.TAURI_DEBUG ? "esbuild" : false, sourcemap: !!process.env.TAURI_DEBUG }
+});
+```
+
+`gui/src-frontend/tsconfig.json`:
+```json
+{
+ "compilerOptions": {
+ "target": "ES2021",
+ "useDefineForClassFields": true,
+ "lib": ["ES2021", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
+```
+
+`gui/src-frontend/index.html`:
+```html
+
+
+
+
+
+ QRGen
+
+
+
+
+
+
+```
+
+`gui/src-frontend/src/main.tsx`:
+```tsx
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+);
+```
+
+`gui/src-frontend/src/index.css`:
+```css
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ @apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100;
+ margin: 0;
+ overflow: hidden;
+ user-select: none;
+}
+
+#root {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+```
+
+`gui/src-frontend/tailwind.config.js`:
+```js
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ darkMode: "media",
+ theme: { extend: {} },
+ plugins: [],
+};
+```
+
+`gui/src-frontend/postcss.config.js`:
+```js
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
+```
+
+- [ ] **Step 7: 安装前端依赖 + 验证编译**
+
+```bash
+cd D:\Code\doing_exercises\programs\QRGen\gui\src-frontend && pnpm install && cd ..\.. && cargo build -p qrgen-gui
+```
+
+- [ ] **Step 8: 提交**
+
+```bash
+git add -A && git commit -m "feat: Tauri 2 + React 脚手架 — gui crate"
+```
+
+---
+
+### Task 3: Rust 后端 — Tauri Commands
+
+**Files:**
+- Modify: `gui/src/lib.rs` (replace stub with full commands)
+
+- [ ] **Step 1: 实现完整的 lib.rs**
+
+```rust
+use qr_core::qr::{QrCode, QrConfig, VersionMode};
+use qr_core::version::EcLevel;
+use serde::{Deserialize, Serialize};
+use tauri::Manager;
+use std::sync::Mutex;
+
+/// QR 编码响应
+#[derive(Debug, Serialize, Clone)]
+struct QrResponse {
+ svg: String,
+ version: u8,
+ size: u8,
+ mask: u8,
+}
+
+/// 历史记录条目
+#[derive(Debug, Serialize, Deserialize, Clone)]
+struct HistoryEntry {
+ id: String,
+ mode: String,
+ content: String,
+ timestamp: u64,
+}
+
+/// 应用状态(内存中的历史记录)
+struct AppState {
+ history: Mutex>,
+}
+
+/// 编码 QR 码,返回 SVG + 元信息
+#[tauri::command]
+fn encode_qr(text: String, level: String, margin: u8) -> Result {
+ let ec_level = match level.to_uppercase().as_str() {
+ "L" => EcLevel::L,
+ "M" => EcLevel::M,
+ "Q" => EcLevel::Q,
+ "H" => EcLevel::H,
+ _ => return Err(format!("无效纠错级别: {}", level)),
+ };
+
+ let config = QrConfig {
+ level: ec_level,
+ version: VersionMode::Auto,
+ margin,
+ };
+
+ let qr = QrCode::encode(&text, config)
+ .map_err(|e| format!("编码失败: {}", e))?;
+
+ let svg = qr.to_svg();
+
+ Ok(QrResponse {
+ svg,
+ version: qr.version.0,
+ size: qr.size(),
+ mask: qr.mask,
+ })
+}
+
+/// 导出 PNG bytes (base64 encoded)
+#[tauri::command]
+fn export_png(text: String, level: String, margin: u8, module_size: u8) -> Result, String> {
+ let ec_level = match level.to_uppercase().as_str() {
+ "L" => EcLevel::L,
+ "M" => EcLevel::M,
+ "Q" => EcLevel::Q,
+ "H" => EcLevel::H,
+ _ => return Err(format!("无效纠错级别: {}", level)),
+ };
+
+ let config = QrConfig {
+ level: ec_level,
+ version: VersionMode::Auto,
+ margin,
+ };
+
+ let qr = QrCode::encode(&text, config)
+ .map_err(|e| format!("编码失败: {}", e))?;
+
+ Ok(qr.to_png_bytes(module_size))
+}
+
+/// 添加历史记录
+#[tauri::command]
+fn save_history(state: tauri::State, entry: HistoryEntry) -> Result<(), String> {
+ let mut history = state.history.lock().map_err(|e| e.to_string())?;
+ history.push(entry);
+ if history.len() > 50 {
+ history.remove(0);
+ }
+ Ok(())
+}
+
+/// 加载历史记录
+#[tauri::command]
+fn load_history(state: tauri::State) -> Result, String> {
+ let history = state.history.lock().map_err(|e| e.to_string())?;
+ Ok(history.clone())
+}
+
+/// 清空历史记录
+#[tauri::command]
+fn clear_history(state: tauri::State) -> Result<(), String> {
+ let mut history = state.history.lock().map_err(|e| e.to_string())?;
+ history.clear();
+ Ok(())
+}
+
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_store::Builder::new().build())
+ .plugin(tauri_plugin_dialog::init())
+ .plugin(tauri_plugin_clipboard_manager::init())
+ .manage(AppState {
+ history: Mutex::new(Vec::new()),
+ })
+ .invoke_handler(tauri::generate_handler![
+ encode_qr,
+ export_png,
+ save_history,
+ load_history,
+ clear_history,
+ ])
+ .run(tauri::generate_context!())
+ .expect("启动 QRGen GUI 失败");
+}
+```
+
+- [ ] **Step 2: 验证编译**
+
+```bash
+cd D:\Code\doing_exercises\programs\QRGen && cargo build -p qrgen-gui
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add -A && git commit -m "feat: Tauri commands — encode/export/history CRUD"
+```
+
+---
+
+### Task 4: React 前端 — 类型、状态、Hook
+
+**Files:**
+- Create: `gui/src-frontend/src/types/index.ts`
+- Create: `gui/src-frontend/src/store/qrContext.tsx`
+- Create: `gui/src-frontend/src/hooks/useQrEncode.ts`
+
+- [ ] **Step 1: 类型定义**
+
+`gui/src-frontend/src/types/index.ts`:
+```typescript
+export type ModeType = 'text' | 'url' | 'wifi' | 'vcard' | 'email' | 'phone' | 'sms';
+
+export interface QrConfig {
+ level: 'L' | 'M' | 'Q' | 'H';
+ margin: number;
+ moduleSize: number;
+}
+
+export interface QrPreview {
+ svg: string | null;
+ version: number;
+ size: number;
+ mask: number;
+}
+
+export interface HistoryEntry {
+ id: string;
+ mode: string;
+ content: string;
+ timestamp: number;
+}
+
+export interface QrState {
+ mode: ModeType;
+ formData: Record;
+ config: QrConfig;
+ preview: QrPreview | null;
+ history: HistoryEntry[];
+ loading: boolean;
+}
+
+export type QrAction =
+ | { type: 'SET_MODE'; payload: ModeType }
+ | { type: 'SET_FORM_DATA'; payload: Record }
+ | { type: 'SET_CONFIG'; payload: Partial }
+ | { type: 'SET_PREVIEW'; payload: QrPreview | null }
+ | { type: 'SET_LOADING'; payload: boolean }
+ | { type: 'SET_HISTORY'; payload: HistoryEntry[] }
+ | { type: 'ADD_HISTORY'; payload: HistoryEntry }
+ | { type: 'REMOVE_HISTORY'; payload: string }
+ | { type: 'RESET' };
+
+export const MODE_LABELS: Record = {
+ text: '文本',
+ url: 'URL',
+ wifi: 'WiFi',
+ vcard: 'vCard',
+ email: 'Email',
+ phone: '电话',
+ sms: 'SMS',
+};
+
+export const MODES: ModeType[] = ['text', 'url', 'wifi', 'vcard', 'email', 'phone', 'sms'];
+```
+
+- [ ] **Step 2: Context + Reducer**
+
+`gui/src-frontend/src/store/qrContext.tsx`:
+```tsx
+import React, { createContext, useContext, useReducer, ReactNode } from 'react';
+import type { QrState, QrAction, ModeType } from '../types';
+
+const initialState: QrState = {
+ mode: 'text',
+ formData: {},
+ config: { level: 'M', margin: 4, moduleSize: 8 },
+ preview: null,
+ history: [],
+ loading: false,
+};
+
+function qrReducer(state: QrState, action: QrAction): QrState {
+ switch (action.type) {
+ case 'SET_MODE':
+ return { ...state, mode: action.payload, formData: {}, preview: null };
+ case 'SET_FORM_DATA':
+ return { ...state, formData: action.payload };
+ case 'SET_CONFIG':
+ return { ...state, config: { ...state.config, ...action.payload } };
+ case 'SET_PREVIEW':
+ return { ...state, preview: action.payload, loading: false };
+ case 'SET_LOADING':
+ return { ...state, loading: action.payload };
+ case 'SET_HISTORY':
+ return { ...state, history: action.payload };
+ case 'ADD_HISTORY':
+ return { ...state, history: [action.payload, ...state.history].slice(0, 50) };
+ case 'REMOVE_HISTORY':
+ return { ...state, history: state.history.filter(h => h.id !== action.payload) };
+ case 'RESET':
+ return { ...initialState, history: state.history };
+ default:
+ return state;
+ }
+}
+
+const QrContext = createContext<{
+ state: QrState;
+ dispatch: React.Dispatch;
+} | null>(null);
+
+export function QrProvider({ children }: { children: ReactNode }) {
+ const [state, dispatch] = useReducer(qrReducer, initialState);
+ return {children};
+}
+
+export function useQrState() {
+ const ctx = useContext(QrContext);
+ if (!ctx) throw new Error('useQrState must be inside QrProvider');
+ return ctx;
+}
+```
+
+- [ ] **Step 3: 编码 Hook**
+
+`gui/src-frontend/src/hooks/useQrEncode.ts`:
+```typescript
+import { useCallback, useRef } from 'react';
+import { invoke } from '@tauri-apps/api/core';
+import { useQrState } from '../store/qrContext';
+
+interface QrResponse {
+ svg: string;
+ version: number;
+ size: number;
+ mask: number;
+}
+
+export function useQrEncode() {
+ const { dispatch } = useQrState();
+ const timerRef = useRef | null>(null);
+
+ const encode = useCallback((text: string) => {
+ if (!text.trim()) {
+ dispatch({ type: 'SET_PREVIEW', payload: null });
+ return;
+ }
+
+ dispatch({ type: 'SET_LOADING', payload: true });
+
+ // 200ms 防抖
+ if (timerRef.current) clearTimeout(timerRef.current);
+ timerRef.current = setTimeout(async () => {
+ try {
+ const result = await invoke('encode_qr', {
+ text,
+ level: 'M',
+ margin: 4,
+ });
+ dispatch({
+ type: 'SET_PREVIEW',
+ payload: {
+ svg: result.svg,
+ version: result.version,
+ size: result.size,
+ mask: result.mask,
+ },
+ });
+ } catch (e) {
+ console.error('QR 编码失败:', e);
+ dispatch({ type: 'SET_PREVIEW', payload: null });
+ }
+ }, 200);
+ }, [dispatch]);
+
+ return { encode };
+}
+```
+
+- [ ] **Step 4: 验证 TypeScript 编译**
+
+```bash
+cd D:\Code\doing_exercises\programs\QRGen\gui\src-frontend && pnpm tsc --noEmit
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add -A && git commit -m "feat: 类型定义 + Context/Reducer + 编码 Hook"
+```
+
+---
+
+### Task 5: 主布局 + 模式面板
+
+**Files:**
+- Modify: `gui/src-frontend/src/App.tsx` (replace stub)
+- Create: `gui/src-frontend/src/components/ModePanel.tsx`
+
+- [ ] **Step 1: App.tsx — 三栏 + 底部布局**
+
+```tsx
+import { QrProvider, useQrState } from './store/qrContext';
+import ModePanel from './components/ModePanel';
+import QrPreview from './components/QrPreview';
+import ExportPanel from './components/ExportPanel';
+import HistoryList from './components/HistoryList';
+
+function AppLayout() {
+ const { state } = useQrState();
+
+ return (
+
+ {/* 顶部标题栏 */}
+
+
+ 🀫 QRGen
+
+
+
+ {/* 三栏主体 */}
+
+ {/* 左侧模式面板 */}
+
+
+ {/* 中间预览区 */}
+
+
+
+
+ {/* 右侧面板 */}
+
+
+
+ {/* 底部输入区 */}
+
+
+
+
+ );
+}
+
+function BottomInput() {
+ const { state, dispatch } = useQrState();
+ const { encode } = useQrEncode(); // 延迟 import 待完成
+
+ const handleInput = (text: string) => {
+ dispatch({ type: 'SET_FORM_DATA', payload: { text } });
+ encode(text);
+ };
+
+ if (state.mode === 'text') {
+ return (
+ handleInput(e.target.value)}
+ className="w-full h-full px-4 text-lg bg-transparent outline-none placeholder-gray-400 dark:placeholder-gray-600"
+ />
+ );
+ }
+
+ // 其他模式:显示"请在下方表单中输入"
+ return (
+
+ 请在下方表单中填写 {state.mode === 'url' ? 'URL' : state.mode} 信息
+
+ );
+}
+
+export default function App() {
+ return (
+
+
+
+ );
+}
+```
+
+- [ ] **Step 2: ModePanel.tsx**
+
+```tsx
+import { useQrState } from '../store/qrContext';
+import { MODES, MODE_LABELS, type ModeType } from '../types';
+
+export default function ModePanel() {
+ const { state, dispatch } = useQrState();
+
+ return (
+
+
+ 编码模式
+
+ {MODES.map(mode => (
+
+ ))}
+
+ );
+}
+```
+
+- [ ] **Step 3: 验证 TypeScript 编译**
+
+```bash
+cd D:\Code\doing_exercises\programs\QRGen\gui\src-frontend && pnpm tsc --noEmit
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add -A && git commit -m "feat: 主布局 + 左侧模式面板"
+```
+
+---
+
+### Task 6: 预览区 + 导出面板
+
+**Files:**
+- Create: `gui/src-frontend/src/components/QrPreview.tsx`
+- Create: `gui/src-frontend/src/components/ExportPanel.tsx`
+
+- [ ] **Step 1: QrPreview.tsx**
+
+```tsx
+import { useQrState } from '../store/qrContext';
+
+export default function QrPreview() {
+ const { state } = useQrState();
+
+ if (!state.preview?.svg) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ 版本: {state.preview.version}
+ {state.preview.size}×{state.preview.size}
+ 掩码: {state.preview.mask}
+
+
+ );
+}
+```
+
+- [ ] **Step 2: ExportPanel.tsx**
+
+```tsx
+import { useState } from 'react';
+import { useQrState } from '../store/qrContext';
+import { writeText } from '@tauri-apps/plugin-clipboard-manager';
+import { save } from '@tauri-apps/plugin-dialog';
+import { writeFile } from '@tauri-apps/plugin-fs';
+import { invoke } from '@tauri-apps/api/core';
+
+export default function ExportPanel() {
+ const { state, dispatch } = useQrState();
+ const [exporting, setExporting] = useState(false);
+
+ 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) return;
+
+ const bytes: number[] = await invoke('export_png', {
+ text: getCurrentText(),
+ 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 handleCopySvg = async () => {
+ if (state.preview?.svg) {
+ await writeText(state.preview.svg);
+ }
+ };
+
+ const getCurrentText = () => {
+ if (state.mode === 'text') return state.formData.text || '';
+ return buildModeText(state.mode, state.formData);
+ };
+
+ return (
+
+
+ 导出选项
+
+
+ {/* 纠错级别 */}
+
+
+ {/* 模块大小 */}
+
+
+ {/* 边距 */}
+
+
+ {/* 导出按钮 */}
+
+
+
+ );
+}
+
+// 各模式文本构建(占位,后续 Task 完善)
+function buildModeText(mode: string, data: Record): string {
+ switch (mode) {
+ case 'url': return data.url || '';
+ case 'wifi': return `WIFI:T:${data.encryption || 'WPA'};S:${data.ssid || ''};P:${data.password || ''};;`;
+ case 'vcard': return `BEGIN:VCARD\nVERSION:3.0\nFN:${data.name || ''}\nTEL:${data.phone || ''}\nEMAIL:${data.email || ''}\nORG:${data.company || ''}\nADR:${data.address || ''}\nEND:VCARD`;
+ case 'email': return `mailto:${data.to || ''}?subject=${encodeURIComponent(data.subject || '')}&body=${encodeURIComponent(data.body || '')}`;
+ case 'phone': return `tel:${data.number || ''}`;
+ case 'sms': return `smsto:${data.number || ''}:${data.message || ''}`;
+ default: return data.text || '';
+ }
+}
+```
+
+- [ ] **Step 3: 验证 TypeScript 编译**
+
+```bash
+cd D:\Code\doing_exercises\programs\QRGen\gui\src-frontend && pnpm tsc --noEmit
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add -A && git commit -m "feat: QR 预览区 + 导出面板(PNG/复制)"
+```
+
+---
+
+### Task 7: 文本模式 + URL 模式表单
+
+**Files:**
+- Create: `gui/src-frontend/src/modes/TextMode.tsx`
+- Create: `gui/src-frontend/src/modes/UrlMode.tsx`
+- Modify: `gui/src-frontend/src/App.tsx` (update BottomInput to render mode forms)
+
+- [ ] **Step 1: TextMode.tsx**
+
+```tsx
+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 (
+