Files
QRGen/gui/src-frontend/src/components/ExportPanel.tsx
T
Serendipity c3956f0f36 chore: 前端工程化 + Git hooks + 对齐 PathEditor 规范
- 新增 .gitattributes(CRLF 统一)+ rust-toolchain.toml
- 新增 Prettier + ESLint + markdownlint 配置
- 新增 Husky Git hooks(pre-commit lint-staged + commit-msg commitlint)
- 新增 vitest 前端测试(12 tests, utils/qrText.ts)
- 新增 @ 路径别名(vite + tsconfig)
- 新增 ROADMAP / SUPPORT / CODEOWNERS / FUNDING / dependabot
- 更新 .gitignore + .editorconfig
- 更新 package.json(新增 lint/format/test 脚本)
- 全项目 prettier 格式化 + eslint 通过
- 更新 CLAUDE.md + README.md
2026-06-19 19:42:13 +08:00

145 lines
4.6 KiB
TypeScript

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';
import type { QrConfig } from '../types';
import { buildEncodedText } from '../utils/qrText';
export default function ExportPanel() {
const { state, dispatch } = useQrState();
const [exporting, setExporting] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const handleCopySvg = async () => {
if (!state.preview?.svg) return;
try {
await writeText(state.preview.svg);
} catch (e) {
setErrorMsg(`复制失败: ${e}`);
}
};
const handleExportPng = async () => {
if (!state.preview?.svg) return;
setExporting(true);
setErrorMsg(null);
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: buildEncodedText(state.mode, state.formData),
level: state.config.level,
margin: state.config.margin,
moduleSize: state.config.moduleSize,
});
await writeFile(filePath, new Uint8Array(bytes));
} catch (e) {
setErrorMsg(`导出 PNG 失败: ${e}`);
}
setExporting(false);
};
const handleExportSvg = async () => {
if (!state.preview?.svg) return;
setErrorMsg(null);
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) {
setErrorMsg(`导出 SVG 失败: ${e}`);
}
};
return (
<div className="flex flex-col gap-2">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider">导出选项</div>
{errorMsg && (
<div className="text-xs text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg px-2 py-1.5 break-all">
{errorMsg}
</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 QrConfig['level'] },
})
}
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>
);
}