四端上传
This commit is contained in:
1 parent
25bbb5f9c8
commit
c6e2cb4d5b
276 files changed
+17812
-3
No files matched your search
@@ -0,0 +1,171 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { ORDER_STATUS_TEXT } from '@/lib/resources';
|
||||
import { Button, Card, Empty, Input, Notice, Select } from '@/components/ui';
|
||||
|
||||
interface OrderRow {
|
||||
id: number;
|
||||
out_trade_no: string;
|
||||
wx_order_id: string | null;
|
||||
status: number;
|
||||
amount_fen: number;
|
||||
nickname: string | null;
|
||||
openid: string;
|
||||
course_title: string;
|
||||
created_at: string;
|
||||
paid_at: string | null;
|
||||
delivered_at: string | null;
|
||||
refunded_at: string | null;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
export default function OrdersPage() {
|
||||
const [items, setItems] = useState<OrderRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [notice, setNotice] = useState('');
|
||||
const [refundTarget, setRefundTarget] = useState<OrderRow | null>(null);
|
||||
const [reason, setReason] = useState('用户申请退款');
|
||||
|
||||
const load = useCallback((nextPage: number, nextStatus: string) => {
|
||||
setError('');
|
||||
api
|
||||
.listOrders(nextPage, PAGE_SIZE, nextStatus === '' ? undefined : Number(nextStatus))
|
||||
.then((res) => {
|
||||
setItems(res.items as unknown as OrderRow[]);
|
||||
setTotal(res.total);
|
||||
setPage(res.page);
|
||||
})
|
||||
.catch((err: { message: string }) => setError(err.message));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load(1, '');
|
||||
}, [load]);
|
||||
|
||||
const submitRefund = () => {
|
||||
if (!refundTarget) return;
|
||||
setError('');
|
||||
setNotice('');
|
||||
api
|
||||
.refundOrder(refundTarget.id, reason)
|
||||
.then(() => {
|
||||
setNotice('退款请求已提交。iOS 端需用户自行向 App Store 申请,开发者无法发起。');
|
||||
setRefundTarget(null);
|
||||
load(page, status);
|
||||
})
|
||||
.catch((err: { message: string }) => setError(err.message));
|
||||
};
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold text-gray-900">订单</h1>
|
||||
<Notice message={error} />
|
||||
{notice && <Notice message={notice} kind="success" />}
|
||||
|
||||
<Card
|
||||
title={`共 ${total} 笔订单`}
|
||||
action={
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
options={[
|
||||
{ value: -1, label: '全部状态' },
|
||||
{ value: 0, label: '待支付' },
|
||||
{ value: 1, label: '已支付' },
|
||||
{ value: 2, label: '已开通' },
|
||||
{ value: 3, label: '已退款' },
|
||||
{ value: 4, label: '已关闭' },
|
||||
]}
|
||||
/>
|
||||
<Button variant="ghost" onClick={() => load(1, status === '-1' ? '' : status)}>
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{items.length === 0 ? (
|
||||
<Empty text="暂无订单" />
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 text-left text-xs text-gray-500">
|
||||
<th className="px-2 py-2">订单号</th>
|
||||
<th className="px-2 py-2">用户</th>
|
||||
<th className="px-2 py-2">课程</th>
|
||||
<th className="px-2 py-2">金额</th>
|
||||
<th className="px-2 py-2">状态</th>
|
||||
<th className="px-2 py-2">下单时间</th>
|
||||
<th className="px-2 py-2 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((o) => (
|
||||
<tr key={o.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="px-2 py-2 font-mono text-xs">{o.out_trade_no}</td>
|
||||
<td className="px-2 py-2">{o.nickname || o.openid.slice(0, 8)}</td>
|
||||
<td className="px-2 py-2">{o.course_title}</td>
|
||||
<td className="px-2 py-2">¥{(o.amount_fen / 100).toFixed(2)}</td>
|
||||
<td className="px-2 py-2">{ORDER_STATUS_TEXT[o.status] ?? o.status}</td>
|
||||
<td className="px-2 py-2 text-gray-500">{String(o.created_at).slice(0, 16).replace('T', ' ')}</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
{o.status === 2 && (
|
||||
<Button variant="danger" onClick={() => setRefundTarget(o)}>
|
||||
退款
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-center justify-between text-sm text-gray-500">
|
||||
<span>
|
||||
第 {page} / {totalPages} 页
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" disabled={page <= 1} onClick={() => load(page - 1, status)}>
|
||||
上一页
|
||||
</Button>
|
||||
<Button variant="ghost" disabled={page >= totalPages} onClick={() => load(page + 1, status)}>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{refundTarget && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-5 shadow-xl">
|
||||
<h3 className="mb-3 text-base font-semibold">发起退款</h3>
|
||||
<p className="mb-3 text-sm text-gray-600">
|
||||
订单 {refundTarget.out_trade_no},金额 ¥{(refundTarget.amount_fen / 100).toFixed(2)}
|
||||
</p>
|
||||
<Input value={reason} onChange={setReason} placeholder="退款原因" />
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={() => setRefundTarget(null)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="danger" onClick={submitRefund}>
|
||||
确认退款
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user