fix: QR 扫描失败 + GUI 导出失败 — PNG margin + separator + fs 插件

🔴 QR 扫描失败根因 (2项):
- render/png: saturating_sub 导致 margin 区域映射到 finder 黑角,
  quiet zone 全黑,扫描器无法定位 QR
- matrix/patterns: 缺少右上 finder 左侧 + 左下 finder 顶部
  隔离带预留,数据模块破坏 finder 检测比率(1:1:3:1:1)

🔴 GUI 导出失败 (2项):
- gui/Cargo.toml + gui/lib.rs: 注册 tauri-plugin-fs 后端插件
  (前端 writeFile 调用缺少 Rust handler)
- capabilities: fs:allow-write-file + $HOME/** 路径 scope
  (ACL 默认不给 fs 写权限,需显式声明)

🔧 其他:
- ExportPanel: 导出失败显示红色错误信息(替代静默吞错)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-18 11:21:27 +08:00
parent 1e9c94eff9
commit 05b1714628
14 changed files with 7210 additions and 16 deletions
@@ -10,17 +10,21 @@ 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 { /* 剪贴板不可用时静默忽略 */ }
} 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'] }],
@@ -35,12 +39,15 @@ export default function ExportPanel() {
moduleSize: state.config.moduleSize,
});
await writeFile(filePath, new Uint8Array(bytes));
} catch { /* 导出失败时静默处理,UI 回到就绪状态 */ }
} 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'] }],
@@ -48,13 +55,21 @@ export default function ExportPanel() {
});
if (!filePath) return;
await writeFile(filePath, new TextEncoder().encode(state.preview.svg));
} catch { /* 导出失败时静默处理 */ }
} 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