优化阅读页跳转逻辑,优先传递章节中间ID(mid),以提升分享功能的一致性。更新相关页面以支持新逻辑,确保用户体验流畅。增加退款功能的相关处理,支持订单退款及退款原因的记录,增强订单管理的灵活性。

This commit is contained in:
Alex-larget
2026-02-28 10:19:46 +08:00
parent 9f77d1cfe2
commit 8af2d808f9
62 changed files with 1168 additions and 1798 deletions

View File

@@ -117,8 +117,10 @@ export function AdminLayout() {
</div>
</div>
<div className="flex-1 overflow-auto bg-[#0a1628]">
<Outlet />
<div className="flex-1 overflow-auto bg-[#0a1628] min-w-0">
<div className="w-full min-w-[1024px] min-h-full">
<Outlet />
</div>
</div>
</div>
)

View File

@@ -7,7 +7,7 @@ import { Link2 } from 'lucide-react'
export function ApiDocPage() {
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="p-8 w-full">
<div className="flex items-center gap-2 mb-8">
<Link2 className="w-8 h-8 text-[#38bdac]" />
<h1 className="text-2xl font-bold text-white">API </h1>

View File

@@ -117,7 +117,7 @@ export function ChaptersPage() {
<div className="min-h-screen bg-black text-white">
{/* 导航栏 */}
<div className="sticky top-0 bg-black/90 backdrop-blur border-b border-white/10 z-50">
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
<div className="w-full min-w-[1024px] px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold"></h1>
<div className="flex items-center gap-4">
<button
@@ -146,7 +146,7 @@ export function ChaptersPage() {
</div>
</div>
<div className="max-w-6xl mx-auto px-4 py-8">
<div className="w-full min-w-[1024px] px-4 py-8">
{error && (
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
<span>{error}</span>

View File

@@ -364,7 +364,7 @@ export function ContentPage() {
const chaptersForPart = currentPart?.chapters ?? []
return (
<div className="p-8 max-w-6xl mx-auto">
<div className="p-8 w-full">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-white"></h2>

View File

@@ -66,7 +66,7 @@ export function DashboardPage() {
if (isLoading) {
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="p-8 w-full">
<h1 className="text-2xl font-bold mb-8 text-white"></h1>
<div className="flex flex-col items-center justify-center py-24">
<RefreshCw className="w-12 h-12 text-[#38bdac] animate-spin mb-4" />
@@ -150,7 +150,7 @@ export function DashboardPage() {
]
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="p-8 w-full">
<h1 className="text-2xl font-bold mb-8 text-white"></h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">

View File

@@ -12,12 +12,20 @@ import {
DollarSign,
Link2,
Eye,
Undo2,
} from 'lucide-react'
import { Pagination } from '@/components/ui/Pagination'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
} from '@/components/ui/dialog'
import { get, put } from '@/api/client'
interface DistributionOverview {
@@ -97,6 +105,8 @@ interface Order {
referrerNickname?: string | null
referrerCode?: string | null
referralCode?: string | null
orderSn?: string
refundReason?: string
createdAt: string
}
@@ -117,6 +127,9 @@ export function DistributionPage() {
const [pageSize, setPageSize] = useState(10)
const [total, setTotal] = useState(0)
const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set())
const [refundOrder, setRefundOrder] = useState<Order | null>(null)
const [refundReason, setRefundReason] = useState('')
const [refundLoading, setRefundLoading] = useState(false)
useEffect(() => {
loadInitialData()
@@ -313,6 +326,30 @@ export function DistributionPage() {
}
}
async function handleRefund() {
if (!refundOrder?.orderSn && !refundOrder?.id) return
setRefundLoading(true)
setError(null)
try {
const res = await put<{ success?: boolean; error?: string }>('/api/admin/orders/refund', {
orderSn: refundOrder.orderSn || refundOrder.id,
reason: refundReason || undefined,
})
if (res?.success) {
setRefundOrder(null)
setRefundReason('')
await loadTabData('orders', true)
} else {
setError(res?.error || '退款失败')
}
} catch (e) {
const err = e as Error & { data?: { error?: string } }
setError(err?.data?.error || '退款失败,请检查网络后重试')
} finally {
setRefundLoading(false)
}
}
function getStatusBadge(status: string) {
const styles: Record<string, string> = {
active: 'bg-green-500/20 text-green-400',
@@ -364,7 +401,7 @@ export function DistributionPage() {
})
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="p-8 w-full">
{error && (
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
<span>{error}</span>
@@ -647,6 +684,7 @@ export function DistributionPage() {
<option value="completed"></option>
<option value="pending"></option>
<option value="failed"></option>
<option value="refunded">退</option>
</select>
</div>
<Card className="bg-[#0f2137] border-gray-700/50">
@@ -664,9 +702,11 @@ export function DistributionPage() {
<th className="p-4 text-left font-medium"></th>
<th className="p-4 text-left font-medium"></th>
<th className="p-4 text-left font-medium"></th>
<th className="p-4 text-left font-medium">退</th>
<th className="p-4 text-left font-medium">/</th>
<th className="p-4 text-left font-medium"></th>
<th className="p-4 text-left font-medium"></th>
<th className="p-4 text-left font-medium"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700/50">
@@ -713,7 +753,11 @@ export function DistributionPage() {
: order.paymentMethod || '微信支付'}
</td>
<td className="p-4">
{order.status === 'completed' || order.status === 'paid' ? (
{order.status === 'refunded' ? (
<Badge className="bg-gray-500/20 text-gray-400 border-0">
退
</Badge>
) : order.status === 'completed' || order.status === 'paid' ? (
<Badge className="bg-green-500/20 text-green-400 border-0">
</Badge>
@@ -727,6 +771,9 @@ export function DistributionPage() {
</Badge>
)}
</td>
<td className="p-4 text-gray-400 text-sm max-w-[120px]" title={order.refundReason}>
{order.status === 'refunded' && order.refundReason ? order.refundReason : '-'}
</td>
<td className="p-4 text-gray-300 text-sm">
{order.referrerId || order.referralCode ? (
<span
@@ -758,6 +805,22 @@ export function DistributionPage() {
? new Date(order.createdAt).toLocaleString('zh-CN')
: '-'}
</td>
<td className="p-4">
{(order.status === 'paid' || order.status === 'completed') && (
<Button
variant="outline"
size="sm"
className="border-orange-500/50 text-orange-400 hover:bg-orange-500/20"
onClick={() => {
setRefundOrder(order)
setRefundReason('')
}}
>
<Undo2 className="w-3 h-3 mr-1" />
退
</Button>
)}
</td>
</tr>
))}
</tbody>
@@ -1025,6 +1088,55 @@ export function DistributionPage() {
)}
</>
)}
<Dialog open={!!refundOrder} onOpenChange={(open) => !open && setRefundOrder(null)}>
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-md">
<DialogHeader>
<DialogTitle className="text-white">退</DialogTitle>
</DialogHeader>
{refundOrder && (
<div className="space-y-4">
<p className="text-gray-400 text-sm">
{refundOrder.orderSn || refundOrder.id}
</p>
<p className="text-gray-400 text-sm">
退¥{typeof refundOrder.amount === 'number' ? refundOrder.amount.toFixed(2) : parseFloat(String(refundOrder.amount || '0')).toFixed(2)}
</p>
<div>
<label className="text-sm text-gray-400 block mb-2">退</label>
<div className="form-input">
<Input
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
placeholder="如:用户申请退款"
value={refundReason}
onChange={(e) => setRefundReason(e.target.value)}
/>
</div>
</div>
<p className="text-orange-400/80 text-xs">
退退
</p>
</div>
)}
<DialogFooter>
<Button
variant="outline"
className="border-gray-600 text-gray-300"
onClick={() => setRefundOrder(null)}
disabled={refundLoading}
>
</Button>
<Button
className="bg-orange-500 hover:bg-orange-600 text-white"
onClick={handleRefund}
disabled={refundLoading}
>
{refundLoading ? '退款中...' : '确认退款'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -76,7 +76,7 @@ export function MatchRecordsPage() {
const totalPages = Math.ceil(total / pageSize) || 1
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="p-8 w-full">
{error && (
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
<span>{error}</span>

View File

@@ -239,7 +239,7 @@ export function MatchPage() {
}
return (
<div className="p-8 max-w-6xl mx-auto space-y-6">
<div className="p-8 w-full space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-white flex items-center gap-2">

View File

@@ -11,10 +11,17 @@ import {
TableRow,
} from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'
import { Search, RefreshCw, Download, Filter } from 'lucide-react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
} from '@/components/ui/dialog'
import { Search, RefreshCw, Download, Filter, Undo2 } from 'lucide-react'
import { useDebounce } from '@/hooks/useDebounce'
import { Pagination } from '@/components/ui/Pagination'
import { get } from '@/api/client'
import { get, put } from '@/api/client'
interface Purchase {
id: string
@@ -24,7 +31,7 @@ interface Purchase {
sectionTitle?: string
productId?: string
amount: number
status: 'pending' | 'completed' | 'failed' | 'paid' | 'created'
status: 'pending' | 'completed' | 'failed' | 'paid' | 'created' | 'refunded'
paymentMethod?: string
referrerEarnings?: number
createdAt: string
@@ -32,6 +39,7 @@ interface Purchase {
userNickname?: string
productType?: string
description?: string
refundReason?: string
}
interface UsersItem {
@@ -53,6 +61,9 @@ export function OrdersPage() {
const [statusFilter, setStatusFilter] = useState<string>('all')
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [refundOrder, setRefundOrder] = useState<Purchase | null>(null)
const [refundReason, setRefundReason] = useState('')
const [refundLoading, setRefundLoading] = useState(false)
async function loadOrders() {
setIsLoading(true)
@@ -130,12 +141,36 @@ export function OrdersPage() {
const totalPages = Math.ceil(total / pageSize) || 1
async function handleRefund() {
if (!refundOrder?.orderSn && !refundOrder?.id) return
setRefundLoading(true)
setError(null)
try {
const res = await put<{ success?: boolean; error?: string }>('/api/admin/orders/refund', {
orderSn: refundOrder.orderSn || refundOrder.id,
reason: refundReason || undefined,
})
if (res?.success) {
setRefundOrder(null)
setRefundReason('')
loadOrders()
} else {
setError(res?.error || '退款失败')
}
} catch (e) {
const err = e as Error & { data?: { error?: string } }
setError(err?.data?.error || '退款失败,请检查网络后重试')
} finally {
setRefundLoading(false)
}
}
function handleExport() {
if (purchases.length === 0) {
alert('暂无数据可导出')
return
}
const headers = ['订单号', '用户', '手机号', '商品', '金额', '支付方式', '状态', '分销佣金', '下单时间']
const headers = ['订单号', '用户', '手机号', '商品', '金额', '支付方式', '状态', '退款原因', '分销佣金', '下单时间']
const rows = purchases.map((p) => {
const product = formatProduct(p)
return [
@@ -145,7 +180,8 @@ export function OrdersPage() {
product.name,
Number(p.amount || 0).toFixed(2),
p.paymentMethod === 'wechat' ? '微信支付' : p.paymentMethod === 'alipay' ? '支付宝' : p.paymentMethod || '微信支付',
p.status === 'paid' || p.status === 'completed' ? '已完成' : p.status === 'pending' || p.status === 'created' ? '待支付' : '已失败',
p.status === 'refunded' ? '已退款' : p.status === 'paid' || p.status === 'completed' ? '已完成' : p.status === 'pending' || p.status === 'created' ? '待支付' : '已失败',
p.status === 'refunded' && p.refundReason ? p.refundReason : '-',
p.referrerEarnings ? Number(p.referrerEarnings).toFixed(2) : '-',
p.createdAt ? new Date(p.createdAt).toLocaleString('zh-CN') : '',
].join(',')
@@ -161,7 +197,7 @@ export function OrdersPage() {
}
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="p-8 w-full">
{error && (
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
<span>{error}</span>
@@ -218,6 +254,7 @@ export function OrdersPage() {
<option value="pending"></option>
<option value="created"></option>
<option value="failed"></option>
<option value="refunded">退</option>
</select>
</div>
<Button
@@ -249,8 +286,10 @@ export function OrdersPage() {
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400">退</TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -291,7 +330,11 @@ export function OrdersPage() {
: purchase.paymentMethod || '微信支付'}
</TableCell>
<TableCell>
{purchase.status === 'paid' || purchase.status === 'completed' ? (
{purchase.status === 'refunded' ? (
<Badge className="bg-gray-500/20 text-gray-400 hover:bg-gray-500/20 border-0">
退
</Badge>
) : purchase.status === 'paid' || purchase.status === 'completed' ? (
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
</Badge>
@@ -305,6 +348,9 @@ export function OrdersPage() {
</Badge>
)}
</TableCell>
<TableCell className="text-gray-400 text-sm max-w-[120px] truncate" title={purchase.refundReason}>
{purchase.status === 'refunded' && purchase.refundReason ? purchase.refundReason : '-'}
</TableCell>
<TableCell className="text-[#FFD700]">
{purchase.referrerEarnings
? `¥${Number(purchase.referrerEarnings).toFixed(2)}`
@@ -313,12 +359,28 @@ export function OrdersPage() {
<TableCell className="text-gray-400 text-sm">
{new Date(purchase.createdAt).toLocaleString('zh-CN')}
</TableCell>
<TableCell>
{(purchase.status === 'paid' || purchase.status === 'completed') && (
<Button
variant="outline"
size="sm"
className="border-orange-500/50 text-orange-400 hover:bg-orange-500/20"
onClick={() => {
setRefundOrder(purchase)
setRefundReason('')
}}
>
<Undo2 className="w-3 h-3 mr-1" />
退
</Button>
)}
</TableCell>
</TableRow>
)
})}
{purchases.length === 0 && (
<TableRow>
<TableCell colSpan={8} className="text-center py-12 text-gray-500">
<TableCell colSpan={10} className="text-center py-12 text-gray-500">
</TableCell>
</TableRow>
@@ -340,6 +402,55 @@ export function OrdersPage() {
)}
</CardContent>
</Card>
<Dialog open={!!refundOrder} onOpenChange={(open) => !open && setRefundOrder(null)}>
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-md">
<DialogHeader>
<DialogTitle className="text-white">退</DialogTitle>
</DialogHeader>
{refundOrder && (
<div className="space-y-4">
<p className="text-gray-400 text-sm">
{refundOrder.orderSn || refundOrder.id}
</p>
<p className="text-gray-400 text-sm">
退¥{Number(refundOrder.amount || 0).toFixed(2)}
</p>
<div>
<label className="text-sm text-gray-400 block mb-2">退</label>
<div className="form-input">
<Input
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
placeholder="如:用户申请退款"
value={refundReason}
onChange={(e) => setRefundReason(e.target.value)}
/>
</div>
</div>
<p className="text-orange-400/80 text-xs">
退退
</p>
</div>
)}
<DialogFooter>
<Button
variant="outline"
className="border-gray-600 text-gray-300"
onClick={() => setRefundOrder(null)}
disabled={refundLoading}
>
</Button>
<Button
className="bg-orange-500 hover:bg-orange-600 text-white"
onClick={handleRefund}
disabled={refundLoading}
>
{refundLoading ? '退款中...' : '确认退款'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -128,7 +128,7 @@ export function PaymentPage() {
const p = localSettings.paypal as Record<string, unknown>
return (
<div className="p-8 max-w-5xl mx-auto">
<div className="p-8 w-full">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-2xl font-bold mb-2 text-white"></h1>

View File

@@ -103,7 +103,7 @@ export function QRCodesPage() {
}
return (
<div className="p-8 max-w-5xl mx-auto">
<div className="p-8 w-full">
<div className="mb-8">
<h2 className="text-2xl font-bold text-white"></h2>
<p className="text-gray-400 mt-1"></p>

View File

@@ -92,7 +92,7 @@ export function ReferralSettingsPage() {
if (loading) return <div className="p-8 text-gray-500">...</div>
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="p-8 w-full">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-white flex items-center gap-2">

View File

@@ -236,7 +236,7 @@ export function SettingsPage() {
if (loading) return <div className="p-8 text-gray-500">...</div>
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="p-8 w-full">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-white"></h2>

View File

@@ -107,7 +107,7 @@ export function SitePage() {
const page = localSettings.pageConfig
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="p-8 w-full">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-white"></h2>

View File

@@ -280,7 +280,7 @@ export function UsersPage() {
}
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="p-8 w-full">
{error && (
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
<span>{error}</span>

View File

@@ -120,7 +120,7 @@ export function VipRolesPage() {
}
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="p-8 w-full">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-white flex items-center gap-2">

View File

@@ -198,7 +198,7 @@ export function WithdrawalsPage() {
}
return (
<div className="p-8 max-w-6xl mx-auto">
<div className="p-8 w-full">
{error && (
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
<span>{error}</span>