feat: 导入 16 枚图章并重构图册为 4x4 圆形布局
- 按 A4 排列顺序导入 16 枚真实商户图章(彩色 + 灰色)到数据库 - 图章素材存放于 packages/server/uploads/stamps/,命名与 sortOrder 一致 - 图册页布局由 3 列改为 4 列,StampCard 采用圆形白底容器承托透明圆章 - 去除邮票打孔/方形渐变背景,已收集态增加金色内描边与柔光阴影 - 优化进度区与兑换按钮视觉:突出数字、显示差额提示、禁用态文案 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BIN
assets/stamps/16个彩色圆章/1.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
assets/stamps/16个彩色圆章/10.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
assets/stamps/16个彩色圆章/11.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
assets/stamps/16个彩色圆章/12.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
assets/stamps/16个彩色圆章/13.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
assets/stamps/16个彩色圆章/14.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
assets/stamps/16个彩色圆章/15.jpg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
assets/stamps/16个彩色圆章/16.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
assets/stamps/16个彩色圆章/2.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
assets/stamps/16个彩色圆章/3.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
assets/stamps/16个彩色圆章/4.jpg
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
assets/stamps/16个彩色圆章/5.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
assets/stamps/16个彩色圆章/6.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
assets/stamps/16个彩色圆章/7.jpg
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
assets/stamps/16个彩色圆章/8.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
assets/stamps/16个彩色圆章/9.jpg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
assets/stamps/16个黑白圆章/1.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
assets/stamps/16个黑白圆章/10.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
assets/stamps/16个黑白圆章/11.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/stamps/16个黑白圆章/12.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
assets/stamps/16个黑白圆章/13.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/stamps/16个黑白圆章/14.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
assets/stamps/16个黑白圆章/15.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
assets/stamps/16个黑白圆章/16.jpg
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
assets/stamps/16个黑白圆章/2.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/stamps/16个黑白圆章/3.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/stamps/16个黑白圆章/4.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/stamps/16个黑白圆章/5.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/stamps/16个黑白圆章/6.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
assets/stamps/16个黑白圆章/7.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
assets/stamps/16个黑白圆章/8.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/stamps/16个黑白圆章/9.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
assets/stamps/章排列16个.jpg
Normal file
|
After Width: | Height: | Size: 439 KiB |
@@ -1,63 +1,71 @@
|
||||
import { prisma } from "@stamp/shared";
|
||||
|
||||
const stampData = [
|
||||
{ name: "尹氏汤包" },
|
||||
{ name: "中国移动 5G" },
|
||||
{ name: "紫金农商银行" },
|
||||
{ name: "孟令军炒货铺" },
|
||||
{ name: "春山酒窖" },
|
||||
{ name: "金陵绣男" },
|
||||
{ name: "LBZ" },
|
||||
{ name: "二条商店" },
|
||||
{ name: "陶玉梅" },
|
||||
{ name: "芳婆糕团" },
|
||||
{ name: "书锦城创" },
|
||||
{ name: "闲鱼循环商店" },
|
||||
{ name: "闽南茶叶店" },
|
||||
{ name: "魏鬼虾神" },
|
||||
{ name: "农家小院" },
|
||||
{ name: "李记清真馆" },
|
||||
];
|
||||
|
||||
async function seed() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
// Create sample stamps
|
||||
const stamps = await Promise.all([
|
||||
prisma.stamp.create({
|
||||
data: { name: "古桥印记", note: "始建于明代的石拱桥", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 1 },
|
||||
// Clear existing stamps (cascades to collections)
|
||||
await prisma.stamp.deleteMany();
|
||||
|
||||
const stamps = await Promise.all(
|
||||
stampData.map((s, idx) => {
|
||||
const pos = String(idx + 1).padStart(2, "0");
|
||||
return prisma.stamp.create({
|
||||
data: {
|
||||
name: s.name,
|
||||
imageColor: `/uploads/stamps/stamp-${pos}-color.jpg`,
|
||||
imageGrey: `/uploads/stamps/stamp-${pos}-grey.jpg`,
|
||||
sortOrder: idx + 1,
|
||||
},
|
||||
});
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "老街风韵", note: "百年历史的商业老街", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 2 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "园林雅趣", note: "江南古典园林", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 3 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "茶馆时光", note: "百年老茶馆", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 4 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "水乡晨曲", note: "清晨的水乡渔市", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 5 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "戏台余韵", note: "古戏台与昆曲", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 6 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "巷弄深处", note: "青石板铺就的幽深小巷", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 7 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "月下拱桥", note: "夜晚灯火映照的拱桥", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 8 },
|
||||
}),
|
||||
prisma.stamp.create({
|
||||
data: { name: "匠心工坊", note: "传统手工艺作坊", imageColor: "/uploads/placeholder-color.svg", imageGrey: "/uploads/placeholder-grey.svg", sortOrder: 9 },
|
||||
}),
|
||||
]);
|
||||
);
|
||||
|
||||
console.log(`Created ${stamps.length} stamps`);
|
||||
|
||||
// Create redemption rules
|
||||
const rules = await Promise.all([
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "纪念书签", description: "精美城市纪念书签一枚", threshold: 3, sortOrder: 1 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "手绘明信片套装", description: "一套 5 张手绘城市明信片", threshold: 5, sortOrder: 2 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "限定徽章礼盒", description: "城市限定版金属徽章礼盒", threshold: 7, sortOrder: 3 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "城市记忆礼包", description: "包含纪念 T 恤、帆布袋和城市画册", threshold: 9, sortOrder: 4 },
|
||||
}),
|
||||
]);
|
||||
// Create redemption rules if none exist
|
||||
const existingRules = await prisma.redemptionRule.count();
|
||||
if (existingRules === 0) {
|
||||
const rules = await Promise.all([
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "纪念书签", description: "精美城市纪念书签一枚", threshold: 4, sortOrder: 1 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "手绘明信片套装", description: "一套 5 张手绘城市明信片", threshold: 8, sortOrder: 2 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "限定徽章礼盒", description: "城市限定版金属徽章礼盒", threshold: 12, sortOrder: 3 },
|
||||
}),
|
||||
prisma.redemptionRule.create({
|
||||
data: { name: "城市记忆礼包", description: "包含纪念 T 恤、帆布袋和城市画册", threshold: 16, sortOrder: 4 },
|
||||
}),
|
||||
]);
|
||||
console.log(`Created ${rules.length} redemption rules`);
|
||||
} else {
|
||||
console.log(`Kept existing ${existingRules} redemption rules`);
|
||||
}
|
||||
|
||||
console.log(`Created ${rules.length} redemption rules`);
|
||||
|
||||
// Print stamp IDs for QR code testing
|
||||
console.log("\nStamp IDs for testing:");
|
||||
stamps.forEach((s) => {
|
||||
console.log(` ${s.name}: /collect/${s.id}`);
|
||||
console.log(` ${s.sortOrder}. ${s.name}: /collect/${s.id}`);
|
||||
});
|
||||
|
||||
console.log("\nSeed complete!");
|
||||
|
||||
BIN
packages/server/uploads/stamps/stamp-01-color.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
packages/server/uploads/stamps/stamp-01-grey.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
packages/server/uploads/stamps/stamp-02-color.jpg
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
packages/server/uploads/stamps/stamp-02-grey.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
packages/server/uploads/stamps/stamp-03-color.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
packages/server/uploads/stamps/stamp-03-grey.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
packages/server/uploads/stamps/stamp-04-color.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
packages/server/uploads/stamps/stamp-04-grey.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
packages/server/uploads/stamps/stamp-05-color.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
packages/server/uploads/stamps/stamp-05-grey.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
packages/server/uploads/stamps/stamp-06-color.jpg
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
packages/server/uploads/stamps/stamp-06-grey.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
packages/server/uploads/stamps/stamp-07-color.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
packages/server/uploads/stamps/stamp-07-grey.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
packages/server/uploads/stamps/stamp-08-color.jpg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
packages/server/uploads/stamps/stamp-08-grey.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
packages/server/uploads/stamps/stamp-09-color.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
packages/server/uploads/stamps/stamp-09-grey.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
packages/server/uploads/stamps/stamp-10-color.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
packages/server/uploads/stamps/stamp-10-grey.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
packages/server/uploads/stamps/stamp-11-color.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
packages/server/uploads/stamps/stamp-11-grey.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
packages/server/uploads/stamps/stamp-12-color.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
packages/server/uploads/stamps/stamp-12-grey.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
packages/server/uploads/stamps/stamp-13-color.jpg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
packages/server/uploads/stamps/stamp-13-grey.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
packages/server/uploads/stamps/stamp-14-color.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
packages/server/uploads/stamps/stamp-14-grey.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
packages/server/uploads/stamps/stamp-15-color.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
packages/server/uploads/stamps/stamp-15-grey.jpg
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
packages/server/uploads/stamps/stamp-16-color.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
packages/server/uploads/stamps/stamp-16-grey.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
@@ -7,58 +7,53 @@ type StampCardProps = {
|
||||
};
|
||||
|
||||
export default function StampCard({ name, imageColor, imageGrey, collected, onClick }: StampCardProps) {
|
||||
const src = collected ? imageColor : imageGrey;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-xl transition-transform active:scale-95"
|
||||
className="flex flex-col items-center gap-1.5 p-1 transition-transform active:scale-95"
|
||||
>
|
||||
{/* Stamp image with perforated border effect */}
|
||||
<div
|
||||
className="relative w-full aspect-square rounded-lg overflow-hidden
|
||||
shadow-[var(--shadow-sm)] border border-[var(--border-muted)]"
|
||||
style={{
|
||||
background: collected
|
||||
? "linear-gradient(135deg, #fdf6ee 0%, #f8eed8 100%)"
|
||||
: "linear-gradient(135deg, #f0f0f0 0%, #e8e8e8 100%)",
|
||||
}}
|
||||
>
|
||||
{/* Perforated edge effect */}
|
||||
<div className="absolute inset-0 stamp-border pointer-events-none z-10" />
|
||||
|
||||
<img
|
||||
src={collected ? imageColor : imageGrey}
|
||||
alt={name}
|
||||
className="w-full h-full object-contain p-3"
|
||||
style={collected ? {} : { filter: "grayscale(1) opacity(0.4)" }}
|
||||
onError={(e) => {
|
||||
// Fallback for missing images: show placeholder
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = "none";
|
||||
target.parentElement!.innerHTML += `
|
||||
<div class="w-full h-full flex items-center justify-center text-3xl"
|
||||
style="color: ${collected ? "var(--gold)" : "var(--text-muted)"}; opacity: ${collected ? 1 : 0.3}">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
<div className="relative w-full aspect-square">
|
||||
<div
|
||||
className="w-full h-full rounded-full bg-white overflow-hidden flex items-center justify-center border border-[var(--border-muted)]"
|
||||
style={{
|
||||
boxShadow: collected
|
||||
? "0 2px 6px rgba(212,165,116,0.25), inset 0 0 0 1px rgba(212,165,116,0.15)"
|
||||
: "0 1px 3px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={name}
|
||||
className="w-[92%] h-[92%] object-contain"
|
||||
style={{ opacity: collected ? 1 : 0.55 }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = "none";
|
||||
target.parentElement!.innerHTML += `
|
||||
<div class="w-full h-full flex items-center justify-center"
|
||||
style="color: var(--text-muted); opacity: 0.3">
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="9"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Collected badge */}
|
||||
{collected && (
|
||||
<div className="absolute top-1 right-1 w-5 h-5 rounded-full bg-[var(--jade)] flex items-center justify-center">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3">
|
||||
<div className="absolute top-0 right-0 w-4 h-4 rounded-full bg-[var(--jade)] flex items-center justify-center shadow-sm z-10">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stamp name */}
|
||||
<span
|
||||
className="text-xs font-medium truncate w-full text-center"
|
||||
className="text-[10px] leading-tight truncate w-full text-center"
|
||||
style={{ color: collected ? "var(--text-primary)" : "var(--text-muted)" }}
|
||||
>
|
||||
{name}
|
||||
|
||||
@@ -8,7 +8,7 @@ type StampGridProps = {
|
||||
|
||||
export default function StampGrid({ stamps, onStampClick }: StampGridProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3 stagger-children">
|
||||
<div className="grid grid-cols-4 gap-3 stagger-children">
|
||||
{stamps.map((stamp) => (
|
||||
<StampCard
|
||||
key={stamp.id}
|
||||
|
||||
@@ -84,14 +84,25 @@ export default function AlbumPage() {
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="px-6 pt-5 pb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-[var(--text-secondary)]">收集进度</span>
|
||||
<span className="text-sm font-semibold text-[var(--gold)]">
|
||||
{collectedCount} / {stamps.length}
|
||||
</span>
|
||||
<div className="px-6 pt-5 pb-5">
|
||||
<div className="flex items-end justify-between mb-2">
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-muted)] mb-0.5">收集进度</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-2xl font-semibold text-[var(--text-primary)]">{collectedCount}</span>
|
||||
<span className="text-sm text-[var(--text-muted)]">/ {stamps.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
{stamps.length > 0 && collectedCount < stamps.length && (
|
||||
<span className="text-xs text-[var(--text-secondary)] pb-1">
|
||||
还差 {stamps.length - collectedCount} 枚集齐
|
||||
</span>
|
||||
)}
|
||||
{stamps.length > 0 && collectedCount === stamps.length && (
|
||||
<span className="text-xs font-medium text-[var(--gold)] pb-1">已集齐全部图章 ✨</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-2 bg-[var(--border-muted)] rounded-full overflow-hidden">
|
||||
<div className="h-1.5 bg-[var(--border-muted)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[var(--gold)] to-[var(--terracotta)] rounded-full transition-all duration-500"
|
||||
style={{ width: stamps.length ? `${(collectedCount / stamps.length) * 100}%` : "0%" }}
|
||||
@@ -105,20 +116,31 @@ export default function AlbumPage() {
|
||||
</div>
|
||||
|
||||
{/* Redeem Section */}
|
||||
{rules.length > 0 && (
|
||||
<div className="px-6 pb-6">
|
||||
<button
|
||||
onClick={handleRedeemClick}
|
||||
className="w-full py-3 rounded-xl text-sm font-medium transition-colors"
|
||||
style={{
|
||||
backgroundColor: collectedCount > 0 ? "var(--jade)" : "var(--border-muted)",
|
||||
color: collectedCount > 0 ? "white" : "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
兑换奖品 ({rules.filter((r) => collectedCount >= r.threshold).length} 个可兑换)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{rules.length > 0 && (() => {
|
||||
const availableCount = rules.filter((r) => collectedCount >= r.threshold).length;
|
||||
const canRedeem = availableCount > 0;
|
||||
return (
|
||||
<div className="px-6 pb-6">
|
||||
<button
|
||||
onClick={handleRedeemClick}
|
||||
disabled={!canRedeem}
|
||||
className="w-full py-3.5 rounded-xl text-sm font-medium transition-all disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
style={{
|
||||
backgroundColor: canRedeem ? "var(--jade)" : "var(--border-muted)",
|
||||
color: canRedeem ? "white" : "var(--text-muted)",
|
||||
boxShadow: canRedeem ? "0 2px 8px rgba(45,106,79,0.25)" : "none",
|
||||
}}
|
||||
>
|
||||
<span>{canRedeem ? "兑换奖品" : "继续收集以解锁奖品"}</span>
|
||||
{canRedeem && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-white/20">
|
||||
{availableCount} 个可兑换
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Redemption History */}
|
||||
{history.length > 0 && (
|
||||
|
||||