Compare commits

..

2 Commits

Author SHA1 Message Date
Serendipity 79ccac3d8e chore: 添加 MIT License
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-18 11:36:56 +08:00
Serendipity 9f76bb31da fix: QR 预览优化 — 移除虚线边框/圆角/padding,专业白底+微阴影
- 去掉 border-dashed / rounded-2xl / p-4(干扰扫描)
- 纯白容器 + shadow-sm,干净专业
- svgToDataUrl 改用 TextEncoder(替代废弃的 unescape)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-18 11:34:05 +08:00
2 changed files with 41 additions and 13 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 刘航宇
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+20 -13
View File
@@ -1,10 +1,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useQrState } from '../store/qrContext'; import { useQrState } from '../store/qrContext';
/** SVG 字符串转为安全的 data URL<img> 标签中浏览器会阻止 SVG 内的脚本执行) */ /** SVG 字符串 data URL安全渲染,img 上下文阻止脚本执行) */
function svgToDataUrl(svg: string): string { function svgToDataUrl(svg: string): string {
const encoded = btoa(unescape(encodeURIComponent(svg))); // TextEncoder 比 btoa(unescape(...)) 更可靠,正确支持 UTF-8
return `data:image/svg+xml;base64,${encoded}`; const bytes = new TextEncoder().encode(svg);
const binStr = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
return `data:image/svg+xml;base64,${btoa(binStr)}`;
} }
export default function QrPreview() { export default function QrPreview() {
@@ -15,10 +17,13 @@ export default function QrPreview() {
[state.preview?.svg], [state.preview?.svg],
); );
const containerCls =
'w-64 h-64 flex items-center justify-center bg-white rounded-xl shadow-sm';
if (!svgDataUrl) { if (!svgDataUrl) {
return ( return (
<div className="flex flex-col items-center justify-center gap-3 text-gray-400"> <div className="flex flex-col items-center justify-center gap-3 text-gray-400">
<div className="w-64 h-64 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-2xl flex items-center justify-center bg-white/50 dark:bg-gray-800/50"> <div className={`${containerCls} border border-gray-200`}>
{state.loading ? ( {state.loading ? (
<span className="text-sm animate-pulse">...</span> <span className="text-sm animate-pulse">...</span>
) : ( ) : (
@@ -31,16 +36,18 @@ export default function QrPreview() {
return ( return (
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
{/* SVG 转为 data URL 通过 <img> 渲染,浏览器在 img 上下文中阻止脚本执行 */} {/* 纯白背景 + 微阴影,无边框/圆角干扰扫描 */}
<img <div className={containerCls}>
src={svgDataUrl} <img
alt="QR 码" src={svgDataUrl}
className="w-64 h-64 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-2xl p-4 bg-white dark:bg-white qr-preview" alt="QR 码"
/> className="w-60 h-60"
<div className="flex gap-3 text-xs text-gray-500 dark:text-gray-400"> />
<span>: {state.preview!.version}</span> </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> <span> {state.preview!.mask}</span>
</div> </div>
</div> </div>
); );