@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Descriptions,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -17,11 +19,22 @@ import {
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { AdminBenefitGrantRequest } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { COUPON_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import { COUPON_STATUS_LABELS, DELIVERY_TYPE_LABELS, fmtTime } from '../lib/constants';
|
||||
import RedeemRecordDetailDescriptions from '../components/RedeemRecordDetailDescriptions';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
||||
|
||||
type CouponOrder = {
|
||||
id?: string;
|
||||
orderNo?: string;
|
||||
productName?: string | null;
|
||||
productSpec?: string | null;
|
||||
quantity?: number | null;
|
||||
saleUnit?: string | null;
|
||||
deliveryType?: string | null;
|
||||
payAmount?: number | string | null;
|
||||
};
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
@@ -32,10 +45,43 @@ type Row = {
|
||||
status: string;
|
||||
sourceProduct: string;
|
||||
createdAt: string;
|
||||
user?: { userNo: string; phone: string | null };
|
||||
order?: { orderNo: string } | null;
|
||||
user?: { id?: string; userNo: string; phone: string | null };
|
||||
order?: CouponOrder | null;
|
||||
};
|
||||
|
||||
function saleUnitLabel(unit?: string | null) {
|
||||
if (unit === 'BOX') return '箱';
|
||||
if (unit === 'BOTTLE') return '瓶';
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatPayAmount(v?: number | string | null) {
|
||||
if (v == null || v === '') return '';
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? `¥${n.toFixed(2)}` : '';
|
||||
}
|
||||
|
||||
/** 来源:有订单时展示商品名、规格、数量、配送方式、金额;手动发放沿用 sourceProduct */
|
||||
function formatCouponSource(row: { sourceProduct?: string; order?: CouponOrder | null }) {
|
||||
const order = row.order;
|
||||
if (!order?.orderNo && !order?.productName) {
|
||||
return row.sourceProduct || '—';
|
||||
}
|
||||
const unit = saleUnitLabel(order.saleUnit);
|
||||
const qty =
|
||||
order.quantity != null ? `${order.quantity}${unit}` : '';
|
||||
const delivery = DELIVERY_TYPE_LABELS[order.deliveryType ?? ''] || order.deliveryType || '';
|
||||
const amount = formatPayAmount(order.payAmount);
|
||||
const parts = [
|
||||
order.productName || row.sourceProduct,
|
||||
order.productSpec,
|
||||
qty,
|
||||
delivery,
|
||||
amount,
|
||||
].filter((p) => p != null && String(p).trim() !== '');
|
||||
return parts.join(' / ') || row.sourceProduct || '—';
|
||||
}
|
||||
|
||||
type CouponRedeemRecord = {
|
||||
id: string;
|
||||
redeemNo: string;
|
||||
@@ -60,11 +106,10 @@ type CouponRedeemSummary = {
|
||||
type CouponDetail = Row & {
|
||||
redeemSummary?: CouponRedeemSummary | null;
|
||||
redeemRecords?: CouponRedeemRecord[];
|
||||
user?: { userNo?: string; phone?: string | null };
|
||||
order?: { orderNo?: string } | null;
|
||||
};
|
||||
|
||||
export default function BenefitCouponsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [grantForm] = Form.useForm<AdminBenefitGrantRequest>();
|
||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||
@@ -102,20 +147,61 @@ export default function BenefitCouponsPage() {
|
||||
}
|
||||
|
||||
const baseColumns: ColumnsType<Row> = [
|
||||
{ title: '券号', dataIndex: 'couponNo', width: 200, ellipsis: false },
|
||||
{ title: '用户', dataIndex: ['user', 'userNo'], width: 120, ellipsis: false },
|
||||
{
|
||||
title: '券号',
|
||||
dataIndex: 'couponNo',
|
||||
width: 200,
|
||||
ellipsis: false,
|
||||
render: (v, row) => (
|
||||
<AdminPrimaryLink
|
||||
onClick={async () => {
|
||||
setDetail(await request(`/admin/benefit/coupons/${row.id}`));
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</AdminPrimaryLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: ['user', 'userNo'],
|
||||
width: 120,
|
||||
ellipsis: false,
|
||||
render: (v: string | undefined, row) =>
|
||||
row.user?.id ? (
|
||||
<AdminPrimaryLink onClick={() => navigate('/users', { state: { openUserId: String(row.user!.id) } })}>
|
||||
{v}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
v || '—'
|
||||
),
|
||||
},
|
||||
{ title: '手机号', dataIndex: ['user', 'phone'], width: 120, render: (v) => v || '—' },
|
||||
{
|
||||
title: '订单',
|
||||
dataIndex: ['order', 'orderNo'],
|
||||
width: 180,
|
||||
ellipsis: false,
|
||||
render: (v) => v || '—',
|
||||
render: (v: string | undefined) =>
|
||||
v ? (
|
||||
<AdminPrimaryLink onClick={() => navigate(`/orders?orderNo=${encodeURIComponent(v)}`)}>
|
||||
{v}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
},
|
||||
{ title: '总额', dataIndex: 'totalAmount', width: 80, render: (v) => `¥${v}` },
|
||||
{ title: '余额', dataIndex: 'balance', width: 80, render: (v) => `¥${v}` },
|
||||
{ title: '状态', dataIndex: 'status', width: 90, render: (s) => <Tag>{COUPON_STATUS_LABELS[s] || s}</Tag> },
|
||||
{ title: '来源', dataIndex: 'sourceProduct' },
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'sourceProduct',
|
||||
width: 360,
|
||||
ellipsis: false,
|
||||
render: (_: string, row) => formatCouponSource(row),
|
||||
},
|
||||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
{
|
||||
title: '操作',
|
||||
@@ -276,15 +362,37 @@ export default function BenefitCouponsPage() {
|
||||
<>
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="券号">{detail.couponNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户">{detail.user?.userNo ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户">
|
||||
{detail.user?.id ? (
|
||||
<AdminPrimaryLink
|
||||
onClick={() => navigate('/users', { state: { openUserId: String(detail.user!.id) } })}
|
||||
>
|
||||
{detail.user?.userNo}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
(detail.user?.userNo ?? '—')
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{detail.user?.phone ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联订单">{detail.order?.orderNo ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联订单">
|
||||
{detail.order?.orderNo ? (
|
||||
<AdminPrimaryLink
|
||||
onClick={() =>
|
||||
navigate(`/orders?orderNo=${encodeURIComponent(detail.order!.orderNo!)}`)
|
||||
}
|
||||
>
|
||||
{detail.order.orderNo}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="总额">¥{Number(detail.totalAmount).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="余额">¥{Number(detail.balance).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{COUPON_STATUS_LABELS[detail.status] || detail.status}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="来源">{detail.sourceProduct}</Descriptions.Item>
|
||||
<Descriptions.Item label="来源">{formatCouponSource(detail)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Typography.Title level={5} style={{ marginTop: 16, marginBottom: 8 }}>
|
||||
|
||||
Reference in New Issue
Block a user