120 lines
5.4 KiB
TypeScript
120 lines
5.4 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
/**
|
|
* D2. 券管理 - 平台券审核与管理
|
|
*
|
|
* 发行方提交的券审核、已上架券管理、券数据统计
|
|
*/
|
|
|
|
interface CouponBatch {
|
|
id: string;
|
|
issuer: string;
|
|
name: string;
|
|
template: string;
|
|
faceValue: number;
|
|
quantity: number;
|
|
sold: number;
|
|
redeemed: number;
|
|
status: 'pending' | 'active' | 'suspended' | 'expired';
|
|
createdAt: string;
|
|
}
|
|
|
|
const mockCoupons: CouponBatch[] = [
|
|
{ id: 'C001', issuer: 'Starbucks', name: '¥25 礼品卡', template: '礼品卡', faceValue: 25, quantity: 5000, sold: 4200, redeemed: 3300, status: 'active', createdAt: '2026-01-15' },
|
|
{ id: 'C002', issuer: 'Amazon', name: '¥100 购物券', template: '代金券', faceValue: 100, quantity: 2000, sold: 1580, redeemed: 980, status: 'active', createdAt: '2026-01-20' },
|
|
{ id: 'C003', issuer: 'Nike', name: '8折运动券', template: '折扣券', faceValue: 80, quantity: 1000, sold: 0, redeemed: 0, status: 'pending', createdAt: '2026-02-08' },
|
|
{ id: 'C004', issuer: 'Walmart', name: '¥50 生活券', template: '代金券', faceValue: 50, quantity: 3000, sold: 3000, redeemed: 2800, status: 'expired', createdAt: '2025-08-01' },
|
|
];
|
|
|
|
const statusColors: Record<string, string> = {
|
|
pending: 'var(--color-warning)',
|
|
active: 'var(--color-success)',
|
|
suspended: 'var(--color-error)',
|
|
expired: 'var(--color-text-tertiary)',
|
|
};
|
|
const statusLabels: Record<string, string> = {
|
|
pending: '待审核',
|
|
active: '在售中',
|
|
suspended: '已暂停',
|
|
expired: '已过期',
|
|
};
|
|
|
|
export const CouponManagementPage: React.FC = () => {
|
|
const [filter, setFilter] = useState('all');
|
|
|
|
const filtered = filter === 'all' ? mockCoupons : mockCoupons.filter(c => c.status === filter);
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
|
<h1 style={{ font: 'var(--text-h1)', color: 'var(--color-text-primary)' }}>券管理</h1>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
{['all', 'pending', 'active', 'suspended', 'expired'].map(f => (
|
|
<button key={f} onClick={() => setFilter(f)} style={{
|
|
padding: '6px 14px', border: 'none', borderRadius: 'var(--radius-full)',
|
|
background: filter === f ? 'var(--color-primary)' : 'var(--color-gray-100)',
|
|
color: filter === f ? 'white' : 'var(--color-text-secondary)',
|
|
cursor: 'pointer', font: 'var(--text-label-sm)',
|
|
}}>
|
|
{f === 'all' ? '全部' : statusLabels[f]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Coupon Table */}
|
|
<div style={{ background: 'var(--color-surface)', borderRadius: 'var(--radius-md)', border: '1px solid var(--color-border-light)', overflow: 'hidden' }}>
|
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<thead>
|
|
<tr style={{ borderBottom: '1px solid var(--color-border-light)' }}>
|
|
{['券ID', '发行方', '券名称', '模板', '面值', '发行量', '已售', '已核销', '状态', '操作'].map(h => (
|
|
<th key={h} style={{ padding: '12px 16px', textAlign: 'left', font: 'var(--text-label-sm)', color: 'var(--color-text-tertiary)' }}>{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filtered.map(coupon => (
|
|
<tr key={coupon.id} style={{ borderBottom: '1px solid var(--color-border-light)' }}>
|
|
<td style={cellStyle}><span style={{ font: 'var(--text-label-sm)', fontFamily: 'var(--font-family-mono)' }}>{coupon.id}</span></td>
|
|
<td style={cellStyle}>{coupon.issuer}</td>
|
|
<td style={cellStyle}><strong>{coupon.name}</strong></td>
|
|
<td style={cellStyle}>{coupon.template}</td>
|
|
<td style={cellStyle}>${coupon.faceValue}</td>
|
|
<td style={cellStyle}>{coupon.quantity.toLocaleString()}</td>
|
|
<td style={cellStyle}>{coupon.sold.toLocaleString()}</td>
|
|
<td style={cellStyle}>{coupon.redeemed.toLocaleString()}</td>
|
|
<td style={cellStyle}>
|
|
<span style={{
|
|
display: 'inline-block', padding: '2px 8px', borderRadius: 'var(--radius-full)',
|
|
background: `${statusColors[coupon.status]}15`, color: statusColors[coupon.status],
|
|
font: 'var(--text-caption)', fontWeight: 600,
|
|
}}>{statusLabels[coupon.status]}</span>
|
|
</td>
|
|
<td style={cellStyle}>
|
|
{coupon.status === 'pending' && (
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button style={btnStyle('var(--color-success)')}>通过</button>
|
|
<button style={btnStyle('var(--color-error)')}>拒绝</button>
|
|
</div>
|
|
)}
|
|
{coupon.status === 'active' && (
|
|
<button style={btnStyle('var(--color-warning)')}>暂停</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const cellStyle: React.CSSProperties = { padding: '12px 16px', font: 'var(--text-body)', color: 'var(--color-text-primary)' };
|
|
const btnStyle = (color: string): React.CSSProperties => ({
|
|
padding: '4px 12px', border: `1px solid ${color}`, borderRadius: 'var(--radius-sm)',
|
|
background: 'transparent', color, cursor: 'pointer', font: 'var(--text-label-sm)',
|
|
});
|