2026-05-09 18:23:21 +08:00
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import type { PointsBalance, PackageInfo } from '../lib/api';
|
|
|
|
|
import { getPointsBalance, getPackages, invalidatePointsCache } from '../lib/api';
|
|
|
|
|
|
2026-05-09 16:00:29 +08:00
|
|
|
interface Props {
|
|
|
|
|
locale: string;
|
|
|
|
|
dashboard: { brandName: string; navHome: string; navStore: string; navDivination: string; navManual: string; navAuto: string; navHistory: string; navLanguage: string; navSettings: string; logout: string };
|
|
|
|
|
store: { title: string; currentPoints: string; pointsLabel: string; rulesTitle: string; rule1: string; rule2: string; rule3: string; popularLabel: string; popularText: string; stepsTitle: string; step1: string; step2: string; step3: string; sideTitle: string; sideDesc: string };
|
|
|
|
|
pricing: { p1Name: string; p1Badge: string; p1Price: string; p1Credits: string; p1Desc: string; p2Name: string; p2Price: string; p2Credits: string; p2Desc: string; p3Name: string; p3Badge: string; p3Price: string; p3Credits: string; p3Desc: string; p4Name: string; p4Price: string; p4Credits: string; p4Desc: string; buyNow: string };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 18:23:21 +08:00
|
|
|
interface PackageDisplay {
|
|
|
|
|
name: string;
|
|
|
|
|
badge: string;
|
|
|
|
|
price: string;
|
|
|
|
|
credits: string;
|
|
|
|
|
desc: string;
|
|
|
|
|
featured: boolean;
|
|
|
|
|
productCode: string;
|
|
|
|
|
appStoreProductId: string;
|
|
|
|
|
starterEligible: boolean;
|
|
|
|
|
isStarter: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map product codes to display names from pricing translations
|
|
|
|
|
const PRODUCT_CODE_MAP: Record<string, string> = {
|
|
|
|
|
'new_user_pack': 'p1', // 新人专享包 60积分
|
|
|
|
|
'starter_pack': 'p2', // 入门补充包 100积分
|
|
|
|
|
'popular_pack': 'p3', // 常用加量包 210积分
|
|
|
|
|
'premium_pack': 'p4', // 高频进阶包 415积分
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function SidePanel({ s }: { s: Props['store'] }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="w-full xl:w-[320px] bg-white rounded-2xl p-5 border border-slate-200 flex flex-col gap-4 shrink-0 overflow-y-auto">
|
|
|
|
|
<div className="w-12 h-12 rounded-xl bg-violet-50 flex items-center justify-center">
|
|
|
|
|
<span className="material-symbols-rounded text-violet-600 text-2xl">shopping_cart</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-slate-900 text-lg font-bold">{s.sideTitle}</p>
|
|
|
|
|
<p className="text-slate-500 text-sm">{s.sideDesc}</p>
|
|
|
|
|
<div className="h-px bg-slate-100" />
|
|
|
|
|
<p className="text-slate-400 text-xs font-semibold">{s.popularLabel}</p>
|
|
|
|
|
<div className="bg-amber-50 rounded-xl p-3.5 text-amber-700 text-sm">{s.popularText}</div>
|
|
|
|
|
<p className="text-slate-400 text-xs font-semibold mt-2">{s.stepsTitle}</p>
|
|
|
|
|
<div className="flex items-center gap-2.5 text-sm text-slate-600"><span className="w-5 h-5 rounded-full bg-violet-100 text-violet-600 text-xs font-bold flex items-center justify-center">1</span>{s.step1}</div>
|
|
|
|
|
<div className="flex items-center gap-2.5 text-sm text-slate-600"><span className="w-5 h-5 rounded-full bg-violet-100 text-violet-600 text-xs font-bold flex items-center justify-center">2</span>{s.step2}</div>
|
|
|
|
|
<div className="flex items-center gap-2.5 text-sm text-slate-600"><span className="w-5 h-5 rounded-full bg-violet-100 text-violet-600 text-xs font-bold flex items-center justify-center">3</span>{s.step3}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-09 16:00:29 +08:00
|
|
|
|
|
|
|
|
export default function StorePage({ store: s, pricing: p }: Props) {
|
2026-05-09 18:23:21 +08:00
|
|
|
const [points, setPoints] = useState<PointsBalance | null>(null);
|
|
|
|
|
const [packages, setPackages] = useState<PackageDisplay[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
Promise.all([
|
|
|
|
|
getPointsBalance(),
|
|
|
|
|
getPackages(),
|
|
|
|
|
])
|
|
|
|
|
.then(([pointsData, packagesData]) => {
|
|
|
|
|
setPoints(pointsData);
|
|
|
|
|
// Map backend packages to display format
|
|
|
|
|
const displayPkgs: PackageDisplay[] = packagesData.packages.map((pkg) => {
|
|
|
|
|
const key = PRODUCT_CODE_MAP[pkg.productCode] || 'p1';
|
|
|
|
|
return {
|
|
|
|
|
name: p[`${key}Name` as keyof typeof p] || pkg.productCode,
|
|
|
|
|
badge: pkg.isStarter ? (pkg.starterEligible ? p.p1Badge : '') : '',
|
|
|
|
|
price: p[`${key}Price` as keyof typeof p] || '',
|
|
|
|
|
credits: `${pkg.credits} ${s.pointsLabel}`,
|
|
|
|
|
desc: p[`${key}Desc` as keyof typeof p] || '',
|
|
|
|
|
featured: pkg.productCode === 'popular_pack', // 只有常用加量包是推荐
|
|
|
|
|
productCode: pkg.productCode,
|
|
|
|
|
appStoreProductId: pkg.appStoreProductId,
|
|
|
|
|
starterEligible: pkg.starterEligible,
|
|
|
|
|
isStarter: pkg.isStarter,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
// Sort by sortOrder
|
|
|
|
|
displayPkgs.sort((a, b) => {
|
|
|
|
|
const pkgA = packagesData.packages.find(pkg => pkg.productCode === a.productCode);
|
|
|
|
|
const pkgB = packagesData.packages.find(pkg => pkg.productCode === b.productCode);
|
|
|
|
|
return (pkgA?.sortOrder || 0) - (pkgB?.sortOrder || 0);
|
|
|
|
|
});
|
|
|
|
|
setPackages(displayPkgs);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
// Fallback to static data if API fails
|
|
|
|
|
setPoints({ balance: 0, frozenBalance: 0, availableBalance: 0, runCost: 20, canRun: false });
|
|
|
|
|
setPackages([]);
|
|
|
|
|
})
|
|
|
|
|
.finally(() => setLoading(false));
|
|
|
|
|
}, [p, s.pointsLabel]);
|
2026-05-09 16:00:29 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-5 min-h-full">
|
2026-05-09 18:23:21 +08:00
|
|
|
<h1 className="text-slate-900 text-xl font-bold">{s.title}</h1>
|
2026-05-09 16:00:29 +08:00
|
|
|
|
|
|
|
|
{/* Top: Points hero + rules */}
|
|
|
|
|
<div className="flex flex-col lg:flex-row gap-5">
|
|
|
|
|
<div className="flex-1 rounded-2xl p-7 flex items-center gap-6" style={{ background: 'linear-gradient(135deg, #673AB7, #9C27B0)' }}>
|
|
|
|
|
<div className="w-[68px] h-[68px] rounded-full flex items-center justify-center" style={{ background: 'rgba(255,255,255,0.14)' }}>
|
|
|
|
|
<span className="material-symbols-rounded text-white text-3xl">account_balance_wallet</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-violet-200 text-sm">{s.currentPoints}</p>
|
2026-05-09 18:23:21 +08:00
|
|
|
<p className="text-white text-3xl font-bold">
|
|
|
|
|
{loading ? '...' : points?.balance ?? 0}
|
|
|
|
|
<span className="text-base font-normal text-violet-200"> {s.pointsLabel}</span>
|
|
|
|
|
</p>
|
2026-05-09 16:00:29 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-full lg:w-[320px] bg-white rounded-2xl p-5 border border-slate-200 flex flex-col gap-3.5 shrink-0">
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<span className="material-symbols-rounded text-violet-600 text-lg">info</span>
|
|
|
|
|
<span className="text-slate-900 text-sm font-bold">{s.rulesTitle}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-slate-500 text-sm">{s.rule1}</p>
|
|
|
|
|
<p className="text-slate-500 text-sm">{s.rule2}</p>
|
|
|
|
|
<p className="text-slate-500 text-sm">{s.rule3}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-09 18:23:21 +08:00
|
|
|
{/* Mobile: Side panel below rules (visible only on mobile) */}
|
|
|
|
|
<div className="xl:hidden">
|
|
|
|
|
<SidePanel s={s} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Body: Packages + side panel (desktop) */}
|
2026-05-09 16:00:29 +08:00
|
|
|
<div className="flex flex-col xl:flex-row gap-5 flex-1 min-h-0">
|
|
|
|
|
<div className="flex-1 flex flex-col gap-4 overflow-y-auto">
|
2026-05-09 18:23:21 +08:00
|
|
|
{loading ? (
|
|
|
|
|
<div className="text-slate-500 text-center py-8">{s.sideDesc}</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
{packages.map((pkg) => (
|
|
|
|
|
<div key={pkg.productCode} className={`bg-white rounded-2xl p-6 flex flex-col gap-3 border ${pkg.featured ? 'border-violet-400 ring-1 ring-violet-100' : 'border-slate-200'}`}>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-slate-900 font-bold text-base">{pkg.name}</span>
|
|
|
|
|
{pkg.badge && <span className={`text-xs px-2.5 py-0.5 rounded-full ${pkg.featured ? 'bg-violet-600 text-white' : 'bg-amber-100 text-amber-700'}`}>{pkg.badge}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-slate-900 text-2xl font-extrabold">{pkg.price}</p>
|
|
|
|
|
<p className="text-violet-600 text-sm font-medium">{pkg.credits}</p>
|
|
|
|
|
<p className="text-slate-500 text-sm">{pkg.desc}</p>
|
|
|
|
|
<button
|
|
|
|
|
className={`w-full py-2.5 rounded-lg font-semibold text-sm mt-auto ${pkg.featured ? 'bg-violet-600 text-white hover:bg-violet-700' : 'bg-white text-violet-600 border border-violet-200 hover:bg-violet-50'} transition-colors`}
|
|
|
|
|
disabled={pkg.isStarter && !pkg.starterEligible}
|
|
|
|
|
>
|
|
|
|
|
{pkg.isStarter && !pkg.starterEligible ? (s.rulesTitle.includes('已购') ? '已购买' : 'Purchased') : p.buyNow}
|
|
|
|
|
</button>
|
2026-05-09 16:00:29 +08:00
|
|
|
</div>
|
2026-05-09 18:23:21 +08:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-09 16:00:29 +08:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-09 18:23:21 +08:00
|
|
|
{/* Desktop: Side panel (visible only on xl+) */}
|
|
|
|
|
<div className="hidden xl:block">
|
|
|
|
|
<SidePanel s={s} />
|
2026-05-09 16:00:29 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|