feat(ops): partner proxy order list and HQ order delete
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Add partner-facing proxy order list/detail; unlock SUPER_ADMIN order delete with cascade cleanup and type filter. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,8 @@ import SettlementPage from './pages/SettlementPage';
|
||||
import WeeklyReportPage from './pages/WeeklyReportPage';
|
||||
import ProxyOrderPage from './pages/ProxyOrderPage';
|
||||
import ProxyOrderProductsPage from './pages/ProxyOrderProductsPage';
|
||||
import ProxyOrderListPage from './pages/ProxyOrderListPage';
|
||||
import ProxyOrderDetailPage from './pages/ProxyOrderDetailPage';
|
||||
import StaffListPage from './pages/StaffListPage';
|
||||
import StaffCreatePage from './pages/StaffCreatePage';
|
||||
import LeaderboardPage from './pages/LeaderboardPage';
|
||||
@@ -33,6 +35,8 @@ function PrimaryRoutes() {
|
||||
<Route path="/center/settlement" element={<SettlementPage />} />
|
||||
<Route path="/center/staff" element={<StaffListPage />} />
|
||||
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
||||
<Route path="/center/proxy-orders" element={<ProxyOrderListPage />} />
|
||||
<Route path="/center/proxy-orders/:id" element={<ProxyOrderDetailPage />} />
|
||||
<Route path="/proxy-order" element={<ProxyOrderPage />} />
|
||||
<Route path="/proxy-order/products" element={<ProxyOrderProductsPage />} />
|
||||
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
|
||||
|
||||
@@ -219,6 +219,15 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to="/center/proxy-orders" className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
<div className="partner-menu-icon">
|
||||
<span className="material-symbols-outlined">receipt_long</span>
|
||||
</div>
|
||||
<span className="body-md" style={{ fontSize: 16 }}>代下单列表</span>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</Link>
|
||||
<Link to="/stores?audit=pending" className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
<div className="partner-menu-icon">
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import type { PartnerProxyOrderListItem } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
|
||||
function fmtMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function fmtTime(value: string | undefined) {
|
||||
if (!value) return '—';
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return value;
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function deliveryLabel(order: PartnerProxyOrderListItem) {
|
||||
const t = String(order.deliveryType || '').toUpperCase();
|
||||
if (t === 'ON_SITE_PICKUP') return '现场提货';
|
||||
if (t === 'CROSS_CITY') return '跨城配送';
|
||||
if (t === 'LOCAL') return '同城配送';
|
||||
return '线下代下单';
|
||||
}
|
||||
|
||||
export default function ProxyOrderDetailPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [order, setOrder] = useState<PartnerProxyOrderListItem | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '代下单详情';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
void request<PartnerProxyOrderListItem>('PARTNER_H5', `/partner/proxy-orders/${id}`)
|
||||
.then(setOrder)
|
||||
.catch((e) => setError(e instanceof Error ? e.message : '加载失败'));
|
||||
}, [id]);
|
||||
|
||||
const img = order?.imageResource?.url || '';
|
||||
|
||||
return (
|
||||
<div className="page-no-tab partner-orders-page">
|
||||
<PageHeader title="代下单详情" onBack={() => navigate('/center/proxy-orders')} />
|
||||
|
||||
{error && (
|
||||
<p className="partner-form-error" role="alert" style={{ margin: '16px 20px' }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!order && !error && <p className="label-md text-muted" style={{ margin: 20 }}>加载中…</p>}
|
||||
|
||||
{order && (
|
||||
<div style={{ padding: '0 16px 32px' }}>
|
||||
<div className="partner-order-card" style={{ pointerEvents: 'none' }}>
|
||||
<div className="partner-order-card-top">
|
||||
<span className="label-md text-muted">NO. {order.orderNo}</span>
|
||||
<span className="label-md" style={{ color: 'var(--color-success-green)', fontWeight: 600 }}>
|
||||
已完成
|
||||
</span>
|
||||
</div>
|
||||
<div className="partner-order-product">
|
||||
<div
|
||||
className="partner-order-product-img"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
background: img ? `center/cover no-repeat url(${img})` : undefined,
|
||||
}}
|
||||
>
|
||||
{!img && <span className="material-symbols-outlined text-muted">liquor</span>}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h3 className="headline-md">{order.productName || '杜康好酒'}</h3>
|
||||
{order.productSpec && (
|
||||
<p className="label-md text-muted" style={{ marginTop: 4 }}>
|
||||
{order.productSpec}
|
||||
</p>
|
||||
)}
|
||||
<p className="body-md" style={{ marginTop: 8 }}>
|
||||
×{order.quantity} · ¥{fmtMoney(Number(order.payAmount || 0))}
|
||||
</p>
|
||||
<p className="label-md text-muted" style={{ marginTop: 4 }}>
|
||||
权益 ¥{fmtMoney(Number(order.benefitAmount || 0))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="partner-form-card" style={{ marginTop: 12 }}>
|
||||
<h3 className="headline-md" style={{ marginBottom: 12 }}>客户信息</h3>
|
||||
<p className="body-md">姓名:{order.receiverName || '—'}</p>
|
||||
<p className="body-md" style={{ marginTop: 8 }}>手机:{order.receiverPhone || '—'}</p>
|
||||
<p className="body-md" style={{ marginTop: 8 }}>
|
||||
地址:{order.receiverAddress || (String(order.deliveryType).toUpperCase() === 'ON_SITE_PICKUP' ? '现场提货' : '—')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="partner-form-card" style={{ marginTop: 12 }}>
|
||||
<h3 className="headline-md" style={{ marginBottom: 12 }}>订单信息</h3>
|
||||
<p className="body-md">配送方式:{deliveryLabel(order)}</p>
|
||||
<p className="body-md" style={{ marginTop: 8 }}>下单时间:{fmtTime(order.createdAt)}</p>
|
||||
<p className="body-md" style={{ marginTop: 8 }}>支付状态:已付款(线下)</p>
|
||||
<p className="body-md" style={{ marginTop: 8 }}>订单状态:已完成并发放权益</p>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useCallback, useEffect, useState, type FormEvent } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import type { PartnerProxyOrderListItem, PartnerProxyOrderListResponse } from '@dukang/shared-types';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
|
||||
function fmtMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function fmtTime(value: string | undefined) {
|
||||
if (!value) return '—';
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return value;
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function deliveryLabel(item: PartnerProxyOrderListItem) {
|
||||
const t = String(item.deliveryType || '').toUpperCase();
|
||||
if (t === 'ON_SITE_PICKUP') return '现场提货';
|
||||
if (t === 'CROSS_CITY') return '跨城配送';
|
||||
if (t === 'LOCAL') return '同城配送';
|
||||
return '线下代下单';
|
||||
}
|
||||
|
||||
function coverUrl(item: PartnerProxyOrderListItem) {
|
||||
return item.imageResource?.url || '';
|
||||
}
|
||||
|
||||
export default function ProxyOrderListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [list, setList] = useState<PartnerProxyOrderListItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [q, setQ] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadList = useCallback(() => {
|
||||
if (!isLoggedIn()) {
|
||||
navigate('/login');
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams({ page: '1', pageSize: '50' });
|
||||
if (q.trim()) params.set('keyword', q.trim());
|
||||
return request<PartnerProxyOrderListResponse>('PARTNER_H5', `/partner/proxy-orders?${params}`)
|
||||
.then((res) => {
|
||||
setList(Array.isArray(res.list) ? res.list : []);
|
||||
setTotal(Number(res.total) || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
setList([]);
|
||||
setTotal(0);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [navigate, q]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '代下单列表';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadList();
|
||||
}, [loadList]);
|
||||
|
||||
function submitSearch(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setQ(keyword.trim());
|
||||
}
|
||||
|
||||
return (
|
||||
<PullToRefresh onRefresh={loadList} className="page-no-tab partner-orders-page">
|
||||
<PageHeader title="代下单列表" onBack={() => navigate('/center')} />
|
||||
|
||||
<div style={{ padding: '0 20px 12px' }}>
|
||||
<form className="partner-search" onSubmit={submitSearch}>
|
||||
<span className="material-symbols-outlined">search</span>
|
||||
<input
|
||||
placeholder="订单号 / 手机号 / 姓名 / 商品"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
{loading ? '加载中…' : `共 ${total} 笔代下单`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '0 16px 24px' }}>
|
||||
{!loading && list.length === 0 && <div className="empty">暂无代下单记录</div>}
|
||||
|
||||
{list.map((o) => {
|
||||
const orderId = String(o.id);
|
||||
const img = coverUrl(o);
|
||||
return (
|
||||
<Link key={orderId} to={`/center/proxy-orders/${orderId}`} className="partner-order-card" style={{ marginBottom: 12 }}>
|
||||
<div className="partner-order-card-top">
|
||||
<span className="label-md text-muted">NO. {o.orderNo}</span>
|
||||
<span className="label-md" style={{ color: 'var(--color-success-green)', fontWeight: 600 }}>
|
||||
已完成
|
||||
</span>
|
||||
</div>
|
||||
<div className="partner-order-product">
|
||||
<div
|
||||
className="partner-order-product-img"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
background: img ? `center/cover no-repeat url(${img})` : undefined,
|
||||
}}
|
||||
>
|
||||
{!img && <span className="material-symbols-outlined text-muted">liquor</span>}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h3 className="headline-md line-2-clamp">{o.productName || '杜康好酒'}</h3>
|
||||
<p className="text-variant body-md" style={{ marginTop: 4 }}>
|
||||
×{o.quantity} · ¥{fmtMoney(Number(o.payAmount || 0))}
|
||||
</p>
|
||||
<p className="label-md text-muted" style={{ marginTop: 4 }}>
|
||||
{deliveryLabel(o)} · {fmtTime(o.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="partner-order-address">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>person</span>
|
||||
<p className="line-2-clamp">
|
||||
{o.receiverName || '客户'}
|
||||
{o.receiverPhone ? ` · ${o.receiverPhone}` : ''}
|
||||
{o.receiverAddress ? ` · ${o.receiverAddress}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
@@ -188,7 +188,7 @@ export default function ProxyOrderPage() {
|
||||
});
|
||||
toastSuccess(`代下单成功:${order.orderNo}`);
|
||||
clearProxyOrderDraft();
|
||||
navigate(`/orders/${order.id}`);
|
||||
navigate(`/center/proxy-orders/${order.id}`);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '下单失败');
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user