'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 Channel { id: string; groupId: number; name: string; platform: string; rateMultiplier: number; description: string | null; models: string | null; features: string | null; sortOrder: number; enabled: boolean; groupExists: boolean; createdAt: string; updatedAt: string; } interface Sub2ApiGroup { id: number; name: string; description: string; platform: string; status: string; rate_multiplier: number; } interface ChannelFormData { group_id: number | ''; name: string; platform: string; rate_multiplier: string; description: string; models: string; features: string; sort_order: string; enabled: boolean; } const PLATFORMS = ['claude', 'openai', 'gemini', 'codex', 'sora'] as const; const PLATFORM_COLORS: Record = { claude: { bg: 'bg-orange-100 dark:bg-orange-900/40', text: 'text-orange-700 dark:text-orange-300' }, openai: { bg: 'bg-green-100 dark:bg-green-900/40', text: 'text-green-700 dark:text-green-300' }, gemini: { bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-700 dark:text-blue-300' }, codex: { bg: 'bg-purple-100 dark:bg-purple-900/40', text: 'text-purple-700 dark:text-purple-300' }, sora: { bg: 'bg-pink-100 dark:bg-pink-900/40', text: 'text-pink-700 dark:text-pink-300' }, }; // ── i18n ── function getTexts(locale: Locale) { return locale === 'en' ? { missingToken: 'Missing admin token', missingTokenHint: 'Please access the admin page from the Sub2API platform.', invalidToken: 'Invalid admin token', title: 'Channel Management', subtitle: 'Configure and manage subscription channels', orders: 'Orders', refresh: 'Refresh', loading: 'Loading...', noChannels: 'No channels found', noChannelsHint: 'Click "Sync from Sub2API" or "New Channel" to get started.', syncFromSub2Api: 'Sync from Sub2API', newChannel: 'New Channel', editChannel: 'Edit Channel', colName: 'Name', colPlatform: 'Platform', colRate: 'Rate', colSub2ApiStatus: 'Sub2API Status', colSortOrder: 'Sort', colEnabled: 'Enabled', colActions: 'Actions', edit: 'Edit', delete: 'Delete', deleteConfirm: 'Are you sure you want to delete this channel?', fieldName: 'Channel Name', fieldPlatform: 'Category', fieldRate: 'Rate Multiplier', fieldRateHint: 'e.g. 0.15 means 0.15x', fieldDescription: 'Description', fieldModels: 'Supported Models (one per line)', fieldFeatures: 'Features (one per line)', fieldSortOrder: 'Sort Order', fieldEnabled: 'Enable Channel', fieldGroupId: 'Sub2API Group ID', cancel: 'Cancel', save: 'Save', saving: 'Saving...', syncTitle: 'Sync from Sub2API', syncHint: 'Select groups to import as channels', syncLoading: 'Loading groups...', syncNoGroups: 'No groups found in Sub2API', syncAlreadyExists: 'Already imported', syncImport: 'Import Selected', syncImporting: 'Importing...', loadFailed: 'Failed to load channels', saveFailed: 'Failed to save channel', deleteFailed: 'Failed to delete channel', syncFetchFailed: 'Failed to fetch Sub2API groups', syncImportFailed: 'Failed to import groups', syncImportSuccess: (n: number) => `Successfully imported ${n} channel(s)`, yes: 'Yes', no: 'No', } : { missingToken: '缺少管理员凭证', missingTokenHint: '请从 Sub2API 平台正确访问管理页面', invalidToken: '管理员凭证无效', title: '渠道管理', subtitle: '配置和管理订阅渠道', orders: '订单管理', refresh: '刷新', loading: '加载中...', noChannels: '暂无渠道', noChannelsHint: '点击「从 Sub2API 同步」或「新建渠道」开始创建。', syncFromSub2Api: '从 Sub2API 同步', newChannel: '新建渠道', editChannel: '编辑渠道', colName: '名称', colPlatform: '平台', colRate: '倍率', colSub2ApiStatus: 'Sub2API 状态', colSortOrder: '排序', colEnabled: '启用', colActions: '操作', edit: '编辑', delete: '删除', deleteConfirm: '确定要删除该渠道吗?', fieldName: '渠道名称', fieldPlatform: '分类', fieldRate: '倍率', fieldRateHint: '如 0.15 表示 0.15 倍', fieldDescription: '描述', fieldModels: '支持模型(每行一个)', fieldFeatures: '功能特性(每行一个)', fieldSortOrder: '排序', fieldEnabled: '启用渠道', fieldGroupId: 'Sub2API 分组 ID', cancel: '取消', save: '保存', saving: '保存中...', syncTitle: '从 Sub2API 同步', syncHint: '选择要导入为渠道的分组', syncLoading: '加载分组中...', syncNoGroups: 'Sub2API 中没有找到分组', syncAlreadyExists: '已导入', syncImport: '导入所选', syncImporting: '导入中...', loadFailed: '加载渠道列表失败', saveFailed: '保存渠道失败', deleteFailed: '删除渠道失败', syncFetchFailed: '获取 Sub2API 分组列表失败', syncImportFailed: '导入分组失败', syncImportSuccess: (n: number) => `成功导入 ${n} 个渠道`, yes: '是', no: '否', }; } // ── Helpers ── function parseJsonArray(value: string | null): string[] { if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function arrayToLines(value: string | null): string { return parseJsonArray(value).join('\n'); } function linesToJsonString(lines: string): string { const arr = lines .split('\n') .map((l) => l.trim()) .filter(Boolean); return JSON.stringify(arr); } const emptyForm: ChannelFormData = { group_id: '', name: '', platform: 'claude', rate_multiplier: '1', description: '', models: '', features: '', sort_order: '0', enabled: true, }; // ── Main Content ── function ChannelsContent() { const searchParams = useSearchParams(); const token = searchParams.get('token') || ''; const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light'; const uiMode = searchParams.get('ui_mode') || 'standalone'; const locale = resolveLocale(searchParams.get('lang')); const isDark = theme === 'dark'; const isEmbedded = uiMode === 'embedded'; const t = getTexts(locale); const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); // Edit modal state const [editModalOpen, setEditModalOpen] = useState(false); const [editingChannel, setEditingChannel] = useState(null); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); // Sync modal state const [syncModalOpen, setSyncModalOpen] = useState(false); const [syncGroups, setSyncGroups] = useState([]); const [syncLoading, setSyncLoading] = useState(false); const [syncSelected, setSyncSelected] = useState>(new Set()); const [syncImporting, setSyncImporting] = useState(false); // ── Fetch channels ── const fetchChannels = useCallback(async () => { if (!token) return; setLoading(true); try { const res = await fetch(`/api/admin/channels?token=${encodeURIComponent(token)}`); if (!res.ok) { if (res.status === 401) { setError(t.invalidToken); return; } throw new Error(); } const data = await res.json(); setChannels(data.channels); } catch { setError(t.loadFailed); } finally { setLoading(false); } }, [token]); useEffect(() => { fetchChannels(); }, [fetchChannels]); // ── Missing token ── if (!token) { return (

{t.missingToken}

{t.missingTokenHint}

); } // ── Edit modal handlers ── const openCreateModal = () => { setEditingChannel(null); setForm(emptyForm); setEditModalOpen(true); }; const openEditModal = (channel: Channel) => { setEditingChannel(channel); setForm({ group_id: channel.groupId, name: channel.name, platform: channel.platform, rate_multiplier: String(channel.rateMultiplier), description: channel.description ?? '', models: arrayToLines(channel.models), features: arrayToLines(channel.features), sort_order: String(channel.sortOrder), enabled: channel.enabled, }); setEditModalOpen(true); }; const closeEditModal = () => { setEditModalOpen(false); setEditingChannel(null); }; const handleSave = async () => { if (!form.name.trim() || form.group_id === '' || !form.rate_multiplier) return; setSaving(true); setError(''); const body = { group_id: Number(form.group_id), name: form.name.trim(), platform: form.platform, rate_multiplier: parseFloat(form.rate_multiplier), description: form.description.trim() || null, models: form.models.trim() ? linesToJsonString(form.models) : null, features: form.features.trim() ? linesToJsonString(form.features) : null, sort_order: parseInt(form.sort_order, 10) || 0, enabled: form.enabled, }; try { const url = editingChannel ? `/api/admin/channels/${editingChannel.id}` : '/api/admin/channels'; const method = editingChannel ? '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(() => ({})); setError(data.error || t.saveFailed); return; } closeEditModal(); fetchChannels(); } catch { setError(t.saveFailed); } finally { setSaving(false); } }; // ── Delete handler ── const handleDelete = async (channel: Channel) => { if (!confirm(t.deleteConfirm)) return; try { const res = await fetch(`/api/admin/channels/${channel.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { const data = await res.json().catch(() => ({})); setError(data.error || t.deleteFailed); return; } fetchChannels(); } catch { setError(t.deleteFailed); } }; // ── Toggle enabled ── const handleToggleEnabled = async (channel: Channel) => { try { const res = await fetch(`/api/admin/channels/${channel.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ enabled: !channel.enabled }), }); if (res.ok) { setChannels((prev) => prev.map((c) => (c.id === channel.id ? { ...c, enabled: !c.enabled } : c))); } } catch { /* ignore */ } }; // ── Sync modal handlers ── const openSyncModal = async () => { setSyncModalOpen(true); setSyncLoading(true); setSyncSelected(new Set()); try { const res = await fetch(`/api/admin/sub2api/groups?token=${encodeURIComponent(token)}`); if (!res.ok) throw new Error(); const data = await res.json(); setSyncGroups(data.groups ?? []); } catch { setError(t.syncFetchFailed); setSyncModalOpen(false); } finally { setSyncLoading(false); } }; const closeSyncModal = () => { setSyncModalOpen(false); setSyncGroups([]); setSyncSelected(new Set()); }; const existingGroupIds = new Set(channels.map((c) => c.groupId)); const toggleSyncGroup = (id: number) => { setSyncSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleSyncImport = async () => { if (syncSelected.size === 0) return; setSyncImporting(true); setError(''); let successCount = 0; for (const groupId of syncSelected) { const group = syncGroups.find((g) => g.id === groupId); if (!group) continue; try { const res = await fetch('/api/admin/channels', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ group_id: group.id, name: group.name, platform: group.platform || 'claude', rate_multiplier: group.rate_multiplier ?? 1, description: group.description || null, sort_order: 0, enabled: true, }), }); if (res.ok) successCount++; } catch { /* continue with remaining */ } } setSyncImporting(false); closeSyncModal(); if (successCount > 0) { fetchChannels(); } else { setError(t.syncImportFailed); } }; // ── Nav params ── const navParams = new URLSearchParams(); if (token) 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(' '); // ── Shared input classes ── const inputCls = [ 'w-full rounded-lg border px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500/50', isDark ? 'border-slate-600 bg-slate-700 text-slate-100 placeholder-slate-400' : 'border-slate-300 bg-white text-slate-900 placeholder-slate-400', ].join(' '); const labelCls = ['block text-sm font-medium mb-1', isDark ? 'text-slate-300' : 'text-slate-700'].join(' '); // ── Render ── return ( {t.orders} } > {/* Error banner */} {error && (
{error}
)} {/* Channel table */}
{loading ? (
{t.loading}
) : channels.length === 0 ? (

{t.noChannels}

{t.noChannelsHint}

) : ( {channels.map((channel) => { const pc = PLATFORM_COLORS[channel.platform] ?? PLATFORM_COLORS.claude; return ( ); })}
{t.colName} {t.colPlatform} {t.colRate} {t.colSub2ApiStatus} {t.colSortOrder} {t.colEnabled} {t.colActions}
{channel.name}
Group #{channel.groupId}
{channel.platform} {channel.rateMultiplier}x {channel.groupExists ? ( ) : ( )} {channel.sortOrder}
)}
{/* ── Edit / Create Modal ── */} {editModalOpen && (
e.stopPropagation()} >

{editingChannel ? t.editChannel : t.newChannel}

{/* Group ID (only for create) */} {!editingChannel && (
setForm({ ...form, group_id: e.target.value ? Number(e.target.value) : '' })} className={inputCls} required />
)} {/* Name */}
setForm({ ...form, name: e.target.value })} className={inputCls} required />
{/* Platform */}
{/* Rate Multiplier */}
setForm({ ...form, rate_multiplier: e.target.value })} className={inputCls} required />

{t.fieldRateHint}

{/* Description */}