feat: QR 预览 + 导出面板(PNG/SVG/复制) + 文本/URL 模式
This commit is contained in:
@@ -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 (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider">导出选项</div>
|
||||
<div className="text-xs text-gray-400">开发中...</div>
|
||||
|
||||
<label className="text-xs text-gray-600 dark:text-gray-400">
|
||||
纠错级别
|
||||
<select
|
||||
value={state.config.level}
|
||||
onChange={e => dispatch({ type: 'SET_CONFIG', payload: { level: e.target.value as any } })}
|
||||
className="w-full mt-1 px-2 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
>
|
||||
<option value="L">L — 7%</option>
|
||||
<option value="M">M — 15%</option>
|
||||
<option value="Q">Q — 25%</option>
|
||||
<option value="H">H — 30%</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="text-xs text-gray-600 dark:text-gray-400">
|
||||
模块大小: {state.config.moduleSize}px
|
||||
<input
|
||||
type="range" min={2} max={20}
|
||||
value={state.config.moduleSize}
|
||||
onChange={e => dispatch({ type: 'SET_CONFIG', payload: { moduleSize: +e.target.value } })}
|
||||
className="w-full mt-1 accent-blue-500"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="text-xs text-gray-600 dark:text-gray-400">
|
||||
边距: {state.config.margin}
|
||||
<input
|
||||
type="range" min={1} max={10}
|
||||
value={state.config.margin}
|
||||
onChange={e => dispatch({ type: 'SET_CONFIG', payload: { margin: +e.target.value } })}
|
||||
className="w-full mt-1 accent-blue-500"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button onClick={handleCopySvg} disabled={!state.preview}
|
||||
className="w-full py-2 rounded-lg bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-40 transition-all">
|
||||
复制 SVG
|
||||
</button>
|
||||
<button onClick={handleExportPng} disabled={!state.preview || exporting}
|
||||
className="w-full py-2 rounded-lg bg-green-500 text-white text-sm font-medium hover:bg-green-600 disabled:opacity-40 transition-all">
|
||||
{exporting ? '导出中...' : '导出 PNG'}
|
||||
</button>
|
||||
<button onClick={handleExportSvg} disabled={!state.preview}
|
||||
className="w-full py-2 rounded-lg bg-purple-500 text-white text-sm font-medium hover:bg-purple-600 disabled:opacity-40 transition-all">
|
||||
导出 SVG
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user