c3956f0f36
- 新增 .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
145 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
}
|