269 lines
8.8 KiB
TypeScript
269 lines
8.8 KiB
TypeScript
'use client'
|
||
|
||
import { useField, useFormFields } from '@payloadcms/ui'
|
||
import React, { useEffect, useState } from 'react'
|
||
|
||
interface PreorderItem {
|
||
id: string
|
||
title: string
|
||
variant_id: string
|
||
variant_sku?: string
|
||
variant_title?: string
|
||
quantity: number
|
||
unit_price: number
|
||
total: number
|
||
}
|
||
|
||
interface Order {
|
||
id: string
|
||
display_id: number
|
||
status: string
|
||
payment_status: string
|
||
fulfillment_status: string
|
||
email: string
|
||
total: number
|
||
preorder_amount: number
|
||
preorder_quantity: number
|
||
currency_code: string
|
||
created_at: string
|
||
preorder_items: PreorderItem[]
|
||
}
|
||
|
||
interface Statistics {
|
||
total_orders: number
|
||
total_quantity: number
|
||
total_amount: number
|
||
status_breakdown: Record<string, number>
|
||
}
|
||
|
||
export const PreorderOrdersField: React.FC = () => {
|
||
const { value: medusaId } = useField<string>({ path: 'medusaId' })
|
||
const { value: seedId } = useField<string>({ path: 'seedId' })
|
||
|
||
const [orders, setOrders] = useState<Order[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [stats, setStats] = useState<Statistics | null>(null)
|
||
|
||
useEffect(() => {
|
||
if (medusaId || seedId) {
|
||
fetchOrders()
|
||
}
|
||
}, [medusaId, seedId])
|
||
|
||
const fetchOrders = async () => {
|
||
const productId = seedId || medusaId
|
||
if (!productId) return
|
||
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
|
||
const response = await fetch(`/api/preorders/${productId}/orders`)
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch orders')
|
||
}
|
||
|
||
const data = await response.json()
|
||
setOrders(data.orders || [])
|
||
setStats(data.statistics ?? null)
|
||
} catch (err: any) {
|
||
console.error('Failed to fetch orders:', err)
|
||
setError(err.message || 'Failed to load orders')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const formatCurrency = (amount: number, currency: string) => {
|
||
return new Intl.NumberFormat('zh-CN', {
|
||
style: 'currency',
|
||
currency: currency.toUpperCase(),
|
||
}).format(amount / 100)
|
||
}
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
}
|
||
|
||
if (!medusaId && !seedId) {
|
||
return (
|
||
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||
<p style={{ margin: 0, color: '#666' }}>
|
||
产品尚未同步到 Medusa,无法查看订单
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||
<p style={{ margin: 0 }}>加载订单中...</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div style={{ padding: '1rem', background: '#fee', borderRadius: '4px', marginBottom: '1rem', border: '1px solid #fcc' }}>
|
||
<p style={{ margin: 0, color: '#c00' }}>加载失败: {error}</p>
|
||
<button
|
||
onClick={fetchOrders}
|
||
style={{
|
||
marginTop: '0.5rem',
|
||
padding: '0.25rem 0.5rem',
|
||
background: '#fff',
|
||
border: '1px solid #ccc',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
重试
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (orders.length === 0) {
|
||
return (
|
||
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||
<p style={{ margin: 0, color: '#666' }}>暂无订单</p>
|
||
<button
|
||
onClick={fetchOrders}
|
||
style={{
|
||
marginTop: '0.5rem',
|
||
padding: '0.25rem 0.5rem',
|
||
background: '#fff',
|
||
border: '1px solid #ccc',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
刷新
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
{/* 统计信息 */}
|
||
{stats && (
|
||
<div style={{
|
||
padding: '1rem',
|
||
background: '#e3f2fd',
|
||
borderRadius: '4px',
|
||
marginBottom: '1rem',
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
|
||
gap: '1rem',
|
||
}}>
|
||
<div>
|
||
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总数</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>{stats.total_orders}</div>
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>预购数量</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>{stats.total_quantity}</div>
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>预购金额</div>
|
||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>
|
||
{formatCurrency(stats.total_amount, orders[0]?.currency_code || 'CNY')}
|
||
</div>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||
<button
|
||
onClick={fetchOrders}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
background: '#fff',
|
||
border: '1px solid #1976d2',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
color: '#1976d2',
|
||
fontSize: '0.875rem',
|
||
}}
|
||
>
|
||
🔄 刷新订单
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 订单列表 */}
|
||
<div style={{
|
||
border: '1px solid #e0e0e0',
|
||
borderRadius: '4px',
|
||
overflow: 'hidden',
|
||
}}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ background: '#f5f5f5', borderBottom: '2px solid #e0e0e0' }}>
|
||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>订单号</th>
|
||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>客户</th>
|
||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>商品</th>
|
||
<th style={{ padding: '0.75rem', textAlign: 'right', fontSize: '0.875rem', fontWeight: 600 }}>金额</th>
|
||
<th style={{ padding: '0.75rem', textAlign: 'center', fontSize: '0.875rem', fontWeight: 600 }}>状态</th>
|
||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{orders.map((order, index) => (
|
||
<tr
|
||
key={order.id}
|
||
style={{
|
||
borderBottom: index < orders.length - 1 ? '1px solid #e0e0e0' : 'none',
|
||
background: index % 2 === 0 ? '#fff' : '#fafafa',
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||
<div style={{ fontWeight: 600 }}>#{order.display_id}</div>
|
||
<div style={{ fontSize: '0.75rem', color: '#999' }}>{order.id.slice(0, 8)}</div>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||
{order.email}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||
{(order.preorder_items || []).map((item, i) => (
|
||
<div key={i} style={{ marginBottom: i < order.preorder_items.length - 1 ? '0.25rem' : 0 }}>
|
||
{item.variant_title ? `${item.title} · ${item.variant_title}` : item.title} × {item.quantity}
|
||
</div>
|
||
))}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', textAlign: 'right', fontWeight: 600 }}>
|
||
{formatCurrency(order.total, order.currency_code)}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span style={{
|
||
display: 'inline-block',
|
||
padding: '0.25rem 0.5rem',
|
||
borderRadius: '4px',
|
||
fontSize: '0.75rem',
|
||
fontWeight: 600,
|
||
background: order.status === 'completed' ? '#e8f5e9' : '#fff3e0',
|
||
color: order.status === 'completed' ? '#2e7d32' : '#f57c00',
|
||
}}>
|
||
{order.status}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||
{formatDate(order.created_at)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|