feat(promo): HQ 推广码订单只计已完成并补扫码订单快链

列表与详情的订单数/转化率按 COMPLETED 实时统计;扫码、订单、事件 ID 可跳到对应页。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 12:55:19 +08:00
parent 737efb89a3
commit fccfd7abbe
15 changed files with 421 additions and 124 deletions
+94 -10
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import {
Alert,
Button,
@@ -153,6 +153,7 @@ type OrderExportFilters = {
fulfillmentHold?: boolean;
excludeTest?: boolean;
deliveryType?: string;
promoCodeId?: string;
dateRange?: [Dayjs, Dayjs];
};
@@ -212,6 +213,15 @@ const DATE_PRESETS: Array<{ key: 'day' | 'week' | 'month' | 'quarter' | 'year';
{ key: 'year', label: '当年' },
];
function formatUserWithRemark(user?: {
userNo?: string;
hqRemark?: string | null;
} | null) {
const userNo = user?.userNo || '—';
const remark = user?.hqRemark?.trim();
return remark ? `${userNo}${remark}` : userNo;
}
function formatBenefitBrief(row: AdminOrderRow) {
const coupon = row.benefitCoupon;
if (coupon) {
@@ -247,6 +257,7 @@ function buildExportPayload(
if (filters.deliveryType) payload.deliveryType = filters.deliveryType;
if (filters.fulfillmentHold) payload.fulfillmentHold = true;
if (filters.excludeTest) payload.excludeTest = true;
if (filters.promoCodeId) payload.promoCodeId = filters.promoCodeId;
if (filters.dateRange?.[0]) payload.createdFrom = filters.dateRange[0].format('YYYY-MM-DD');
if (filters.dateRange?.[1]) payload.createdTo = filters.dateRange[1].format('YYYY-MM-DD');
return payload;
@@ -285,8 +296,15 @@ function warehouseToDefaults(wh: WarehouseOption, base?: ShipDefaults | null): S
export default function OrdersPage() {
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const initialOrderNo = searchParams.get('orderNo')?.trim() || '';
const initialPromoCodeId = searchParams.get('promoCodeId')?.trim() || '';
const initialStatusParam = searchParams.getAll('status').join(',');
const initialStatuses = useMemo(
() => initialStatusParam.split(',').map((s) => s.trim()).filter(Boolean),
[initialStatusParam],
);
const [form] = Form.useForm();
const [shipForm] = Form.useForm();
const [logisticsForm] = Form.useForm();
@@ -336,6 +354,20 @@ export default function OrdersPage() {
}
}, [form, initialOrderNo]);
useEffect(() => {
if (initialStatuses.length) {
form.setFieldsValue({ status: initialStatuses });
}
}, [form, initialStatuses]);
useEffect(() => {
const openOrderId = (location.state as { openOrderId?: string } | null)?.openOrderId;
if (openOrderId) {
void openDetail(openOrderId);
navigate(`${location.pathname}${location.search}`, { replace: true, state: null });
}
}, [location.state, location.pathname, location.search, navigate]);
useEffect(() => {
if (!initialOrderNo || deepLinkOpenedRef.current || loading) return;
const first = data?.items?.[0];
@@ -366,13 +398,16 @@ export default function OrdersPage() {
const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
if (initialOrderNo) qs.set('orderNo', initialOrderNo);
if (values.orderNo) qs.set('orderNo', values.orderNo);
for (const status of selectedStatuses(values.status)) qs.append('status', status);
const statuses = selectedStatuses(values.status);
const statusList = statuses.length ? statuses : initialStatuses;
for (const status of statusList) qs.append('status', status);
if (values.orderType) qs.set('orderType', values.orderType);
if (values.cityId) qs.set('cityId', values.cityId);
if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone);
if (values.fulfillmentHold) qs.set('fulfillmentHold', 'true');
if (values.excludeTest) qs.set('excludeTest', 'true');
if (values.deliveryType) qs.set('deliveryType', values.deliveryType);
if (initialPromoCodeId) qs.set('promoCodeId', initialPromoCodeId);
if (values.dateRange?.[0]) qs.set('createdFrom', values.dateRange[0].format('YYYY-MM-DD'));
if (values.dateRange?.[1]) qs.set('createdTo', values.dateRange[1].format('YYYY-MM-DD'));
const res = await request<Paginated<AdminOrderRow>>(`/admin/orders?${qs}`);
@@ -380,7 +415,7 @@ export default function OrdersPage() {
} finally {
setLoading(false);
}
}, [form, page, pageSize, initialOrderNo]);
}, [form, page, pageSize, initialOrderNo, initialPromoCodeId, initialStatuses]);
useEffect(() => {
void load();
@@ -589,7 +624,16 @@ export default function OrdersPage() {
}
setExporting(true);
try {
const payload = buildExportPayload(scope, exportFormat, values, selectedRowKeys);
const payload = buildExportPayload(
scope,
exportFormat,
{
...values,
promoCodeId: initialPromoCodeId || values.promoCodeId,
status: selectedStatuses(values.status).length ? values.status : initialStatuses,
},
selectedRowKeys,
);
const result = await request<OrderExportResult>('/admin/orders/export', {
method: 'POST',
body: JSON.stringify(payload),
@@ -618,14 +662,14 @@ export default function OrdersPage() {
{
title: '用户',
key: 'user',
width: 120,
width: 200,
render: (_, row) =>
row.user?.id ? (
<AdminPrimaryLink onClick={() => navigate('/users', { state: { openUserId: String(row.user!.id) } })}>
{row.user.userNo}
<AdminPrimaryLink onClick={() => navigate(`/users?userId=${encodeURIComponent(String(row.user!.id))}`)}>
{formatUserWithRemark(row.user)}
</AdminPrimaryLink>
) : (
(row.user?.userNo || '—')
formatUserWithRemark(row.user)
),
},
{
@@ -778,6 +822,26 @@ export default function OrdersPage() {
/>
) : null}
{initialPromoCodeId ? (
<Alert
type="info"
showIcon
closable
style={{ marginBottom: 16 }}
message={
initialStatuses.includes('COMPLETED') && initialStatuses.length === 1
? '已按推广码筛选已完成订单'
: '已按推广码筛选订单'
}
action={
<Button size="small" onClick={() => navigate('/orders')}>
</Button>
}
onClose={() => navigate('/orders')}
/>
) : null}
<Form form={form} layout="vertical" onFinish={() => { setPage(1); void load(); }}>
<Row gutter={[16, 8]}>
<Col xs={24} md={14} lg={12}>
@@ -877,7 +941,16 @@ export default function OrdersPage() {
) : <span />}
<Space size={12} wrap>
<Button type="primary" htmlType="submit"></Button>
<Button onClick={() => { form.resetFields(); setPage(1); void load(); }}></Button>
<Button
onClick={() => {
form.resetFields();
setPage(1);
if (initialPromoCodeId || initialOrderNo || initialStatuses.length) navigate('/orders');
else void load();
}}
>
</Button>
{canDeleteOrders ? (
<Button
danger
@@ -987,7 +1060,18 @@ export default function OrdersPage() {
{[detail.proxyPartnerName, detail.proxyPartnerPhone].filter(Boolean).join(' / ') || '—'}
</Descriptions.Item>
) : null}
<Descriptions.Item label="用户">{detail.user?.userNo} / {detail.user?.nickname}</Descriptions.Item>
<Descriptions.Item label="用户">
{detail.user?.id ? (
<AdminPrimaryLink
onClick={() => navigate(`/users?userId=${encodeURIComponent(String(detail.user!.id))}`)}
>
{formatUserWithRemark(detail.user)}
</AdminPrimaryLink>
) : (
formatUserWithRemark(detail.user)
)}
{detail.user?.nickname ? ` / ${detail.user.nickname}` : ''}
</Descriptions.Item>
<Descriptions.Item label="商品额">¥{detail.productAmount}</Descriptions.Item>
<Descriptions.Item label="实付">¥{detail.payAmount}</Descriptions.Item>
<Descriptions.Item label="好客权益">¥{detail.benefitAmount}</Descriptions.Item>