'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'; /* ---------- types ---------- */ interface SubscriptionPlan { id: string; name: string; description: string | null; price: number; originalPrice: number | null; validDays: number; features: string[]; groupId: string; groupName: string | null; sortOrder: number; enabled: boolean; groupExists: boolean; } interface Sub2ApiGroup { id: string; name: string; } interface UserSubscription { userId: number; groupId: string; status: string; startsAt: string | null; expiresAt: string | null; dailyUsage: number | null; weeklyUsage: number | null; monthlyUsage: number | null; } /* ---------- 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 (days)', 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: 'Group Status', colActions: 'Actions', edit: 'Edit', delete: 'Delete', enabled: 'Yes', disabled: 'No', groupExists: 'Exists', groupMissing: 'Missing', noPlans: 'No plans configured', searchUserId: 'Search by user ID', search: 'Search', colUserId: 'User ID', colStatus: 'Status', colStartsAt: 'Starts At', colExpiresAt: 'Expires At', colDailyUsage: 'Daily Usage', colWeeklyUsage: 'Weekly Usage', colMonthlyUsage: 'Monthly Usage', noSubs: 'No subscription records found', enterUserId: 'Enter a user ID to search', saveFailed: 'Failed to save plan', deleteFailed: 'Failed to delete plan', loadFailed: 'Failed to load data', days: 'days', } : { 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: '有效天数', fieldFeatures: '特性描述(每行一个)', fieldSortOrder: '排序', fieldEnabled: '启用售卖', colName: '名称', colGroup: '分组 ID', colPrice: '价格', colOriginalPrice: '原价', colValidDays: '有效期', colEnabled: '售卖', colGroupStatus: '分组状态', colActions: '操作', edit: '编辑', delete: '删除', enabled: '是', disabled: '否', groupExists: '存在', groupMissing: '缺失', noPlans: '暂无套餐配置', searchUserId: '按用户 ID 搜索', search: '搜索', colUserId: '用户 ID', colStatus: '状态', colStartsAt: '开始时间', colExpiresAt: '到期时间', colDailyUsage: '日用量', colWeeklyUsage: '周用量', colMonthlyUsage: '月用量', noSubs: '未找到订阅记录', enterUserId: '请输入用户 ID 进行搜索', saveFailed: '保存套餐失败', deleteFailed: '删除套餐失败', loadFailed: '加载数据失败', days: '天', }; } /* ---------- 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 [formFeatures, setFormFeatures] = useState(''); const [formSortOrder, setFormSortOrder] = useState('0'); const [formEnabled, setFormEnabled] = useState(true); const [saving, setSaving] = useState(false); /* --- subs state --- */ const [subsUserId, setSubsUserId] = useState(''); const [subs, setSubs] = useState([]); 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]); /* --- modal helpers --- */ const openCreate = () => { setEditingPlan(null); setFormGroupId(''); setFormName(''); setFormDescription(''); setFormPrice(''); setFormOriginalPrice(''); setFormValidDays('30'); setFormFeatures(''); setFormSortOrder('0'); setFormEnabled(true); 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)); setFormFeatures((plan.features ?? []).join('\n')); setFormSortOrder(String(plan.sortOrder)); setFormEnabled(plan.enabled); setModalOpen(true); }; const closeModal = () => { setModalOpen(false); setEditingPlan(null); }; /* --- save plan --- */ const handleSave = async () => { if (!formName.trim() || !formPrice) return; setSaving(true); setError(''); const body = { groupId: formGroupId || undefined, name: formName.trim(), description: formDescription.trim() || null, price: parseFloat(formPrice), originalPrice: formOriginalPrice ? parseFloat(formOriginalPrice) : null, validDays: parseInt(formValidDays, 10) || 30, features: formFeatures .split('\n') .map((l) => l.trim()) .filter(Boolean), sortOrder: parseInt(formSortOrder, 10) || 0, enabled: formEnabled, }; 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); } 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); } }; /* --- fetch user subs --- */ const fetchSubs = async () => { if (!token || !subsUserId.trim()) return; setSubsLoading(true); setSubsSearched(true); try { const res = await fetch( `/api/admin/subscriptions?token=${encodeURIComponent(token)}&user_id=${encodeURIComponent(subsUserId.trim())}`, ); if (!res.ok) { if (res.status === 401) { setError(t.invalidToken); return; } throw new Error(t.requestFailed); } const data = await res.json(); setSubs(Array.isArray(data) ? data : data.subscriptions ?? []); } 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 (exclude groups already used by other plans, unless editing that plan) */ const usedGroupIds = new Set(plans.filter((p) => p.id !== editingPlan?.id).map((p) => p.groupId)); const availableGroups = groups.filter((g) => !usedGroupIds.has(g.id)); /* --- 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(' '); return ( {t.orders} {t.dashboard} } > {/* Error banner */} {error && (
{error}
)} {/* Tab switcher */}
{/* ====== Tab: Plan Configuration ====== */} {activeTab === 'plans' && ( <> {/* New plan button */}
{/* Plans table */}
{plansLoading ? (
{t.loading}
) : plans.length === 0 ? (
{t.noPlans}
) : ( {plans.map((plan) => ( ))}
{t.colName} {t.colGroup} {t.colPrice} {t.colOriginalPrice} {t.colValidDays} {t.colEnabled} {t.colGroupStatus} {t.colActions}
{plan.name} {plan.groupId} {plan.groupName && ( ({plan.groupName}) )} {plan.price.toFixed(2)} {plan.originalPrice != null ? plan.originalPrice.toFixed(2) : '-'} {plan.validDays} {t.days} {plan.enabled ? t.enabled : t.disabled} {plan.groupExists ? t.groupExists : t.groupMissing}
)}
)} {/* ====== Tab: User Subscriptions ====== */} {activeTab === 'subs' && ( <> {/* Search bar */}
setSubsUserId(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && fetchSubs()} placeholder={t.searchUserId} className={[inputCls, 'max-w-xs'].join(' ')} />
{/* Subs table */}
{subsLoading ? (
{t.loading}
) : !subsSearched ? (
{t.enterUserId}
) : subs.length === 0 ? (
{t.noSubs}
) : ( {subs.map((sub, idx) => ( ))}
{t.colUserId} {t.colGroup} {t.colStatus} {t.colStartsAt} {t.colExpiresAt} {t.colDailyUsage} {t.colWeeklyUsage} {t.colMonthlyUsage}
{sub.userId} {sub.groupId} {sub.status} {sub.startsAt ?? '-'} {sub.expiresAt ?? '-'} {sub.dailyUsage ?? '-'} {sub.weeklyUsage ?? '-'} {sub.monthlyUsage ?? '-'}
)}
)} {/* ====== Edit / Create Modal ====== */} {modalOpen && (

{editingPlan ? t.editPlan : t.newPlan}

{/* Group */}
{/* Name */}
setFormName(e.target.value)} className={inputCls} required />
{/* Description */}