diff --git a/README.md b/README.md index 51fb66e..4c4f264 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ docker compose exec app npx prisma migrate deploy | -------- | ------------------------------------ | ----------------------- | | 充值页面 | `https://pay.example.com/pay` | 用户充值入口 | | 我的订单 | `https://pay.example.com/pay/orders` | 用户查看自己的充值记录 | -| 订单管理 | `https://pay.example.com/admin` | 仅 Sub2API 管理员可访问 | +| 管理后台 | `https://pay.example.com/admin` | 管理后台入口(仅管理员)| Sub2API **v0.1.88** 及以上版本会自动拼接以下参数,无需手动添加: @@ -288,13 +288,13 @@ Sub2API **v0.1.88** 及以上版本会自动拼接以下参数,无需手动添 访问:`https://pay.example.com/admin?token=YOUR_ADMIN_TOKEN` -| 功能 | 说明 | -| -------- | ------------------------------------------- | -| 订单列表 | 按状态筛选、分页浏览,支持每页 20/50/100 条 | -| 订单详情 | 查看完整字段与操作审计日志 | -| 重试充值 | 对已支付但充值失败的订单重新发起充值 | -| 取消订单 | 强制取消待支付订单 | -| 退款 | 对已完成订单发起退款并扣减 Sub2API 余额 | +| 模块 | 路径 | 说明 | +| -------- | ---------------------- | ------------------------------------------- | +| 总览 | `/admin` | 聚合入口,卡片式导航到各管理模块 | +| 订单管理 | `/admin/orders` | 按状态筛选、分页浏览、订单详情、重试/取消/退款 | +| 数据概览 | `/admin/dashboard` | 收入统计、订单趋势、支付方式分布 | +| 渠道管理 | `/admin/channels` | 配置 API 渠道与倍率,支持从 Sub2API 同步 | +| 订阅管理 | `/admin/subscriptions` | 管理订阅套餐与用户订阅 | --- diff --git a/src/app/admin/channels/page.tsx b/src/app/admin/channels/page.tsx index d511cf1..6d2df3e 100644 --- a/src/app/admin/channels/page.tsx +++ b/src/app/admin/channels/page.tsx @@ -502,7 +502,7 @@ function ChannelsContent() { locale={locale} actions={ <> - + {t.orders} ))} - + {text.orders} + + } + > + {error && ( +
+ {error} + +
+ )} + + {/* Filters */} +
+ {statuses.map((s) => ( + + ))} +
+ + {/* Table */} +
+ {loading ? ( +
{text.loading}
+ ) : ( + + )} +
+ + setPage(p)} + onPageSizeChange={(s) => { + setPageSize(s); + setPage(1); + }} + locale={locale} + isDark={isDark} + /> + + {/* Order Detail */} + {detailOrder && ( + setDetailOrder(null)} dark={isDark} locale={locale} /> + )} + + ); +} + +function AdminPageFallback() { + const searchParams = useSearchParams(); + const locale = resolveLocale(searchParams.get('lang')); + + return ( +
+
{locale === 'en' ? 'Loading...' : '加载中...'}
+
+ ); +} + +export default function AdminPage() { + return ( + }> + + + ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 671aea4..1766b31 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,49 +1,54 @@ 'use client'; import { useSearchParams } from 'next/navigation'; -import { useState, useEffect, useCallback, Suspense } from 'react'; -import OrderTable from '@/components/admin/OrderTable'; -import OrderDetail from '@/components/admin/OrderDetail'; -import PaginationBar from '@/components/PaginationBar'; +import { Suspense } from 'react'; import PayPageLayout from '@/components/PayPageLayout'; -import { resolveLocale, type Locale } from '@/lib/locale'; +import { resolveLocale } from '@/lib/locale'; -interface AdminOrder { - id: string; - userId: number; - userName: string | null; - userEmail: string | null; - userNotes: string | null; - amount: number; - status: string; - paymentType: string; - createdAt: string; - paidAt: string | null; - completedAt: string | null; - failedReason: string | null; - expiresAt: string; - srcHost: string | null; -} +const MODULES = [ + { + path: '/admin/orders', + label: { zh: '订单管理', en: 'Order Management' }, + desc: { zh: '查看和管理所有充值订单', en: 'View and manage all recharge orders' }, + icon: ( + + + + ), + }, + { + path: '/admin/dashboard', + label: { zh: '数据概览', en: 'Dashboard' }, + desc: { zh: '收入统计与订单趋势', en: 'Revenue statistics and order trends' }, + icon: ( + + + + ), + }, + { + path: '/admin/channels', + label: { zh: '渠道管理', en: 'Channel Management' }, + desc: { zh: '配置 API 渠道与倍率', en: 'Configure API channels and rate multipliers' }, + icon: ( + + + + ), + }, + { + path: '/admin/subscriptions', + label: { zh: '订阅管理', en: 'Subscription Management' }, + desc: { zh: '管理订阅套餐与用户订阅', en: 'Manage subscription plans and user subscriptions' }, + icon: ( + + + + ), + }, +]; -interface AdminOrderDetail extends AdminOrder { - rechargeCode: string; - paymentTradeNo: string | null; - refundAmount: number | null; - refundReason: string | null; - refundAt: string | null; - forceRefund: boolean; - failedAt: string | null; - updatedAt: string; - clientIp: string | null; - srcHost: string | null; - srcUrl: string | null; - paymentSuccess?: boolean; - rechargeSuccess?: boolean; - rechargeStatus?: string; - auditLogs: { id: string; action: string; detail: string | null; operator: string | null; createdAt: string }[]; -} - -function AdminContent() { +function AdminOverviewContent() { const searchParams = useSearchParams(); const token = searchParams.get('token'); const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light'; @@ -57,106 +62,16 @@ function AdminContent() { ? { missingToken: 'Missing admin token', missingTokenHint: 'Please access the admin page from the Sub2API platform.', - invalidToken: 'Invalid admin token', - requestFailed: 'Request failed', - loadOrdersFailed: 'Failed to load orders', - retryConfirm: 'Retry recharge for this order?', - retryFailed: 'Retry failed', - retryRequestFailed: 'Retry request failed', - cancelConfirm: 'Cancel this order?', - cancelFailed: 'Cancel failed', - cancelRequestFailed: 'Cancel request failed', - loadDetailFailed: 'Failed to load order details', - title: 'Order Management', - subtitle: 'View and manage all recharge orders', - dashboard: 'Dashboard', - refresh: 'Refresh', - loading: 'Loading...', - statuses: { - '': 'All', - PENDING: 'Pending', - PAID: 'Paid', - RECHARGING: 'Recharging', - COMPLETED: 'Completed', - EXPIRED: 'Expired', - CANCELLED: 'Cancelled', - FAILED: 'Recharge failed', - REFUNDED: 'Refunded', - }, + title: 'Admin Panel', + subtitle: 'Manage orders, analytics, channels and subscriptions', } : { missingToken: '缺少管理员凭证', missingTokenHint: '请从 Sub2API 平台正确访问管理页面', - invalidToken: '管理员凭证无效', - requestFailed: '请求失败', - loadOrdersFailed: '加载订单列表失败', - retryConfirm: '确认重试充值?', - retryFailed: '重试失败', - retryRequestFailed: '重试请求失败', - cancelConfirm: '确认取消该订单?', - cancelFailed: '取消失败', - cancelRequestFailed: '取消请求失败', - loadDetailFailed: '加载订单详情失败', - title: '订单管理', - subtitle: '查看和管理所有充值订单', - dashboard: '数据概览', - refresh: '刷新', - loading: '加载中...', - statuses: { - '': '全部', - PENDING: '待支付', - PAID: '已支付', - RECHARGING: '充值中', - COMPLETED: '已完成', - EXPIRED: '已超时', - CANCELLED: '已取消', - FAILED: '充值失败', - REFUNDED: '已退款', - }, + title: '管理后台', + subtitle: '订单、数据、渠道与订阅的统一管理入口', }; - const [orders, setOrders] = useState([]); - const [total, setTotal] = useState(0); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(20); - const [totalPages, setTotalPages] = useState(1); - const [statusFilter, setStatusFilter] = useState(''); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - const [detailOrder, setDetailOrder] = useState(null); - - const fetchOrders = useCallback(async () => { - if (!token) return; - setLoading(true); - try { - const params = new URLSearchParams({ token, page: String(page), page_size: String(pageSize) }); - if (statusFilter) params.set('status', statusFilter); - - const res = await fetch(`/api/admin/orders?${params}`); - if (!res.ok) { - if (res.status === 401) { - setError(text.invalidToken); - return; - } - throw new Error(text.requestFailed); - } - - const data = await res.json(); - setOrders(data.orders); - setTotal(data.total); - setTotalPages(data.total_pages); - } catch { - setError(text.loadOrdersFailed); - } finally { - setLoading(false); - } - }, [token, page, pageSize, statusFilter]); - - useEffect(() => { - fetchOrders(); - }, [fetchOrders]); - if (!token) { return (
@@ -168,168 +83,67 @@ function AdminContent() { ); } - const handleRetry = async (orderId: string) => { - if (!confirm(text.retryConfirm)) return; - try { - const res = await fetch(`/api/admin/orders/${orderId}/retry?token=${token}`, { - method: 'POST', - }); - if (res.ok) { - fetchOrders(); - } else { - const data = await res.json(); - setError(data.error || text.retryFailed); - } - } catch { - setError(text.retryRequestFailed); - } - }; - - const handleCancel = async (orderId: string) => { - if (!confirm(text.cancelConfirm)) return; - try { - const res = await fetch(`/api/admin/orders/${orderId}/cancel?token=${token}`, { - method: 'POST', - }); - if (res.ok) { - fetchOrders(); - } else { - const data = await res.json(); - setError(data.error || text.cancelFailed); - } - } catch { - setError(text.cancelRequestFailed); - } - }; - - const handleViewDetail = async (orderId: string) => { - try { - const res = await fetch(`/api/admin/orders/${orderId}?token=${token}`); - if (res.ok) { - const data = await res.json(); - setDetailOrder(data); - } - } catch { - setError(text.loadDetailFailed); - } - }; - - const statuses = ['', 'PENDING', 'PAID', 'RECHARGING', 'COMPLETED', 'EXPIRED', 'CANCELLED', 'FAILED', 'REFUNDED']; - const statusLabels: Record = text.statuses; - 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(' '); - return ( - - - {text.dashboard} - - - - } - > - {error && ( -
- {error} - -
- )} - - {/* Filters */} -
- {statuses.map((s) => ( - +
+ {mod.icon} +
+
+

+ {mod.label[locale]} +

+

{mod.desc[locale]}

+
+ + + +
))}
- - {/* Table */} -
- {loading ? ( -
{text.loading}
- ) : ( - - )} -
- - setPage(p)} - onPageSizeChange={(s) => { - setPageSize(s); - setPage(1); - }} - locale={locale} - isDark={isDark} - /> - - {/* Order Detail */} - {detailOrder && ( - setDetailOrder(null)} dark={isDark} locale={locale} /> - )}
); } -function AdminPageFallback() { +function AdminOverviewFallback() { const searchParams = useSearchParams(); const locale = resolveLocale(searchParams.get('lang')); @@ -342,8 +156,8 @@ function AdminPageFallback() { export default function AdminPage() { return ( - }> - + }> + ); } diff --git a/src/app/admin/subscriptions/page.tsx b/src/app/admin/subscriptions/page.tsx index a289327..e5573da 100644 --- a/src/app/admin/subscriptions/page.tsx +++ b/src/app/admin/subscriptions/page.tsx @@ -595,7 +595,7 @@ function SubscriptionsContent() { locale={locale} actions={ <> - + {t.orders}