四端上传

This commit is contained in:
tianmo committed 2026-09-29 15:25:40 +08:00
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>
);
}