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
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import React, { Component, type ReactNode } from 'react';
|
||||
|
||||
interface Props { children: ReactNode; }
|
||||
interface State { hasError: boolean; error: Error | null; }
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false, error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error) { return { hasError: true, error }; }
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
// 生产环境错误日志记录入口
|
||||
@@ -21,8 +28,10 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
<span className="text-4xl">⚠</span>
|
||||
<h2 className="text-lg font-semibold">应用发生错误</h2>
|
||||
<p className="text-sm max-w-md text-center">{this.state.error?.message}</p>
|
||||
<button onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 rounded-lg bg-blue-500 text-white text-sm hover:bg-blue-600 transition-all">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 rounded-lg bg-blue-500 text-white text-sm hover:bg-blue-600 transition-all"
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,10 @@ export default function ExportPanel() {
|
||||
filters: [{ name: 'PNG 图片', extensions: ['png'] }],
|
||||
defaultPath: 'qrcode.png',
|
||||
});
|
||||
if (!filePath) { setExporting(false); return; }
|
||||
if (!filePath) {
|
||||
setExporting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes: number[] = await invoke('export_png', {
|
||||
text: buildEncodedText(state.mode, state.formData),
|
||||
@@ -74,7 +77,12 @@ export default function ExportPanel() {
|
||||
纠错级别
|
||||
<select
|
||||
value={state.config.level}
|
||||
onChange={e => dispatch({ type: 'SET_CONFIG', payload: { level: e.target.value as QrConfig['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>
|
||||
@@ -87,9 +95,13 @@ export default function ExportPanel() {
|
||||
<label className="text-xs text-gray-600 dark:text-gray-400">
|
||||
模块大小: {state.config.moduleSize}px
|
||||
<input
|
||||
type="range" min={2} max={20}
|
||||
type="range"
|
||||
min={2}
|
||||
max={20}
|
||||
value={state.config.moduleSize}
|
||||
onChange={e => dispatch({ type: 'SET_CONFIG', payload: { moduleSize: +e.target.value } })}
|
||||
onChange={(e) =>
|
||||
dispatch({ type: 'SET_CONFIG', payload: { moduleSize: +e.target.value } })
|
||||
}
|
||||
className="w-full mt-1 accent-blue-500"
|
||||
/>
|
||||
</label>
|
||||
@@ -97,23 +109,34 @@ export default function ExportPanel() {
|
||||
<label className="text-xs text-gray-600 dark:text-gray-400">
|
||||
边距: {state.config.margin}
|
||||
<input
|
||||
type="range" min={1} max={10}
|
||||
type="range"
|
||||
min={1}
|
||||
max={10}
|
||||
value={state.config.margin}
|
||||
onChange={e => dispatch({ type: 'SET_CONFIG', payload: { margin: +e.target.value } })}
|
||||
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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function HistoryList() {
|
||||
|
||||
const handleDelete = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation();
|
||||
const updated = state.history.filter(h => h.id !== id);
|
||||
const updated = state.history.filter((h) => h.id !== id);
|
||||
dispatch({ type: 'SET_HISTORY', payload: updated });
|
||||
persistHistory(updated);
|
||||
};
|
||||
@@ -39,8 +39,10 @@ export default function HistoryList() {
|
||||
📋 历史记录
|
||||
</span>
|
||||
{state.history.length > 0 && (
|
||||
<button onClick={handleClear}
|
||||
className="text-xs text-red-400 hover:text-red-600 transition-colors">
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="text-xs text-red-400 hover:text-red-600 transition-colors"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
)}
|
||||
@@ -50,10 +52,12 @@ export default function HistoryList() {
|
||||
{state.history.length === 0 && (
|
||||
<p className="text-xs text-gray-400 text-center py-4">暂无记录</p>
|
||||
)}
|
||||
{state.history.map(entry => (
|
||||
<div key={entry.id}
|
||||
{state.history.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
onClick={() => handleClick(entry)}
|
||||
className="group flex items-center justify-between px-2 py-1.5 rounded-lg text-xs cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-all">
|
||||
className="group flex items-center justify-between px-2 py-1.5 rounded-lg text-xs cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-all"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="px-1 py-0.5 rounded text-[10px] font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">
|
||||
@@ -65,8 +69,10 @@ export default function HistoryList() {
|
||||
{entry.content.length > 20 ? entry.content.slice(0, 20) + '...' : entry.content}
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={(e) => handleDelete(e, entry.id)}
|
||||
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 ml-1 transition-all text-lg leading-none">
|
||||
<button
|
||||
onClick={(e) => handleDelete(e, entry.id)}
|
||||
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 ml-1 transition-all text-lg leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function ModePanel() {
|
||||
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2 px-2">
|
||||
编码模式
|
||||
</div>
|
||||
{MODES.map(mode => (
|
||||
{MODES.map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => dispatch({ type: 'SET_MODE', payload: mode })}
|
||||
|
||||
@@ -17,8 +17,7 @@ export default function QrPreview() {
|
||||
[state.preview?.svg],
|
||||
);
|
||||
|
||||
const containerCls =
|
||||
'w-64 h-64 flex items-center justify-center bg-white rounded-xl shadow-sm';
|
||||
const containerCls = 'w-64 h-64 flex items-center justify-center bg-white rounded-xl shadow-sm';
|
||||
|
||||
if (!svgDataUrl) {
|
||||
return (
|
||||
@@ -38,15 +37,13 @@ export default function QrPreview() {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{/* 纯白背景 + 微阴影,无边框/圆角干扰扫描 */}
|
||||
<div className={containerCls}>
|
||||
<img
|
||||
src={svgDataUrl}
|
||||
alt="QR 码"
|
||||
className="w-60 h-60"
|
||||
/>
|
||||
<img src={svgDataUrl} alt="QR 码" className="w-60 h-60" />
|
||||
</div>
|
||||
<div className="flex gap-3 text-xs text-gray-400">
|
||||
<span>版本 {state.preview!.version}</span>
|
||||
<span>{state.preview!.size}×{state.preview!.size}</span>
|
||||
<span>
|
||||
{state.preview!.size}×{state.preview!.size}
|
||||
</span>
|
||||
<span>掩码 {state.preview!.mask}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user