style: format all files with Prettier

This commit is contained in:
erio
2026-03-14 03:45:37 +08:00
parent 78ecd206de
commit 886389939e
33 changed files with 1082 additions and 588 deletions

View File

@@ -285,7 +285,9 @@ function ChannelsContent() {
if (c.key === 'BALANCE_PAYMENT_DISABLED') setRcBalanceEnabled(c.value !== 'true');
}
}
} catch { /* ignore */ }
} catch {
/* ignore */
}
}, [token]);
const saveRechargeConfig = async () => {
@@ -302,7 +304,12 @@ function ChannelsContent() {
configs: [
{ key: 'PRODUCT_NAME_PREFIX', value: rcPrefix.trim(), group: 'payment', label: '商品名前缀' },
{ key: 'PRODUCT_NAME_SUFFIX', value: rcSuffix.trim(), group: 'payment', label: '商品名后缀' },
{ key: 'BALANCE_PAYMENT_DISABLED', value: rcBalanceEnabled ? 'false' : 'true', group: 'payment', label: '余额充值禁用' },
{
key: 'BALANCE_PAYMENT_DISABLED',
value: rcBalanceEnabled ? 'false' : 'true',
group: 'payment',
label: '余额充值禁用',
},
],
}),
});
@@ -633,7 +640,12 @@ function ChannelsContent() {
</div>
<div>
<label className={labelCls}>{t.preview}</label>
<div className={['rounded-lg border px-3 py-2 text-sm', isDark ? 'border-slate-600 bg-slate-700 text-slate-300' : 'border-slate-300 bg-slate-50 text-slate-600'].join(' ')}>
<div
className={[
'rounded-lg border px-3 py-2 text-sm',
isDark ? 'border-slate-600 bg-slate-700 text-slate-300' : 'border-slate-300 bg-slate-50 text-slate-600',
].join(' ')}
>
{`${rcPrefix.trim() || 'Sub2API'} 100 ${rcSuffix.trim() || 'CNY'}`.trim()}
</div>
</div>
@@ -687,7 +699,11 @@ function ChannelsContent() {
) : (
<table className="w-full text-sm">
<thead>
<tr className={isDark ? 'border-b border-slate-700 text-slate-400' : 'border-b border-slate-200 text-slate-500'}>
<tr
className={
isDark ? 'border-b border-slate-700 text-slate-400' : 'border-b border-slate-200 text-slate-500'
}
>
<th className="px-4 py-3 text-left font-medium">{t.colName}</th>
<th className="px-4 py-3 text-left font-medium">{t.colPlatform}</th>
<th className="px-4 py-3 text-left font-medium">{t.colRate}</th>
@@ -721,14 +737,30 @@ function ChannelsContent() {
</td>
<td className="px-4 py-3 text-center">
{channel.groupExists ? (
<span className={`inline-flex h-5 w-5 items-center justify-center rounded-full ${isDark ? 'bg-emerald-900/40 text-emerald-400' : 'bg-emerald-100 text-emerald-600'}`}>
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<span
className={`inline-flex h-5 w-5 items-center justify-center rounded-full ${isDark ? 'bg-emerald-900/40 text-emerald-400' : 'bg-emerald-100 text-emerald-600'}`}
>
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</span>
) : (
<span className={`inline-flex h-5 w-5 items-center justify-center rounded-full ${isDark ? 'bg-red-900/40 text-red-400' : 'bg-red-100 text-red-600'}`}>
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<span
className={`inline-flex h-5 w-5 items-center justify-center rounded-full ${isDark ? 'bg-red-900/40 text-red-400' : 'bg-red-100 text-red-600'}`}
>
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</span>
@@ -1004,9 +1036,7 @@ function ChannelsContent() {
<span className={`text-sm font-medium ${isDark ? 'text-slate-100' : 'text-slate-900'}`}>
{group.name}
</span>
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>
#{group.id}
</span>
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>#{group.id}</span>
<PlatformBadge platform={group.platform} className="text-[10px]" />
{alreadyImported && (
<span className="text-[10px] text-amber-500 font-medium">{t.syncAlreadyExists}</span>

View File

@@ -350,7 +350,9 @@ function SubscriptionsContent() {
/* --- subs state --- */
const [subsUserId, setSubsUserId] = useState('');
const [subsKeyword, setSubsKeyword] = useState('');
const [searchResults, setSearchResults] = useState<{ id: number; email: string; username: string; notes?: string }[]>([]);
const [searchResults, setSearchResults] = useState<{ id: number; email: string; username: string; notes?: string }[]>(
[],
);
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
const [searchTimer, setSearchTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
const [subs, setSubs] = useState<Sub2ApiSubscription[]>([]);
@@ -358,7 +360,6 @@ function SubscriptionsContent() {
const [subsLoading, setSubsLoading] = useState(false);
const [subsSearched, setSubsSearched] = useState(false);
/* --- fetch plans --- */
const fetchPlans = useCallback(async () => {
if (!token) return;
@@ -373,7 +374,7 @@ function SubscriptionsContent() {
throw new Error(t.requestFailed);
}
const data = await res.json();
setPlans(Array.isArray(data) ? data : data.plans ?? []);
setPlans(Array.isArray(data) ? data : (data.plans ?? []));
} catch {
setError(t.loadFailed);
} finally {
@@ -388,7 +389,7 @@ function SubscriptionsContent() {
const res = await fetch(`/api/admin/sub2api/groups?token=${encodeURIComponent(token)}`);
if (res.ok) {
const data = await res.json();
setGroups(Array.isArray(data) ? data : data.groups ?? []);
setGroups(Array.isArray(data) ? data : (data.groups ?? []));
}
} catch {
/* ignore */
@@ -467,9 +468,7 @@ function SubscriptionsContent() {
product_name: formProductName.trim() || null,
};
try {
const url = editingPlan
? `/api/admin/subscription-plans/${editingPlan.id}`
: '/api/admin/subscription-plans';
const url = editingPlan ? `/api/admin/subscription-plans/${editingPlan.id}` : '/api/admin/subscription-plans';
const method = editingPlan ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
@@ -729,12 +728,7 @@ function SubscriptionsContent() {
)}
{/* Tab switcher */}
<div
className={[
'mb-5 flex gap-1 rounded-xl p-1',
isDark ? 'bg-slate-800' : 'bg-slate-100',
].join(' ')}
>
<div className={['mb-5 flex gap-1 rounded-xl p-1', isDark ? 'bg-slate-800' : 'bg-slate-100'].join(' ')}>
<button type="button" className={tabCls(activeTab === 'plans')} onClick={() => setActiveTab('plans')}>
{t.tabPlans}
</button>
@@ -781,15 +775,23 @@ function SubscriptionsContent() {
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<h3 className={['text-base font-semibold', isDark ? 'text-slate-100' : 'text-slate-900'].join(' ')}>
<h3
className={['text-base font-semibold', isDark ? 'text-slate-100' : 'text-slate-900'].join(
' ',
)}
>
{plan.name}
</h3>
<span
className={[
'inline-block rounded-full px-2 py-0.5 text-xs font-medium',
plan.groupExists
? isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-50 text-green-700'
: isDark ? 'bg-red-500/20 text-red-300' : 'bg-red-50 text-red-600',
? isDark
? 'bg-green-500/20 text-green-300'
: 'bg-green-50 text-green-700'
: isDark
? 'bg-red-500/20 text-red-300'
: 'bg-red-50 text-red-600',
].join(' ')}
>
{plan.groupExists ? t.groupExists : t.groupMissing}
@@ -844,31 +846,50 @@ function SubscriptionsContent() {
{/* Plan fields grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-x-4 gap-y-2 text-sm">
<div>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>{t.colGroup}</span>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>
{t.colGroup}
</span>
<div className={isDark ? 'text-slate-200' : 'text-slate-800'}>
<span className="font-mono text-xs">{plan.groupId}</span>
{plan.groupName && <span className={`ml-1 text-xs ${isDark ? 'text-slate-400' : 'text-slate-500'}`}>({plan.groupName})</span>}
{plan.groupName && (
<span className={`ml-1 text-xs ${isDark ? 'text-slate-400' : 'text-slate-500'}`}>
({plan.groupName})
</span>
)}
</div>
</div>
<div>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>{t.colPrice}</span>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>
{t.colPrice}
</span>
<div className={isDark ? 'text-slate-200' : 'text-slate-800'}>
¥{plan.price.toFixed(2)}
{plan.originalPrice != null && (
<span className={`ml-1 line-through text-xs ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>
<span
className={`ml-1 line-through text-xs ${isDark ? 'text-slate-500' : 'text-slate-400'}`}
>
¥{plan.originalPrice.toFixed(2)}
</span>
)}
</div>
</div>
<div>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>{t.colValidDays}</span>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>
{t.colValidDays}
</span>
<div className={isDark ? 'text-slate-200' : 'text-slate-800'}>
{plan.validDays} {plan.validityUnit === 'month' ? t.unitMonth : plan.validityUnit === 'week' ? t.unitWeek : t.unitDay}
{plan.validDays}{' '}
{plan.validityUnit === 'month'
? t.unitMonth
: plan.validityUnit === 'week'
? t.unitWeek
: t.unitDay}
</div>
</div>
<div>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>{t.fieldSortOrder}</span>
<span className={['text-xs', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}>
{t.fieldSortOrder}
</span>
<div className={isDark ? 'text-slate-200' : 'text-slate-800'}>{plan.sortOrder}</div>
</div>
</div>
@@ -876,9 +897,16 @@ function SubscriptionsContent() {
{/* ── Sub2API 分组信息(嵌套只读区域) ── */}
{plan.groupExists && (
<div className={['border-t px-4 py-3', isDark ? 'border-slate-700 bg-slate-900/40' : 'border-slate-100 bg-slate-50/80'].join(' ')}>
<div
className={[
'border-t px-4 py-3',
isDark ? 'border-slate-700 bg-slate-900/40' : 'border-slate-100 bg-slate-50/80',
].join(' ')}
>
<div className="flex items-center gap-2 mb-2">
<span className={['text-xs font-medium', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
<span
className={['text-xs font-medium', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}
>
{t.groupInfo}
</span>
<span className={['text-[10px]', isDark ? 'text-slate-600' : 'text-slate-400'].join(' ')}>
@@ -889,13 +917,17 @@ function SubscriptionsContent() {
{plan.groupPlatform && (
<div>
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}>{t.platform}</span>
<div className="mt-0.5"><PlatformBadge platform={plan.groupPlatform} /></div>
<div className="mt-0.5">
<PlatformBadge platform={plan.groupPlatform} />
</div>
</div>
)}
{plan.groupRateMultiplier != null && (
<div>
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}>{t.rateMultiplier}</span>
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>{plan.groupRateMultiplier}x</div>
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>
{plan.groupRateMultiplier}x
</div>
</div>
)}
<div>
@@ -920,14 +952,30 @@ function SubscriptionsContent() {
<>
<div>
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}>/v1/messages </span>
<div className={['mt-0.5 text-xs font-medium', plan.groupAllowMessagesDispatch ? (isDark ? 'text-green-400' : 'text-green-600') : isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
<div
className={[
'mt-0.5 text-xs font-medium',
plan.groupAllowMessagesDispatch
? isDark
? 'text-green-400'
: 'text-green-600'
: isDark
? 'text-slate-400'
: 'text-slate-500',
].join(' ')}
>
{plan.groupAllowMessagesDispatch ? '已启用' : '未启用'}
</div>
</div>
{plan.groupDefaultMappedModel && (
<div className="sm:col-span-2">
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}></span>
<div className={['mt-0.5 font-mono text-xs', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
<div
className={[
'mt-0.5 font-mono text-xs',
isDark ? 'text-slate-300' : 'text-slate-600',
].join(' ')}
>
{plan.groupDefaultMappedModel}
</div>
</div>
@@ -960,7 +1008,9 @@ function SubscriptionsContent() {
fetchSubs();
}
}}
onFocus={() => { if (searchResults.length > 0) setSearchDropdownOpen(true); }}
onFocus={() => {
if (searchResults.length > 0) setSearchDropdownOpen(true);
}}
placeholder={t.searchUserId}
className={inputCls}
/>
@@ -994,7 +1044,10 @@ function SubscriptionsContent() {
</div>
<button
type="button"
onClick={() => { setSearchDropdownOpen(false); fetchSubs(); }}
onClick={() => {
setSearchDropdownOpen(false);
fetchSubs();
}}
disabled={subsLoading}
className={[
'inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50',
@@ -1029,9 +1082,7 @@ function SubscriptionsContent() {
</div>
<div className={`text-xs ${isDark ? 'text-slate-400' : 'text-slate-500'}`}>{subsUser.email}</div>
</div>
<div className={`ml-auto text-xs ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>
ID: {subsUser.id}
</div>
<div className={`ml-auto text-xs ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>ID: {subsUser.id}</div>
</div>
)}
@@ -1040,9 +1091,7 @@ function SubscriptionsContent() {
{subsLoading ? (
<div className={`py-12 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{t.loading}</div>
) : !subsSearched ? (
<div className={`py-12 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{t.loading}
</div>
<div className={`py-12 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{t.loading}</div>
) : subs.length === 0 ? (
<div className={`py-12 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{t.noSubs}</div>
) : (
@@ -1134,13 +1183,10 @@ function SubscriptionsContent() {
: 'text-slate-500'
}`}
>
{remaining > 0
? `${remaining} ${t.days} ${t.remaining}`
: t.expired}
{remaining > 0 ? `${remaining} ${t.days} ${t.remaining}` : t.expired}
</div>
)}
</td>
</tr>
);
})}
@@ -1160,12 +1206,7 @@ function SubscriptionsContent() {
isDark ? 'border-slate-700 bg-slate-900' : 'border-slate-200 bg-white',
].join(' ')}
>
<h2
className={[
'mb-5 text-lg font-semibold',
isDark ? 'text-slate-100' : 'text-slate-900',
].join(' ')}
>
<h2 className={['mb-5 text-lg font-semibold', isDark ? 'text-slate-100' : 'text-slate-900'].join(' ')}>
{editingPlan ? t.editPlan : t.newPlan}
</h2>
@@ -1173,11 +1214,7 @@ function SubscriptionsContent() {
{/* Group */}
<div>
<label className={labelCls}>{t.fieldGroup}</label>
<select
value={formGroupId}
onChange={(e) => setFormGroupId(e.target.value)}
className={inputCls}
>
<select value={formGroupId} onChange={(e) => setFormGroupId(e.target.value)} className={inputCls}>
<option value="">{t.fieldGroupPlaceholder}</option>
{availableGroups.map((g) => (
<option key={g.id} value={g.id}>
@@ -1198,7 +1235,12 @@ function SubscriptionsContent() {
const selectedGroup = groups.find((g) => String(g.id) === formGroupId);
if (!selectedGroup) return null;
return (
<div className={['rounded-lg border p-3 text-xs', isDark ? 'border-slate-700 bg-slate-800/60' : 'border-slate-200 bg-slate-50'].join(' ')}>
<div
className={[
'rounded-lg border p-3 text-xs',
isDark ? 'border-slate-700 bg-slate-800/60' : 'border-slate-200 bg-slate-50',
].join(' ')}
>
<div className="flex items-center gap-2 mb-2">
<span className={['font-medium', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
{t.groupInfo}
@@ -1211,13 +1253,17 @@ function SubscriptionsContent() {
{selectedGroup.platform && (
<div>
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}>{t.platform}</span>
<div className="mt-0.5"><PlatformBadge platform={selectedGroup.platform} /></div>
<div className="mt-0.5">
<PlatformBadge platform={selectedGroup.platform} />
</div>
</div>
)}
{selectedGroup.rate_multiplier != null && (
<div>
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}>{t.rateMultiplier}</span>
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>{selectedGroup.rate_multiplier}x</div>
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>
{selectedGroup.rate_multiplier}x
</div>
</div>
)}
<div>
@@ -1235,13 +1281,26 @@ function SubscriptionsContent() {
<div>
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}>{t.monthlyLimit}</span>
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>
{selectedGroup.monthly_limit_usd != null ? `$${selectedGroup.monthly_limit_usd}` : t.unlimited}
{selectedGroup.monthly_limit_usd != null
? `$${selectedGroup.monthly_limit_usd}`
: t.unlimited}
</div>
</div>
{selectedGroup.platform?.toLowerCase() === 'openai' && (
<div>
<span className={isDark ? 'text-slate-500' : 'text-slate-400'}>/v1/messages </span>
<div className={['mt-0.5 font-medium', selectedGroup.allow_messages_dispatch ? (isDark ? 'text-green-400' : 'text-green-600') : isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
<div
className={[
'mt-0.5 font-medium',
selectedGroup.allow_messages_dispatch
? isDark
? 'text-green-400'
: 'text-green-600'
: isDark
? 'text-slate-400'
: 'text-slate-500',
].join(' ')}
>
{selectedGroup.allow_messages_dispatch ? '已启用' : '未启用'}
</div>
</div>

View File

@@ -30,7 +30,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
if (body.price !== undefined && (typeof body.price !== 'number' || body.price <= 0 || body.price > 99999999.99)) {
return NextResponse.json({ error: 'price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 });
}
if (body.original_price !== undefined && body.original_price !== null && (typeof body.original_price !== 'number' || body.original_price <= 0 || body.original_price > 99999999.99)) {
if (
body.original_price !== undefined &&
body.original_price !== null &&
(typeof body.original_price !== 'number' || body.original_price <= 0 || body.original_price > 99999999.99)
) {
return NextResponse.json({ error: 'original_price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 });
}
if (body.validity_days !== undefined && (!Number.isInteger(body.validity_days) || body.validity_days <= 0)) {
@@ -100,10 +104,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
});
if (activeOrderCount > 0) {
return NextResponse.json(
{ error: `该套餐仍有 ${activeOrderCount} 个活跃订单,无法删除` },
{ status: 409 },
);
return NextResponse.json({ error: `该套餐仍有 ${activeOrderCount} 个活跃订单,无法删除` }, { status: 409 });
}
await prisma.subscriptionPlan.delete({ where: { id } });

View File

@@ -65,7 +65,19 @@ export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { group_id, name, description, price, original_price, validity_days, validity_unit, features, for_sale, sort_order, product_name } = body;
const {
group_id,
name,
description,
price,
original_price,
validity_days,
validity_unit,
features,
for_sale,
sort_order,
product_name,
} = body;
if (!group_id || !name || price === undefined) {
return NextResponse.json({ error: '缺少必填字段: group_id, name, price' }, { status: 400 });
@@ -74,7 +86,11 @@ export async function POST(request: NextRequest) {
if (typeof price !== 'number' || price <= 0 || price > 99999999.99) {
return NextResponse.json({ error: 'price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 });
}
if (original_price !== undefined && original_price !== null && (typeof original_price !== 'number' || original_price <= 0 || original_price > 99999999.99)) {
if (
original_price !== undefined &&
original_price !== null &&
(typeof original_price !== 'number' || original_price <= 0 || original_price > 99999999.99)
) {
return NextResponse.json({ error: 'original_price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 });
}
if (validity_days !== undefined && (!Number.isInteger(validity_days) || validity_days <= 0)) {
@@ -90,10 +106,7 @@ export async function POST(request: NextRequest) {
});
if (existing) {
return NextResponse.json(
{ error: `分组 ID ${group_id} 已被套餐「${existing.name}」使用` },
{ status: 409 },
);
return NextResponse.json({ error: `分组 ID ${group_id} 已被套餐「${existing.name}」使用` }, { status: 409 });
}
const plan = await prisma.subscriptionPlan.create({

View File

@@ -25,9 +25,7 @@ export async function GET(request: NextRequest) {
getUser(parsedUserId).catch(() => null),
]);
const filtered = groupId
? subscriptions.filter((s) => s.group_id === Number(groupId))
: subscriptions;
const filtered = groupId ? subscriptions.filter((s) => s.group_id === Number(groupId)) : subscriptions;
return NextResponse.json({
subscriptions: filtered,

View File

@@ -25,7 +25,11 @@ export async function GET(request: NextRequest) {
plans.map(async (plan) => {
let groupActive = false;
let group: Awaited<ReturnType<typeof getGroup>> = null;
let groupInfo: { daily_limit_usd: number | null; weekly_limit_usd: number | null; monthly_limit_usd: number | null } | null = null;
let groupInfo: {
daily_limit_usd: number | null;
weekly_limit_usd: number | null;
monthly_limit_usd: number | null;
} | null = null;
try {
group = await getGroup(plan.groupId);
groupActive = group !== null && group.status === 'active';

View File

@@ -16,10 +16,7 @@ export async function GET(request: NextRequest) {
}
try {
const [subscriptions, groups] = await Promise.all([
getUserSubscriptions(userId),
getAllGroups().catch(() => []),
]);
const [subscriptions, groups] = await Promise.all([getUserSubscriptions(userId), getAllGroups().catch(() => [])]);
const groupMap = new Map(groups.map((g) => [g.id, g]));

View File

@@ -242,7 +242,9 @@ function OrdersPageFallback() {
return (
<div className={`flex min-h-screen items-center justify-center ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>
{pickLocaleText(locale, '加载中...', 'Loading...')}
</div>
</div>
);
}

View File

@@ -102,19 +102,28 @@ function PayContent() {
const renderHelpSection = () => {
if (!hasHelpContent) return null;
return (
<div className={[
'mt-6 rounded-2xl border p-4',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
].join(' ')}>
<div
className={[
'mt-6 rounded-2xl border p-4',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
].join(' ')}
>
<div className={['text-xs font-medium', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
{pickLocaleText(locale, '帮助', 'Support')}
</div>
{helpImageUrl && (
<img src={helpImageUrl} alt="help" onClick={() => setHelpImageOpen(true)} className={`mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain p-2 ${isDark ? 'bg-slate-700/50' : 'bg-white/70'}`} />
<img
src={helpImageUrl}
alt="help"
onClick={() => setHelpImageOpen(true)}
className={`mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain p-2 ${isDark ? 'bg-slate-700/50' : 'bg-white/70'}`}
/>
)}
{helpText && (
<div className={['mt-3 space-y-1 text-sm leading-6', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
{helpText.split('\n').map((line, i) => (<p key={i}>{line}</p>))}
{helpText.split('\n').map((line, i) => (
<p key={i}>{line}</p>
))}
</div>
)}
</div>
@@ -233,7 +242,8 @@ function PayContent() {
const subData = await subRes.json();
setUserSubscriptions(subData.subscriptions ?? []);
}
} catch {} finally {
} catch {
} finally {
setChannelsLoaded(true);
}
}, [token]);
@@ -282,10 +292,7 @@ function PayContent() {
// 检查订单完成后是否是订阅分组消失的情况
useEffect(() => {
if (step !== 'result' || !finalOrderState) return;
if (
finalOrderState.status === 'FAILED' &&
finalOrderState.failedReason?.includes('SUBSCRIPTION_GROUP_GONE')
) {
if (finalOrderState.status === 'FAILED' && finalOrderState.failedReason?.includes('SUBSCRIPTION_GROUP_GONE')) {
setSubscriptionError(
pickLocaleText(
locale,
@@ -302,7 +309,11 @@ function PayContent() {
<div className="text-center text-red-500">
<p className="text-lg font-medium">{pickLocaleText(locale, '缺少认证信息', 'Missing authentication info')}</p>
<p className={`mt-2 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{pickLocaleText(locale, '请从 Sub2API 平台正确访问充值页面', 'Please open the recharge page from the Sub2API platform')}
{pickLocaleText(
locale,
'请从 Sub2API 平台正确访问充值页面',
'Please open the recharge page from the Sub2API platform',
)}
</p>
</div>
</div>
@@ -315,7 +326,11 @@ function PayContent() {
<div className="text-center text-red-500">
<p className="text-lg font-medium">{pickLocaleText(locale, '用户不存在', 'User not found')}</p>
<p className={`mt-2 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{pickLocaleText(locale, '请检查链接是否正确,或联系管理员', 'Please check whether the link is correct or contact the administrator')}
{pickLocaleText(
locale,
'请检查链接是否正确,或联系管理员',
'Please check whether the link is correct or contact the administrator',
)}
</p>
</div>
</div>
@@ -380,7 +395,9 @@ function PayContent() {
METHOD_DAILY_LIMIT_EXCEEDED: data.error,
PAYMENT_GATEWAY_ERROR: data.error,
};
setError(codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order'));
setError(
codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order'),
);
return;
}
@@ -492,10 +509,15 @@ function PayContent() {
<>
<button
type="button"
onClick={() => { loadUserAndOrders(); loadChannelsAndPlans(); }}
onClick={() => {
loadUserAndOrders();
loadChannelsAndPlans();
}}
className={[
'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors',
isDark ? 'border-slate-600 text-slate-200 hover:bg-slate-800' : 'border-slate-300 text-slate-700 hover:bg-slate-100',
isDark
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
].join(' ')}
>
{pickLocaleText(locale, '刷新', 'Refresh')}
@@ -504,7 +526,9 @@ function PayContent() {
href={ordersUrl}
className={[
'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors',
isDark ? 'border-slate-600 text-slate-200 hover:bg-slate-800' : 'border-slate-300 text-slate-700 hover:bg-slate-100',
isDark
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
].join(' ')}
>
{pickLocaleText(locale, '我的订单', 'My Orders')}
@@ -515,10 +539,12 @@ function PayContent() {
>
{/* 订阅分组消失的常驻错误 */}
{subscriptionError && (
<div className={[
'mb-4 rounded-lg border-2 p-4 text-sm',
isDark ? 'border-red-600 bg-red-900/40 text-red-300' : 'border-red-400 bg-red-50 text-red-700',
].join(' ')}>
<div
className={[
'mb-4 rounded-lg border-2 p-4 text-sm',
isDark ? 'border-red-600 bg-red-900/40 text-red-300' : 'border-red-400 bg-red-50 text-red-700',
].join(' ')}
>
<div className="font-semibold mb-1">{pickLocaleText(locale, '订阅开通失败', 'Subscription Failed')}</div>
<div>{subscriptionError}</div>
{orderResult && (
@@ -530,10 +556,12 @@ function PayContent() {
)}
{error && (
<div className={[
'mb-4 rounded-lg border p-3 text-sm',
isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600',
].join(' ')}>
<div
className={[
'mb-4 rounded-lg border p-3 text-sm',
isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600',
].join(' ')}
>
{error}
</div>
)}
@@ -543,18 +571,24 @@ function PayContent() {
<>
{/* 移动端 Tab充值/订单 */}
{isMobile && (
<div className={[
'mb-4 grid grid-cols-2 rounded-xl border p-1',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-300 bg-slate-100/90',
].join(' ')}>
<div
className={[
'mb-4 grid grid-cols-2 rounded-xl border p-1',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-300 bg-slate-100/90',
].join(' ')}
>
<button
type="button"
onClick={() => setActiveMobileTab('pay')}
className={[
'rounded-lg px-3 py-2 text-sm font-semibold transition-all duration-200',
activeMobileTab === 'pay'
? isDark ? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm' : 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50'
: isDark ? 'text-slate-400 hover:text-slate-200' : 'text-slate-500 hover:text-slate-700',
? isDark
? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm'
: 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50'
: isDark
? 'text-slate-400 hover:text-slate-200'
: 'text-slate-500 hover:text-slate-700',
].join(' ')}
>
{pickLocaleText(locale, '充值', 'Recharge')}
@@ -565,8 +599,12 @@ function PayContent() {
className={[
'rounded-lg px-3 py-2 text-sm font-semibold transition-all duration-200',
activeMobileTab === 'orders'
? isDark ? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm' : 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50'
: isDark ? 'text-slate-400 hover:text-slate-200' : 'text-slate-500 hover:text-slate-700',
? isDark
? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm'
: 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50'
: isDark
? 'text-slate-400 hover:text-slate-200'
: 'text-slate-500 hover:text-slate-700',
].join(' ')}
>
{pickLocaleText(locale, '我的订单', 'My Orders')}
@@ -586,12 +624,20 @@ function PayContent() {
{/* R7: 所有入口关闭提示 */}
{allEntriesClosed && (activeMobileTab === 'pay' || !isMobile) && (
<div className={[
'rounded-2xl border p-8 text-center',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-white shadow-sm',
].join(' ')}>
<div
className={[
'rounded-2xl border p-8 text-center',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-white shadow-sm',
].join(' ')}
>
<div className={['text-4xl mb-4'].join(' ')}>
<svg className={['mx-auto h-12 w-12', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<svg
className={['mx-auto h-12 w-12', isDark ? 'text-slate-500' : 'text-slate-400'].join(' ')}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<circle cx="12" cy="12" r="10" />
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
</svg>
@@ -600,136 +646,194 @@ function PayContent() {
{pickLocaleText(locale, '充值/订阅 入口未开放', 'Recharge / Subscription entry is not available')}
</p>
<p className={['text-sm', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
{pickLocaleText(locale, '如有疑问,请联系管理员', 'Please contact the administrator if you have questions')}
{pickLocaleText(
locale,
'如有疑问,请联系管理员',
'Please contact the administrator if you have questions',
)}
</p>
</div>
)}
{/* ── 有渠道配置新版UI ── */}
{channelsLoaded && showMainTabs && (activeMobileTab === 'pay' || !isMobile) && !selectedPlan && !showTopUpForm && (
<>
<MainTabs activeTab={!canTopUp ? 'subscribe' : mainTab} onTabChange={setMainTab} showSubscribeTab={hasPlans} showTopUpTab={canTopUp} isDark={isDark} locale={locale} />
{channelsLoaded &&
showMainTabs &&
(activeMobileTab === 'pay' || !isMobile) &&
!selectedPlan &&
!showTopUpForm && (
<>
<MainTabs
activeTab={!canTopUp ? 'subscribe' : mainTab}
onTabChange={setMainTab}
showSubscribeTab={hasPlans}
showTopUpTab={canTopUp}
isDark={isDark}
locale={locale}
/>
{mainTab === 'topup' && canTopUp && (
<div className="mt-6">
{/* 按量付费说明 banner */}
<div className={[
'mb-6 rounded-2xl border p-6',
isDark
? 'border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 to-purple-500/10'
: 'border-emerald-500/20 bg-gradient-to-r from-emerald-50 to-purple-50',
].join(' ')}>
<div className="flex items-start gap-4">
<div className={[
'flex-shrink-0 rounded-lg p-2',
isDark ? 'bg-emerald-500/20' : 'bg-emerald-500/15',
].join(' ')}>
<svg className="h-6 w-6 text-emerald-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<div className="flex-1">
<h3 className={['text-lg font-semibold mb-2', isDark ? 'text-emerald-400' : 'text-emerald-700'].join(' ')}>
{pickLocaleText(locale, '按量付费模式', 'Pay-as-you-go')}
</h3>
<p className={['text-sm mb-4', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
{pickLocaleText(
locale,
'无需订阅充值即用按实际消耗扣费。余额所有渠道通用可自由切换。价格以美元计价当前比例1美元≈1人民币',
'No subscription needed. Top up and use. Charged by actual usage. Balance works across all channels. Priced in USD (current rate: 1 USD ≈ 1 CNY)',
)}
</p>
<div className="flex flex-wrap gap-4 text-sm">
<div className={['flex items-center gap-2', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
<svg className="h-4 w-4 text-green-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
<polyline points="17 6 23 6 23 12" />
</svg>
<span>{pickLocaleText(locale, '倍率越低越划算', 'Lower rate = better value')}</span>
</div>
<div className={['flex items-center gap-2', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
<svg className="h-4 w-4 text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
<span>{pickLocaleText(locale, '0.15倍率 = 1元可用约6.67美元额度', '0.15 rate = 1 CNY ≈ $6.67 quota')}</span>
{mainTab === 'topup' && canTopUp && (
<div className="mt-6">
{/* 按量付费说明 banner */}
<div
className={[
'mb-6 rounded-2xl border p-6',
isDark
? 'border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 to-purple-500/10'
: 'border-emerald-500/20 bg-gradient-to-r from-emerald-50 to-purple-50',
].join(' ')}
>
<div className="flex items-start gap-4">
<div
className={[
'flex-shrink-0 rounded-lg p-2',
isDark ? 'bg-emerald-500/20' : 'bg-emerald-500/15',
].join(' ')}
>
<svg
className="h-6 w-6 text-emerald-500"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<div className="flex-1">
<h3
className={[
'text-lg font-semibold mb-2',
isDark ? 'text-emerald-400' : 'text-emerald-700',
].join(' ')}
>
{pickLocaleText(locale, '按量付费模式', 'Pay-as-you-go')}
</h3>
<p className={['text-sm mb-4', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
{pickLocaleText(
locale,
'无需订阅充值即用按实际消耗扣费。余额所有渠道通用可自由切换。价格以美元计价当前比例1美元≈1人民币',
'No subscription needed. Top up and use. Charged by actual usage. Balance works across all channels. Priced in USD (current rate: 1 USD ≈ 1 CNY)',
)}
</p>
<div className="flex flex-wrap gap-4 text-sm">
<div
className={['flex items-center gap-2', isDark ? 'text-slate-400' : 'text-slate-500'].join(
' ',
)}
>
<svg
className="h-4 w-4 text-green-500"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
<polyline points="17 6 23 6 23 12" />
</svg>
<span>{pickLocaleText(locale, '倍率越低越划算', 'Lower rate = better value')}</span>
</div>
<div
className={['flex items-center gap-2', isDark ? 'text-slate-400' : 'text-slate-500'].join(
' ',
)}
>
<svg
className="h-4 w-4 text-blue-500"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
<span>
{pickLocaleText(
locale,
'0.15倍率 = 1元可用约6.67美元额度',
'0.15 rate = 1 CNY ≈ $6.67 quota',
)}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
{hasChannels ? (
<ChannelGrid
channels={channels}
onTopUp={() => setShowTopUpForm(true)}
isDark={isDark}
locale={locale}
userBalance={userInfo?.balance}
/>
) : (
<PaymentForm
userId={resolvedUserId ?? 0}
userName={userInfo?.username}
userBalance={userInfo?.balance}
enabledPaymentTypes={config.enabledPaymentTypes}
methodLimits={config.methodLimits}
minAmount={config.minAmount}
maxAmount={config.maxAmount}
onSubmit={handleSubmit}
loading={loading}
dark={isDark}
pendingBlocked={pendingBlocked}
pendingCount={pendingCount}
locale={locale}
/>
)}
{renderHelpSection()}
</div>
)}
{mainTab === 'subscribe' && (
<div className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{plans.map((plan) => (
<SubscriptionPlanCard
key={plan.id}
plan={plan}
onSubscribe={() => setSelectedPlan(plan)}
{hasChannels ? (
<ChannelGrid
channels={channels}
onTopUp={() => setShowTopUpForm(true)}
isDark={isDark}
locale={locale}
userBalance={userInfo?.balance}
/>
))}
) : (
<PaymentForm
userId={resolvedUserId ?? 0}
userName={userInfo?.username}
userBalance={userInfo?.balance}
enabledPaymentTypes={config.enabledPaymentTypes}
methodLimits={config.methodLimits}
minAmount={config.minAmount}
maxAmount={config.maxAmount}
onSubmit={handleSubmit}
loading={loading}
dark={isDark}
pendingBlocked={pendingBlocked}
pendingCount={pendingCount}
locale={locale}
/>
)}
{renderHelpSection()}
</div>
)}
{renderHelpSection()}
</div>
)}
{mainTab === 'subscribe' && (
<div className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{plans.map((plan) => (
<SubscriptionPlanCard
key={plan.id}
plan={plan}
onSubscribe={() => setSelectedPlan(plan)}
isDark={isDark}
locale={locale}
/>
))}
</div>
{/* 用户已有订阅 — 所有 tab 共用 */}
{userSubscriptions.length > 0 && (
<div className="mt-8">
<h3 className={['text-lg font-semibold mb-3', isDark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>
{pickLocaleText(locale, '我的订阅', 'My Subscriptions')}
</h3>
<UserSubscriptions
subscriptions={userSubscriptions}
onRenew={(groupId) => {
const plan = plans.find((p) => p.groupId === groupId);
if (plan) {
setSelectedPlan(plan);
setMainTab('subscribe');
}
}}
isDark={isDark}
locale={locale}
/>
</div>
)}
{renderHelpSection()}
</div>
)}
<PurchaseFlow isDark={isDark} locale={locale} />
</>
)}
{/* 用户已有订阅 — 所有 tab 共用 */}
{userSubscriptions.length > 0 && (
<div className="mt-8">
<h3
className={['text-lg font-semibold mb-3', isDark ? 'text-slate-200' : 'text-slate-800'].join(' ')}
>
{pickLocaleText(locale, '我的订阅', 'My Subscriptions')}
</h3>
<UserSubscriptions
subscriptions={userSubscriptions}
onRenew={(groupId) => {
const plan = plans.find((p) => p.groupId === groupId);
if (plan) {
setSelectedPlan(plan);
setMainTab('subscribe');
}
}}
isDark={isDark}
locale={locale}
/>
</div>
)}
<PurchaseFlow isDark={isDark} locale={locale} />
</>
)}
{/* 点击"立即充值"后:直接显示 PaymentForm含金额选择 */}
{showTopUpForm && step === 'form' && (
@@ -834,29 +938,60 @@ function PayContent() {
/>
</div>
<div className="space-y-4">
<div className={['rounded-2xl border p-4', isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50'].join(' ')}>
<div
className={[
'rounded-2xl border p-4',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
].join(' ')}
>
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
{pickLocaleText(locale, '支付说明', 'Payment Notes')}
</div>
<ul className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
<li>{pickLocaleText(locale, '订单完成后会自动到账', 'Balance will be credited automatically')}</li>
<li>{pickLocaleText(locale, '如需历史记录请查看「我的订单」', 'Check "My Orders" for history')}</li>
<ul
className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}
>
<li>
{pickLocaleText(locale, '订单完成后会自动到账', 'Balance will be credited automatically')}
</li>
<li>
{pickLocaleText(locale, '如需历史记录请查看「我的订单」', 'Check "My Orders" for history')}
</li>
{config.maxDailyAmount > 0 && (
<li>{pickLocaleText(locale, '每日最大充值', 'Max daily recharge')} ¥{config.maxDailyAmount.toFixed(2)}</li>
<li>
{pickLocaleText(locale, '每日最大充值', 'Max daily recharge')} ¥
{config.maxDailyAmount.toFixed(2)}
</li>
)}
</ul>
</div>
{hasHelpContent && (
<div className={['rounded-2xl border p-4', isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50'].join(' ')}>
<div
className={[
'rounded-2xl border p-4',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
].join(' ')}
>
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>
{pickLocaleText(locale, '帮助', 'Support')}
</div>
{helpImageUrl && (
<img src={helpImageUrl} alt="help" onClick={() => setHelpImageOpen(true)} className={`mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain p-2 ${isDark ? 'bg-slate-700/50' : 'bg-white/70'}`} />
<img
src={helpImageUrl}
alt="help"
onClick={() => setHelpImageOpen(true)}
className={`mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain p-2 ${isDark ? 'bg-slate-700/50' : 'bg-white/70'}`}
/>
)}
{helpText && (
<div className={['mt-3 space-y-1 text-sm leading-6', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
{helpText.split('\n').map((line, i) => (<p key={i}>{line}</p>))}
<div
className={[
'mt-3 space-y-1 text-sm leading-6',
isDark ? 'text-slate-300' : 'text-slate-600',
].join(' ')}
>
{helpText.split('\n').map((line, i) => (
<p key={i}>{line}</p>
))}
</div>
)}
</div>
@@ -924,8 +1059,16 @@ function PayContent() {
{/* 帮助图片放大 */}
{helpImageOpen && helpImageUrl && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm" onClick={() => setHelpImageOpen(false)}>
<img src={helpImageUrl} alt="help" className="max-h-[90vh] max-w-full rounded-xl object-contain shadow-2xl" onClick={(e) => e.stopPropagation()} />
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm"
onClick={() => setHelpImageOpen(false)}
>
<img
src={helpImageUrl}
alt="help"
className="max-h-[90vh] max-w-full rounded-xl object-contain shadow-2xl"
onClick={(e) => e.stopPropagation()}
/>
</div>
)}
</PayPageLayout>
@@ -938,7 +1081,9 @@ function PayPageFallback() {
const isDark = searchParams.get('theme') === 'dark';
return (
<div className={`flex min-h-screen items-center justify-center ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>
{pickLocaleText(locale, '加载中...', 'Loading...')}
</div>
</div>
);
}

View File

@@ -54,7 +54,12 @@ function closeCurrentWindow() {
}, 250);
}
function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale, hasAccessToken: boolean, isDark = false) {
function getStatusConfig(
order: PublicOrderStatusSnapshot | null,
locale: Locale,
hasAccessToken: boolean,
isDark = false,
) {
if (!order) {
return locale === 'en'
? {
@@ -81,7 +86,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale
icon: '✓',
message: 'Your balance has been credited successfully.',
}
: { label: '充值成功', color: isDark ? 'text-green-400' : 'text-green-600', icon: '✓', message: '余额已成功到账!' };
: {
label: '充值成功',
color: isDark ? 'text-green-400' : 'text-green-600',
icon: '✓',
message: '余额已成功到账!',
};
}
if (order.paymentSuccess) {
@@ -93,7 +103,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale
icon: '⟳',
message: 'Payment succeeded, and the balance top-up is being processed.',
}
: { label: '充值处理中', color: isDark ? 'text-blue-400' : 'text-blue-600', icon: '⟳', message: '支付成功,余额正在充值中...' };
: {
label: '充值处理中',
color: isDark ? 'text-blue-400' : 'text-blue-600',
icon: '⟳',
message: '支付成功,余额正在充值中...',
};
}
if (order.rechargeStatus === 'failed') {
@@ -116,8 +131,18 @@ function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale
if (order.status === 'PENDING') {
return locale === 'en'
? { label: 'Awaiting Payment', color: isDark ? 'text-yellow-400' : 'text-yellow-600', icon: '⏳', message: 'The order has not been paid yet.' }
: { label: '等待支付', color: isDark ? 'text-yellow-400' : 'text-yellow-600', icon: '⏳', message: '订单尚未完成支付。' };
? {
label: 'Awaiting Payment',
color: isDark ? 'text-yellow-400' : 'text-yellow-600',
icon: '⏳',
message: 'The order has not been paid yet.',
}
: {
label: '等待支付',
color: isDark ? 'text-yellow-400' : 'text-yellow-600',
icon: '⏳',
message: '订单尚未完成支付。',
};
}
if (order.status === 'EXPIRED') {
@@ -128,17 +153,37 @@ function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale
icon: '⏰',
message: 'This order has expired. Please create a new order.',
}
: { label: '订单已超时', color: isDark ? 'text-slate-400' : 'text-gray-500', icon: '⏰', message: '订单已超时,请重新充值。' };
: {
label: '订单已超时',
color: isDark ? 'text-slate-400' : 'text-gray-500',
icon: '⏰',
message: '订单已超时,请重新充值。',
};
}
if (order.status === 'CANCELLED') {
return locale === 'en'
? { label: 'Order Cancelled', color: isDark ? 'text-slate-400' : 'text-gray-500', icon: '✗', message: 'This order has been cancelled.' }
: { label: '订单已取消', color: isDark ? 'text-slate-400' : 'text-gray-500', icon: '✗', message: '订单已被取消。' };
? {
label: 'Order Cancelled',
color: isDark ? 'text-slate-400' : 'text-gray-500',
icon: '✗',
message: 'This order has been cancelled.',
}
: {
label: '订单已取消',
color: isDark ? 'text-slate-400' : 'text-gray-500',
icon: '✗',
message: '订单已被取消。',
};
}
return locale === 'en'
? { label: 'Payment Error', color: isDark ? 'text-red-400' : 'text-red-600', icon: '✗', message: 'Please contact the administrator.' }
? {
label: 'Payment Error',
color: isDark ? 'text-red-400' : 'text-red-600',
icon: '✗',
message: 'Please contact the administrator.',
}
: { label: '支付异常', color: isDark ? 'text-red-400' : 'text-red-600', icon: '✗', message: '请联系管理员处理。' };
}
@@ -261,7 +306,11 @@ function ResultContent() {
</div>
)
) : (
<button type="button" onClick={goBack} className={`mt-4 text-sm underline ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}>
<button
type="button"
onClick={goBack}
className={`mt-4 text-sm underline ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
{text.back}
</button>
)}
@@ -281,7 +330,9 @@ function ResultPageFallback() {
return (
<div className={`flex min-h-screen items-center justify-center ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>
{pickLocaleText(locale, '加载中...', 'Loading...')}
</div>
</div>
);
}

View File

@@ -281,7 +281,9 @@ function StripePopupContent() {
className={[
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
stripeSubmitting
? isDark ? 'bg-slate-700 text-slate-400 cursor-not-allowed' : 'bg-gray-400 cursor-not-allowed'
? isDark
? 'bg-slate-700 text-slate-400 cursor-not-allowed'
: 'bg-gray-400 cursor-not-allowed'
: getPaymentMeta('stripe').buttonClass,
].join(' ')}
>
@@ -308,7 +310,9 @@ function StripePopupFallback() {
return (
<div className={`flex min-h-screen items-center justify-center ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>{pickLocaleText(locale, '加载中...', 'Loading...')}</div>
<div className={isDark ? 'text-slate-400' : 'text-gray-500'}>
{pickLocaleText(locale, '加载中...', 'Loading...')}
</div>
</div>
);
}