'use client';
import { useSearchParams } from 'next/navigation';
import { useState, useEffect, useCallback, Suspense } from 'react';
import PayPageLayout from '@/components/PayPageLayout';
import { resolveLocale, type Locale } from '@/lib/locale';
import { PlatformBadge } from '@/lib/platform-style';
/* ---------- types ---------- */
interface SubscriptionPlan {
id: string;
name: string;
description: string | null;
price: number;
originalPrice: number | null;
validDays: number;
validityUnit: 'day' | 'week' | 'month';
features: string[];
groupId: string | null;
groupName: string | null;
sortOrder: number;
enabled: boolean;
groupExists: boolean;
groupPlatform: string | null;
groupRateMultiplier: number | null;
groupDailyLimit: number | null;
groupWeeklyLimit: number | null;
groupMonthlyLimit: number | null;
groupModelScopes: string[] | null;
productName: string | null;
groupAllowMessagesDispatch: boolean;
groupDefaultMappedModel: string | null;
}
interface Sub2ApiGroup {
id: string;
name: string;
subscription_type: string;
daily_limit_usd: number | null;
weekly_limit_usd: number | null;
monthly_limit_usd: number | null;
platform: string | null;
rate_multiplier: number | null;
allow_messages_dispatch: boolean;
default_mapped_model: string | null;
}
interface Sub2ApiSubscription {
id: number;
user_id: number;
group_id: number;
starts_at: string;
expires_at: string;
status: string;
daily_usage_usd: number;
weekly_usage_usd: number;
monthly_usage_usd: number;
daily_window_start: string | null;
weekly_window_start: string | null;
monthly_window_start: string | null;
notes: string | null;
}
interface SubsUserInfo {
id: number;
username: string;
email: string;
}
/* ---------- i18n ---------- */
function buildText(locale: Locale) {
return locale === 'en'
? {
missingToken: 'Missing admin token',
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
invalidToken: 'Invalid admin token',
requestFailed: 'Request failed',
title: 'Subscription Management',
subtitle: 'Manage subscription plans and user subscriptions',
orders: 'Order Management',
dashboard: 'Dashboard',
refresh: 'Refresh',
loading: 'Loading...',
tabPlans: 'Plan Configuration',
tabSubs: 'User Subscriptions',
newPlan: 'New Plan',
editPlan: 'Edit Plan',
deletePlan: 'Delete Plan',
deleteConfirm: 'Delete this plan?',
save: 'Save',
cancel: 'Cancel',
fieldGroup: 'Sub2API Group',
fieldGroupPlaceholder: 'Select a group',
fieldName: 'Plan Name',
fieldDescription: 'Description',
fieldPrice: 'Price (CNY)',
fieldOriginalPrice: 'Original Price (CNY)',
fieldValidDays: 'Validity',
fieldValidUnit: 'Unit',
unitDay: 'Day(s)',
unitWeek: 'Week(s)',
unitMonth: 'Month(s)',
fieldFeatures: 'Features (one per line)',
fieldSortOrder: 'Sort Order',
fieldEnabled: 'For Sale',
colName: 'Name',
colGroup: 'Group ID',
colPrice: 'Price',
colOriginalPrice: 'Original Price',
colValidDays: 'Validity',
colEnabled: 'For Sale',
colGroupStatus: 'Sub2API Status',
colActions: 'Actions',
edit: 'Edit',
delete: 'Delete',
enabled: 'Yes',
disabled: 'No',
groupExists: 'Exists',
groupMissing: 'Missing',
noPlans: 'No plans configured',
searchUserId: 'Email / Username / Notes / API Key',
search: 'Search',
noSubs: 'No subscription records found',
enterUserId: 'Enter a keyword to search users',
fieldProductName: 'Payment Product Name',
fieldProductNamePlaceholder: 'Leave empty for default',
saveFailed: 'Failed to save plan',
deleteFailed: 'Failed to delete plan',
loadFailed: 'Failed to load data',
days: 'days',
user: 'User',
group: 'Group',
usage: 'Usage',
expiresAt: 'Expires At',
status: 'Status',
active: 'Active',
expired: 'Expired',
suspended: 'Suspended',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
remaining: 'remaining',
unlimited: 'Unlimited',
resetIn: 'Reset in',
noGroup: 'Unknown Group',
groupInfo: 'Sub2API Group Info',
groupInfoReadonly: '(read-only, from Sub2API)',
platform: 'Platform',
rateMultiplier: 'Rate',
dailyLimit: 'Daily Limit',
weeklyLimit: 'Weekly Limit',
monthlyLimit: 'Monthly Limit',
modelScopes: 'Models',
}
: {
missingToken: '缺少管理员凭证',
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
invalidToken: '管理员凭证无效',
requestFailed: '请求失败',
title: '订阅管理',
subtitle: '管理订阅套餐与用户订阅',
orders: '订单管理',
dashboard: '数据概览',
refresh: '刷新',
loading: '加载中...',
tabPlans: '套餐配置',
tabSubs: '用户订阅',
newPlan: '新建套餐',
editPlan: '编辑套餐',
deletePlan: '删除套餐',
deleteConfirm: '确认删除该套餐?',
save: '保存',
cancel: '取消',
fieldGroup: 'Sub2API 分组',
fieldGroupPlaceholder: '请选择分组',
fieldName: '套餐名称',
fieldDescription: '描述',
fieldPrice: '价格(元)',
fieldOriginalPrice: '原价(元)',
fieldValidDays: '有效期',
fieldValidUnit: '单位',
unitDay: '天',
unitWeek: '周',
unitMonth: '月',
fieldFeatures: '特性描述(每行一个)',
fieldSortOrder: '排序',
fieldEnabled: '启用售卖',
colName: '名称',
colGroup: '分组 ID',
colPrice: '价格',
colOriginalPrice: '原价',
colValidDays: '有效期',
colEnabled: '启用售卖',
colGroupStatus: 'Sub2API 状态',
colActions: '操作',
edit: '编辑',
delete: '删除',
enabled: '是',
disabled: '否',
groupExists: '存在',
groupMissing: '缺失',
noPlans: '暂无套餐配置',
searchUserId: '邮箱/用户名/备注/API Key',
search: '搜索',
noSubs: '未找到订阅记录',
enterUserId: '输入关键词搜索用户',
fieldProductName: '支付商品名称',
fieldProductNamePlaceholder: '留空使用默认名称',
saveFailed: '保存套餐失败',
deleteFailed: '删除套餐失败',
loadFailed: '加载数据失败',
days: '天',
user: '用户',
group: '分组',
usage: '用量',
expiresAt: '到期时间',
status: '状态',
active: '生效中',
expired: '已过期',
suspended: '已暂停',
daily: '日用量',
weekly: '周用量',
monthly: '月用量',
remaining: '剩余',
unlimited: '无限制',
resetIn: '重置于',
noGroup: '未知分组',
groupInfo: 'Sub2API 分组信息',
groupInfoReadonly: '(只读,来自 Sub2API)',
platform: '平台',
rateMultiplier: '倍率',
dailyLimit: '日限额',
weeklyLimit: '周限额',
monthlyLimit: '月限额',
modelScopes: '模型',
};
}
/* ---------- helpers ---------- */
function formatDate(dateStr: string | null): string {
if (!dateStr) return '-';
const d = new Date(dateStr);
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
function daysRemaining(expiresAt: string | null): number | null {
if (!expiresAt) return null;
const now = new Date();
const exp = new Date(expiresAt);
const diff = exp.getTime() - now.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
}
function resetCountdown(windowStart: string | null, periodDays: number): string | null {
if (!windowStart) return null;
const start = new Date(windowStart);
const resetAt = new Date(start.getTime() + periodDays * 24 * 60 * 60 * 1000);
const now = new Date();
const diffMs = resetAt.getTime() - now.getTime();
if (diffMs <= 0) return null;
const hours = Math.floor(diffMs / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
if (hours >= 24) {
const d = Math.floor(hours / 24);
const h = hours % 24;
return `${d}d ${h}h`;
}
return `${hours}h ${minutes}m`;
}
/* ---------- UsageBar component ---------- */
function UsageBar({
label,
usage,
limit,
resetText,
isDark,
}: {
label: string;
usage: number;
limit: number | null;
resetText: string | null;
isDark: boolean;
}) {
const pct = limit && limit > 0 ? Math.min((usage / limit) * 100, 100) : 0;
const barColor = pct > 80 ? 'bg-red-500' : pct > 50 ? 'bg-yellow-500' : 'bg-green-500';
return (
{label}
${usage.toFixed(2)} {limit != null ? `/ $${limit.toFixed(2)}` : ''}
{limit != null && limit > 0 ? (
) : null}
{resetText && (
{resetText}
)}
);
}
/* ---------- main content ---------- */
function SubscriptionsContent() {
const searchParams = useSearchParams();
const token = searchParams.get('token') || '';
const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light';
const locale = resolveLocale(searchParams.get('lang'));
const isDark = theme === 'dark';
const uiMode = searchParams.get('ui_mode') || 'standalone';
const isEmbedded = uiMode === 'embedded';
const t = buildText(locale);
/* --- shared state --- */
const [activeTab, setActiveTab] = useState<'plans' | 'subs'>('plans');
const [error, setError] = useState('');
/* --- plans state --- */
const [plans, setPlans] = useState([]);
const [groups, setGroups] = useState([]);
const [plansLoading, setPlansLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editingPlan, setEditingPlan] = useState(null);
/* form state */
const [formGroupId, setFormGroupId] = useState('');
const [formName, setFormName] = useState('');
const [formDescription, setFormDescription] = useState('');
const [formPrice, setFormPrice] = useState('');
const [formOriginalPrice, setFormOriginalPrice] = useState('');
const [formValidDays, setFormValidDays] = useState('30');
const [formValidUnit, setFormValidUnit] = useState<'day' | 'week' | 'month'>('day');
const [formFeatures, setFormFeatures] = useState('');
const [formSortOrder, setFormSortOrder] = useState('0');
const [formEnabled, setFormEnabled] = useState(true);
const [formProductName, setFormProductName] = useState('');
const [saving, setSaving] = useState(false);
/* --- subs state --- */
const [subsUserId, setSubsUserId] = useState('');
const [subsKeyword, setSubsKeyword] = useState('');
const [searchResults, setSearchResults] = useState<{ id: number; email: string; username: string; notes?: string }[]>(
[],
);
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
const [searchTimer, setSearchTimer] = useState | null>(null);
const [subs, setSubs] = useState([]);
const [subsUser, setSubsUser] = useState(null);
const [subsLoading, setSubsLoading] = useState(false);
const [subsSearched, setSubsSearched] = useState(false);
/* --- fetch plans --- */
const fetchPlans = useCallback(async () => {
if (!token) return;
setPlansLoading(true);
try {
const res = await fetch(`/api/admin/subscription-plans?token=${encodeURIComponent(token)}`);
if (!res.ok) {
if (res.status === 401) {
setError(t.invalidToken);
return;
}
throw new Error(t.requestFailed);
}
const data = await res.json();
setPlans(Array.isArray(data) ? data : (data.plans ?? []));
} catch {
setError(t.loadFailed);
} finally {
setPlansLoading(false);
}
}, [token]);
/* --- fetch groups --- */
const fetchGroups = useCallback(async () => {
if (!token) return;
try {
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 ?? []));
}
} catch {
/* ignore */
}
}, [token]);
useEffect(() => {
fetchPlans();
fetchGroups();
}, [fetchPlans, fetchGroups]);
/* auto-fetch subs when switching to subs tab */
useEffect(() => {
if (activeTab === 'subs' && !subsSearched) {
fetchSubs();
}
}, [activeTab]);
/* --- modal helpers --- */
const openCreate = () => {
setEditingPlan(null);
setFormGroupId('');
setFormName('');
setFormDescription('');
setFormPrice('');
setFormOriginalPrice('');
setFormValidDays('1');
setFormValidUnit('month');
setFormFeatures('');
setFormSortOrder('0');
setFormEnabled(true);
setFormProductName('');
setModalOpen(true);
};
const openEdit = (plan: SubscriptionPlan) => {
setEditingPlan(plan);
setFormGroupId(plan.groupId ?? '');
setFormName(plan.name);
setFormDescription(plan.description ?? '');
setFormPrice(String(plan.price));
setFormOriginalPrice(plan.originalPrice != null ? String(plan.originalPrice) : '');
setFormValidDays(String(plan.validDays));
setFormValidUnit(plan.validityUnit ?? 'day');
setFormFeatures((plan.features ?? []).join('\n'));
setFormSortOrder(String(plan.sortOrder));
setFormEnabled(plan.enabled);
setFormProductName(plan.productName ?? '');
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
setEditingPlan(null);
};
/* --- save plan (snake_case for backend) --- */
const handleSave = async () => {
if (!formName.trim() || !formPrice || !formGroupId) return;
setSaving(true);
setError('');
const body = {
group_id: Number(formGroupId),
name: formName.trim(),
description: formDescription.trim() || null,
price: parseFloat(formPrice),
original_price: formOriginalPrice ? parseFloat(formOriginalPrice) : null,
validity_days: parseInt(formValidDays, 10) || 30,
validity_unit: formValidUnit,
features: formFeatures
.split('\n')
.map((l) => l.trim())
.filter(Boolean),
sort_order: parseInt(formSortOrder, 10) || 0,
for_sale: formEnabled,
product_name: formProductName.trim() || null,
};
try {
const url = editingPlan ? `/api/admin/subscription-plans/${editingPlan.id}` : '/api/admin/subscription-plans';
const method = editingPlan ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || t.saveFailed);
}
closeModal();
fetchPlans();
} catch (e) {
// 分组被删除等错误:刷新列表使前端状态同步
setError(e instanceof Error ? e.message : t.saveFailed);
fetchPlans();
} finally {
setSaving(false);
}
};
/* --- delete plan --- */
const handleDelete = async (plan: SubscriptionPlan) => {
if (!confirm(t.deleteConfirm)) return;
try {
const res = await fetch(`/api/admin/subscription-plans/${plan.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || t.deleteFailed);
}
fetchPlans();
} catch (e) {
setError(e instanceof Error ? e.message : t.deleteFailed);
}
};
/* --- toggle plan enabled --- */
const handleToggleEnabled = async (plan: SubscriptionPlan) => {
try {
const res = await fetch(`/api/admin/subscription-plans/${plan.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ for_sale: !plan.enabled }),
});
if (res.ok) {
setPlans((prev) => prev.map((p) => (p.id === plan.id ? { ...p, enabled: !p.enabled } : p)));
}
} catch {
/* ignore */
}
};
/* --- search users (R1) --- */
const handleKeywordChange = (value: string) => {
setSubsKeyword(value);
if (searchTimer) clearTimeout(searchTimer);
if (!value.trim()) {
setSubsUserId('');
setSearchResults([]);
setSearchDropdownOpen(false);
return;
}
const timer = setTimeout(async () => {
try {
const res = await fetch(
`/api/admin/sub2api/search-users?token=${encodeURIComponent(token)}&keyword=${encodeURIComponent(value.trim())}`,
);
if (res.ok) {
const data = await res.json();
setSearchResults(data.users ?? []);
setSearchDropdownOpen(true);
}
} catch {
/* ignore */
}
}, 300);
setSearchTimer(timer);
};
const selectUser = (user: { id: number; email: string; username: string }) => {
setSubsUserId(String(user.id));
setSubsKeyword(`${user.email} #${user.id}`);
setSearchDropdownOpen(false);
setSearchResults([]);
};
/* --- fetch user subs --- */
const fetchSubs = async () => {
if (!token) return;
setSubsLoading(true);
setSubsSearched(true);
setSubsUser(null);
try {
const qs = new URLSearchParams({ token });
if (subsUserId.trim()) qs.set('user_id', subsUserId.trim());
const res = await fetch(`/api/admin/subscriptions?${qs}`);
if (!res.ok) {
if (res.status === 401) {
setError(t.invalidToken);
return;
}
throw new Error(t.requestFailed);
}
const data = await res.json();
setSubs(data.subscriptions ?? []);
setSubsUser(data.user ?? null);
} catch {
setError(t.loadFailed);
} finally {
setSubsLoading(false);
}
};
/* --- no token guard --- */
if (!token) {
return (
{t.missingToken}
{t.missingTokenHint}
);
}
/* --- nav params --- */
const navParams = new URLSearchParams();
navParams.set('token', token);
if (locale === 'en') navParams.set('lang', 'en');
if (isDark) navParams.set('theme', 'dark');
if (isEmbedded) navParams.set('ui_mode', 'embedded');
const btnBase = [
'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',
].join(' ');
/* available groups for the form: only subscription type, exclude already used */
const subscriptionGroups = groups.filter((g) => g.subscription_type === 'subscription');
const usedGroupIds = new Set(
plans.filter((p) => p.id !== editingPlan?.id && p.groupId != null).map((p) => p.groupId!),
);
const availableGroups = subscriptionGroups.filter((g) => !usedGroupIds.has(String(g.id)));
/* group id → name map (all groups, for subscription display) */
const groupNameMap = new Map(groups.map((g) => [String(g.id), g.name]));
/* --- tab classes --- */
const tabCls = (active: boolean) =>
[
'flex-1 rounded-lg py-2 text-center text-sm font-medium transition-colors cursor-pointer',
active
? isDark
? 'bg-indigo-500/30 text-indigo-200 ring-1 ring-indigo-400/40'
: 'bg-blue-600 text-white'
: isDark
? 'text-slate-400 hover:text-slate-200'
: 'text-slate-600 hover:text-slate-800',
].join(' ');
/* --- table cell style --- */
const thCls = [
'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider',
isDark ? 'text-slate-400' : 'text-slate-500',
].join(' ');
const tdCls = ['px-4 py-3 text-sm', isDark ? 'text-slate-300' : 'text-slate-700'].join(' ');
const tableWrapCls = [
'overflow-x-auto rounded-xl border',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-white shadow-sm',
].join(' ');
const rowBorderCls = isDark ? 'border-slate-700/50' : 'border-slate-100';
/* --- input classes --- */
const inputCls = [
'w-full rounded-lg border px-3 py-2 text-sm outline-none transition-colors',
isDark
? 'border-slate-600 bg-slate-700 text-slate-200 focus:border-indigo-400'
: 'border-slate-300 bg-white text-slate-800 focus:border-blue-500',
].join(' ');
const labelCls = ['block text-sm font-medium mb-1', isDark ? 'text-slate-300' : 'text-slate-700'].join(' ');
/* --- status badge --- */
const statusBadge = (status: string) => {
const map: Record = {
active: {
label: t.active,
cls: isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-50 text-green-700',
},
expired: {
label: t.expired,
cls: isDark ? 'bg-red-500/20 text-red-300' : 'bg-red-50 text-red-600',
},
suspended: {
label: t.suspended,
cls: isDark ? 'bg-yellow-500/20 text-yellow-300' : 'bg-yellow-50 text-yellow-700',
},
};
const info = map[status] ?? {
label: status,
cls: isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-100 text-gray-500',
};
return (
{info.label}
);
};
return (
{t.orders}
{t.dashboard}
>
}
>
{/* Error banner */}
{error && (
{error}
)}
{/* Tab switcher */}
{/* ====== Tab: Plan Configuration ====== */}
{activeTab === 'plans' && (
<>
{/* New plan button */}
{/* Plans cards */}
{plansLoading ? (
{t.loading}
) : plans.length === 0 ? (
{t.noPlans}
) : (
{plans.map((plan) => (
{/* ── 套餐配置(上半部分) ── */}
{plan.name}
{plan.groupExists ? t.groupExists : t.groupMissing}
{/* Toggle */}
{t.colEnabled}
{/* Actions */}
{/* Plan fields grid */}
{t.colGroup}
{plan.groupId ? (
<>
{plan.groupId}
{plan.groupName && (
({plan.groupName})
)}
>
) : (
{locale === 'en' ? 'Unbound' : '未绑定'}
)}
{t.colPrice}
¥{plan.price.toFixed(2)}
{plan.originalPrice != null && (
¥{plan.originalPrice.toFixed(2)}
)}
{t.colValidDays}
{plan.validDays}{' '}
{plan.validityUnit === 'month'
? t.unitMonth
: plan.validityUnit === 'week'
? t.unitWeek
: t.unitDay}
{t.fieldSortOrder}
{plan.sortOrder}
{/* ── Sub2API 分组信息(嵌套只读区域) ── */}
{plan.groupExists && (
{t.groupInfo}
{t.groupInfoReadonly}
{plan.groupPlatform && (
)}
{plan.groupRateMultiplier != null && (
{t.rateMultiplier}
{plan.groupRateMultiplier}x
)}
{t.dailyLimit}
{plan.groupDailyLimit != null ? `$${plan.groupDailyLimit}` : t.unlimited}
{t.weeklyLimit}
{plan.groupWeeklyLimit != null ? `$${plan.groupWeeklyLimit}` : t.unlimited}
{t.monthlyLimit}
{plan.groupMonthlyLimit != null ? `$${plan.groupMonthlyLimit}` : t.unlimited}
{plan.groupPlatform?.toLowerCase() === 'openai' && (
<>
/v1/messages 调度
{plan.groupAllowMessagesDispatch ? '已启用' : '未启用'}
{plan.groupDefaultMappedModel && (
默认模型
{plan.groupDefaultMappedModel}
)}
>
)}
)}
))}
)}
>
)}
{/* ====== Tab: User Subscriptions ====== */}
{activeTab === 'subs' && (
<>
{/* Search bar (R1: fuzzy search) */}
handleKeywordChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setSearchDropdownOpen(false);
fetchSubs();
}
}}
onFocus={() => {
if (searchResults.length > 0) setSearchDropdownOpen(true);
}}
placeholder={t.searchUserId}
className={inputCls}
/>
{/* Dropdown */}
{searchDropdownOpen && searchResults.length > 0 && (
{searchResults.map((u) => (
))}
)}
{/* User info card */}
{subsUser && (
{(subsUser.email?.[0] ?? subsUser.username?.[0] ?? '?').toUpperCase()}
{subsUser.username}
{subsUser.email}
ID: {subsUser.id}
)}
{/* Subs list */}
{subsLoading ? (
{t.loading}
) : !subsSearched ? (
{t.loading}
) : subs.length === 0 ? (
{t.noSubs}
) : (
| {t.group} |
{t.status} |
{t.usage} |
{t.expiresAt} |
{subs.map((sub) => {
const gName = groupNameMap.get(String(sub.group_id)) ?? t.noGroup;
const remaining = daysRemaining(sub.expires_at);
const group = groups.find((g) => String(g.id) === String(sub.group_id));
const dailyLimit = group?.daily_limit_usd ?? null;
const weeklyLimit = group?.weekly_limit_usd ?? null;
const monthlyLimit = group?.monthly_limit_usd ?? null;
return (
{/* Group */}
|
{gName}
ID: {sub.group_id}
|
{/* Status */}
{statusBadge(sub.status)} |
{/* Usage */}
|
{/* Expires */}
{formatDate(sub.expires_at)}
{remaining != null && (
{remaining > 0 ? `${remaining} ${t.days} ${t.remaining}` : t.expired}
)}
|
);
})}
)}
>
)}
{/* ====== Edit / Create Modal ====== */}
{modalOpen && (
{editingPlan ? t.editPlan : t.newPlan}
{/* Group */}
{/* Selected group info card (read-only) */}
{(() => {
const selectedGroup = groups.find((g) => String(g.id) === formGroupId);
if (!selectedGroup) return null;
return (
{t.groupInfo}
{t.groupInfoReadonly}
{selectedGroup.platform && (
)}
{selectedGroup.rate_multiplier != null && (
{t.rateMultiplier}
{selectedGroup.rate_multiplier}x
)}
{t.dailyLimit}
{selectedGroup.daily_limit_usd != null ? `$${selectedGroup.daily_limit_usd}` : t.unlimited}
{t.weeklyLimit}
{selectedGroup.weekly_limit_usd != null ? `$${selectedGroup.weekly_limit_usd}` : t.unlimited}
{t.monthlyLimit}
{selectedGroup.monthly_limit_usd != null
? `$${selectedGroup.monthly_limit_usd}`
: t.unlimited}
{selectedGroup.platform?.toLowerCase() === 'openai' && (
/v1/messages 调度
{selectedGroup.allow_messages_dispatch ? '已启用' : '未启用'}
)}
);
})()}
{/* Name */}
setFormName(e.target.value)}
className={inputCls}
required
/>
{/* Description */}
{/* Price */}
{/* Valid days + Unit */}
{/* Sort Order */}
setFormSortOrder(e.target.value)}
className={inputCls}
/>
{/* Features */}
{/* Product Name (R3) */}
setFormProductName(e.target.value)}
placeholder={t.fieldProductNamePlaceholder}
className={inputCls}
/>
{/* Enabled */}
{t.fieldEnabled}
{/* Modal actions */}
)}
{/* ====== Extend Confirmation Modal ====== */}
);
}
/* ---------- fallback + export ---------- */
function SubscriptionsPageFallback() {
const searchParams = useSearchParams();
const locale = resolveLocale(searchParams.get('lang'));
return (
{locale === 'en' ? 'Loading...' : '加载中...'}
);
}
export default function SubscriptionsPage() {
return (
}>
);
}