refactor: 重构 QrCodePreview 组件,继承 HTMLDiv 属性并统一 shadcn 样式

- 组件改为继承 `React.HTMLAttributes<HTMLDivElement>`,支持外部传入 className 和事件
- 使用 `cn()` 函数合并外部类名
- 空白状态改用 `border border-dashed` 和更浅的 `bg-muted/40` 背景
- 正常状态改为纯白背景柔边圆角容器包裹二维码,确保暗黑模式下黑白对比度
- 下载/复制按钮完全对齐 shadcn 的 Outline 与 Default 样式规范
- 更新测试用例中的 alt 文本为 "QR Code Preview"
This commit is contained in:
雨霖铃
2026-05-22 20:48:14 +08:00
parent 1bfb7d4e6d
commit da667ccb13
2 changed files with 54 additions and 18 deletions
+52 -16
View File
@@ -1,7 +1,10 @@
import { Download, Copy } from 'lucide-react'; import React from 'react';
import { Copy, Download } from 'lucide-react';
import { useLazyTranslation } from '@/utils/useLazyTranslation'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
interface QrCodePreviewProps { // 继承原生 HTML Div 属性,方便外部无缝扩充类名或监听事件
interface QrCodePreviewProps extends React.HTMLAttributes<HTMLDivElement> {
/** 二维码 Data URL */ /** 二维码 Data URL */
qrCodeDataUrl: string; qrCodeDataUrl: string;
/** 下载回调 */ /** 下载回调 */
@@ -10,39 +13,72 @@ interface QrCodePreviewProps {
onCopy: () => void; onCopy: () => void;
} }
const QrCodePreview = ({ qrCodeDataUrl, onDownload, onCopy }: QrCodePreviewProps) => { const QrCodePreview = ({
qrCodeDataUrl,
onDownload,
onCopy,
className,
...props
}: QrCodePreviewProps) => {
const { t } = useLazyTranslation('qrCode'); const { t } = useLazyTranslation('qrCode');
// 空状态下的虚线骨架屏
if (!qrCodeDataUrl) { if (!qrCodeDataUrl) {
return ( return (
<div className="flex flex-col justify-center items-center min-h-[200px] border-2 border-dashed border-input rounded-xl p-4 bg-muted"> <div
<span className="text-sm text-muted-foreground text-center"> className={cn(
{t('qrCode:qrCodeWillShow')} 'flex flex-col justify-center items-center min-h-[200px] border border-dashed border-input rounded-xl p-4 bg-muted/40',
</span> className,
)}
{...props}
>
<p className="text-sm text-muted-foreground text-center">{t('qrCode:qrCodeWillShow')}</p>
</div> </div>
); );
} }
return ( return (
<div className="flex flex-col justify-center items-center min-h-[200px] border-2 border-dashed border-input rounded-xl p-4 bg-muted"> <div
<div className="flex flex-col items-center w-full"> className={cn(
<img src={qrCodeDataUrl} alt="QR Code" className="w-[250px] h-[250px] block" /> 'flex flex-col justify-center items-center min-h-[200px] border border-input rounded-xl p-6 bg-muted/40',
<div className="flex gap-2 mt-4"> className,
)}
{...props}
>
<div className="flex flex-col items-center w-full max-w-xs">
{/*
2. 二维码容器适配:
在暗黑模式下,纯黑白的二维码如果直接暴露在暗色背景下,会导致手机摄像头极难识别。
通过裹一层 bg-white 和 p-3,确保黑白对比度绝对安全,同时加入 shadow 增强卡片感。
*/}
<div className="p-3 bg-white rounded-lg shadow-sm border border-border/40">
<img
src={qrCodeDataUrl}
alt="QR Code Preview"
className="w-56 h-56 max-w-full object-contain block animate-in fade-in duration-300"
/>
</div>
{/* 3. 按钮群全面向 shadcn 官方 Button 视觉规范对齐 */}
<div className="flex w-full gap-2 mt-5">
{/* 下载按钮:使用标准的次要按钮风格 (Outline) */}
<button <button
type="button" type="button"
onClick={onDownload} onClick={onDownload}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-green-600 bg-background border border-green-600 rounded-lg hover:bg-green-500/10 transition-colors" className="flex-1 inline-flex h-9 items-center justify-center gap-2 px-3 text-sm font-medium rounded-md border border-input bg-background shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4 text-muted-foreground" />
{t('qrCode:downloadButton')} <span className="truncate">{t('qrCode:downloadButton')}</span>
</button> </button>
{/* 复制按钮:使用标准的主要行动按钮风格 (Default) */}
<button <button
type="button" type="button"
onClick={onCopy} onClick={onCopy}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors" className="flex-1 inline-flex h-9 items-center justify-center gap-2 px-3 text-sm font-medium rounded-md bg-primary text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
> >
<Copy className="w-4 h-4" /> <Copy className="w-4 h-4" />
{t('qrCode:copyQrButton')} <span className="truncate">{t('qrCode:copyQrButton')}</span>
</button> </button>
</div> </div>
</div> </div>
+2 -2
View File
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import QrCodePreview from '@/components/QrCodePreview'; import QrCodePreview from '@/components/QrCodePreview';
describe('QrCodePreview 组件', () => { describe('QrCodePreview 组件', () => {
@@ -30,7 +30,7 @@ describe('QrCodePreview 组件', () => {
onCopy={mockOnCopy} onCopy={mockOnCopy}
/>, />,
); );
const img = screen.getByAltText('QR Code'); const img = screen.getByAltText('QR Code Preview');
expect(img).toBeInTheDocument(); expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', testDataUrl); expect(img).toHaveAttribute('src', testDataUrl);
}); });