From 3186502edbb22ad5e1e1aa010eba1d4129344bce 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:20:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=20?= =?UTF-8?q?+=20Context/Reducer=20+=20=E7=BC=96=E7=A0=81=20Hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui/src-frontend/src/hooks/useQrEncode.ts | 54 +++++++++++++++++++++++ gui/src-frontend/src/store/qrContext.tsx | 52 ++++++++++++++++++++++ gui/src-frontend/src/types/index.ts | 53 ++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 gui/src-frontend/src/hooks/useQrEncode.ts create mode 100644 gui/src-frontend/src/store/qrContext.tsx create mode 100644 gui/src-frontend/src/types/index.ts diff --git a/gui/src-frontend/src/hooks/useQrEncode.ts b/gui/src-frontend/src/hooks/useQrEncode.ts new file mode 100644 index 0000000..3685973 --- /dev/null +++ b/gui/src-frontend/src/hooks/useQrEncode.ts @@ -0,0 +1,54 @@ +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 { state, dispatch } = useQrState(); + const timerRef = useRef | null>(null); + const modeRef = useRef(state.mode); + modeRef.current = state.mode; + + const encode = useCallback((text: string) => { + if (!text.trim()) { + dispatch({ type: 'SET_PREVIEW', payload: null }); + return; + } + + dispatch({ type: 'SET_LOADING', payload: true }); + + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(async () => { + try { + const result = await invoke('encode_qr', { + text, + level: state.config.level, + margin: state.config.margin, + }); + dispatch({ type: 'SET_PREVIEW', payload: result }); + + // 保存到历史 + dispatch({ + type: 'ADD_HISTORY', + payload: { + id: Date.now().toString(), + mode: modeRef.current, + content: text, + timestamp: Date.now(), + }, + }); + } catch (e) { + console.error('QR 编码失败:', e); + dispatch({ type: 'SET_PREVIEW', payload: null }); + } + }, 200); + }, [state.config.level, state.config.margin, dispatch]); + + return { encode }; +} diff --git a/gui/src-frontend/src/store/qrContext.tsx b/gui/src-frontend/src/store/qrContext.tsx new file mode 100644 index 0000000..14ec3e0 --- /dev/null +++ b/gui/src-frontend/src/store/qrContext.tsx @@ -0,0 +1,52 @@ +import React, { createContext, useContext, useReducer, type ReactNode } from 'react'; +import type { QrState, QrAction } 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; +} diff --git a/gui/src-frontend/src/types/index.ts b/gui/src-frontend/src/types/index.ts new file mode 100644 index 0000000..6aa0df9 --- /dev/null +++ b/gui/src-frontend/src/types/index.ts @@ -0,0 +1,53 @@ +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'];