import React, { useState, useRef } from ‘react’;
import { Download, ShieldCheck, Plus, Loader2, Image as ImageIcon, Sparkles, X, Check } from ‘lucide-react’;
export default function App() {
const [rank, setRank] = useState(‘A’);
const [suit, setSuit] = useState(‘♠’);
const [imageSrc, setImageSrc] = useState(null);
const [isDownloading, setIsDownloading] = useState(false);
const [showExportModal, setShowExportModal] = useState(false);
// 新增:文件命名称状态
const [fileName, setFileName] = useState(”);
const cardRef = useRef(null);
const alphaRanks = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i));
const suits = [
{ symbol: ‘♠’, name: ‘黑桃’, color: ‘text-zinc-100’, glow: ‘shadow-[0_0_15px_rgba(255,255,255,0.2)]’ },
{ symbol: ‘♥’, name: ‘红心’, color: ‘text-[#FF2E2E]’, glow: ‘shadow-[0_0_15px_rgba(255,46,46,0.3)]’ },
{ symbol: ‘♣’, name: ‘梅花’, color: ‘text-[#10B981]’, glow: ‘shadow-[0_0_15px_rgba(16,185,129,0.3)]’ },
{ symbol: ‘♦’, name: ‘方块’, color: ‘text-[#FBBF24]’, glow: ‘shadow-[0_0_15px_rgba(251,191,36,0.3)]’ }
];
const currentSuitObj = suits.find(s => s.symbol === suit) || suits[0];
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
setImageSrc(url);
}
};
// 第一步:触发下载意向(打开输入框)
const triggerExportFlow = () => {
// 预设一个默认名字:[花色][数值]-StarCard
setFileName(${suit}${rank}-StarCard);
setShowExportModal(true);
};
// 第二步:正式执行下载逻辑
const executeDownload = async () => {
if (!cardRef.current) return;
setIsDownloading(true);
setShowExportModal(false); // 关闭模态框
try {
if (!window.html2canvas) {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
document.head.appendChild(script);
await new Promise((resolve) => (script.onload = resolve));
}
const canvas = await window.html2canvas(cardRef.current, {
useCORS: true,
scale: 4, // 4倍缩放确保高清打印 (300DPI级别)
backgroundColor: null,
});
const link = document.createElement('a');
link.download = `${fileName || 'poker-card'}.png`;
link.href = canvas.toDataURL('image/png', 1.0);
link.click();
} catch (error) {
console.error("Export failed:", error);
} finally {
setIsDownloading(false);
}
};
return (
{/* 背景动态星尘 */}
<div className="absolute inset-0 pointer-events-none opacity-20"
style={{ backgroundImage: `radial-gradient(1px 1px at 20px 30px, #eee, rgba(0,0,0,0)), radial-gradient(1px 1px at 40px 70px, #fff, rgba(0,0,0,0)), radial-gradient(2px 2px at 50px 160px, #ddd, rgba(0,0,0,0))`, backgroundSize: '200px 200px' }}></div>
{/* 控制台 */}
<div className="w-full md:w-80 bg-zinc-900/50 backdrop-blur-3xl p-8 rounded-[2rem] border border-zinc-800/50 flex flex-col gap-8 h-fit z-50 shadow-2xl">
<header className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-zinc-700 to-zinc-900 rounded-xl flex items-center justify-center text-white shadow-inner border border-zinc-700/50">
<Sparkles size={20} className="text-blue-400" />
</div>
<div>
<h1 className="text-lg font-bold text-zinc-100 tracking-tight">Starry Lab</h1>
<p className="text-[9px] text-zinc-500 font-mono tracking-widest uppercase">Premium Edition</p>
</div>
</header>
<section className="space-y-6">
<div className="space-y-3">
<span className="text-[10px] font-bold text-zinc-600 uppercase tracking-[0.2em] block">Alpha Rank / 字母全集</span>
<div className="grid grid-cols-7 gap-1 p-1 bg-black/40 rounded-xl border border-zinc-800/50 max-h-40 overflow-y-auto scrollbar-hide">
{alphaRanks.map(r => (
<button key={r} onClick={() => setRank(r)}
className={`aspect-square text-[10px] rounded-lg transition-all ${rank === r ? 'bg-zinc-100 text-black shadow-lg scale-105' : 'hover:text-zinc-200 text-zinc-600'}`}>
{r}
</button>
))}
</div>
</div>
<div className="flex gap-2">
{suits.map(s => (
<button key={s.symbol} onClick={() => setSuit(s.symbol)}
className={`flex-1 py-3 rounded-xl border transition-all ${suit === s.symbol ? 'border-zinc-100 bg-zinc-100 text-black' : 'border-zinc-800 bg-black/30 hover:border-zinc-700'}`}>
<span className={`text-xl ${suit === s.symbol ? 'text-black' : s.color}`}>{s.symbol}</span>
</button>
))}
</div>
</section>
{/* 触发导出流程 */}
<button
onClick={triggerExportFlow}
disabled={isDownloading}
className="w-full py-4 rounded-2xl bg-zinc-100 text-black font-bold text-xs tracking-[0.2em] uppercase hover:bg-white active:scale-[0.98] transition-all shadow-[0_0_20px_rgba(255,255,255,0.1)] flex items-center justify-center gap-2"
>
{isDownloading ? <Loader2 className="animate-spin" size={16} /> : "Finalize & Export"}
</button>
</div>
{/* 美术展示区 */}
<div className="relative group">
<div
ref={cardRef}
className="relative flex items-center justify-center bg-[#020617] transition-all duration-700"
style={{
width: '340px', height: '476px', borderRadius: '36px',
border: '8px solid #111827',
boxShadow: '0 50px 100px -20px rgba(0,0,0,1), inset 0 0 20px rgba(255,255,255,0.02)'
}}
>
<div className="absolute inset-0 opacity-40 pointer-events-none rounded-[24px] overflow-hidden"
style={{ backgroundImage: `radial-gradient(circle at 50% 50%, #1e293b 0%, transparent 80%)` }}></div>
<div className={`absolute top-2 left-3 flex flex-col items-center leading-none ${currentSuitObj.color} z-40 select-none drop-shadow-[0_0_8px_rgba(255,255,255,0.1)]`}>
<span className="font-serif font-black text-[26px] tracking-[-0.05em] h-[24px] w-[30px] flex items-center justify-center text-center">{rank}</span>
<span className="text-[18px] mt-1 opacity-90">{suit}</span>
</div>
<div className={`absolute bottom-2 right-3 flex flex-col items-center leading-none rotate-180 ${currentSuitObj.color} z-40 select-none drop-shadow-[0_0_8px_rgba(255,255,255,0.1)]`}>
<span className="font-serif font-black text-[26px] tracking-[-0.05em] h-[24px] w-[30px] flex items-center justify-center text-center">{rank}</span>
<span className="text-[18px] mt-1 opacity-90">{suit}</span>
</div>
<div className="relative z-20 flex p-[8px]" style={{ width: '304px', height: '416px' }}>
<div className="absolute top-0 left-0 w-8 h-8 border-t border-l border-zinc-700/30 rounded-tl-[45px] pointer-events-none"></div>
<div className="absolute bottom-0 right-0 w-8 h-8 border-b border-r border-zinc-700/30 rounded-br-[45px] pointer-events-none"></div>
<div
className="absolute inset-0 bg-white/[0.02] backdrop-blur-[20px] border border-white/[0.08] shadow-[0_20px_50px_rgba(0,0,0,0.5),inset_0_1px_1px_rgba(255,255,255,0.05)] overflow-hidden"
style={{ borderRadius: '90px 25px 90px 25px' }}
>
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.04] via-transparent to-black/30 pointer-events-none"></div>
</div>
<div
className="relative w-full h-full overflow-hidden flex items-center justify-center bg-black/40 shadow-[inset_0_4px_30px_rgba(0,0,0,0.7)] z-10"
style={{ borderRadius: '82px 17px 82px 17px' }}
>
{imageSrc ? (
<img src={imageSrc} alt="Art" className="w-full h-full object-cover grayscale-[10%] hover:grayscale-0 transition-all duration-700" />
) : (
<div className="flex flex-col items-center gap-3 opacity-10 group cursor-pointer">
<ImageIcon size={40} strokeWidth={1} />
<span className="text-[7px] font-black uppercase tracking-[0.8em]">A-Z Full Set</span>
</div>
)}
<input type="file" accept="image/*" onChange={handleImageUpload} className="absolute inset-0 opacity-0 cursor-pointer z-50" />
</div>
</div>
<div className="absolute inset-[0.5px] border border-white/5 rounded-[34px] pointer-events-none"></div>
</div>
</div>
{/* 导出重命名模态框 (Modal Layer) */}
{showExportModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-[#020617]/80 backdrop-blur-md" onClick={() => setShowExportModal(false)}></div>
<div className="relative bg-zinc-900 border border-zinc-800 rounded-[2rem] p-8 w-full max-w-sm shadow-[0_30px_100px_rgba(0,0,0,0.8)] flex flex-col gap-6 animate-in fade-in zoom-in duration-300">
<div className="space-y-2">
<h2 className="text-zinc-100 font-bold text-xl tracking-tight">Naming Your Asset</h2>
<p className="text-zinc-500 text-xs">请输入导出文件的名称,无需填写后缀名。</p>
</div>
<div className="relative">
<input
autoFocus
type="text"
value={fileName}
onChange={(e) => setFileName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && executeDownload()}
className="w-full bg-black/50 border border-zinc-800 rounded-xl px-4 py-4 text-zinc-100 outline-none focus:border-blue-500 transition-all font-mono text-sm"
placeholder="Card name..."
/>
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-[10px] text-zinc-600 font-mono">.png</div>
</div>
<div className="flex gap-3 mt-2">
<button
onClick={() => setShowExportModal(false)}
className="flex-1 py-3 rounded-xl border border-zinc-800 text-zinc-400 font-bold text-[10px] uppercase tracking-widest hover:bg-zinc-800 transition-all"
>
Cancel
</button>
<button
onClick={executeDownload}
className="flex-1 py-3 rounded-xl bg-zinc-100 text-black font-bold text-[10px] uppercase tracking-widest hover:bg-white shadow-xl transition-all flex items-center justify-center gap-2"
>
<Download size={14} />
Download
</button>
</div>
</div>
</div>
)}
</div>
);
}